LoginSignup
9
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-04-15

はじめに

最近は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」にコードを書くだけです。
最終的には以下のようになります。

完成したコード

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 です。
この型はタイピングゲームのアルゴリズム実装には不向きですので、KeyCodechar に変換する関数 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';
    }
}

(うん、これはひどい・・・でもこれが私の最適解です(笑))

こうすることによって、KeyCodechar に変換しています。

また、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() の戻り値 charInputKey() の引数となって、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 の中身を空にし、_romanIndex0 に設定し、その後、クラス配列 questions からランダムに一つ取り出して格納されたクラス questionroman プロパティ( 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をアタッチする。
}

そしてインスペクタ上で文字列とローマ字表記を入力します。
aa91cb22d34f84c15cd717790448cf615fcc24c3406f5.png
(必ず日本式(「し」→「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)」をダブルクリックして、インスペクタ上で以下のようにタグとタグ情報を追加します。
e215fa1cf26a117976f2c709592c0e015fcc2305f0a7c.png
こうすることによって、<style>は表示されなくなり、タイピングの入力前と入力後の文字の色が変わります。

さいごに

コードの解説は以上です。

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

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

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

次の記事で柔軟な入力に対応させます。
【Unity】柔軟な入力に対応したタイピングゲームの作り方(改良編)【C#】

9
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7