アマゾンバナーリンク

.Net Frameworkでのタイピングゲームの作り方(ローマ字入力対応編)

2020年1月21日

前回で最低限のタイピングできる状況にしました。
今回はローマ字入力ができるところまでやります。

ローマ字入力に対応させるには

まず実装しなければならない機能として、かな文字をローマ字に変換する必要があります。
これについては、色々なやり方がありますが、私自身が実装した中で一番良い方法を紹介します。
結論から言うと、最大4文字分かな文字を見れば、どんなローマ字にも対応できます。

かな文字をローマ字に変換する

まず文字変換するための辞書をstring型の二次元配列で定義する

「ん」から始まる場合

・その辞書を4文字分かな文字を見て該当する文字がないかチェック
・なければ、3文字分かな文字を見て該当する文字がないかチェック
・それでもなければ、2文字分かな文字を見て該当する文字がないかチェック
・最後は1文字分かな文字を見て該当する文字がないかチェック
・すべてなければエラー

・以上の処理で見つけられたかな文字を二次元配列に格納しているローマ字を代入する
・該当した文字数分だけ、元のかな文字からその文字を文字数文だけ抜き取って消去をかな文字すべてがなくなるまで繰り返す

「っ」から始まる場合

・その辞書を3文字分かな文字を見て該当する文字がないかチェック
・なければ、2文字分かな文字を見て該当する文字がないかチェック
・最後は1文字分かな文字を見て該当する文字がないかチェック
・すべてなければエラー

・以上の処理で見つけられたかな文字を二次元配列に格納しているローマ字を代入する
・該当した文字数分だけ、元のかな文字からその文字を文字数文だけ抜き取って消去

「ん」か「っ」で始まる以外の場合

・その辞書を2文字分かな文字を見て該当する文字がないかチェック
・なければ、1文字分かな文字を見て該当する文字がないかチェック
・すべてなければエラー

・以上の処理で見つけられたかな文字を二次元配列に格納しているローマ字を代入する
・該当した文字数分だけ、元のかな文字からその文字を文字数文だけ抜き取って消去

という手順でかな文字をローマ字に変換できます。
実際のコードでは、SetInputStr()という関数で以上の処理が行っています。

CRomaTyping.csのSetInputStr関数

