Unityで作るローマ字入力とかな入力に両対応した多機能なタイピングゲームの制作講座 2回目
こんにちは!ジェイです。
前回に引き続き、Unityタイピングゲーム制作講座やっていきます!
happynetwork2019.hatenablog.com
前回まで説明したこと
前回、配布したunityプロジェクト内に以下の3つスクリプトがあります。ダウンロードはこちら
CTypeEngine
CRomaTypeEngine(CTypeEngineを継承)
CKanaTypeEngine(CTypeEngineを継承)
CRomaTypeEngineとCKanaTypeEngineのどちらかをインスタンス化するかによってローマ字入力とかな入力の処理を切り替えられるのを実践しました。
例
CTypeEngine TypeEngine = new CRomaTypeEngine(); // ローマ字入力
CTypeEngine TypeEngine = new CKanaTypeEngine(); // かな入力
C#の多様性を利用して、インスタンス化した以降に呼ばれるオーバーライドした関数は、そのクラス内の関数が呼ばれることになります。
new CRomaTypeEngine();とした場合は、CRomaTypeEngineクラス内に定義したMakeInputStr()やMakeSearchStrが呼ばれて、new CKanaTypeEngine();とした場合は、CKanaTypeEngineクラス内のMakeInputStr()やMakeSearchStrが呼ばれます。
タイピングゲームを作るにあたって重要な処理は
1.ひらがなからタイピング文章を生成する(MakeInputStr)
2.ひらがなから入力判定するのに必要な文字を作る(MakeSearthStr)
3.キー入力から正誤判定をして正解、間違いそれぞれの処理を行う
4.テキストメッシュに代入して文字を表示させる
前回は、以上の大まかな流れを説明しました。今回は1~3でCTypeEngine内の処理を具体的に説明していきます。
ローマ字入力の場合
まず最初にCRomaTypeEngineをインスタンス化した場合の処理について考えてみましょう。
ひらがなからローマ字を生成する処理(MakeInputStr)
MakeInputStr("であいとわかれのきせつ");と入力すると「deaitowakarenokisetu」と出力されます。
このようにひらがなを見てローマ字の文章を出力するのがMakeInputStr関数の役割です。
では、具体的に処理を見てどうやってローマ字にしているのか見てみます。
結論から言うとひらがな4文字分見て辞書登録してある文字と比べて同じなら、4文字分のひらがなをローマ字に変換して次の文字へ
それでも、もし見つからなかったら3文字分見て見つかったら3文字分のひらがなをローマ字に変換して次の文字へ
それでも、もし見つからなかったら2文字分見て見つかったら3文字分のひらがなをローマ字に変換して次の文字へ
それでも、もし見つからなかったら1文字分見て見つかったら3文字分のひらがなをローマ字に変換して次の文字へ
最期まで見つからなかったらエラーを出力する
という処理を繰り返していけば、すべてのローマ字に対応して変換できます。
更に「ん」や「っ」は特殊な打ち分けができるために正確に処理の流れを説明すると
「ん」が先頭の4文字を調べて見つかったらローマ字に変換してewordに追加、見つからなかったら「ん」が先頭の3文字を調べて見つかったらローマ字に変換してewordに追加というのを最後の1文字「ん」になるまで繰り返します。
「っ」も同じ要領で先頭の4文字を調べて見つかったらローマ字に変換してewordに追加、見つからなかったら「っ」が先頭の3文字を調べて見つかったらローマ字に変換してewordに追加というのを見つからなかった場合のみ1文字「っ」になるまで繰り返します。
ここまでの処理で特殊なローマ字の生成は終わるので、最後は通常の文字と辞書を比較して、同じように見つからなかった場合のみ1文字の「っ」と比較してローマ字をewordに追加します。
>|C#|
public class CRomaTypeEngine : CTypeEngine
{
public override string MakeInputText(string input_textk)
{
string eword = string.Empty;
string subtext = input_textk;
string strbuf;
bool exflg;
int err = 0;
int textlength;
while (!String.IsNullOrEmpty(subtext))
{
if (++err > 700)
{
Debug.Log("SubTextが読み取れません");
return null;
}
textlength = subtext.Length;
exflg = false;
strbuf = subtext.Substring(0, 1);
if (strbuf == "ん" || strbuf == "ン") // 一文字目が「ん」
{
//「ん」「っ」付き4文字
if (!exflg && textlength >= 4)
{
for (int k = 0; k < STNTEX_COL; k++)
{
if (subtext.Substring(0, 4) == Stntex[k, 0])
{
eword += Stntex[k, 1];
// subtextを4文字目から最後まで切り取る(最初の4文字を切り捨てる)
subtext = subtext.Substring(4, textlength - 4);
exflg = true;
break;
}
}
}
//「ん」「っ」付き3文字
if (!exflg && textlength >= 3)
{
for (int k = 0; k < STNT_COL; k++)
{
if (subtext.Substring(0, 3) == Stnt[k, 0])
{
eword += Stnt[k, 1];
// subtextを3文字目から最後まで切り取る(最初の3文字を切り捨てる)
subtext = subtext.Substring(3, textlength - 3);
exflg = true;
break;
}
}
}
//「ん」付き3文字
if (!exflg && textlength >= 3)
{
for (int k = 0; k < STNEX_COL; k++)
{
if (subtext.Substring(0, 3) == Stnex[k, 0])
{
eword += Stnex[k, 1];
// subtextを3文字目から最後まで切り取る(最初の3文字を切り捨てる)
subtext = subtext.Substring(3, textlength - 3);
exflg = true;
break;
}
}
}
//「ん」付き2文字
if (!exflg && textlength >= 2)
{
for (int k = 0; k < STN_COL; k++)
{
if (subtext.Substring(0, 2) == Stn[k, 0])
{
eword += Stn[k, 1];
// subtextを2文字目から最後まで切り取る(最初の2文字を切り捨てる)
subtext = subtext.Substring(2, textlength - 2);
exflg = true;
break;
}
}
}
if (!exflg)
{
eword += "nn";
// subtextを1文字目から最後まで切り取る(最初の1文字を切り捨てる)
subtext = subtext.Substring(1, textlength - 1);
}
}
else if (strbuf == "っ" || strbuf == "ッ") // 一文字目が「っ」
{
//「っ」付き3文字
if (!exflg && textlength >= 3)
{
for (int k = 0; k < STTEX_COL; k++)
{
if (subtext.Substring(0, 3) == Sttex[k, 0])
{
eword += Sttex[k, 1];
// subtextを3文字目から最後まで切り取る(最初の3文字を切り捨てる)
subtext = subtext.Substring(3, textlength - 3);
exflg = true;
break;
}
}
}
//「っ」付き2文字
if (!exflg && textlength >= 2)
{
for (int k = 0; k < STT_COL; k++)
{
if (subtext.Substring(0, 2) == Stt[k, 0])
{
eword += Stt[k, 1];
// subtextを2文字目から最後まで切り取る(最初の2文字を切り捨てる)
subtext = subtext.Substring(2, textlength - 2);
exflg = true;
break;
}
}
}
if (!exflg)
{
eword += "ltu";
// subtextを1文字目から最後まで切り取る(最初の1文字を切り捨てる)
subtext = subtext.Substring(1, textlength - 1);
}
}
else // その他
{
//2文字
if (!exflg && textlength >= 2)
{
for (int k = 0; k < STEX_COL; k++)
{
// subtextを先頭から2文字切り取る
if (subtext.Substring(0, 2) == Stex[k, 0])
{
eword += Stex[k, 1];
// subtextを2文字目から最後まで切り取る(最初の2文字を切り捨てる)
subtext = subtext.Substring(2, textlength - 2);
exflg = true;
break;
}
}
}
//1文字
if (!exflg)
{
for (int k = 0; k < ST_COL; k++)
{
if (subtext.Substring(0, 1) == St[k, 0])
{
if (St[k, 0] == " " || St[k, 0] == " ")
{
eword += " ";
}
else
{
eword += St[k, 1];
}
// subtextを1文字目から最後まで切り取る(最初の1文字を切り捨てる)
subtext = subtext.Substring(1, textlength - 1);
exflg = true;
break;
}
}
}
}
}
NowInputText = eword;
return eword;
}
}
||<
どんなに複雑なローマ字入力でもこのように最初に用意しておいた辞書と比較していけば、ひらがなからローマ字を生成する処理が実行できます。
辞書を登録する処理は長くなるので割愛しますが、CRomaTypeEngineのコンストラクタで単に配列に文字を入れているだけなので、見ていただければわかると思います。
ひらがなから入力判定するのに必要な文字を作る(MakeSearthStr)
先ほどのひらがなからローマ字を生成するMakeInputStr()によく似ていますが、MakeInputStr()はひらがなから1文章を最後までローマ字を生成するのに対して、MakeSearchStr()はひらがなからタイピングした時に入力が正解か間違いか判定するためのローマ字候補を生成する処理を行います。
例
foreach (var str in TypeEngine.MakeSearchStr("であいとわかれのきせつ", 10))
{
Debug.Log(str);
}
出力 tu tsu
第1引数にひらがなの文章を入れて第2引数に何文字目のひらがなを変換するかというのを入れます。
「つ」なら入力候補の「tu」と「tsu」が入って、「か」なら入力候補の「ka」と「ca」がSearchStrsに格納されます。
MakeInputStr()とひらがなの4文字分比較して見つからなかったら、3文字分比較するという処理はほとんど似ています。
>|C#|
public class CRomaTypeEngine : CTypeEngine
{
public override List<string> MakeSearchStr(string input_textk, int str_posk)
{
int i, k;
SearchStrs.Clear();
string subtext;
bool exflg = false;
string strbuf = string.Empty;
List<string> return_str = new List<string>();
return_str.Clear();
strbuf = input_textk.Substring(str_posk, 1);
int restword = input_textk.Length - str_posk;
if (strbuf == "ん" || strbuf == "ン") // 一文字目が「ん」
{
//「ん」「っ」付き4文字 12候補 1-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
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;
SearchStrs.Add(new TagSearchStrings(Stntex[i, k], 4));
return_str.Add(Stntex[i, k]);
}
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;
SearchStrs.Add(new TagSearchStrings(Stnt[i, k], 3));
return_str.Add(Stnt[i, k]);
}
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;
SearchStrs.Add(new TagSearchStrings(Stnex[i, k], 3));
return_str.Add(Stnex[i, k]);
}
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;
SearchStrs.Add(new TagSearchStrings(Stn[i, k], 2));
return_str.Add(Stn[i, k]);
}
exflg = true;
break;
}
}
}
if (!exflg)
{
SearchStrs.Add(new TagSearchStrings("nn", 1));
SearchStrs.Add(new TagSearchStrings("xn", 1));
return_str.Add("nn");
return_str.Add("xn");
}
}
else if (strbuf == "っ" || strbuf == "ッ") // 一文字目が「っ」
{
//「っ」付き3文字 6候補 0-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
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;
SearchStrs.Add(new TagSearchStrings(Sttex[i, k], 3));
return_str.Add(Sttex[i, k]);
}
exflg = true;
break;
}
}
}
//「っ」付き2文字 5候補
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;
SearchStrs.Add(new TagSearchStrings(Stt[i, k], 2));
return_str.Add(Stt[i, k]);
}
exflg = true;
break;
}
}
}
if (!exflg)
{
SearchStrs.Add(new TagSearchStrings("ltu", 1));
SearchStrs.Add(new TagSearchStrings("xtu", 1));
SearchStrs.Add(new TagSearchStrings("ltsu", 1));
return_str.Add("ltu");
return_str.Add("xtu");
return_str.Add("ltsu");
}
}
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;
SearchStrs.Add(new TagSearchStrings(Stex[i, k], 2));
return_str.Add(Stex[i, k]);
}
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;
SearchStrs.Add(new TagSearchStrings(St[i, k], 1));
return_str.Add(St[i, k]);
}
break;
}
}
}
}
// 検索配列のローマ表示テキストにおける先頭文字位置(入力時の文字が表示している例と違う場合の修正用)
foreach (var search_str in SearchStrs)
{
if (!String.IsNullOrEmpty(search_str.SearchStr))
{
// 現在1番手候補の文字長さ保存
EnglishLen = search_str.SearchStr.Length;
break;
}
}
return return_str;
}
||<
MakeInputStr()との違いは、1文章を全部終わりまで処理するのではなくて、1ワード分の入力候補を生成することです。
例
「あ」→「a」
「しゃ」→「sya」「sha」
「ちゃ」→「tya」「cha」「cya」
以上のような感じで辞書から、ローマ字で入力する候補すべてをSearchStrsに格納しています。
このタイピングゲームで一番大事な「ひらがなからローマ字を生成する(MakeInputStr)」と「ひらがなからローマ字入力候補を保存しておく(MakeSearchStr)」の説明が終わりました。
最期に入力した時の判定について考えてみましょう!
入力判定処理
CGameManagerのOnGUIでは以下の様にキーを取得してCTypeEngineクラスのKeyPress関数にキーの情報を渡しています。
するとタイピングが終了したかがbool型で返ってくるので、それに元づいて文字の色を変更して表示してるのは前回説明しました。
>|C#|
public class CGameManager : MonoBehaviour
{
private void OnGUI()
{
Event e = Event.current;
if (e.type == EventType.KeyDown && e.type != EventType.KeyUp && e.keyCode != KeyCode.None
&& e.keyCode != KeyCode.LeftShift && e.keyCode != KeyCode.RightShift)
{
if (TypeEngine.KeyPress(InputText, e, LeftShift || RightShift))
{
ViewTextMesh.text = "終了";
InputTextMesh.text = "";
}
else
{
// 漢字文字を表示させる
ViewTextMesh.text = ViewText[TypeEngine.TextPos];
// すでに打ち終わった文字
string str1 = "<color=#808080>" + TypeEngine.NowInputText.Substring(0, TypeEngine.InputPos) + "</color>";
// これから打つ1文字
string str2 = string.Empty;
if (TypeEngine.TypeMissFlag)
{
str2 = "<color=#FF0000>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos, 1) + "</color>";
}
else
{
str2 = "<color=#0000FF>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos, 1) + "</color>";
}
// これから打つ1文字より後ろの最期の文字まで
string str3 = "<color=#FFFFFF>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos + 1) + "</color>";
// 入力文字に反映させる
InputTextMesh.text = str1 + str2 + str3;
}
}
}
}
||<
今回はKeyPress関数について説明していきます。
この部分が今回のサンプルを作るにあたって一番苦労したところで、Unityでタイピングゲームを作る際にハマるポイントだと思います。
具体的にはInputManagerやInputSystemを使用すると「ー」や「ろ」のキーの判定が上手くできないです。
なので、以下のようにOnGUIでキーイベントを取得して、BackSlashかどうか判定して、キーコードが226なら「ー」でそうでないなら「ろ」でBackSlashでなければ、InputJudgeに渡しています。
>|C#|
public class CTypeEngine
{
private readonly KeyCode[] keyCodes = Enum.GetValues(typeof(KeyCode)).Cast<KeyCode>().ToArray();
public bool KeyPress(string[] input_textk, Event e, bool shift)
{
bool type_end = false;
foreach (KeyCode key_code in keyCodes)
{
if (e.keyCode == key_code)
{
if ((e.keyCode == KeyCode.Backslash && e.functionKey) || e.keyCode.ToString() == "226")
{
// ろ
type_end = InputJudge(input_textk, "ろ", shift);
}
else if (e.keyCode == KeyCode.Backslash)
{
// ー
type_end = InputJudge(input_textk,"ー", shift);
}
else
{
type_end = InputJudge(input_textk, key_code.ToString(), shift);
}
break;
}
}
return type_end;
}
}
||<
このKeyPress()の処理はローマ字入力、かな入力ともに共通してる部分なので基底クラスのCTypeEingeに定義してます。
その後の処理は、ローマ字入力とかな入力では、まったく別の処理をするので、InputJudgeをオーバーライドして、それを実現してます。
>|C#|
public class CRomaTypeEngine : CTypeEngine
{
protected override bool InputJudge(string[] input_textk, string key, bool shift)
{
string temp_str = GetKeyString(key, shift);
if (temp_str == null) return false;
StrBuf += temp_str;
bool agreeflg = false;
foreach (var search_str in SearchStrs)
{
// 完全一致した場合(一文字終了)
if (search_str.SearchStr == StrBuf)
{
if (1 < search_str.SearchStr.Length)
{
// 表示文字と違う場合修正
StrChangedFlag = StrChange(ref NowInputText,
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;
EnglishPos = ++StrPosE;
TypeMissFlag = false;
// 一文終了時
if (NowInputText.Length <= StrPosE)
{
InputPos = 0;
StrPosK = 0; // 入力文(かな)ポジション
StrPosE = 0; // ローマ字文ポジション
EnglishPos = 0;
// 全文終了
if (input_textk.Length <= ++TextPos)
{
TextPos = 0;
return true; // タイプ終了
}
// 次のローマ字文章を作成してInputTextEに代入
NowInputText = MakeInputText(input_textk[TextPos]);
if (NowInputText == null)
{
Debug.Log("InputTextE is null");
return false;
}
}
MakeSearchStr(input_textk[TextPos], StrPosK);
//DrawTextMeshPro(InputTextMesh, NowInputText, StrPosE, ScrollBaseE, ScrollDispMaxE, TypeMissFlag);
InputPos = StrPosE;
return false; // タイプ継続
}
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) // タイプ文字が不正解の時の処理
{
TypeMissFlag = true;
StrBuf = StrBuf.Substring(0, StrBuf.Length - 1); //間違えた一文字をバッファから消す
}
else // タイプ文字が正解の時の処理
{
TypeMissFlag = false;
string str_temp = input_textk[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; }
}
StrChangedFlag = StrChange(
ref NowInputText, ref EnglishLen, StrBuf, SearchStrs, EnglishPos
);
InputPos = ++StrPosE;
}
return false;
}
}
||<
InputJudge()の最初でGetKeyString(key, shift)としてますが、これはシフトを押しているかどうかの情報を1文字だけに詰めるためです。これにより、キーコードとシフトの情報が1つの文字にまとまって使いやすくなります。
その後は、一旦StrBufに格納して、その文字がMakeSearchStr()で保存した検索文字と比較します。
ローマ字入力は、ひらがな1文字で2文字分の判定をしなければならないので、部分的に正解した場合と完全に正解した場合の2パターンの正解判定をします。
「し」の場合は「si」と「shi」のどちらでも入力できるので、例えば、StrBufの1文字目が「s」なら部分的に正解と判定します。
その次の入力した文字が「i」の場合は、SearchStrに「si」と「shi」が入っているので、「si」と完全一致して、正解になります。
もしこれが「h」の場合は、部分正解になって、StrBuf内の文字は「sh」になります。続いて「i」が入力された場合には、完全正解になってStrBufは空文字で初期化されます。
不正解だった場合には、その入力文字だけStrBufから削られて不正解の処理を実行します。
また、ローマ字入力では、複数の打ち方があるので、完全一致で正解した時も表示されてる文字と違っていても正解の場合もあります。その時には、表示されてる文字を入力した文字に変更してから、ひらがなに対応した文字数(1~4文字)進めます。
例えば、完全一致した文字が
「あ」ならひらがなに対応した文字を1文字進める
「しゃ」ならひらがなに対応した文字を2文字進める
「んしゃ」ならひらがなに対応した文字を3文字進める
「んっしゃ」ならひらがなに対応した文字を4文字進める
といった感じです。
次に1つの文章を全部打ち終わったか判定して、終わっていたらTextPosを1加算して次の文章に進めてから、MakeInputText()で次の文章のローマ字を作成して関数を抜けます。
打ち終わってなければ、MakeSearchStr()で次の検索候補をSearchStrsに保存して、関数を抜けます。
基本的には文章が完全に打ち終わるまでこれの繰り返しです。
抑えておくポイントは、正解時には完全一致正解と不完全一致正解があって、そのすべてに当てはまらなければ、不正解の処理を行うというのが一番大事です。
かなり長くなってしまいましたが、CTypeEngine内部の処理とローマ字入力での具体的な例を上げて説明しました。
細かい部分を説明するとまだまだありますが、自分独自のタイピングゲームを作るとしたら、大まかな流れだけわかっていれば作れるので十分だと思います。
ソースコードの気になるところに、F9でブレークポイント仕掛けてF5で実行して、F10のステップオーバーやF11のステップインなどしてみたり、Debug.Logで出力して結果を見てみると理解が深まるので、ぜひやってみてください!
デバッグのやり方【Unity初心者入門講座】【ゲームの作り方】#31 - YouTube
次回は、かな入力での説明をしてから、最後には完成版のプロジェクトを配布して、それを元にオリジナルのタイピングゲームを作るために必要な知識を解説していきます。
それではまた次の講座でお会いしましょう~!