【Unity】柔軟な入力に対応したタイピングゲームの作り方（導入編）【C#】

## はじめに

最近は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」にコードを書くだけです。
最終的には以下のようになります。
### 完成したコード
```csharp: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()`が存在します。
これは何かしらの入力イベントが生じた時に呼び出され、以下のように書くことによってキーボードの入力が発生した時に処理を走らせることができます。

```csharp
private void OnGUI()
{
    if (Event.current.type == EventType.KeyDown)
    {
        // キーが入力された時に処理を実行する
    }
}
```
`OnGUI()`関数に`Event.current.type == EventType.KeyDown`という条件式を書けば、キーの入力時のみ`true`となって、処理が実行されます。
これをタイピングゲームとして機能させるには、以下の通りです。

```csharp
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入力は省略しています。

```csharp
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()` を実装します。

```csharp
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`
タイピングゲームを作るからには、当然タイピング用の入力文の入力状態をコントロールする変数が必要になります。

```csharp
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()` を実装します。

```csharp
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]` でインスペクタ上から文字列を編集できるようにします。

```csharp
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をアタッチする。
}
```
そしてインスペクタ上で文字列とローマ字表記を入力します。
![aa91cb22d34f84c15cd717790448cf615fcc24c3406f5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188551/069655ad-289c-b278-3305-5e535ca23a8d.png)
（必ず日本式（「し」→「si」、「ち」→「ti」）で入力して、「ん」の後に「な行」や「あ行」が来る場合は「nnna」及び「nna」などと入力してください。後ほどの記事で柔軟な入力方法に響きます。
　また、諸事情でjapaneseがTitleになっています。）

また、`_textJapanese` `_textRoman` は、画面に表示するためのTextMeshProオブジェクトを格納するインスタンス変数です。
インスペクタ上でアタッチを行います。
### 表示用のテキスト情報を生成する関数 `GenerateRomanText()`
タイピングゲームでは当たり前のように、入力前の文字と入力後の文字で色が異なりますので、それを実装するための関数 `GenerateRomanText()` を実装します。

```csharp
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)」をダブルクリックして、インスペクタ上で以下のようにタグとタグ情報を追加します。
![e215fa1cf26a117976f2c709592c0e015fcc2305f0a7c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188551/57789261-a45f-19cd-cdfe-9e2a3a1f74f4.png)
こうすることによって、`<style>`は表示されなくなり、タイピングの入力前と入力後の文字の色が変わります。
## さいごに
コードの解説は以上です。

長々としたコード及び記事をお読みいただき、誠にありがとうございます。

タイピングゲームとして動作しましたでしょうか？

しかし、これでは表示されているローマ字以外のタイピングができないため、非常に操作性の悪いタイピングゲームとなっているはずです。

次の記事で柔軟な入力に対応させます。
[【Unity】柔軟な入力に対応したタイピングゲームの作り方（改良編）【C#】](https://qiita.com/AkioMabuchi/items/b74c3dfda070294a89c0)