public void SetSearchStr()
{
    string input_textk = InputTextk[TextPos];
    List<TagSearchStrings> search_strs = SearchStrs;
    int i, k;
    search_strs.Clear();
    string subtext;
    bool exflg = false;
    string strbuf = string.Empty;

    while (true)
    {
        strbuf = InputTextk[TextPos].Substring(StrPosK, 1);
        if (strbuf == " " || strbuf == " ")
        {
            // " "である分だけ進める
            ++StrPosK;
            ++StrPosA;
            ++StrPosE;
            ++EnglishPos;
            //AddWeakWordFlag(-1);
        }
        else
        {
            break;
        }
    }
    int str_posk = StrPosK;
    int restword = input_textk.Length - str_posk;
    if (strbuf == "ん" || strbuf == "ン") // 一文字目が「ん」
    {
        //「ん」「っ」付き4文字
        if (!exflg && restword >= 4)
        {
            subtext = input_textk.Substring(str_posk, 4);
            for (i = 0; i < STNTEX_COL; i++)
            {
                if (subtext == Stntex[i, 0])
                {
                    for (k = 1; k < STNTEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stntex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stntex[i, k], 4));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「ん」「っ」付き3文字 10候補
        if (!exflg && restword >= 3)
        {
            subtext = input_textk.Substring(str_posk, 3);
            for (i = 0; i < STNT_COL; i++)
            {
                if (subtext == Stnt[i, 0])
                {
                    for (k = 1; k < STNT_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stnt[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stnt[i, k], 3));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「ん」付き3文字 6候補
        if (!exflg && restword >= 3)
        {
            subtext = input_textk.Substring(str_posk, 3);
            for (i = 0; i < STNEX_COL; i++)
            {
                if (subtext == Stnex[i, 0])
                {
                    for (k = 1; k < STNEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stntex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stnex[i, k], 3));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「ん」付き2文字 8候補
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STN_COL; i++)
            {
                if (subtext == Stn[i, 0])
                {
                    for (k = 1; k < STN_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stn[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stn[i, k], 2));
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            search_strs.Add(new TagSearchStrings("nn", 1));
            search_strs.Add(new TagSearchStrings("xn", 1));
        }
    }
    else if (strbuf == "っ" || strbuf == "ッ") // 一文字目が「っ」
    {
        //「っ」付き3文字
        if (!exflg && restword >= 3)
        {
            subtext = input_textk.Substring(str_posk, 3);
            for (i = 0; i < STTEX_COL; i++)
            {
                if (subtext == Sttex[i, 0])
                {
                    for (k = 1; k < STTEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Sttex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Sttex[i, k], 3));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「っ」付き2文字
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STT_COL; i++)
            {
                if (subtext == Stt[i, 0])
                {
                    for (k = 1; k < STT_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stt[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stt[i, k], 2));
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            search_strs.Add(new TagSearchStrings("ltu", 1));
            search_strs.Add(new TagSearchStrings("xtu", 1));
            search_strs.Add(new TagSearchStrings("ltsu", 1));
        }
    }
    else  // その他
    {
        //2文字 5候補  0-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STEX_COL; i++)
            {
                if (subtext == Stex[i, 0])
                {
                    for (k = 1; k < STEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stex[i, k], 2));
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            subtext = input_textk.Substring(str_posk, 1);
            for (i = 0; i < ST_COL; i++)
            {
                if (subtext == St[i, 0])
                {
                    for (k = 1; k < ST_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(St[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(St[i, k], 1));
                    }
                    break;
                }
            }
        }
    }

    // 検索配列のローマ表示テキストにおける先頭文字位置(入力時の文字が表示している例と違う場合の修正用)
    foreach (var search_str in search_strs)
    {
        if (!String.IsNullOrEmpty(search_str.SearchStr))
        {
            // 現在1番手候補の文字長さ保存
            EnglishLen = search_str.SearchStr.Length;
            break;
        }
    }
}

ここまでのプロジェクトはこちらからダウンロードできます。

入力可能なローマ字をすべて格納する関数を作る

SetInputStr関数は、かな文字を1つのワードを最後まで、ローマ字に変換する機能をもつ関数ですが、実際にローマ字の入力を判定するのには、入力候補すべてのローマ字が必要になります。それを実装しているのが、SetSearchStr関数です。この関数でsearch_strsに入力候補を格納し、入力があった時に、KeyPressFunc関数で入力候補のうちのどれかに当てはまっていれば正解という処理にします。

CRomaTyping.csのSetSearchStr関数

public void SetSearchStr()
{
    string input_textk = InputTextk[TextPos];
    List<TagSearchStrings> search_strs = SearchStrs;
    int i, k;
    search_strs.Clear();
    string subtext;
    bool exflg = false;
    string strbuf = string.Empty;

    while (true)
    {
        strbuf = InputTextk[TextPos].Substring(StrPosK, 1);
        if (strbuf == " " || strbuf == " ")
        {
            // " "である分だけ進める
            ++StrPosK;
            ++StrPosA;
            ++StrPosE;
            ++EnglishPos;
            //AddWeakWordFlag(-1);
        }
        else
        {
            break;
        }
    }
    int str_posk = StrPosK;
    int restword = input_textk.Length - str_posk;
    if (strbuf == "ん" || strbuf == "ン") // 一文字目が「ん」
    {
        //「ん」「っ」付き4文字
        if (!exflg && restword >= 4)
        {
            subtext = input_textk.Substring(str_posk, 4);
            for (i = 0; i < STNTEX_COL; i++)
            {
                if (subtext == Stntex[i, 0])
                {
                    for (k = 1; k < STNTEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stntex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stntex[i, k], 4));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「ん」「っ」付き3文字 10候補
        if (!exflg && restword >= 3)
        {
            subtext = input_textk.Substring(str_posk, 3);
            for (i = 0; i < STNT_COL; i++)
            {
                if (subtext == Stnt[i, 0])
                {
                    for (k = 1; k < STNT_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stnt[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stnt[i, k], 3));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「ん」付き3文字 6候補
        if (!exflg && restword >= 3)
        {
            subtext = input_textk.Substring(str_posk, 3);
            for (i = 0; i < STNEX_COL; i++)
            {
                if (subtext == Stnex[i, 0])
                {
                    for (k = 1; k < STNEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stntex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stnex[i, k], 3));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「ん」付き2文字 8候補
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STN_COL; i++)
            {
                if (subtext == Stn[i, 0])
                {
                    for (k = 1; k < STN_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stn[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stn[i, k], 2));
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            search_strs.Add(new TagSearchStrings("nn", 1));
            search_strs.Add(new TagSearchStrings("xn", 1));
        }
    }
    else if (strbuf == "っ" || strbuf == "ッ") // 一文字目が「っ」
    {
        //「っ」付き3文字
        if (!exflg && restword >= 3)
        {
            subtext = input_textk.Substring(str_posk, 3);
            for (i = 0; i < STTEX_COL; i++)
            {
                if (subtext == Sttex[i, 0])
                {
                    for (k = 1; k < STTEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Sttex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Sttex[i, k], 3));
                    }
                    exflg = true;
                    break;
                }
            }
        }

        //「っ」付き2文字
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STT_COL; i++)
            {
                if (subtext == Stt[i, 0])
                {
                    for (k = 1; k < STT_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stt[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stt[i, k], 2));
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            search_strs.Add(new TagSearchStrings("ltu", 1));
            search_strs.Add(new TagSearchStrings("xtu", 1));
            search_strs.Add(new TagSearchStrings("ltsu", 1));
        }
    }
    else  // その他
    {
        //2文字 5候補  0-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STEX_COL; i++)
            {
                if (subtext == Stex[i, 0])
                {
                    for (k = 1; k < STEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stex[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(Stex[i, k], 2));
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            subtext = input_textk.Substring(str_posk, 1);
            for (i = 0; i < ST_COL; i++)
            {
                if (subtext == St[i, 0])
                {
                    for (k = 1; k < ST_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(St[i, k])) continue;
                        search_strs.Add(new TagSearchStrings(St[i, k], 1));
                    }
                    break;
                }
            }
        }
    }

    // 検索配列のローマ表示テキストにおける先頭文字位置(入力時の文字が表示している例と違う場合の修正用)
    foreach (var search_str in search_strs)
    {
        if (!String.IsNullOrEmpty(search_str.SearchStr))
        {
            // 現在1番手候補の文字長さ保存
            EnglishLen = search_str.SearchStr.Length;
            break;
        }
    }
}

ポリモーフィズム(多様性)で、かな入力とローマ字入力を切り替える

CTypingクラスの派生クラスでCRomaTypingクラスを作りました。
これは後々に、かな入力にも対応する時に派生クラスにCKanaTypingクラスを作り、ユーザーに選択してもらった時点で、どちらかの入力方法に対応したクラスをインスタンス化します。

具体的に多様性を使ってる場所はKeypressFuncという関数です
この関数を基底クラスのCTypingクラスに定義して、派生クラスのCRomaTypiingとCKanaTypingで上書きすれば、同じKeypressFuncという関数でもインスタンス化したクラスによって、異なった処理を実行することができます。このように基底クラスで定義した関数を上書きして、派生クラスで定義しなおすことをオーバーライドといいます。

以下がオーバーライドしたKeyPressFunc関数です。

public override void KeypressFunc(string input_str)
{
    System.StringSplitOptions option = System.StringSplitOptions.RemoveEmptyEntries;
    string[] lines = input_str.Split(new char[] { ',' }, option);
    //シフトを押してるなら
    if (lines.Length == 2)
    {
        ShiftFlag = true;
    }

    // シフトキーしか押してない時は無効
    if (lines[0] == "ShiftKey")
    {
        return;
    }
    // 文字の保存
    string temp_str = GetKeyString(lines[0], ShiftFlag);
    if (temp_str == null) return;
    StrBuf += temp_str;
    bool agreeflg = false;
    foreach (var search_str in SearchStrs)
    {
        // 完全一致した場合(一文字終了)
        if (search_str.SearchStr == StrBuf)
        {
            if (1 < search_str.SearchStr.Length)
            {
                // 表示文字と違う場合修正
                StrChange(ref InputTextE,
                    ref EnglishLen, StrBuf, SearchStrs, EnglishPos);
            }
        
            StrBuf = string.Empty;
            // 4文字適合
            if (search_str.SearchNum == 4)
            {
                StrPosK += 4;
            }
            // 3文字適合
            else if (search_str.SearchNum == 3)
            {
                StrPosK += 3;
            }
            // 2文字適合
            else if (search_str.SearchNum == 2)
            {
                StrPosK += 2;
            }
            else // 1文字適合
            {
                ++StrPosK;
            }
            StrPosK -= KanaPos; // かな表示の途中経過分を差し引く
            KanaPos = 0;
            StrPosA = StrPosK;
            EnglishPos = ++StrPosE;
            // 一文終了時
            if (InputTextE.Length <= StrPosE)
            {
                StrPosK = 0; // 入力文(かな)ポジション
                StrPosA = 0; // 基本文ポジション
                StrPosE = 0; // ローマ字文ポジション
                EnglishPos = 0;

                // 全文終了時
                if (InputTextk.Count <= ++TextPos)
                {
                    // 打ち切り終了
                    TypeEnd();
                    return;
                }

                if (!SetInputStr(ref InputTextE, InputTextk[TextPos]))
                {
                    MessageBox.Show("setInputStr失敗");
                    return;
                }
            }
            SetSearchStr();
            DrawSearchStr(Form1.Label1);
            ViewRichTextBox.Text = ViewText[TextPos];
            InputRichTextBox.Text = InputTextE.Substring(StrPosE);
            return;
        }
        else if (!string.IsNullOrEmpty(search_str.SearchStr)) // SearchStrが空かどうか調べる
        {
            if (StrBuf.Length <= search_str.SearchStr.Length) // 文字を切り取りすぎないための処理
            {
                if (search_str.SearchStr.Substring(0, StrBuf.Length) == StrBuf)
                {
                    agreeflg = true;
                    break;
                }
            }
        }
    }

    // まだ完全に一致してない場合のみここを通る(完全一致の場合は上でreturnされるのでここは通らない
    // 合致していなければバッファから消す
    if (agreeflg) // タイプ文字が正解の時の処理
    {
        string str_temp = InputTextk[TextPos].Substring(StrPosK, 1);
        if (StrBuf == "ltu" || StrBuf == "xtu")
        {
            if (str_temp == "っ" || str_temp == "ッ") { StrPosK++; KanaPos = 1; }
        }
        else if (StrBuf == "nn")
        {
            if (str_temp == "ん" || str_temp == "ン") { StrPosK++; KanaPos = 1; }
        }
        StrPosA = StrPosK;
        StrChange(
            ref InputTextE, ref EnglishLen, StrBuf, SearchStrs, EnglishPos
        );
        ++StrPosE;
    }
    else // タイプミスした時
    {
        StrBuf = StrBuf.Substring(0, StrBuf.Length - 1); //間違えた一文字をバッファから消す
    }
    InputRichTextBox.Text = InputTextE.Substring(StrPosE);
}

まずキーが押されるとKeyPressFuncが呼ばれ今押したキーをtemp_strに格納します。
それをStrBufに文字を足しミスしたら足した文字だけ消します。search_str.SearchStrには正解のローマ字がすべて格納されています。

search_str.SearchStrに入ってるローマ字

例えば「し」だとshi,siの二種類が入ってます。

一文字めだけでは完全な一致判定はできないので、もし1文字目だけはあっていた場合は89行目の
if (search_str.SearchStr.Substring(0, StrBuf.Length) == StrBuf)がtrueになるので91行目のagreeflgがtrueになります。その後、文字列の調整を行いStrPosEをインクリメントし、次の文字へ判定します。正解であればStrBufに1文字ずつローマ字が増えますが、不正解だとStrBufから不正解の文字を取り除きます。

これを繰り返していると、24行目のif (search_str.SearchStr == StrBuf)がいつかtrueになるので、完全一致した場合(一文字終了)の中身の処理を実行します。

StrChangeはローマ字入力には複数の打ち方があるので、表示と違う打ち方をした時に修正する機能があります。そして1ワードを打ち終わるとInputTextE.Length <= StrPosEがtrueになるので、次のワードを出題する準備をします。66行目のInputTextE.Length <= StrPosEがtrueになったなら、全文打ち切りで終了処理をします。

実行結果

今回のプロジェクトのダウンロードする。
次回は表示文字の色を変えるのとスクロール処理やかな入力への対応について説明します。