tag:crieit.net,2005:https://crieit.net/users/AkioMabuchi/feed アキオの投稿 - Crieit Crieitでユーザーアキオによる最近の投稿 2020-12-10T08:30:04+09:00 https://crieit.net/users/AkioMabuchi/feed tag:crieit.net,2005:PublicArticle/16261 2020-12-05T16:08:03+09:00 2020-12-10T08:30:04+09:00 https://crieit.net/posts/dc0eb0f0a74f6e15e362e1f384d28f0d 柔軟な入力方法に対応したタイピングゲームの作り方 <p>この記事は<a target="_blank" rel="nofollow noopener" href="https://adventar.org/calendars/5226" target="_blank">「Unityゲーム開発者ギルド2 Advent Calender 2020」</a>12日目の記事です。</p> <h2 id="まえがき"><a href="#%E3%81%BE%E3%81%88%E3%81%8C%E3%81%8D">まえがき</a></h2> <p>おはようございます。こんにちは。こんばんは。そしてはじめまして。アキオと申します。ちょうど記事を公開した12月12日が私の誕生日でして、今回で26歳になりました。この26歳の間はとにかくUnityとRailsを触って、様々なゲームやサービスを多数公開運営したいと思います。</p> <p>早速質問ですが、皆様はタイピングゲームはお好きでしょうか?</p> <p>最近はPC離れも加速してモバイルゲームが主流となった今、タイピングゲームは減少傾向です。けれでもPCメインのunityroomは、同じPCメインのタイピングゲームとの相性は抜群です。特にunity1weekに毎回参加している人々は、タイピングゲームを作ってみたいとは思ったことがあるはずです。</p> <p>けれどもunity1weekで300~500近い作品が公開される中でタイピングゲームは多くて3作品しかありません。全体では8000近いゲームがunity1weekで公開されていますが。タイピングゲームは1%しか存在しません。</p> <p>では、なぜ相性抜群のタイピングゲームがunityroomに少ないかというと<br /> 「実力差が大きい」「ありがちなゲームになる」という理由もあるかと思いますが、何よりも「作るのが難しい」のではないかと私は思います。</p> <p>というのも現在ネット上に公開されている日本語入力のタイピングゲームのほぼ全てが、「し」を「shi」、「つ」を「tsu」と入力できます。</p> <p>そのような柔軟な入力方法を実装するのが難しいから、タイピングゲームが少ないのではないかと私は思います。</p> <p>逆を言えば、その柔軟な入力方法に対応していないタイピングゲームの作成は簡単だと私は思います。けれどもそれでは「やりにくい」「今時のタイピングゲームではない」という感想を抱えて、作るのをためらうのではないでしょうか?</p> <p>なので、今回はunityroom等にタイピングゲームを増やすべく、その柔軟な入力方法に対応したタイピングゲームの作り方を紹介したいと思います。</p> <p>対象読者は、ある程度Unityが触れて、TextMeshPro等などが使えることを想定しています。なのでちょっと説明が雑かもしれませんが、ご了承願います。</p> <h2 id="とりあえずUnityを起動してタイピングゲームを作ろう!"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9AUnity%E3%82%92%E8%B5%B7%E5%8B%95%E3%81%97%E3%81%A6%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%92%E4%BD%9C%E3%82%8D%E3%81%86%EF%BC%81">とりあえずUnityを起動してタイピングゲームを作ろう!</a></h2> <p>まず普段通りにプロジェクトを作成します。(2Dでも3Dでも可能)今回はTextMeshProを使いますので、TextMeshProをインポートして、TextMeshProのGameObject「TextMeshProTitle」と「TextMeshProRoman」のオブジェクトを作成します。</p> <p>そして「TypingManager.cs」を作成します。そして空のGameObject「TypingManager」をHierarchyにアタッチして、そのGameObjectに先ほど作成した「TypingManager.cs」をInspector上でアタッチします。</p> <p>ここからは、延々と「TypingManager.cs」にコードを書きます。</p> <h3 id="タイピングゲームの要となる「OnGUI()」を定義しよう!"><a href="#%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%82%B2%E3%83%BC%E3%83%A0%E3%81%AE%E8%A6%81%E3%81%A8%E3%81%AA%E3%82%8B%E3%80%8COnGUI%28%29%E3%80%8D%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%97%E3%82%88%E3%81%86%EF%BC%81">タイピングゲームの要となる「OnGUI()」を定義しよう!</a></h3> <p>Unityではスクリプトの生成時に最初から存在する<code>Update()</code>というイベント関数が存在しますが、これは毎フレーム呼び出される関数なので、1フレームあたりに2回以上の入力が発生すると、2回目以降のキー入力が検知できないので、基本使うべきではありません。<br /> なので今回は、Unityでは標準で<code>OnGUI()</code>というイベント関数を使います。この関数はキーボードやマウスのクリックならびにクリック解除の時に呼び出されます。今回はあくまでもキーの入力時のみに処理を行いたいので<code>if</code>を使って以下のように書きます。</p> <pre><code class="csharp">private void OnGUI() { if (Event.current.type == EventType.KeyDown) { // キーが入力された時に処理される。 } } </code></pre> <p><code>OnGUI()</code>関数に<code>Event.current.type == EventType.KeyDown</code>という条件式を書けば、キーの入力時のみ<code>true</code>になり、処理が実行されるようになります。</p> <p>もう早速ですが、タイピングゲームに使われる<code>OnGUI()</code>の中身を全部書きましょう。以下のようになります。</p> <pre><code class="csharp">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; } } } </code></pre> <p>当然ですが<code>InputKey()</code>や<code>GetCharFromKeyCode()</code>、<code>InitializeQuestion()</code>、<code>GenerateRomanText()</code>ならびに<code>_roman[]</code>、 <code>_romanIndex</code>、 <code>_textMeshProRoman</code>は未定義ですので、このままでは動作しません。なのでそれぞれのメソッドや変数の解説をしながら実装したいと思います。</p> <h3 id="KeyCodeをcharに変換する「GetCharFromKeyCode()」関数を実装しよう!"><a href="#KeyCode%E3%82%92char%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B%E3%80%8CGetCharFromKeyCode%28%29%E3%80%8D%E9%96%A2%E6%95%B0%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%82%88%E3%81%86%EF%BC%81">KeyCodeをcharに変換する「GetCharFromKeyCode()」関数を実装しよう!</a></h3> <p>キーが入力され<code>OnGUI()</code>が実行されると<code>Event.current.keyCode</code>に入力されたキーコードが格納されます。型は<code>KeyCode</code>です。<br /> この型はタイピングゲームのアルゴリズム実装には向いていないので、<code>KeyCode</code>を<code>char</code>に変換する関数<code>GetCharFromKeyCode()</code>を実装しましょう。<br /> 以下のようになります。今回はShift入力は省略しました。</p> <pre><code class="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: return '\0'; } } </code></pre> <p>(誰かこのコードを簡潔にできるなら教えて)</p> <p>ひどいコードですね(笑)でも私の技術力ではこうなってしまいました。こうやって、<code>KeyCode</code>を<code>char</code>に変換しているわけです。今回はShiftキーによる大文字入力には対応していません。Functionキーなどには入力対応しておらず、それらが入力された場合にも<code>OnGUI()</code>が実行され<code>Event.current.keyCode</code>に格納され、上記の関数が実行されますが、その場合はnull文字<code>\0</code>を返しています。</p> <h3 id="「InputKey()」を実装しよう!"><a href="#%E3%80%8CInputKey%28%29%E3%80%8D%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%82%88%E3%81%86%EF%BC%81">「InputKey()」を実装しよう!</a></h3> <p>さて、<code>OnGUI()</code>関数を確認すると、<code>GetCharFromKeyCode()</code>の<code>char</code>の返り値が、<code>InputKey()</code>の引数となっているわけですが、この関数は入力が正しいか否かを判断します。この関数の返り値は<code>int</code>で、正しい入力があれば<code>1</code>を、ミスタイプであれば<code>3</code>を返し、null文字ならば<code>0</code>を返します。</p> <pre><code class="csharp">int InputKey(char inputChar) { char currentChar = _roman[_romanIndex]; if(inputChar == '\0') { return 0; } if(inputChar == currentChar) { return 1; } return 3; } </code></pre> <p>返り値の<code>2</code>については、後ほどの「柔軟な入力方法」によって解説します。</p> <p>ところで「<code>_roman</code>とか<code>_romanIndex</code>って何?」と思った方、今から解説します。</p> <h3 id="タイピングの状態を格納するインスタンス変数を作成しよう!"><a href="#%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%81%AE%E7%8A%B6%E6%85%8B%E3%82%92%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E5%A4%89%E6%95%B0%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%82%88%E3%81%86%EF%BC%81">タイピングの状態を格納するインスタンス変数を作成しよう!</a></h3> <p>タイピングゲームを実装するからには、当然タイピング用の状態を格納する変数が必要になります。<br /> なので今回は<code>_roman</code>と<code>romanIndex</code>のインスタンス変数を定義しましょう。</p> <pre><code class="csharp">public class TypingManager : MonoBehaviour { private List<char> _roman = new List<char>(); private int _romanIndex = 0; } </code></pre> <p><code>_roman</code>はタイピングの処理に用いられる<code>List<char></code>のインスタンス変数で、頻繁に<code>Add()</code>や<code>Clear()</code>を用いますので、<code>List<T></code>となっております。<br /> そして<code>_romanIndex</code>は<code>_roman</code>の参照に用いられるだけの<code>int</code>型のインスタンス変数です。</p> <h3 id="問題を初期化する「InitializeQuestion()」関数を実装しよう!"><a href="#%E5%95%8F%E9%A1%8C%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%99%E3%82%8B%E3%80%8CInitializeQuestion%28%29%E3%80%8D%E9%96%A2%E6%95%B0%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%82%88%E3%81%86%EF%BC%81">問題を初期化する「InitializeQuestion()」関数を実装しよう!</a></h3> <p>どんなタイピングゲームにも「問題の初期化」というのは必ず存在しますので、それを行うための関数<code>InitializeQuestion()</code>を実装します。</p> <pre><code class="csharp">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(); } </code></pre> <p><code>Clear()</code>メソッドで<code>_roman</code>の中身を空にし、その後<code>question</code>の<code>roman</code>プロパティ(<code>string</code>型)を<code>ToCharArray()</code>メソッドで<code>char</code>型の配列に変換して、<code>foreach</code>で<code>_roman</code>に次から次へと<code>Add()</code>メソッドで追加します。</p> <p>そして<code>_roman</code>の最後に<code>@</code>を追加します。この<code>@</code>が「タイピングの終わり」であることを示します。</p> <p>ところで<code>Question</code>が登場しました。タイピングゲームにはタイピング用の文字列のリストが必要になります。</p> <p>なので<code>Question</code>クラスを作成して、<code>[SerializeField]</code>でインスペクタ上から文字列を編集できるようにします。</p> <p><code>_textMeshProTitle</code>や<code>_textMeshProRoman</code>は、画面に表示するためのTextMeshProのオブジェクトが格納されたインスタンス変数です。<br /> ついでに、TextMeshProのGameObjectを取得するコードも<code>Start()</code>関数等に追加します。</p> <pre><code class="csharp">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(); // ここまで } } </code></pre> <h3 id="表示用のテキストを作る「GenerateRomanText()」関数を実装しよう!"><a href="#%E8%A1%A8%E7%A4%BA%E7%94%A8%E3%81%AE%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B%E3%80%8CGenerateRomanText%28%29%E3%80%8D%E9%96%A2%E6%95%B0%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%82%88%E3%81%86%EF%BC%81">表示用のテキストを作る「GenerateRomanText()」関数を実装しよう!</a></h3> <p>タイピングゲームには当たり前のように、入力後の文字と入力前の文字で色が異なりますので、それを実装するための関数を実装します。</p> <pre><code class="csharp">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; } </code></pre> <p>TextMeshProにはタグ機能が存在しており、特定の部分のみスタイルを変更する「」を使用しました。</p> <p>ただ、このままでは画面上に「」などが表示されてしまいます。なのでUnityエディタ上で「Project Settings」→「TextMeh Pro」の「Settings」→「Default Style Sheet」の「Default Style Sheet (TMP_StyleSheet)」をダブルクリックして、Inspector上で以下の図のようにタグを追加します。<br /> <a href="https://crieit.now.sh/upload_images/e215fa1cf26a117976f2c709592c0e015fcc2305f0a7c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e215fa1cf26a117976f2c709592c0e015fcc2305f0a7c.png?mw=700" alt="image" /></a></p> <p>こうすることによって、「」は表示されなくなり、タイピング済の文字とそうでない文字のスタイルが変わります。</p> <h3 id="とりあえず完成だ!"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E5%AE%8C%E6%88%90%E3%81%A0%EF%BC%81">とりあえず完成だ!</a></h3> <p>最後にUnityエディタ上の「Hierarchy」から「TypingManager」を選択して、Questionsの値をセットすれば完成です。<br /> (Romanのセットには最短になるようセットします(「chi」→「ti」、「zyo」→「jo」等))<br /> <a href="https://crieit.now.sh/upload_images/aa91cb22d34f84c15cd717790448cf615fcc24c3406f5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aa91cb22d34f84c15cd717790448cf615fcc24c3406f5.png?mw=700" alt="image" /></a></p> <p>これで「Play」を実行しましょう!</p> <h2 id="柔軟な入力方法に対応させよう!"><a href="#%E6%9F%94%E8%BB%9F%E3%81%AA%E5%85%A5%E5%8A%9B%E6%96%B9%E6%B3%95%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%95%E3%81%9B%E3%82%88%E3%81%86%EF%BC%81">柔軟な入力方法に対応させよう!</a></h2> <p>タイピング機能はうまく動作しましたか?<br /> (動作しなかった場合、自力でソースコード等を修正できますか?🙇‍♂️)</p> <p>しかし先ほど作成したタイピングゲームは柔軟な入力方法には対応していません。<br /> このままでは、非常に操作性の悪いタイピングゲームとなってしまい、今時リリースできるようなものではありません。</p> <p>なので本題である柔軟な入力方法に対応させます。</p> <h3 id="WindowsとMacで異なる入力方法"><a href="#Windows%E3%81%A8Mac%E3%81%A7%E7%95%B0%E3%81%AA%E3%82%8B%E5%85%A5%E5%8A%9B%E6%96%B9%E6%B3%95">WindowsとMacで異なる入力方法</a></h3> <p>タイピングの入力方法については、以下のURLが参考になります。</p> <p>Windows:<br /> <a target="_blank" rel="nofollow noopener" href="https://www.cc.saga-u.ac.jp/system/CenterSystem/ime_romaji.htm">https://www.cc.saga-u.ac.jp/system/CenterSystem/ime_romaji.htm</a></p> <p>Mac:<br /> <a target="_blank" rel="nofollow noopener" href="https://support.apple.com/ja-jp/guide/japanese-input-method/jpim10277/6.2.1/mac/10.15">https://support.apple.com/ja-jp/guide/japanese-input-method/jpim10277/6.2.1/mac/10.15</a></p> <p>よくよく確認すると、WindowsとMacでは入力方法が異なることがわかります。(例:Windowsでは「ca」と入力できるが、Macではできない)<br /> もしもWindowsおよびMac専用のソフトとしてリリースするのであれば、両方に対応する必要はないと思いますが、WebGLでビルドしてunityroomにアップロードする場合、WindowsとMacを区別する必要があります<br /> 再び「TypingManager.cs」を編集します。</p> <pre><code class="csharp">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(); } } </code></pre> <h3 id="柔軟な入力方法に対応するアルゴリズム"><a href="#%E6%9F%94%E8%BB%9F%E3%81%AA%E5%85%A5%E5%8A%9B%E6%96%B9%E6%B3%95%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0">柔軟な入力方法に対応するアルゴリズム</a></h3> <p>現在の日本語入力タイピングにおいて、柔軟な入力方法に対応させるアルゴリズムはいくつか存在します。<br /> 今回は「前後比較法(私が勝手に名付けた方法です)」を使います。</p> <p>「前後比較法」は、今入力すべき文字とその文字の前後を比較することによって「どのひらがなを入力するのか」を判断して、入力してもOKな文字を抽出するアルゴリズムです。</p> <p>例えば「うちわ(utiwa)」を考えましょう。</p> <p>まず最初の入力文字が「u」なので、入力するひらがなは「う」であることが判明します。「う」の別入力が「wu」であるので「w」を入力しても構わないことがわかります。</p> <p>当然ですが、別入力を行いますとローマ字を改良する必要があります。今回の例で「w」を入力するとローマ字表記は「wutiwa」に変化します。</p> <p>次の入力文字も「u」ですが、前の文字が「w」なので「う」の入力途中であることが判明します。よって今回は「w」を入力することができません。</p> <p>そして「u」の入力後、入力文字が「t」になります。この入力文字の前が母音で、後の文字が「i」なので「ち」を入力することが判明します。同様に「ち」の別入力は「chi」なので「c」を入力してもOKとなります。</p> <p>もちろん「c」を入力した後は「wuchiwa」に変形する必要があります。</p> <p>こんな感じでほかの文字に対応させることができます。</p> <h3 id="「InputKey()」関数を大規模改造する。"><a href="#%E3%80%8CInputKey%28%29%E3%80%8D%E9%96%A2%E6%95%B0%E3%82%92%E5%A4%A7%E8%A6%8F%E6%A8%A1%E6%94%B9%E9%80%A0%E3%81%99%E3%82%8B%E3%80%82">「InputKey()」関数を大規模改造する。</a></h3> <p>上記のアルゴリズムを用いて<code>InputKey()</code>を改良します。<br /> 以下のコードを解説するとかなり長いので省略します。</p> <pre><code class="csharp">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; } </code></pre> <p>こうすることによって、「し」を「shi」、「ふ」を「fu」と入力できるようになります。<br /> 実際、上記のコードは完璧ではありませんが、これでもゲームとして十分だと思います。</p> <h2 id="さいごに"><a href="#%E3%81%95%E3%81%84%E3%81%94%E3%81%AB">さいごに</a></h2> <p>私は文章下手なので、少々読みにくい点はあったかもしれませんが、それでも、長々とした記事をお読みいただき、誠にありがとうございます。</p> <p>上記のコードは自由にアレンジしても、そのままコピペしても構いません。<br /> むしろ、それでタイピングゲームの製作に関心を持ってもらえたら幸いです。</p> <h2 id="サンプル"><a href="#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB">サンプル</a></h2> <p>サンプルはunityroom上にて公開しております。<br /> なお、今回はミスタイプ数などの要素が加わています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://unityroom.com/games/typingsample" target="_blank">https://unityroom.com/games/typingsample</a></p> <h2 id="GitHub"><a href="#GitHub">GitHub</a></h2> <p>内容は、上記のサンプルのプロジェクトです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/AkioMabuchi/SampleTyping" target="_blank">https://github.com/AkioMabuchi/SampleTyping</a></p> アキオ