2020-12-10に更新

柔軟な入力方法に対応したタイピングゲームの作り方

読了目安:30分

この記事は「Unityゲーム開発者ギルド2 Advent Calender 2020」12日目の記事です。

まえがき

おはようございます。こんにちは。こんばんは。そしてはじめまして。アキオと申します。ちょうど記事を公開した12月12日が私の誕生日でして、今回で26歳になりました。この26歳の間はとにかくUnityとRailsを触って、様々なゲームやサービスを多数公開運営したいと思います。

早速質問ですが、皆様はタイピングゲームはお好きでしょうか?

最近はPC離れも加速してモバイルゲームが主流となった今、タイピングゲームは減少傾向です。けれでもPCメインのunityroomは、同じPCメインのタイピングゲームとの相性は抜群です。特にunity1weekに毎回参加している人々は、タイピングゲームを作ってみたいとは思ったことがあるはずです。

けれどもunity1weekで300~500近い作品が公開される中でタイピングゲームは多くて3作品しかありません。全体では8000近いゲームがunity1weekで公開されていますが。タイピングゲームは1%しか存在しません。

では、なぜ相性抜群のタイピングゲームがunityroomに少ないかというと
「実力差が大きい」「ありがちなゲームになる」という理由もあるかと思いますが、何よりも「作るのが難しい」のではないかと私は思います。

というのも現在ネット上に公開されている日本語入力のタイピングゲームのほぼ全てが、「し」を「shi」、「つ」を「tsu」と入力できます。

そのような柔軟な入力方法を実装するのが難しいから、タイピングゲームが少ないのではないかと私は思います。

逆を言えば、その柔軟な入力方法に対応していないタイピングゲームの作成は簡単だと私は思います。けれどもそれでは「やりにくい」「今時のタイピングゲームではない」という感想を抱えて、作るのをためらうのではないでしょうか?

なので、今回はunityroom等にタイピングゲームを増やすべく、その柔軟な入力方法に対応したタイピングゲームの作り方を紹介したいと思います。

対象読者は、ある程度Unityが触れて、TextMeshPro等などが使えることを想定しています。なのでちょっと説明が雑かもしれませんが、ご了承願います。

とりあえずUnityを起動してタイピングゲームを作ろう!

まず普段通りにプロジェクトを作成します。(2Dでも3Dでも可能)今回はTextMeshProを使いますので、TextMeshProをインポートして、TextMeshProのGameObject「TextMeshProTitle」と「TextMeshProRoman」のオブジェクトを作成します。

そして「TypingManager.cs」を作成します。そして空のGameObject「TypingManager」をHierarchyにアタッチして、そのGameObjectに先ほど作成した「TypingManager.cs」をInspector上でアタッチします。

ここからは、延々と「TypingManager.cs」にコードを書きます。

タイピングゲームの要となる「OnGUI()」を定義しよう!

Unityではスクリプトの生成時に最初から存在するUpdate()というイベント関数が存在しますが、これは毎フレーム呼び出される関数なので、1フレームあたりに2回以上の入力が発生すると、2回目以降のキー入力が検知できないので、基本使うべきではありません。
なので今回は、Unityでは標準でOnGUI()というイベント関数を使います。この関数はキーボードやマウスのクリックならびにクリック解除の時に呼び出されます。今回はあくまでもキーの入力時のみに処理を行いたいのでifを使って以下のように書きます。

private void OnGUI()
{
    if (Event.current.type == EventType.KeyDown)
    {
        // キーが入力された時に処理される。
    }
}

OnGUI()関数にEvent.current.type == EventType.KeyDownという条件式を書けば、キーの入力時のみtrueになり、処理が実行されるようになります。

もう早速ですが、タイピングゲームに使われるOnGUI()の中身を全部書きましょう。以下のようになります。

private void OnGUI()
{
    if (Event.current.type == EventType.KeyDown)
    {
        switch(InputKey(GetCharFromKeyCode(Event.current.keyCode)))
        {
            case 1:
            case 2:
                _romanIndex++;
                if(_roman[_romanIndex] == '@')
                {
                    InitializeQuestion();
                }
                else
                {
                    _textMeshProRoman.text = GenerateRomanText();
                }
                break;
            case 3:
                // ここにミスタイプ時の処理を記述する
                break;
        }
    }
}

