はじめに
最近はPC離れが加速し、モバイルゲームが主流となった今、タイピングゲームは減少傾向です。
しかしながら普段からキーボードを叩いてコーディングを行っている人々は、タイピングゲームを作りたいと思ったことはないでしょうか?
また、タイピングゲームの作成に挫折してしまう人々も少なくないと思います。
なぜなら、タイピングゲームは作りにくいと思うからです。
というのも、今のタイピングゲームは、「し」を「shi」、「ち」を「chi」など、柔軟な入力に対応しています。
これらがなくてもタイピングゲームとしては成立しますが、それでは非常に操作性の悪いタイピングゲームとなってしまい、今時公開できるようなものではありません。
また「柔軟な入力に対応していないタイピングゲームなんて公開できないよ!」と思っている開発者も多いかもしれません。
なので今回はUnityで柔軟な入力に対応したタイピングゲームを作る方法を記述します。
今回はUnityの記事ですが、Unity以外の言語やフレームワークでも参考になれば光栄です。
対象読者は、ある程度Unityが触れて、TextMeshProなどが使えることを想定しています。
とりあえずUnityを起動してプロジェクトを作成しよう
とにかく、まずUnityのプロジェクトが存在しないことには、何も始まりません。
なので普段通り、Unityのプロジェクトを作成します。(2Dでも3DでもURPでもお好きに)
今回はTextMeshProを使いますので、TextMeshProをインポートします。
そして、タイピングゲームに必ずといっていいほど存在する「日本語表示用のテキスト」と「ローマ字表示用のテキスト」のTextMeshProオブジェクトを作成します。
また、日本語表示用のテキストには、日本語表示に対応したTextMeshPro用のフォントアセットを用意します。
次に「TypingManager.cs」を作成します。
そして、Hierarchy上に「TypingManager」という空のGameObjectを作成して、そこに先ほど作成した「TypingManager.cs」をInspector上でアタッチします。
ここからは延々と「TypingManager.cs」にコードを書くだけです。
最終的には以下のようになります。
完成したコード
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
[Serializable]
public class Question
{
public string japanese;
public string roman;
}
public class TypingManager : MonoBehaviour
{
[SerializeField] private Question[] questions;
[SerializeField] private TextMeshProUGUI textJapanese; // ここに日本語表示のTextMeshProをアタッチする。
[SerializeField] private TextMeshProUGUI textRoman; // ここにローマ字表示のTextMeshProをアタッチする。
private readonly List<char> _roman = new List<char>();
private int _romanIndex;
private void Start()
{
InitializeQuestion();
}
private void OnGUI()
{
if (Event.current.type == EventType.KeyDown)
{
switch (InputKey(GetCharFromKeyCode(Event.current.keyCode)))
{
case 1: // 正解タイプ時
_romanIndex++;
if (_roman[_romanIndex] == '@') // 「@」がタイピングの終わりの判定となる。
{
InitializeQuestion();
}
else
{
textRoman.text = GenerateTextRoman();
}
break;
case 2: // ミスタイプ時
break;
}
}
}
void InitializeQuestion()
{
Question question = questions[UnityEngine.Random.Range(0, questions.Length)];
_roman.Clear();
_romanIndex = 0;
char[] characters = question.roman.ToCharArray();
foreach (char character in characters)
{
_roman.Add(character);
}
_roman.Add('@');
textJapanese.text = question.japanese;
textRoman.text = GenerateTextRoman();
}
string GenerateTextRoman()
{
string text = "<style=typed>";
for (int i = 0; i < _roman.Count; i++)
{
if (_roman[i] == '@')
{
break;
}
if (i == _romanIndex)
{
text += "</style><style=untyped>";
}
text += _roman[i];
}
text += "</style>";
return text;
}
int InputKey(char inputChar)
{
if (inputChar == '\0')
{
return 0;
}
if (inputChar == _roman[_romanIndex])
{
return 1;
}
return 2;
}
char GetCharFromKeyCode(KeyCode keyCode)
{
switch (keyCode)
{
case KeyCode.A:
return 'a';
case KeyCode.B:
return 'b';
case KeyCode.C:
return 'c';
case KeyCode.D:
return 'd';
case KeyCode.E:
return 'e';
case KeyCode.F:
return 'f';
case KeyCode.G:
return 'g';
case KeyCode.H:
return 'h';
case KeyCode.I:
return 'i';
case KeyCode.J:
return 'j';
case KeyCode.K:
return 'k';
case KeyCode.L:
return 'l';
case KeyCode.M:
return 'm';
case KeyCode.N:
return 'n';
case KeyCode.O:
return 'o';
case KeyCode.P:
return 'p';
case KeyCode.Q:
return 'q';
case KeyCode.R:
return 'r';
case KeyCode.S:
return 's';
case KeyCode.T:
return 't';
case KeyCode.U:
return 'u';
case KeyCode.V:
return 'v';
case KeyCode.W:
return 'w';
case KeyCode.X:
return 'x';
case KeyCode.Y:
return 'y';
case KeyCode.Z:
return 'z';
case KeyCode.Alpha0:
return '0';
case KeyCode.Alpha1:
return '1';
case KeyCode.Alpha2:
return '2';
case KeyCode.Alpha3:
return '3';
case KeyCode.Alpha4:
return '4';
case KeyCode.Alpha5:
return '5';
case KeyCode.Alpha6:
return '6';
case KeyCode.Alpha7:
return '7';
case KeyCode.Alpha8:
return '8';
case KeyCode.Alpha9:
return '9';
case KeyCode.Minus:
return '-';
case KeyCode.Caret:
return '^';
case KeyCode.Backslash:
return '\\';
case KeyCode.At:
return '@';
case KeyCode.LeftBracket:
return '[';
case KeyCode.Semicolon:
return ';';
case KeyCode.Colon:
return ':';
case KeyCode.RightBracket:
return ']';
case KeyCode.Comma:
return ',';
case KeyCode.Period:
return '.';
case KeyCode.Slash:
return '/';
case KeyCode.Underscore:
return '_';
case KeyCode.Backspace:
return '\b';
case KeyCode.Return:
return '\r';
case KeyCode.Space:
return ' ';
default: //上記以外のキーが押された場合は「null文字」を返す。
return '\0';
}
}
}
これだけだとわかりにくいと思うので、コードを解説します。
コードの解説
何かしらの入力イベントが生じた時に呼ばれるイベント関数 OnGUI()
Unityではスクリプトの生成時に最初から存在するUpdate()
というのが存在しますが、もっと便利なイベント関数OnGUI()
が存在します。
これは何かしらの入力イベントが生じた時に呼び出され、以下のように書くことによってキーボードの入力が発生した時に処理を走らせることができます。
private void OnGUI()
{
if (Event.current.type == EventType.KeyDown)
{
// キーが入力された時に処理を実行する
}
}
OnGUI()
関数にEvent.current.type == EventType.KeyDown
という条件式を書けば、キーの入力時のみtrue
となって、処理が実行されます。
これをタイピングゲームとして機能させるには、以下の通りです。
private void OnGUI()
{
if (Event.current.type == EventType.KeyDown)
{
switch (InputKey(GetCharFromKeyCode(Event.current.keyCode)))
{
case 1: // 正解タイプ時
_romanIndex++;
if (_roman[_romanIndex] == '@') // 「@」がタイピングの終わりの判定となる。
{
InitializeQuestion();
}
else
{
textRoman.text = GenerateTextRoman();
}
break;
case 2: // ミスタイプ時
break;
}
}
}
ここから InputKey()
や GetCharFromKeyCode()
InitializeQuestion()
GenerateRomanText()
ならびに _roman[]
_romanIndex
について解説していきます。
KeyCodeをcharに変換する関数 GetCharFromKeyCode()
キーが入力され OnGUI()
が実行されると Event.current.keyCode
に入力されたキーコードが格納されます。型は KeyCode
です。
この型はタイピングゲームのアルゴリズム実装には不向きですので、KeyCode
を char
に変換する関数 GetCharFromKeyCode()
を実装しました。
以下のようになりました。今回はShift入力は省略しています。
char GetCharFromKeyCode(KeyCode keyCode)
{
switch (keyCode)
{
case KeyCode.A:
return 'a';
case KeyCode.B:
return 'b';
case KeyCode.C:
return 'c';
case KeyCode.D:
return 'd';
case KeyCode.E:
return 'e';
case KeyCode.F:
return 'f';
case KeyCode.G:
return 'g';
case KeyCode.H:
return 'h';
case KeyCode.I:
return 'i';
case KeyCode.J:
return 'j';
case KeyCode.K:
return 'k';
case KeyCode.L:
return 'l';
case KeyCode.M:
return 'm';
case KeyCode.N:
return 'n';
case KeyCode.O:
return 'o';
case KeyCode.P:
return 'p';
case KeyCode.Q:
return 'q';
case KeyCode.R:
return 'r';
case KeyCode.S:
return 's';
case KeyCode.T:
return 't';
case KeyCode.U:
return 'u';
case KeyCode.V:
return 'v';
case KeyCode.W:
return 'w';
case KeyCode.X:
return 'x';
case KeyCode.Y:
return 'y';
case KeyCode.Z:
return 'z';
case KeyCode.Alpha0:
return '0';
case KeyCode.Alpha1:
return '1';
case KeyCode.Alpha2:
return '2';
case KeyCode.Alpha3:
return '3';
case KeyCode.Alpha4:
return '4';
case KeyCode.Alpha5:
return '5';
case KeyCode.Alpha6:
return '6';
case KeyCode.Alpha7:
return '7';
case KeyCode.Alpha8:
return '8';
case KeyCode.Alpha9:
return '9';
case KeyCode.Minus:
return '-';
case KeyCode.Caret:
return '^';
case KeyCode.Backslash:
return '\\';
case KeyCode.At:
return '@';
case KeyCode.LeftBracket:
return '[';
case KeyCode.Semicolon:
return ';';
case KeyCode.Colon:
return ':';
case KeyCode.RightBracket:
return ']';
case KeyCode.Comma:
return ',';
case KeyCode.Period:
return '.';
case KeyCode.Slash:
return '/';
case KeyCode.Underscore:
return '_';
case KeyCode.Backspace:
return '\b';
case KeyCode.Return:
return '\r';
case KeyCode.Space:
return ' ';
default: //上記以外のキーが押された場合は「null文字」を返す。
return '\0';
}
}
(うん、これはひどい・・・でもこれが私の最適解です(笑))
こうすることによって、KeyCode
を char
に変換しています。
また、Functionキーなどが入力された場合でも OnGUI()
が実行され Event.current.keyCode
に格納され、上記の関数が実行されますが、その場合はnull文字 \0
を返しています。
タイピングの正誤判定を行う関数 InputKey()
タイピングゲームには、キーの入力が正しいか否かを判断する機能が必要になります。
なので、それを行うための関数 InputKey()
を実装します。
int InputKey(char inputChar)
{
if (inputChar == '\0')
{
return 0;
}
if (inputChar == _roman[_romanIndex])
{
return 1;
}
return 2;
}
キーの入力によって OnGUI()
関数が呼ばれ、GetCharFromKeyCode()
の戻り値 char
が InputKey()
の引数となって、int
を返します。
正しい入力であれば 1
を返し、ミスタイプであれば 2
を返します。また入力に対応していないキーが入力された場合は 0
を返しています。
後の記事で上記の関数を編集し、柔軟な入力に対応させます。
タイピングの状態を格納するインスタンス変数 _roman
_romanIndex
タイピングゲームを作るからには、当然タイピング用の入力文の入力状態をコントロールする変数が必要になります。
using System.Collections.Generic;
public class TypingManager : MonoBehaviour
{
private readonly List<char> _roman = new List<char>();
private int _romanIndex;
}
_roman
はタイピングの入力文字の処理に用いられる List<char>
のインスタンス変数で、頻繁に Add()
Clear()
が用いられますので、今回は List<T>
となっております。
Unityではスクリプトの生成時に作成されますが、using System.Collections.Generic;
を忘れないようにしてください。
そして、_romanIndex
は _roman
の参照に用いられるだけの int
型のインスタンス変数です。
問題を初期化する関数 InitializeQuestion()
と、問題文を格納するクラス配列 questions
どんなタイピングゲームにも、問題を初期化する必要がありますので、それを行うための関数 InitializeQuestion()
を実装します。
void InitializeQuestion()
{
Question question = questions[UnityEngine.Random.Range(0, questions.Length)];
_roman.Clear();
_romanIndex = 0;
char[] characters = question.roman.ToCharArray();
foreach (char character in characters)
{
_roman.Add(character);
}
_roman.Add('@');
textJapanese.text = question.japanese;
textRoman.text = GenerateTextRoman();
}
Clear()
によって _roman
の中身を空にし、_romanIndex
を 0
に設定し、その後、クラス配列 questions
からランダムに一つ取り出して格納されたクラス question
の roman
プロパティ( string
型)を ToCharArray()
によって char[]
に変換し、foreach
を用いることによって、_roman
に次から次へと Add()
で文字を追加します。
そして、_roman
の最後に @
を Add()
します。この @
が「タイピングの終わり」であることを示します。
タイピングゲームには、タイピング用の文字列のリストが必要になります。
よって、Question
クラスを作成して、[Serializable]
[SerializeField]
でインスペクタ上から文字列を編集できるようにします。
using System;
using TMPro;
using UnityEngine;
[Serializable]
public class Question
{
public string japanese;
public string roman;
}
public class TypingManager : MonoBehaviour
{
[SerializeField] private Question[] questions;
[SerializeField] private TextMeshProUGUI textJapanese; // ここに日本語表示のTextMeshProをアタッチする。
[SerializeField] private TextMeshProUGUI textRoman; // ここにローマ字表示のTextMeshProをアタッチする。
}
そしてインスペクタ上で文字列とローマ字表記を入力します。
(必ず日本式(「し」→「si」、「ち」→「ti」)で入力して、「ん」の後に「な行」や「あ行」が来る場合は「nnna」及び「nna」などと入力してください。後ほどの記事で柔軟な入力方法に響きます。
また、諸事情でjapaneseがTitleになっています。)
また、_textJapanese
_textRoman
は、画面に表示するためのTextMeshProオブジェクトを格納するインスタンス変数です。
インスペクタ上でアタッチを行います。
表示用のテキスト情報を生成する関数 GenerateRomanText()
タイピングゲームでは当たり前のように、入力前の文字と入力後の文字で色が異なりますので、それを実装するための関数 GenerateRomanText()
を実装します。
string GenerateTextRoman()
{
string text = "<style=typed>";
for (int i = 0; i < _roman.Count; i++)
{
if (_roman[i] == '@')
{
break;
}
if (i == _romanIndex)
{
text += "</style><style=untyped>";
}
text += _roman[i];
}
text += "</style>";
return text;
}
TextMeshProにはタグ機能が搭載されており、特定の部分のみスタイルを変更する <style>
タグを使用します。
ただし、このままでは画面上に <style>
が表示されてしまいます。なのでUnityエディタ上で「Project Settings」→「TextMesh Pro」の「Settings」→「Default Style Sheet」の「Default Style Sheet (TMP_StyleSheet)」をダブルクリックして、インスペクタ上で以下のようにタグとタグ情報を追加します。
こうすることによって、<style>
は表示されなくなり、タイピングの入力前と入力後の文字の色が変わります。
さいごに
コードの解説は以上です。
長々としたコード及び記事をお読みいただき、誠にありがとうございます。
タイピングゲームとして動作しましたでしょうか?
しかし、これでは表示されているローマ字以外のタイピングができないため、非常に操作性の悪いタイピングゲームとなっているはずです。
次の記事で柔軟な入力に対応させます。
【Unity】柔軟な入力に対応したタイピングゲームの作り方(改良編)【C#】