当然ですがInputKey()GetCharFromKeyCode()InitializeQuestion()GenerateRomanText()ならびに_roman[]_romanIndex_textMeshProRomanは未定義ですので、このままでは動作しません。なのでそれぞれのメソッドや変数の解説をしながら実装したいと思います。

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:
        return '\0';
    }
}

(誰かこのコードを簡潔にできるなら教えて)

ひどいコードですね(笑)でも私の技術力ではこうなってしまいました。こうやって、KeyCodecharに変換しているわけです。今回はShiftキーによる大文字入力には対応していません。Functionキーなどには入力対応しておらず、それらが入力された場合にもOnGUI()が実行されEvent.current.keyCodeに格納され、上記の関数が実行されますが、その場合はnull文字\0を返しています。

「InputKey()」を実装しよう!

さて、OnGUI()関数を確認すると、GetCharFromKeyCode()charの返り値が、InputKey()の引数となっているわけですが、この関数は入力が正しいか否かを判断します。この関数の返り値はintで、正しい入力があれば1を、ミスタイプであれば3を返し、null文字ならば0を返します。

int InputKey(char inputChar)
{
    char currentChar = _roman[_romanIndex];

    if(inputChar == '\0')
    {
        return 0;
    }

    if(inputChar == currentChar)
    {
        return 1;
    }

    return 3;
}

返り値の2については、後ほどの「柔軟な入力方法」によって解説します。

ところで「_romanとか_romanIndexって何?」と思った方、今から解説します。

タイピングの状態を格納するインスタンス変数を作成しよう!

タイピングゲームを実装するからには、当然タイピング用の状態を格納する変数が必要になります。
なので今回は_romanromanIndexのインスタンス変数を定義しましょう。

public class TypingManager : MonoBehaviour
{
    private List<char> _roman = new List<char>();
    private int _romanIndex = 0;
}

_romanはタイピングの処理に用いられるList<char>のインスタンス変数で、頻繁にAdd()Clear()を用いますので、List<T>となっております。
そして_romanIndex_romanの参照に用いられるだけのint型のインスタンス変数です。

問題を初期化する「InitializeQuestion()」関数を実装しよう!

どんなタイピングゲームにも「問題の初期化」というのは必ず存在しますので、それを行うための関数InitializeQuestion()を実装します。

void InitializeQuestion()
{
    Question question = _questions[UnityEngine.Random.Range(0, _questions.Length)];

    _romanIndex = 0;

    _roman.Clear();

    char[] characters = question.roman.ToCharArray();

    foreach(char character in characters)
    {
        _roman.Add(character);
    }

    _roman.Add('@');

    _textMeshProTitle.text = question.title;
    _textMeshProRoman.text = GenerateRomanText();
}

Clear()メソッドで_romanの中身を空にし、その後questionromanプロパティ(string型)をToCharArray()メソッドでchar型の配列に変換して、foreach_romanに次から次へとAdd()メソッドで追加します。

そして_romanの最後に@を追加します。この@が「タイピングの終わり」であることを示します。

ところでQuestionが登場しました。タイピングゲームにはタイピング用の文字列のリストが必要になります。

なのでQuestionクラスを作成して、[SerializeField]でインスペクタ上から文字列を編集できるようにします。

_textMeshProTitle_textMeshProRomanは、画面に表示するためのTextMeshProのオブジェクトが格納されたインスタンス変数です。
ついでに、TextMeshProのGameObjectを取得するコードもStart()関数等に追加します。

using System; //追加する
using System.Collections;
using System.Collections.Generic;
using TMPro; //これも追加する

// 以下の追加する
[Serializable]
public class Question
{
    public string title;
    public string roman;
}
// ここまで

public class TypingManager : MonoBehaviour
{
    // 以下を追加する
    [SerializedField] Question[] _questions = new Question[12]; //お好きな数字をどうぞ

    private TextMeshProUGUI _textMeshProTitle;
    private TextMeshProUGUI _textMeshProRoman;
    // ここまで
    void Start()
    {
        // 以下を追加する
        _textMeshProTitle = GameObject.Find("TextMeshProTitle").GetComponent<TextMeshProUGUI>();
        _textMeshProRoman = GameObject.Find("TextMeshProRoman").GetComponent<TextMeshProUGUI>();
        InitializeQuestion();
        // ここまで
    }
}

表示用のテキストを作る「GenerateRomanText()」関数を実装しよう!

タイピングゲームには当たり前のように、入力後の文字と入力前の文字で色が異なりますので、それを実装するための関数を実装します。

string GenerateRomanText()
{
    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にはタグ機能が存在しており、特定の部分のみスタイルを変更する「」を使用しました。

ただ、このままでは画面上に「」などが表示されてしまいます。なのでUnityエディタ上で「Project Settings」→「TextMeh Pro」の「Settings」→「Default Style Sheet」の「Default Style Sheet (TMP_StyleSheet)」をダブルクリックして、Inspector上で以下の図のようにタグを追加します。
image

こうすることによって、「」は表示されなくなり、タイピング済の文字とそうでない文字のスタイルが変わります。

とりあえず完成だ!

最後にUnityエディタ上の「Hierarchy」から「TypingManager」を選択して、Questionsの値をセットすれば完成です。
(Romanのセットには最短になるようセットします(「chi」→「ti」、「zyo」→「jo」等))
image

これで「Play」を実行しましょう!

柔軟な入力方法に対応させよう!

タイピング機能はうまく動作しましたか?
(動作しなかった場合、自力でソースコード等を修正できますか?🙇‍♂️)

しかし先ほど作成したタイピングゲームは柔軟な入力方法には対応していません。
このままでは、非常に操作性の悪いタイピングゲームとなってしまい、今時リリースできるようなものではありません。

なので本題である柔軟な入力方法に対応させます。

WindowsとMacで異なる入力方法

タイピングの入力方法については、以下のURLが参考になります。

Windows:
https://www.cc.saga-u.ac.jp/system/CenterSystem/ime_romaji.htm

Mac:
https://support.apple.com/ja-jp/guide/japanese-input-method/jpim10277/6.2.1/mac/10.15

よくよく確認すると、WindowsとMacでは入力方法が異なることがわかります。(例:Windowsでは「ca」と入力できるが、Macではできない)
もしもWindowsおよびMac専用のソフトとしてリリースするのであれば、両方に対応する必要はないと思いますが、WebGLでビルドしてunityroomにアップロードする場合、WindowsとMacを区別する必要があります
再び「TypingManager.cs」を編集します。

public class TypingManager : MonoBehaviour
{
    private bool _isWindows; //追加する
    private bool _isMac; //追加する

    void Start()
    {
        // 以下を追加する
        if(SystemInfo.operatingSystem.Contains("Windows"))
        {
            _isWindows = true;
        }

        if(SystemInfo.operatingSystem.Contains("Mac"))
        {
            _isMac = true;
        }
        // ここまで

        InitializeQuestion();
    }
}

柔軟な入力方法に対応するアルゴリズム

現在の日本語入力タイピングにおいて、柔軟な入力方法に対応させるアルゴリズムはいくつか存在します。
今回は「前後比較法(私が勝手に名付けた方法です)」を使います。

「前後比較法」は、今入力すべき文字とその文字の前後を比較することによって「どのひらがなを入力するのか」を判断して、入力してもOKな文字を抽出するアルゴリズムです。

例えば「うちわ(utiwa)」を考えましょう。

まず最初の入力文字が「u」なので、入力するひらがなは「う」であることが判明します。「う」の別入力が「wu」であるので「w」を入力しても構わないことがわかります。

当然ですが、別入力を行いますとローマ字を改良する必要があります。今回の例で「w」を入力するとローマ字表記は「wutiwa」に変化します。

次の入力文字も「u」ですが、前の文字が「w」なので「う」の入力途中であることが判明します。よって今回は「w」を入力することができません。

そして「u」の入力後、入力文字が「t」になります。この入力文字の前が母音で、後の文字が「i」なので「ち」を入力することが判明します。同様に「ち」の別入力は「chi」なので「c」を入力してもOKとなります。

もちろん「c」を入力した後は「wuchiwa」に変形する必要があります。

こんな感じでほかの文字に対応させることができます。

「InputKey()」関数を大規模改造する。

上記のアルゴリズムを用いてInputKey()を改良します。
以下のコードを解説するとかなり長いので省略します。

int InputKey(char inputChar)
{
    char prevChar3 = _romanIndex >= 3 ? _roman[_romanIndex - 3] : '\0';
    char prevChar2 = _romanIndex >= 2 ? _roman[_romanIndex - 2] : '\0';
    char prevChar = _romanIndex >= 1 ? _roman[_romanIndex - 1] : '\0';
    char currentChar = _roman[_romanIndex];
    char nextChar = _roman[_romanIndex + 1];
    char nextChar2 = nextChar == '@' ? '@' : _roman[_romanIndex + 2];

    if (inputChar == '\0')
    {
        return 0;
    }

    if (inputChar == currentChar)
    {
        return 1;
    }

    //「い」の曖昧入力判定(Windowsのみ)
    if (_isWindows && inputChar == 'y' && currentChar == 'i' &&
        (prevChar == '\0' || prevChar == 'a' || prevChar == 'i' || prevChar == 'u' || prevChar == 'e' ||
         prevChar == 'o')) 
    {
        _roman.Insert(_romanIndex, 'y');
        return 2;
    }

    if (_isWindows && inputChar == 'y' && currentChar == 'i' && prevChar == 'n' && prevChar2 == 'n' &&
        prevChar3 != 'n')
    {
        _roman.Insert(_romanIndex, 'y');
        return 2;
    }

    if (_isWindows && inputChar == 'y' && currentChar == 'i' && prevChar == 'n' && prevChar2 == 'x')
    {
        _roman.Insert(_romanIndex, 'y');
        return 2;
    }

    //「う」の曖昧入力判定(「whu」はWindowsのみ)
    if (inputChar == 'w' && currentChar == 'u' && (prevChar == '\0' || prevChar == 'a' || prevChar == 'i' ||
                                                   prevChar == 'u' || prevChar == 'e' || prevChar == 'o'))
    {
        _roman.Insert(_romanIndex, 'w');
        return 2;
    }

    if (inputChar == 'w' && currentChar == 'u' && prevChar == 'n' && prevChar2 == 'n' && prevChar3 != 'n')
    {
        _roman.Insert(_romanIndex, 'w');
        return 2;
    }

    if (inputChar == 'w' && currentChar == 'u' && prevChar == 'n' && prevChar2 == 'x')
    {
        _roman.Insert(_romanIndex, 'w');
        return 2;
    }

    if (_isWindows && inputChar == 'h' && prevChar2 != 't' && prevChar2 != 'd' && prevChar == 'w' &&
        currentChar == 'u') 
    {
        _roman.Insert(_romanIndex, 'h');
        return 2;
    }

    //「か」「く」「こ」の曖昧入力判定(Windowsのみ)
    if (_isWindows && inputChar == 'c' && prevChar != 'k' &&
        currentChar == 'k' && (nextChar == 'a' || nextChar == 'u' || nextChar == 'o'))
    {
        _roman[_romanIndex] = 'c';
        return 2;
    }

    //「く」の曖昧入力判定(Windowsのみ)
    if (_isWindows && inputChar == 'q' && prevChar != 'k' && currentChar == 'k' && nextChar == 'u')
    {
        _roman[_romanIndex] = 'q';
        return 2;
    }

    //「し」の曖昧入力判定
    if (inputChar == 'h' && prevChar == 's' && currentChar == 'i')
    {
        _roman.Insert(_romanIndex, 'h');
        return 2;
    }

    //「じ」の曖昧入力判定
    if (inputChar == 'j' && currentChar == 'z' && nextChar == 'i')
    {
        _roman[_romanIndex] = 'j';
        return 2;
    }

    //「しゃ」「しゅ」「しぇ」「しょ」の曖昧入力判定
    if (inputChar == 'h' && prevChar == 's' && currentChar == 'y')
    {
        _roman[_romanIndex] = 'h';
        return 2;
    }

    //「じゃ」「じゅ」「じぇ」「じょ」の曖昧入力判定
    if (inputChar == 'z' && prevChar != 'j' && currentChar == 'j' &&
        (nextChar == 'a' || nextChar == 'u' || nextChar == 'e' || nextChar == 'o'))
    {
        _roman[_romanIndex] = 'z';
        _roman.Insert(_romanIndex + 1, 'y');
        return 2;
    }

    //「し」「せ」の曖昧入力判定(Windowsのみ)
    if (_isWindows && inputChar == 'c' && prevChar != 's' && currentChar == 's' &&
        (nextChar == 'i' || nextChar == 'e'))
    {
        _roman[_romanIndex] = 'c';
        return 2;
    }

    //「ち」の曖昧入力判定
    if (inputChar == 'c' && prevChar != 't' && currentChar == 't' && nextChar == 'i')
    {
        _roman[_romanIndex] = 'c';
        _roman.Insert(_romanIndex + 1, 'h');
        return 2;
    }

    //「ちゃ」「ちゅ」「ちぇ」「ちょ」の曖昧入力判定
    if (inputChar == 'c' && prevChar != 't' && currentChar == 't' && nextChar == 'y')
    {
        _roman[_romanIndex] = 'c';
        return 2;
    }

    //「cya」=>「cha」
    if (inputChar == 'h' && prevChar == 'c' && currentChar == 'y')
    {
        _roman[_romanIndex] = 'h';
        return 2;
    }

    //「つ」の曖昧入力判定
    if (inputChar == 's' && prevChar == 't' && currentChar == 'u')
    {
        _roman.Insert(_romanIndex, 's');
        return 2;
    }

    //「つぁ」「つぃ」「つぇ」「つぉ」の分解入力判定
    if (inputChar == 'u' && prevChar == 't' && currentChar == 's' &&
        (nextChar == 'a' || nextChar == 'i' || nextChar == 'e' || nextChar == 'o'))
    {
        _roman[_romanIndex] = 'u';
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    if (inputChar == 'u' && prevChar2 == 't' && prevChar == 's' &&
        (currentChar == 'a' || currentChar == 'i' || currentChar == 'e' || currentChar == 'o'))
    {
        _roman.Insert(_romanIndex, 'u');
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    //「てぃ」の分解入力判定
    if (inputChar == 'e' && prevChar == 't' && currentChar == 'h' && nextChar == 'i')
    {
        _roman[_romanIndex] = 'e';
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    //「でぃ」の分解入力判定
    if (inputChar == 'e' && prevChar == 'd' && currentChar == 'h' && nextChar == 'i')
    {
        _roman[_romanIndex] = 'e';
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    //「でゅ」の分解入力判定
    if (inputChar == 'e' && prevChar == 'd' && currentChar == 'h' && nextChar == 'u')
    {
        _roman[_romanIndex] = 'e';
        _roman.Insert(_romanIndex + 1, 'x');
        _roman.Insert(_romanIndex + 2, 'y');
        return 2;
    }

    //「とぅ」の分解入力判定
    if (inputChar == 'o' && prevChar == 't' && currentChar == 'w' && nextChar == 'u')
    {
        _roman[_romanIndex] = 'o';
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    //「どぅ」の分解入力判定
    if (inputChar == 'o' && prevChar == 'd' && currentChar == 'w' && nextChar == 'u')
    {
        _roman[_romanIndex] = 'o';
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    //「ふ」の曖昧入力判定
    if (inputChar == 'f' && currentChar == 'h' && nextChar == 'u')
    {
        _roman[_romanIndex] = 'f';
        return 2;
    }

    //「ふぁ」「ふぃ」「ふぇ」「ふぉ」の分解入力判定(一部Macのみ)
    if (inputChar == 'w' && prevChar == 'f' &&
        (currentChar == 'a' || currentChar == 'i' || currentChar == 'e' || currentChar == 'o'))
    {
        _roman.Insert(_romanIndex,'w');
        return 2;
    }

    if (inputChar == 'y' && prevChar == 'f' && (currentChar == 'i' || currentChar == 'e'))
    { 
        _roman.Insert(_romanIndex,'y');
        return 2;
    }

    if (inputChar == 'h' && prevChar != 'f' && currentChar == 'f' &&
        (nextChar == 'a' || nextChar == 'i' || nextChar == 'e' || nextChar == 'o'))
    {
        if (_isMac)
        {
            _roman[_romanIndex] = 'h';
            _roman.Insert(_romanIndex + 1, 'w');
        }
        else
        {
            _roman[_romanIndex] = 'h';
            _roman.Insert(_romanIndex + 1, 'u');
            _roman.Insert(_romanIndex + 2, 'x');
        }
        return 2;
    }

    if (inputChar == 'u' && prevChar == 'f' &&
        (currentChar == 'a' || currentChar == 'i' || currentChar == 'e' || currentChar == 'o'))
    {
        _roman.Insert(_romanIndex, 'u');
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    if (_isMac && inputChar == 'u' && prevChar == 'h' && currentChar == 'w' &&
        (nextChar == 'a' || nextChar == 'i' || nextChar == 'e' || nextChar == 'o'))
    {
        _roman[_romanIndex] = 'u';
        _roman.Insert(_romanIndex + 1, 'x');
        return 2;
    }

    //「ん」の曖昧入力判定(「n'」には未対応)
    if (inputChar == 'n' && prevChar2 != 'n' && prevChar == 'n' && currentChar != 'a' && currentChar != 'i' &&
        currentChar != 'u' && currentChar != 'e' && currentChar != 'o' && currentChar != 'y') 
    {
        _roman.Insert(_romanIndex, 'n');
        return 2;
    }

    if (inputChar == 'x' && prevChar != 'n' && currentChar == 'n' && nextChar != 'a' && nextChar != 'i' &&
        nextChar != 'u' && nextChar != 'e' && nextChar != 'o' && nextChar != 'y')
    {
        if (nextChar == 'n')
        {
            _roman[_romanIndex] = 'x';
        }
        else
        {
            _roman.Insert(_romanIndex, 'x');
        }

        return 2;
    }

    //「きゃ」「にゃ」などを分解する
    if (inputChar == 'i' && currentChar == 'y' &&
        (prevChar == 'k' || prevChar == 's' || prevChar == 't' || prevChar == 'n' || prevChar == 'h' ||
         prevChar == 'm' || prevChar == 'r' || prevChar == 'g' || prevChar == 'z' || prevChar == 'd' ||
         prevChar == 'b' || prevChar == 'p') &&
        (nextChar == 'a' || nextChar == 'u' || nextChar == 'e' || nextChar == 'o'))
    {
        if (nextChar == 'e')
        {
            _roman[_romanIndex] = 'i';
            _roman.Insert(_romanIndex + 1, 'x');
        }
        else
        {               
            _roman.Insert(_romanIndex, 'i');
            _roman.Insert(_romanIndex + 1, 'x');
        }

        return 2;
    }

    //「しゃ」「ちゃ」などを分解する
    if (inputChar == 'i' &&
        (currentChar == 'a' || currentChar == 'u' || currentChar == 'e' || currentChar == 'o') &&
        (prevChar2 == 's' || prevChar2 == 'c') && prevChar == 'h')
    {
        if (nextChar == 'e')
        {
            _roman.Insert(_romanIndex,'i');
            _roman.Insert(_romanIndex + 1, 'x');
        }
        else
        {
            _roman.Insert(_romanIndex, 'i');
            _roman.Insert(_romanIndex + 1, 'x');
            _roman.Insert(_romanIndex + 2, 'y');
        }

        return 2;
    }

    //「しゃ」を「c」で分解する(Windows限定)
    if (_isWindows && inputChar == 'c' && currentChar == 's' && prevChar != 's' && nextChar == 'y' &&
        (nextChar2 == 'a' || nextChar2 == 'u' || nextChar2 == 'e' || nextChar2 == 'o'))
    {
        if (nextChar2 == 'e')
        {
            _roman[_romanIndex] = 'c';
            _roman[_romanIndex + 1] = 'i';
            _roman.Insert(_romanIndex + 1, 'x');
        }
        else
        {
            _roman[_romanIndex] = 'c';
            _roman.Insert(_romanIndex + 1, 'i');
            _roman.Insert(_romanIndex + 2, 'x');
        }

        return 2;
    }

    //「っ」の分解入力判定
    if ((inputChar == 'x' || inputChar == 'l') &&
        (currentChar == 'k' && nextChar == 'k' || currentChar == 's' && nextChar == 's' ||
         currentChar == 't' && nextChar == 't' || currentChar == 'g' && nextChar == 'g' ||
         currentChar == 'z' && nextChar == 'z' || currentChar == 'j' && nextChar == 'j' ||
         currentChar == 'd' && nextChar == 'd' || currentChar == 'b' && nextChar == 'b' || 
         currentChar == 'p' && nextChar == 'p'))
    {
        _roman[_romanIndex] = inputChar;
        _roman.Insert(_romanIndex + 1, 't');
        _roman.Insert(_romanIndex + 2, 'u');
        return 2;
    }

    //「っか」「っく」「っこ」の特殊入力(Windows限定)
    if (_isWindows && inputChar == 'c' && currentChar == 'k' && nextChar == 'k' &&
        (nextChar2 == 'a' || nextChar2 == 'u' || nextChar2 == 'o'))
    {
        _roman[_romanIndex] = 'c';
        _roman[_romanIndex + 1] = 'c';
        return 2;
    }

    //「っく」の特殊入力(Windows限定)
    if (_isWindows && inputChar == 'q' && currentChar == 'k' && nextChar == 'k' && nextChar2 == 'u')
    {
        _roman[_romanIndex] = 'q';
        _roman[_romanIndex + 1] = 'q';
        return 2;
    }

    //「っし」「っせ」の特殊入力(Windows限定)
    if (_isWindows && inputChar == 'c' && currentChar == 's' && nextChar == 's' &&
    (nextChar2 == 'i' || nextChar2 == 'e'))
    {
        _roman[_romanIndex] = 'c';
        _roman[_romanIndex + 1] = 'c';
        return 2;
    }

    //「っちゃ」「っちゅ」「っちぇ」「っちょ」の曖昧入力判定
    if (inputChar == 'c' && currentChar == 't' && nextChar == 't' && nextChar2 == 'y')
    {
        _roman[_romanIndex] = 'c';
        _roman[_romanIndex + 1] = 'c';
        return 2;
    }

    //「っち」の曖昧入力判定
    if (inputChar == 'c' && currentChar == 't' && nextChar == 't' && nextChar2 == 'i')
    {
        _roman[_romanIndex] = 'c';
        _roman[_romanIndex + 1] = 'c';
        _roman.Insert(_romanIndex + 2, 'h');
        return 2;
    }

    //「l」と「x」の完全互換性
    if (inputChar == 'x' && currentChar == 'l')
    {
        _roman[_romanIndex] = 'x';
        return 2;
    }

    if (inputChar == 'l' && currentChar == 'x')
    {
        _roman[_romanIndex] = 'l';
        return 2;
    }

    return 3;
}

こうすることによって、「し」を「shi」、「ふ」を「fu」と入力できるようになります。
実際、上記のコードは完璧ではありませんが、これでもゲームとして十分だと思います。

さいごに

私は文章下手なので、少々読みにくい点はあったかもしれませんが、それでも、長々とした記事をお読みいただき、誠にありがとうございます。

上記のコードは自由にアレンジしても、そのままコピペしても構いません。
むしろ、それでタイピングゲームの製作に関心を持ってもらえたら幸いです。

サンプル

サンプルはunityroom上にて公開しております。
なお、今回はミスタイプ数などの要素が加わています。

https://unityroom.com/games/typingsample

GitHub

内容は、上記のサンプルのプロジェクトです。

https://github.com/AkioMabuchi/SampleTyping

ツイッターでシェア
みんなに共有、忘れないようにメモ

アキオ

本業は機械系の技術者の趣味プログラマー。Unityで開発された無料のWebGLゲームが遊べる会員登録制のゲームサイト『Atelier144』を運営しています。電子工作やボードゲーム、筋トレも趣味。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント