柔軟に入力できる日本語タイピングゲームを本格的に作ってみた。
この記事は、Unityゲーム開発者ギルド Advent Calendar 2023、12日目の記事です。
はじめに
ローマ字入力タイピングゲームの多くは「し」を「shi」「ふ」を「fu」と入力できるなど、柔軟に入力することができます。率直に言いますと、このプログラムを組むのは結構むずかしかったです。この記事はアドカレ記事で技術解説はほとんどありませんが、ローマ字入力タイピングゲームを作るのに苦戦している方の手助けになれば幸いかと思います。
作ったもの
unityroomにて体験できます。
https://unityroom.com/games/adventartyping
使用技術
Unity 2022.3.14f
Universal Render Pipeline(URP)
TextMeshPro
UniRx
VContainer
設計思想
今回は「GameManager.cs」にまとめてただ動かすだけではなく、MV(R)P(Model-View-(Reactive)Presenter)パターンを用いて開発します。また独自なりに入力処理を行うProcessという概念をプラスした「MV(R)P+P(Model-View-(Reactive)Presenter+Process)」という設計思想を採用しました。
私なりの、各々のスクリプトの役割ルールは以下の通りです。
ViewだけがMonoBehaviourを継承できる
ViewとModelは別のスクリプトに依存しない
PresenterはModelのReactivePropertyをSubscribeし、Viewに値を渡す
Presenterだけが、ReactivePropertyをSubscribleできる
Processだけが、ModelとViewのメソッドを叩ける
Processだけが、ViewのObservableをSubscribeできる
ソースコード
アーキテクチャを構築するのに必要となったEnum、Interface、Structを始めに紹介します。
GameStateName.cs
namespace Enums
{
public enum GameStateName
{
None,
Typing
}
}
OperatingSystemName.cs
namespace Enums
{
public enum OperatingSystemName
{
None,
Windows,
Mac
}
}
TypeResult.cs
namespace Enums
{
public enum TypeResult
{
Correct,
Incorrect,
Finished
}
}
ITypingTextDifference.cs
後述するTypingTextView.csないしTypingTextViewPresenter.csのための空のInterfaceです。
namespace Interfaces
{
public interface ITypingTextDifference
{
}
}
TypingText.cs
using System;
namespace Structs
{
[Serializable]
public struct TypingText
{
public string title;
public string hiragana;
}
}
TypingTextDifferenceCharacters.cs
using System.Collections.Generic;
using Interfaces;
namespace Structs
{
public struct TypingTextDifferenceCharacters: ITypingTextDifference
{
public IReadOnlyList<char> characters;
}
}
TypingTextDifferenceCharactersIndex.cs
using Interfaces;
namespace Structs
{
public struct TypingTextDifferenceCharactersIndex: ITypingTextDifference
{
public int charactersIndex;
}
}
TypingTextDifferenceTitle.cs
using Interfaces;
namespace Structs
{
public struct TypingTextDifferenceTitle: ITypingTextDifference
{
public string title;
}
}
必要なEnum、Interface、Structの記述は以上です。
Modelから紹介します。Modelは主にゲームロジックを処理するPure C#なスクリプトです。また、Modelに記載される変数は全てReactivePropertyとしました。
ConvertHiraganaToRomanModel.cs
「ひらがな文字列」から「入力用のローマ字」に変換するModelスクリプトです。1メソッドで1200行にもなりました。
using System.Collections.Generic;
namespace Models
{
public class ConvertHiraganaToRomanModel
{
public IEnumerable<char> ConvertHiraganaToRoman(IReadOnlyList<char> hiragana)
{
var roman = new List<char>();
for (var i = 0; i < hiragana.Count; i++)
{
var nextHiragana = i < hiragana.Count - 1 ? hiragana[i + 1] : '@';
switch (hiragana[i])
{
case 'あ':
{
roman.Add('a');
break;
}
case 'い':
{
roman.Add('i');
break;
}
case 'う':
{
switch (nextHiragana)
{
case 'ぁ':
{
roman.Add('w');
roman.Add('h');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('w');
roman.Add('h');
roman.Add('i');
break;
}
case 'ぇ':
{
roman.Add('w');
roman.Add('h');
roman.Add('e');
break;
}
case 'ぉ':
{
roman.Add('w');
roman.Add('h');
roman.Add('o');
break;
}
default:
{
roman.Add('u');
break;
}
}
break;
}
case 'え':
{
roman.Add('e');
break;
}
case 'お':
{
roman.Add('o');
break;
}
case 'か':
{
roman.Add('k');
roman.Add('a');
break;
}
case 'き':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('k');
roman.Add('y');
roman.Add('o');
break;
}
case 'ゅ':
{
roman.Add('k');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('k');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('k');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('k');
roman.Add('i');
break;
}
}
break;
}
case 'く':
{
roman.Add('k');
roman.Add('u');
break;
}
case 'け':
{
roman.Add('k');
roman.Add('e');
break;
}
case 'こ':
{
roman.Add('k');
roman.Add('o');
break;
}
case 'さ':
{
roman.Add('s');
roman.Add('a');
break;
}
case 'し':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('s');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('s');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('s');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('s');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('s');
roman.Add('i');
break;
}
}
break;
}
case 'す':
{
roman.Add('s');
roman.Add('u');
break;
}
case 'せ':
{
roman.Add('s');
roman.Add('e');
break;
}
case 'そ':
{
roman.Add('s');
roman.Add('o');
break;
}
case 'た':
{
roman.Add('t');
roman.Add('a');
break;
}
case 'ち':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('t');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('t');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('t');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('t');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('t');
roman.Add('i');
break;
}
}
break;
}
case 'つ':
{
switch (nextHiragana)
{
case 'ぁ':
{
roman.Add('t');
roman.Add('s');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('t');
roman.Add('s');
roman.Add('i');
break;
}
case 'ぇ':
{
roman.Add('t');
roman.Add('s');
roman.Add('e');
break;
}
case 'ぉ':
{
roman.Add('t');
roman.Add('s');
roman.Add('o');
break;
}
default:
{
roman.Add('t');
roman.Add('u');
break;
}
}
break;
}
case 'て':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('t');
roman.Add('h');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('t');
roman.Add('h');
roman.Add('i');
break;
}
case 'ゅ':
{
roman.Add('t');
roman.Add('h');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('t');
roman.Add('h');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('t');
roman.Add('h');
roman.Add('o');
break;
}
default:
{
roman.Add('t');
roman.Add('e');
break;
}
}
break;
}
case 'と':
{
switch (nextHiragana)
{
case 'ぁ':
{
roman.Add('t');
roman.Add('w');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('t');
roman.Add('w');
roman.Add('i');
break;
}
case 'ぅ':
{
roman.Add('t');
roman.Add('w');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('t');
roman.Add('w');
roman.Add('e');
break;
}
case 'ぉ':
{
roman.Add('t');
roman.Add('w');
roman.Add('o');
break;
}
default:
{
roman.Add('t');
roman.Add('o');
break;
}
}
break;
}
case 'な':
{
roman.Add('n');
roman.Add('a');
break;
}
case 'に':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('n');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('n');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('n');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('n');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('n');
roman.Add('i');
break;
}
}
break;
}
case 'ぬ':
{
roman.Add('n');
roman.Add('u');
break;
}
case 'ね':
{
roman.Add('n');
roman.Add('e');
break;
}
case 'の':
{
roman.Add('n');
roman.Add('o');
break;
}
case 'は':
{
roman.Add('h');
roman.Add('a');
break;
}
case 'ひ':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('h');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('h');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('h');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('h');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('n');
roman.Add('i');
break;
}
}
break;
}
case 'ふ':
{
switch (nextHiragana)
{
case 'ぁ':
{
roman.Add('f');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('f');
roman.Add('i');
break;
}
case 'ぇ':
{
roman.Add('f');
roman.Add('e');
break;
}
case 'ぉ':
{
roman.Add('f');
roman.Add('o');
break;
}
default:
{
roman.Add('h');
roman.Add('u');
break;
}
}
break;
}
case 'へ':
{
roman.Add('h');
roman.Add('e');
break;
}
case 'ほ':
{
roman.Add('h');
roman.Add('o');
break;
}
case 'ま':
{
roman.Add('m');
roman.Add('a');
break;
}
case 'み':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('m');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('m');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('m');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('m');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('m');
roman.Add('i');
break;
}
}
break;
}
case 'む':
{
roman.Add('m');
roman.Add('u');
break;
}
case 'め':
{
roman.Add('m');
roman.Add('e');
break;
}
case 'も':
{
roman.Add('m');
roman.Add('o');
break;
}
case 'や':
{
roman.Add('y');
roman.Add('a');
break;
}
case 'ゆ':
{
roman.Add('y');
roman.Add('u');
break;
}
case 'よ':
{
roman.Add('y');
roman.Add('o');
break;
}
case 'ら':
{
roman.Add('r');
roman.Add('a');
break;
}
case 'り':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('r');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('r');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('r');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('r');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('r');
roman.Add('i');
break;
}
}
break;
}
case 'る':
{
roman.Add('r');
roman.Add('u');
break;
}
case 'れ':
{
roman.Add('r');
roman.Add('e');
break;
}
case 'ろ':
{
roman.Add('r');
roman.Add('o');
break;
}
case 'わ':
{
roman.Add('w');
roman.Add('a');
break;
}
case 'を':
{
roman.Add('w');
roman.Add('o');
break;
}
case 'ん':
{
var hashSet = new HashSet<char>
{
'あ', 'い', 'う', 'え', 'お', 'な', 'に', 'ぬ', 'ね', 'の', 'や', 'ゆ', 'よ', 'ん', '@'
};
if (hashSet.Contains(nextHiragana))
{
roman.Add('n');
roman.Add('n');
}
else
{
roman.Add('n');
}
break;
}
case 'が':
{
roman.Add('g');
roman.Add('a');
break;
}
case 'ぎ':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('g');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('g');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('g');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('g');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('g');
roman.Add('i');
break;
}
}
break;
}
case 'ぐ':
{
roman.Add('g');
roman.Add('u');
break;
}
case 'げ':
{
roman.Add('g');
roman.Add('e');
break;
}
case 'ご':
{
roman.Add('g');
roman.Add('o');
break;
}
case 'ざ':
{
roman.Add('z');
roman.Add('a');
break;
}
case 'じ':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('z');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('z');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('z');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('z');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('z');
roman.Add('i');
break;
}
}
break;
}
case 'ず':
{
roman.Add('z');
roman.Add('u');
break;
}
case 'ぜ':
{
roman.Add('z');
roman.Add('e');
break;
}
case 'ぞ':
{
roman.Add('z');
roman.Add('o');
break;
}
case 'だ':
{
roman.Add('d');
roman.Add('a');
break;
}
case 'ぢ':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('d');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('d');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('d');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('d');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('d');
roman.Add('i');
break;
}
}
break;
}
case 'づ':
{
roman.Add('d');
roman.Add('u');
break;
}
case 'で':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('d');
roman.Add('h');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('d');
roman.Add('h');
roman.Add('i');
break;
}
case 'ゅ':
{
roman.Add('d');
roman.Add('h');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('d');
roman.Add('h');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('d');
roman.Add('h');
roman.Add('o');
break;
}
default:
{
roman.Add('d');
roman.Add('e');
break;
}
}
break;
}
case 'ど':
{
switch (nextHiragana)
{
case 'ぁ':
{
roman.Add('d');
roman.Add('w');
roman.Add('a');
break;
}
case 'ぃ':
{
roman.Add('d');
roman.Add('w');
roman.Add('i');
break;
}
case 'ぅ':
{
roman.Add('d');
roman.Add('w');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('d');
roman.Add('w');
roman.Add('e');
break;
}
case 'ぉ':
{
roman.Add('d');
roman.Add('w');
roman.Add('o');
break;
}
default:
{
roman.Add('d');
roman.Add('o');
break;
}
}
break;
}
case 'ば':
{
roman.Add('b');
roman.Add('a');
break;
}
case 'び':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('b');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('b');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('b');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('b');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('b');
roman.Add('i');
break;
}
}
break;
}
case 'ぶ':
{
roman.Add('b');
roman.Add('u');
break;
}
case 'べ':
{
roman.Add('b');
roman.Add('e');
break;
}
case 'ぼ':
{
roman.Add('b');
roman.Add('o');
break;
}
case 'ぱ':
{
roman.Add('p');
roman.Add('a');
break;
}
case 'ぴ':
{
switch (nextHiragana)
{
case 'ゃ':
{
roman.Add('p');
roman.Add('y');
roman.Add('a');
break;
}
case 'ゅ':
{
roman.Add('p');
roman.Add('y');
roman.Add('u');
break;
}
case 'ぇ':
{
roman.Add('p');
roman.Add('y');
roman.Add('e');
break;
}
case 'ょ':
{
roman.Add('p');
roman.Add('y');
roman.Add('o');
break;
}
default:
{
roman.Add('p');
roman.Add('i');
break;
}
}
break;
}
case 'ぷ':
{
roman.Add('p');
roman.Add('u');
break;
}
case 'ぺ':
{
roman.Add('p');
roman.Add('e');
break;
}
case 'ぽ':
{
roman.Add('p');
roman.Add('o');
break;
}
case 'っ':
{
switch (nextHiragana)
{
case 'か' or 'き' or 'く' or 'け' or 'こ':
{
roman.Add('k');
break;
}
case 'さ' or 'し' or 'す' or 'せ' or 'そ':
{
roman.Add('s');
break;
}
case 'た' or 'ち' or 'つ' or 'て' or 'と':
{
roman.Add('t');
break;
}
case 'は' or 'ひ' or 'ふ' or 'へ' or 'ほ':
{
roman.Add('h');
break;
}
case 'ま' or 'み' or 'む' or 'め' or 'も':
{
roman.Add('m');
break;
}
case 'や' or 'ゆ' or 'よ':
{
roman.Add('y');
break;
}
case 'が' or 'ぎ' or 'ぐ' or 'げ' or 'ご':
{
roman.Add('g');
break;
}
case 'ざ' or 'じ' or 'ず' or 'ぜ' or 'ぞ':
{
roman.Add('z');
break;
}
case 'だ' or 'ぢ' or 'づ' or 'で' or 'ど':
{
roman.Add('d');
break;
}
case 'ば' or 'び' or 'ぶ' or 'べ' or 'ぼ':
{
roman.Add('b');
break;
}
case 'ぱ' or 'ぴ' or 'ぷ' or 'ぺ' or 'ぽ':
{
roman.Add('p');
break;
}
default:
{
roman.Add('x');
roman.Add('t');
roman.Add('u');
break;
}
}
break;
}
case 'ー':
{
roman.Add('-');
break;
}
case '、':
{
roman.Add(',');
break;
}
case '。':
{
roman.Add('.');
break;
}
}
}
return roman;
}
}
}
CurrentTypingTextModel.cs
現在入力中の問題文を処理するModelスクリプトです。これも長い・・・。
using System;
using System.Collections.Generic;
using Enums;
using UniRx;
namespace Models
{
public class CurrentTypingTextModel
{
private readonly ReactiveProperty<string> _reactivePropertyTitle = new("");
public IObservable<string> OnChangedTitle => _reactivePropertyTitle;
private readonly ReactiveProperty<IReadOnlyList<char>> _reactivePropertyCharacters = new(new List<char>());
public IObservable<IReadOnlyList<char>> OnChangedCharacters => _reactivePropertyCharacters;
private readonly ReactiveProperty<int> _reactivePropertyCharactersIndex = new(0);
public IObservable<int> OnChangedCharactersIndex => _reactivePropertyCharactersIndex;
private readonly ReactiveProperty<OperatingSystemName>
_reactivePropertyOperatingSystemName = new(OperatingSystemName.None);
public void SetOperatingSystemName(OperatingSystemName operatingSystemName)
{
_reactivePropertyOperatingSystemName.Value = operatingSystemName;
}
public void SetTitle(string title)
{
_reactivePropertyTitle.Value = title;
}
public void SetCharacters(IEnumerable<char> characters)
{
_reactivePropertyCharacters.Value = new List<char>(characters);
}
public void ResetCharactersIndex()
{
_reactivePropertyCharactersIndex.Value = 0;
}
public TypeResult TypeCharacter(char inputCharacter)
{
var currentCharactersIndex = _reactivePropertyCharactersIndex.Value;
var currentCharacters = new List<char>(_reactivePropertyCharacters.Value);
var prevChar3 = currentCharactersIndex >= 3 ? currentCharacters[currentCharactersIndex - 3] : '\0';
var prevChar2 = currentCharactersIndex >= 2 ? currentCharacters[currentCharactersIndex - 2] : '\0';
var prevChar = currentCharactersIndex >= 1 ? currentCharacters[currentCharactersIndex - 1] : '\0';
var currentChar = currentCharacters[currentCharactersIndex];
var nextChar = currentCharactersIndex < currentCharacters.Count - 1
? currentCharacters[currentCharactersIndex + 1]
: '\0';
var nextChar2 = currentCharactersIndex < currentCharacters.Count - 2
? currentCharacters[currentCharactersIndex + 2]
: '\0';
var newCharacters = new List<char>(currentCharacters);
var isCorrect = false;
if (inputCharacter == newCharacters[currentCharactersIndex])
{
isCorrect = true;
}
//「い」の入力判定(Windowsのみ)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter is 'y' &&
currentChar is 'i' &&
prevChar is '\0' or 'a' or 'i' or 'u' or 'e' or 'o')
{
newCharacters.Insert(currentCharactersIndex, 'y');
isCorrect = true;
}
//「う」の入力判定(「whu」はWindowsのみ)
else if (inputCharacter is 'w' &&
currentChar is 'u' &&
prevChar is '\0' or 'a' or 'i' or 'u' or 'e' or 'o')
{
newCharacters.Insert(currentCharactersIndex, 'w');
isCorrect = true;
}
else if (inputCharacter == 'w' &&
currentChar == 'u' &&
prevChar == 'n' &&
prevChar2 == 'n' &&
prevChar3 != 'n')
{
newCharacters.Insert(currentCharactersIndex, 'w');
isCorrect = true;
}
else if (inputCharacter == 'w' &&
currentChar == 'u' &&
prevChar == 'n' &&
prevChar2 == 'x')
{
newCharacters.Insert(currentCharactersIndex, 'w');
isCorrect = true;
}
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'h' &&
currentChar == 'u' &&
prevChar2 != 't' && prevChar2 != 'd' &&
prevChar == 'w')
{
newCharacters.Insert(currentCharactersIndex, 'h');
isCorrect = true;
}
//「か」「く」「こ」の柔軟な入力(Windowsのみ)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'c' &&
prevChar != 'k' &&
currentChar == 'k' && nextChar is 'a' or 'u' or 'o')
{
newCharacters[currentCharactersIndex] = 'c';
isCorrect = true;
}
//「く」の柔軟な入力(Windowsのみ)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'q' &&
prevChar != 'k' &&
currentChar == 'k' && nextChar == 'u')
{
newCharacters[currentCharactersIndex] = 'q';
isCorrect = true;
}
//「し」の柔軟な入力
else if (inputCharacter == 'h' && prevChar == 's' && currentChar == 'i')
{
newCharacters.Insert(currentCharactersIndex, 'h');
isCorrect = true;
}
//「じ」の柔軟な入力
else if (inputCharacter == 'j' && currentChar == 'z' && nextChar == 'i')
{
newCharacters[currentCharactersIndex] = 'j';
isCorrect = true;
}
//「しゃ」「しゅ」「しぇ」「しょ」の柔軟な入力
else if (inputCharacter == 'h' && prevChar == 's' && currentChar == 'y')
{
newCharacters[currentCharactersIndex] = 'h';
isCorrect = true;
}
//「じゃ」「じゅ」「じぇ」「じょ」の柔軟な入力
else if (inputCharacter == 'j' && prevChar != 'z' && currentChar == 'z' &&
nextChar == 'y' && nextChar2 is 'a' or 'u' or 'e' or 'o')
{
newCharacters[currentCharactersIndex] = 'j';
newCharacters.RemoveAt(currentCharactersIndex + 1);
isCorrect = true;
}
else if (inputCharacter == 'y' && prevChar == 'j' &&
currentChar is 'a' or 'u' or 'e' or 'o')
{
newCharacters.Insert(currentCharactersIndex, 'y');
isCorrect = true;
}
//「し」「せ」の柔軟な入力(Windowsのみ)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'c' && prevChar != 's' &&
currentChar == 's' &&
nextChar is 'i' or 'e')
{
newCharacters[currentCharactersIndex] = 'c';
isCorrect = true;
}
//「ち」の柔軟な入力
else if (inputCharacter == 'c' && prevChar != 't' && currentChar == 't' && nextChar == 'i')
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters.Insert(currentCharactersIndex + 1, 'h');
isCorrect = true;
}
//「ちゃ」「ちゅ」「ちぇ」「ちょ」の柔軟な入力
else if (inputCharacter == 'c' && prevChar != 't' && currentChar == 't' && nextChar == 'y')
{
newCharacters[currentCharactersIndex] = 'c';
isCorrect = true;
}
//「cya」=>「cha」
else if (inputCharacter == 'h' && prevChar == 'c' && currentChar == 'y')
{
newCharacters[currentCharactersIndex] = 'h';
isCorrect = true;
}
//「つ」の柔軟な入力
else if (inputCharacter == 's' && prevChar == 't' && currentChar == 'u')
{
newCharacters.Insert(currentCharactersIndex, 's');
isCorrect = true;
}
//「つぁ」「つぃ」「つぇ」「つぉ」の柔軟な入力
else if (inputCharacter == 'u' && prevChar == 't' && currentChar == 's' &&
nextChar is 'a' or 'i' or 'e' or 'o')
{
newCharacters[currentCharactersIndex] = 'u';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
else if (inputCharacter == 'u' && prevChar2 == 't' && prevChar == 's' &&
currentChar is 'a' or 'i' or 'e' or 'o')
{
newCharacters.Insert(currentCharactersIndex, 'u');
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
//「てぃ」の柔軟な入力
else if (inputCharacter == 'e' && prevChar == 't' && currentChar == 'h' && nextChar == 'i')
{
newCharacters[currentCharactersIndex] = 'e';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
//「でぃ」の柔軟な入力
else if (inputCharacter == 'e' && prevChar == 'd' && currentChar == 'h' && nextChar == 'i')
{
newCharacters[currentCharactersIndex] = 'e';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
//「でゅ」の柔軟な入力
else if (inputCharacter == 'e' && prevChar == 'd' && currentChar == 'h' && nextChar == 'u')
{
newCharacters[currentCharactersIndex] = 'e';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
newCharacters.Insert(currentCharactersIndex + 2, 'y');
isCorrect = true;
}
//「とぅ」の柔軟な入力
else if (inputCharacter == 'o' && prevChar == 't' && currentChar == 'w' && nextChar == 'u')
{
newCharacters[currentCharactersIndex] = 'o';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
//「どぅ」の柔軟な入力
else if (inputCharacter == 'o' && prevChar == 'd' && currentChar == 'w' && nextChar == 'u')
{
newCharacters[currentCharactersIndex] = 'o';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
//「ふ」の柔軟な入力
else if (inputCharacter == 'f' && currentChar == 'h' && nextChar == 'u')
{
newCharacters[currentCharactersIndex] = 'f';
isCorrect = true;
}
//「ふぁ」「ふぃ」「ふぇ」「ふぉ」の柔軟な入力(一部Macのみ)
else if (inputCharacter == 'w' && prevChar == 'f' &&
currentChar is 'a' or 'i' or 'e' or 'o')
{
newCharacters.Insert(currentCharactersIndex, 'w');
isCorrect = true;
}
else if (inputCharacter == 'y' && prevChar == 'f' && currentChar is 'i' or 'e')
{
newCharacters.Insert(currentCharactersIndex, 'y');
isCorrect = true;
}
else if (inputCharacter == 'h' && prevChar != 'f' && currentChar == 'f' &&
nextChar is 'a' or 'i' or 'e' or 'o')
{
if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Mac)
{
newCharacters[currentCharactersIndex] = 'h';
newCharacters.Insert(currentCharactersIndex + 1, 'w');
}
else
{
newCharacters[currentCharactersIndex] = 'h';
newCharacters.Insert(currentCharactersIndex + 1, 'u');
newCharacters.Insert(currentCharactersIndex + 2, 'x');
}
isCorrect = true;
}
else if (inputCharacter == 'u' && prevChar == 'f' &&
currentChar is 'a' or 'i' or 'e' or 'o')
{
newCharacters.Insert(currentCharactersIndex, 'u');
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Mac && inputCharacter == 'u' &&
prevChar == 'h' &&
currentChar == 'w' &&
nextChar is 'a' or 'i' or 'e' or 'o')
{
newCharacters[currentCharactersIndex] = 'u';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
isCorrect = true;
}
//「ん」の柔軟な入力(「n'」には未対応)
else if (inputCharacter == 'n' && prevChar2 != 'n' && prevChar == 'n' && currentChar != 'a' &&
currentChar != 'i' &&
currentChar != 'u' && currentChar != 'e' && currentChar != 'o' && currentChar != 'y')
{
newCharacters.Insert(currentCharactersIndex, 'n');
isCorrect = true;
}
else if (inputCharacter == 'x' && prevChar != 'n' && currentChar == 'n' && nextChar != 'a' &&
nextChar != 'i' &&
nextChar != 'u' && nextChar != 'e' && nextChar != 'o' && nextChar != 'y')
{
if (nextChar == 'n')
{
newCharacters[currentCharactersIndex] = 'x';
}
else
{
newCharacters.Insert(currentCharactersIndex, 'x');
}
isCorrect = true;
}
//「うぃ」「うぇ」「うぉ」を分解する
else if (inputCharacter == 'u' && currentChar == 'w' && nextChar == 'h' &&
nextChar2 is 'a' or 'i' or 'e' or 'o')
{
newCharacters[currentCharactersIndex] = 'u';
newCharacters[currentCharactersIndex + 1] = 'x';
isCorrect = true;
}
//「きゃ」「にゃ」などを分解する
else if (inputCharacter == 'i' && currentChar == 'y' &&
prevChar is 'k' or 's' or 't' or 'n' or 'h' or 'm' or 'r' or 'g' or 'z' or 'd' or 'b' or 'p' &&
nextChar is 'a' or 'u' or 'e' or 'o')
{
if (nextChar == 'e')
{
newCharacters[currentCharactersIndex] = 'i';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
}
else
{
newCharacters.Insert(currentCharactersIndex, 'i');
newCharacters.Insert(currentCharactersIndex + 1, 'x');
}
isCorrect = true;
}
//「しゃ」「ちゃ」などを分解する
else if (inputCharacter == 'i' &&
currentChar is 'a' or 'u' or 'e' or 'o' &&
prevChar2 is 's' or 'c' && prevChar == 'h')
{
if (nextChar == 'e')
{
newCharacters.Insert(currentCharactersIndex, 'i');
newCharacters.Insert(currentCharactersIndex + 1, 'x');
}
else
{
newCharacters.Insert(currentCharactersIndex, 'i');
newCharacters.Insert(currentCharactersIndex + 1, 'x');
newCharacters.Insert(currentCharactersIndex + 2, 'y');
}
isCorrect = true;
}
//「しゃ」を「c」で分解する(Windows限定)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'c' &&
currentChar == 's' &&
prevChar != 's' && nextChar == 'y' &&
nextChar2 is 'a' or 'u' or 'e' or 'o')
{
if (nextChar2 == 'e')
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters[currentCharactersIndex + 1] = 'i';
newCharacters.Insert(currentCharactersIndex + 1, 'x');
}
else
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters.Insert(currentCharactersIndex + 1, 'i');
newCharacters.Insert(currentCharactersIndex + 2, 'x');
}
isCorrect = true;
}
//「っ」の柔軟な入力
else if (inputCharacter is 'x' or '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'))
{
newCharacters[currentCharactersIndex] = inputCharacter;
newCharacters.Insert(currentCharactersIndex + 1, 't');
newCharacters.Insert(currentCharactersIndex + 2, 'u');
isCorrect = true;
}
//「っか」「っく」「っこ」の柔軟な入力(Windows限定)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'c' && currentChar == 'k' &&
nextChar == 'k' &&
nextChar2 is 'a' or 'u' or 'o')
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters[currentCharactersIndex + 1] = 'c';
isCorrect = true;
}
//「っく」の柔軟な入力(Windows限定)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'q' && currentChar == 'k' &&
nextChar == 'k' && nextChar2 == 'u')
{
newCharacters[currentCharactersIndex] = 'q';
newCharacters[currentCharactersIndex + 1] = 'q';
isCorrect = true;
}
//「っし」「っせ」の柔軟な入力(Windows限定)
else if (_reactivePropertyOperatingSystemName.Value == OperatingSystemName.Windows &&
inputCharacter == 'c' && currentChar == 's' &&
nextChar == 's' &&
nextChar2 is 'i' or 'e')
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters[currentCharactersIndex + 1] = 'c';
isCorrect = true;
}
//「っちゃ」「っちゅ」「っちぇ」「っちょ」の柔軟な入力
else if (inputCharacter == 'c' && currentChar == 't' && nextChar == 't' && nextChar2 == 'y')
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters[currentCharactersIndex + 1] = 'c';
isCorrect = true;
}
//「っち」の柔軟な入力
else if (inputCharacter == 'c' && currentChar == 't' && nextChar == 't' && nextChar2 == 'i')
{
newCharacters[currentCharactersIndex] = 'c';
newCharacters[currentCharactersIndex + 1] = 'c';
newCharacters.Insert(currentCharactersIndex + 2, 'h');
isCorrect = true;
}
//「l」と「x」の完全互換性
else if (inputCharacter == 'x' && currentChar == 'l')
{
newCharacters[currentCharactersIndex] = 'x';
isCorrect = true;
}
else if (inputCharacter == 'l' && currentChar == 'x')
{
newCharacters[currentCharactersIndex] = 'l';
isCorrect = true;
}
if (isCorrect)
{
_reactivePropertyCharacters.Value = newCharacters;
_reactivePropertyCharactersIndex.Value++;
if (_reactivePropertyCharactersIndex.Value >= _reactivePropertyCharacters.Value.Count)
{
return TypeResult.Finished;
}
return TypeResult.Correct;
}
return TypeResult.Incorrect;
}
}
}
GameStateModel.cs
ステートを管理することによるModelです。単純に名前が記載されるだけです。
using Enums;
using UniRx;
namespace Models
{
public class GameStateModel
{
private readonly ReactiveProperty<GameStateName> _reactivePropertyGameStateName = new(GameStateName.None);
public GameStateName GameStateName => _reactivePropertyGameStateName.Value;
public void ChangeGameStateName(GameStateName gameStateName)
{
_reactivePropertyGameStateName.Value = gameStateName;
}
}
}
NextTypingTextModel.cs
次の問題文をあらかじめ用意するModelスクリプトです。
using System;
using Structs;
using UniRx;
namespace Models
{
public class NextTypingTextModel
{
private readonly ReactiveProperty<string> _reactivePropertyTitle = new("");
public IObservable<string> OnChangedTitle => _reactivePropertyTitle;
private readonly ReactiveProperty<string> _reactivePropertyCharacters = new("");
public TypingText TypingText => new()
{
title = _reactivePropertyTitle.Value,
hiragana = _reactivePropertyCharacters.Value,
};
public void SetTypingText(TypingText typingText)
{
_reactivePropertyTitle.Value = typingText.title;
_reactivePropertyCharacters.Value = typingText.hiragana;
}
}
}
TypingSpeedModel.cs
タイピングの速度(key / min)を計測するModelスクリプトです。
using System;
using UniRx;
using UnityEngine;
namespace Models
{
public class TypingSpeedModel
{
private readonly ReactiveProperty<int> _reactivePropertyTypingSpeed = new(0);
public IObservable<int> OnChangedTypingSpeed => _reactivePropertyTypingSpeed;
private readonly ReactiveProperty<float> _reactivePropertyTime = new(0.0f);
private readonly ReactiveProperty<int> _reactivePropertyTypeCount = new(0);
public void AddTime(float deltaTime)
{
_reactivePropertyTime.Value += deltaTime;
}
public void IncreaseTypeCount()
{
_reactivePropertyTypeCount.Value++;
}
public void CalculateTypingSpeed()
{
if (_reactivePropertyTime.Value > 0.0f)
{
_reactivePropertyTypingSpeed.Value =
Mathf.RoundToInt(_reactivePropertyTypeCount.Value * 60 / _reactivePropertyTime.Value);
}
else
{
_reactivePropertyTypingSpeed.Value = 0;
}
}
public void StartMeasure()
{
_reactivePropertyTypeCount.Value = 0;
_reactivePropertyTime.Value = 0.0f;
}
}
}
TypingTextStoreModel.cs
タイピングの問題文を格納するModelスクリプトです。
using Structs;
using UniRx;
namespace Models
{
public class TypingTextStoreModel
{
private readonly ReactiveCollection<TypingText> _reactiveCollectionTypingTexts = new()
{
new TypingText
{
title = "犬も歩けば棒に当たる",
hiragana = "いぬもあるけばぼうにあたる"
},
new TypingText
{
title = "論より証拠",
hiragana = "ろんよりしょうこ"
},
new TypingText
{
title = "花より団子",
hiragana = "はなよりだんご"
},
new TypingText
{
title = "需要と供給",
hiragana = "じゅようときょうきゅう"
},
new TypingText
{
title = "七福神",
hiragana = "しちふくじん"
},
new TypingText
{
title = "モッツァレラチーズ",
hiragana = "もっつぁれらちーず"
},
};
public TypingText RandomTypingText =>
_reactiveCollectionTypingTexts[UnityEngine.Random.Range(0, _reactiveCollectionTypingTexts.Count)];
}
}
Modelスクリプトの紹介は以上となります。
次にViewを紹介をします。Viewは主に表示の処理を行うMonoBehaviour継承のスクリプトです。個人的にはコンポーネントの操作に関係なく、MonoBehaviourを継承しGameObjectにアタッチするスクリプト=Viewとしております。
GameStarter.cs
ゲームを開始してから、一定時間後にイベントを発行するViewスクリプトです。
using System;
using System.Collections;
using UniRx;
using UnityEngine;
namespace Views
{
public class GameStarter : MonoBehaviour
{
[SerializeField] private float duration;
private readonly Subject<Unit> _subjectOnGameStart = new();
public IObservable<Unit> OnGameStart => _subjectOnGameStart;
private IEnumerator Start()
{
yield return new WaitForSeconds(duration);
_subjectOnGameStart.OnNext(Unit.Default);
}
}
}
KeyDetector.cs
MonoBehaviour標準のイベント関数OnGUIを用いて、入力キーを検出しイベント発行するスクリプトです。
using System;
using UniRx;
using UnityEngine;
namespace Views
{
public class KeyDetector : MonoBehaviour
{
private readonly Subject<char> _subjectOnCharacterDetected = new();
public IObservable<char> OnCharacterDetected => _subjectOnCharacterDetected;
private void OnGUI()
{
if (Event.current.type == EventType.KeyDown)
{
switch (Event.current.keyCode)
{
case KeyCode.A:
{
_subjectOnCharacterDetected.OnNext('a');
break;
}
case KeyCode.B:
{
_subjectOnCharacterDetected.OnNext('b');
break;
}
case KeyCode.C:
{
_subjectOnCharacterDetected.OnNext('c');
break;
}
case KeyCode.D:
{
_subjectOnCharacterDetected.OnNext('d');
break;
}
case KeyCode.E:
{
_subjectOnCharacterDetected.OnNext('e');
break;
}
case KeyCode.F:
{
_subjectOnCharacterDetected.OnNext('f');
break;
}
case KeyCode.G:
{
_subjectOnCharacterDetected.OnNext('g');
break;
}
case KeyCode.H:
{
_subjectOnCharacterDetected.OnNext('h');
break;
}
case KeyCode.I:
{
_subjectOnCharacterDetected.OnNext('i');
break;
}
case KeyCode.J:
{
_subjectOnCharacterDetected.OnNext('j');
break;
}
case KeyCode.K:
{
_subjectOnCharacterDetected.OnNext('k');
break;
}
case KeyCode.L:
{
_subjectOnCharacterDetected.OnNext('l');
break;
}
case KeyCode.M:
{
_subjectOnCharacterDetected.OnNext('m');
break;
}
case KeyCode.N:
{
_subjectOnCharacterDetected.OnNext('n');
break;
}
case KeyCode.O:
{
_subjectOnCharacterDetected.OnNext('o');
break;
}
case KeyCode.P:
{
_subjectOnCharacterDetected.OnNext('p');
break;
}
case KeyCode.Q:
{
_subjectOnCharacterDetected.OnNext('q');
break;
}
case KeyCode.R:
{
_subjectOnCharacterDetected.OnNext('r');
break;
}
case KeyCode.S:
{
_subjectOnCharacterDetected.OnNext('s');
break;
}
case KeyCode.T:
{
_subjectOnCharacterDetected.OnNext('t');
break;
}
case KeyCode.U:
{
_subjectOnCharacterDetected.OnNext('u');
break;
}
case KeyCode.V:
{
_subjectOnCharacterDetected.OnNext('v');
break;
}
case KeyCode.W:
{
_subjectOnCharacterDetected.OnNext('w');
break;
}
case KeyCode.X:
{
_subjectOnCharacterDetected.OnNext('x');
break;
}
case KeyCode.Y:
{
_subjectOnCharacterDetected.OnNext('y');
break;
}
case KeyCode.Z:
{
_subjectOnCharacterDetected.OnNext('z');
break;
}
case KeyCode.Alpha0:
{
_subjectOnCharacterDetected.OnNext('0');
break;
}
case KeyCode.Alpha1:
{
_subjectOnCharacterDetected.OnNext('1');
break;
}
case KeyCode.Alpha2:
{
_subjectOnCharacterDetected.OnNext('2');
break;
}
case KeyCode.Alpha3:
{
_subjectOnCharacterDetected.OnNext('3');
break;
}
case KeyCode.Alpha4:
{
_subjectOnCharacterDetected.OnNext('4');
break;
}
case KeyCode.Alpha5:
{
_subjectOnCharacterDetected.OnNext('5');
break;
}
case KeyCode.Alpha6:
{
_subjectOnCharacterDetected.OnNext('6');
break;
}
case KeyCode.Alpha7:
{
_subjectOnCharacterDetected.OnNext('7');
break;
}
case KeyCode.Alpha8:
{
_subjectOnCharacterDetected.OnNext('8');
break;
}
case KeyCode.Alpha9:
{
_subjectOnCharacterDetected.OnNext('9');
break;
}
case KeyCode.Minus:
{
_subjectOnCharacterDetected.OnNext('-');
break;
}
case KeyCode.Caret:
{
_subjectOnCharacterDetected.OnNext('^');
break;
}
case KeyCode.Backslash:
{
_subjectOnCharacterDetected.OnNext('\\');
break;
}
case KeyCode.At:
{
_subjectOnCharacterDetected.OnNext('@');
break;
}
case KeyCode.LeftBracket:
{
_subjectOnCharacterDetected.OnNext('[');
break;
}
case KeyCode.Semicolon:
{
_subjectOnCharacterDetected.OnNext(';');
break;
}
case KeyCode.Colon:
{
_subjectOnCharacterDetected.OnNext(':');
break;
}
case KeyCode.RightBracket:
{
_subjectOnCharacterDetected.OnNext(']');
break;
}
case KeyCode.Comma:
{
_subjectOnCharacterDetected.OnNext(',');
break;
}
case KeyCode.Period:
{
_subjectOnCharacterDetected.OnNext('.');
break;
}
case KeyCode.Slash:
{
_subjectOnCharacterDetected.OnNext('/');
break;
}
case KeyCode.Underscore:
{
_subjectOnCharacterDetected.OnNext('_');
break;
}
case KeyCode.Backspace:
{
_subjectOnCharacterDetected.OnNext('\b');
break;
}
case KeyCode.Return:
{
_subjectOnCharacterDetected.OnNext('\r');
break;
}
case KeyCode.Space:
{
_subjectOnCharacterDetected.OnNext(' ');
break;
}
}
}
}
}
}
TypingTextView.cs
タイピング中の問題文ないし入力文字列を表示制御するViewスクリプトです。
using System.Collections.Generic;
using Interfaces;
using Structs;
using TMPro;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
namespace Views
{
public class TypingTextView : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI textMeshProTitle;
[SerializeField] private TextMeshProUGUI textMeshProCharacters;
[SerializeField] private TMP_FontAsset fontUntyped;
[SerializeField] private TMP_FontAsset fontTyped;
[SerializeField] private TMP_FontAsset fontCurrent;
private string _title;
private List<char> _characters = new();
private int _charactersIndex;
private readonly Queue<ITypingTextDifference> _queueTypingTextDifferences = new();
private void Awake()
{
this.UpdateAsObservable()
.Subscribe(_ =>
{
var isTitleUpdated = false;
var isCharactersUpdated = false;
while (_queueTypingTextDifferences.Count > 0)
{
var typingTextDifference = _queueTypingTextDifferences.Dequeue();
switch (typingTextDifference)
{
case TypingTextDifferenceTitle typingTextDifferenceTitle:
{
_title = typingTextDifferenceTitle.title;
isTitleUpdated = true;
break;
}
case TypingTextDifferenceCharacters typingTextDifferenceCharacters:
{
_characters = new List<char>(typingTextDifferenceCharacters.characters);
isCharactersUpdated = true;
break;
}
case TypingTextDifferenceCharactersIndex typingTextDifferenceCharactersIndex:
{
_charactersIndex = typingTextDifferenceCharactersIndex.charactersIndex;
isCharactersUpdated = true;
break;
}
}
}
if (isTitleUpdated)
{
textMeshProTitle.text = _title;
}
if (isCharactersUpdated)
{
var textTypingCharacters = "<font=\"" + fontTyped.name + "\">";
for (var i = 0; i < _characters.Count; i++)
{
if (i == _charactersIndex)
{
textTypingCharacters += "</font><font=\"" + fontCurrent.name + "\">" + _characters[i] +
"</font><font=\"" + fontUntyped.name + "\">";
}
else
{
textTypingCharacters += _characters[i];
}
}
textTypingCharacters += "</font>";
textMeshProCharacters.text = textTypingCharacters;
}
}).AddTo(gameObject);
}
public void EnqueueTypingTextDifference(ITypingTextDifference typingTextDifference)
{
_queueTypingTextDifferences.Enqueue(typingTextDifference);
}
}
}
3種類のTextMeshProのFontAssetを格納する変数があり、TextMeshProのリッチテキスト機能であるFontタグを用いて文字の色や光り具合を変えています。
TextMesh Proをインポートした際に「Assets」ディレクトリに自動生成される「TextMesh Pro」ディレクトリの中にある「Resources/Fonts & Materials」ディレクトリに3種類のTextMesh ProのFont Assetを作成(もとい複製)し、そのFont Assetに付属しているMaterialの標準Shaderプロパティを変更しています。
NextTypingTextView.cs
NEXTの問題文を表示制御するViewスクリプトです。
using System.Collections.Generic;
using TMPro;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
namespace Views
{
public class NextTypingTextView : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI textMeshProTitle;
private readonly Queue<string> _queueTitle = new();
private string _title;
private void Awake()
{
this.UpdateAsObservable()
.Subscribe(_ =>
{
var isTitleUpdated = false;
while (_queueTitle.Count > 0)
{
var title = _queueTitle.Dequeue();
_title = title;
isTitleUpdated = true;
}
if (isTitleUpdated)
{
textMeshProTitle.text = _title;
}
}).AddTo(gameObject);
}
public void EnqueueTitle(string title)
{
_queueTitle.Enqueue(title);
}
}
}
TypingSpeedView.cs
タイピング速度を表示制御するViewスクリプトです。
using System.Collections.Generic;
using TMPro;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
namespace Views
{
public class TypingSpeedView : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI textMeshPro;
private readonly Queue<int> _queueTypingSpeed = new();
private int _typingSpeed;
private void Awake()
{
this.UpdateAsObservable()
.Subscribe(_ =>
{
var isTypingSpeedUpdated = false;
while (_queueTypingSpeed.Count > 0)
{
var typingSpeed = _queueTypingSpeed.Dequeue();
_typingSpeed = typingSpeed;
isTypingSpeedUpdated = true;
}
if (isTypingSpeedUpdated)
{
textMeshPro.text = "タイピング速度:" + _typingSpeed + " key/min";
}
}).AddTo(gameObject);
}
public void EnqueueTypingSpeed(int typingSpeed)
{
_queueTypingSpeed.Enqueue(typingSpeed);
}
}
}
Viewの紹介はこれにて以上です。
(Reactive)Presenterの領域に入ります。個人的な(Reactive)Presenterのルールとして「Modelに記載されたReactivePropertyをSubscribeして、Viewに値を渡す」としております。「PresenterだけがReactivePropertyをSubscribeできる」ものとしております。
TypingTextViewPresenter.cs
TypingTextModel.csに記述されたReactivePropertyをSubscribeし、TypingTextView.csに値を渡す(Reactive)Presenterスクリプトです。
using System;
using System.Collections.Generic;
using Models;
using Structs;
using UniRx;
using VContainer;
using VContainer.Unity;
using Views;
namespace Presenters
{
public class TypingTextViewPresenter : IInitializable, IDisposable
{
private readonly TypingTextModel _typingTextModel;
private readonly TypingTextView _typingTextView;
private readonly CompositeDisposable _compositeDisposable = new();
[Inject]
public TypingTextViewPresenter(TypingTextModel typingTextModel, TypingTextView typingTextView)
{
_typingTextModel = typingTextModel;
_typingTextView = typingTextView;
}
public void Initialize()
{
_typingTextModel.OnChangedTitle.Subscribe(title =>
{
_typingTextView.EnqueueTypingTextDifference(new TypingTextDifferenceTitle
{
title = title
});
}).AddTo(_compositeDisposable);
_typingTextModel.OnChangedCharacters.Subscribe(characters =>
{
_typingTextView.EnqueueTypingTextDifference(new TypingTextDifferenceCharacters
{
characters = new List<char>(characters)
});
}).AddTo(_compositeDisposable);
_typingTextModel.OnChangedCharactersIndex.Subscribe(charactersIndex =>
{
_typingTextView.EnqueueTypingTextDifference(new TypingTextDifferenceCharactersIndex
{
charactersIndex = charactersIndex
});
}).AddTo(_compositeDisposable);
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}
}
NextTypingTextViewPresenter.cs
NextTypingTextModel.csに記述されたReactivePropertyをSubsrcibeし、NextTypingTextView.csに値を渡す(Reactive)Presenterスクリプトです。
using System;
using Models;
using UniRx;
using VContainer;
using VContainer.Unity;
using Views;
namespace Presenters
{
public class NextTypingTextViewPresenter : IInitializable, IDisposable
{
private readonly NextTypingTextModel _nextTypingTextModel;
private readonly NextTypingTextView _nextTypingTextView;
private readonly CompositeDisposable _compositeDisposable = new();
[Inject]
public NextTypingTextViewPresenter(NextTypingTextModel nextTypingTextModel,
NextTypingTextView nextTypingTextView)
{
_nextTypingTextModel = nextTypingTextModel;
_nextTypingTextView = nextTypingTextView;
}
public void Initialize()
{
_nextTypingTextModel.OnChangedTitle.Subscribe(title =>
{
_nextTypingTextView.EnqueueTitle(title);
}).AddTo(_compositeDisposable);
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}
}
TypingSpeedViewPresenter.cs
TypingSpeedModel.csに記述されたReactivePropertyをSubscribeし、TypingSpeedView.csに値を渡す(Reactive)Presenterスクリプトです。
using System;
using Models;
using UniRx;
using VContainer;
using VContainer.Unity;
using Views;
namespace Presenters
{
public class TypingSpeedViewPresenter : IInitializable, IDisposable
{
private readonly TypingSpeedModel _typingSpeedModel;
private readonly TypingSpeedView _typingSpeedView;
private readonly CompositeDisposable _compositeDisposable = new();
[Inject]
public TypingSpeedViewPresenter(TypingSpeedModel typingSpeedModel, TypingSpeedView typingSpeedView)
{
_typingSpeedModel = typingSpeedModel;
_typingSpeedView = typingSpeedView;
}
public void Initialize()
{
_typingSpeedModel.OnChangedTypingSpeed.Subscribe(typingSpeed =>
{
_typingSpeedView.EnqueueTypingSpeed(typingSpeed);
}).AddTo(_compositeDisposable);
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}
}
(Reactive)Presenterの領域は以上です。
ここからは、独自なりの設計思想である「Process」スクリプトの紹介です。
Presenterが「Model→View」ならば、Processは「View→Model」といえるでしょう。またProcessは「View→View」もありえます。
GameStarterEventProcess.cs
GameStarter.csのイベントを監視し、ゲームがスタートしてから一定時間経過後の処理を行うProcessスクリプトです。
using System;
using System.Collections.Generic;
using Enums;
using Models;
using UniRx;
using UnityEngine;
using VContainer;
using VContainer.Unity;
using Views;
namespace Processes.Events
{
public class GameStarterEventProcess : IInitializable, IDisposable
{
private readonly ConvertHiraganaToRomanModel _convertHiraganaToRomanModel;
private readonly CurrentTypingTextModel _currentTypingTextModel;
private readonly GameStateModel _gameStateModel;
private readonly TypingSpeedModel _typingSpeedModel;
private readonly TypingTextStoreModel _typingTextStoreModel;
private readonly NextTypingTextModel _nextTypingTextModel;
private readonly GameStarter _gameStarter;
private readonly CompositeDisposable _compositeDisposable = new();
[Inject]
public GameStarterEventProcess(ConvertHiraganaToRomanModel convertHiraganaToRomanModel,
CurrentTypingTextModel currentTypingTextModel, GameStateModel gameStateModel,
NextTypingTextModel nextTypingTextModel, TypingSpeedModel typingSpeedModel,
TypingTextStoreModel typingTextStoreModel, GameStarter gameStarter)
{
_convertHiraganaToRomanModel = convertHiraganaToRomanModel;
_currentTypingTextModel = currentTypingTextModel;
_gameStateModel = gameStateModel;
_nextTypingTextModel = nextTypingTextModel;
_typingSpeedModel = typingSpeedModel;
_typingTextStoreModel = typingTextStoreModel;
_gameStarter = gameStarter;
}
public void Initialize()
{
_gameStarter.OnGameStart.Subscribe(_ =>
{
var isValid = false;
if (SystemInfo.operatingSystem.Contains("Windows"))
{
_currentTypingTextModel.SetOperatingSystemName(OperatingSystemName.Windows);
isValid = true;
}
if (SystemInfo.operatingSystem.Contains("Mac"))
{
_currentTypingTextModel.SetOperatingSystemName(OperatingSystemName.Mac);
isValid = true;
}
if (isValid)
{
_nextTypingTextModel.SetTypingText(_typingTextStoreModel.RandomTypingText);
var currentTypingText = _typingTextStoreModel.RandomTypingText;
_currentTypingTextModel.SetTitle(currentTypingText.title);
_currentTypingTextModel.SetCharacters(
_convertHiraganaToRomanModel.ConvertHiraganaToRoman(
new List<char>(currentTypingText.hiragana)));
_typingSpeedModel.StartMeasure();
_gameStateModel.ChangeGameStateName(GameStateName.Typing);
}
}).AddTo(_compositeDisposable);
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}
}
デバイスがWindowsないしMacならゲームを開始させます。
KeyDetectorEventProcess.cs
KeyDetector.csを監視し、キーの入力処理を行うスクリプトです。
using System;
using System.Collections.Generic;
using Enums;
using Models;
using UniRx;
using VContainer;
using VContainer.Unity;
using Views;
namespace Processes.Events
{
public class KeyDetectorEventProcess : IInitializable, IDisposable
{
private readonly ConvertHiraganaToRomanModel _convertHiraganaToRomanModel;
private readonly CurrentTypingTextModel _currentTypingTextModel;
private readonly GameStateModel _gameStateModel;
private readonly NextTypingTextModel _nextTypingTextModel;
private readonly TypingSpeedModel _typingSpeedModel;
private readonly TypingTextStoreModel _typingTextStoreModel;
private readonly KeyDetector _keyDetector;
private readonly CompositeDisposable _compositeDisposable = new();
[Inject]
public KeyDetectorEventProcess(ConvertHiraganaToRomanModel convertHiraganaToRomanModel,
CurrentTypingTextModel currentTypingTextModel, GameStateModel gameStateModel,
NextTypingTextModel nextTypingTextModel, TypingSpeedModel typingSpeedModel,
TypingTextStoreModel typingTextStoreModel, KeyDetector keyDetector)
{
_convertHiraganaToRomanModel = convertHiraganaToRomanModel;
_currentTypingTextModel = currentTypingTextModel;
_gameStateModel = gameStateModel;
_nextTypingTextModel = nextTypingTextModel;
_typingSpeedModel = typingSpeedModel;
_typingTextStoreModel = typingTextStoreModel;
_keyDetector = keyDetector;
}
public void Initialize()
{
_keyDetector.OnCharacterDetected.Subscribe(character =>
{
switch (_gameStateModel.GameStateName)
{
case GameStateName.Typing:
{
switch (_currentTypingTextModel.TypeCharacter(character))
{
case TypeResult.Correct:
{
_typingSpeedModel.IncreaseTypeCount();
break;
}
case TypeResult.Incorrect:
{
break;
}
case TypeResult.Finished:
{
_typingSpeedModel.IncreaseTypeCount();
_typingSpeedModel.CalculateTypingSpeed();
_typingSpeedModel.StartMeasure();
var currentTypingText = _nextTypingTextModel.TypingText;
_currentTypingTextModel.SetTitle(currentTypingText.title);
_currentTypingTextModel.SetCharacters(
_convertHiraganaToRomanModel.ConvertHiraganaToRoman(
new List<char>(currentTypingText.hiragana)));
_currentTypingTextModel.ResetCharactersIndex();
_nextTypingTextModel.SetTypingText(_typingTextStoreModel.RandomTypingText);
break;
}
}
break;
}
}
}).AddTo(_compositeDisposable);
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}
}
UpdateProcess.cs
毎フレーム処理をするスクリプトです。
using Enums;
using Models;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Processes
{
public class UpdateProcess : ITickable
{
private readonly GameStateModel _gameStateModel;
private readonly TypingSpeedModel _typingSpeedModel;
[Inject]
public UpdateProcess(GameStateModel gameStateModel, TypingSpeedModel typingSpeedModel)
{
_gameStateModel = gameStateModel;
_typingSpeedModel = typingSpeedModel;
}
public void Tick()
{
switch (_gameStateModel.GameStateName)
{
case GameStateName.Typing:
{
_typingSpeedModel.AddTime(Time.deltaTime);
break;
}
}
}
}
}
タイピングの速度の計測のために用いました。
最後に、これらのコンポーネントの依存関係を解決するためのLifetimeScopeスクリプトを作成します。
GameLifetimeScope.cs
using Models;
using Presenters;
using Processes;
using Processes.Events;
using VContainer;
using VContainer.Unity;
using Views;
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// ------------------------------ Models ------------------------------
builder.Register<ConvertHiraganaToRomanModel>(Lifetime.Singleton);
builder.Register<CurrentTypingTextModel>(Lifetime.Singleton);
builder.Register<GameStateModel>(Lifetime.Singleton);
builder.Register<NextTypingTextModel>(Lifetime.Singleton);
builder.Register<TypingSpeedModel>(Lifetime.Singleton);
builder.Register<TypingTextStoreModel>(Lifetime.Singleton);
// ------------------------------ Views ------------------------------
builder.RegisterComponentInHierarchy<GameStarter>();
builder.RegisterComponentInHierarchy<KeyDetector>();
builder.RegisterComponentInHierarchy<NextTypingTextView>();
builder.RegisterComponentInHierarchy<TypingSpeedView>();
builder.RegisterComponentInHierarchy<TypingTextView>();
// ------------------------------ Presenters ------------------------------
builder.RegisterEntryPoint<NextTypingTextViewPresenter>();
builder.RegisterEntryPoint<TypingSpeedViewPresenter>();
builder.RegisterEntryPoint<TypingTextViewPresenter>();
// ------------------------------ Processes ------------------------------
builder.RegisterEntryPoint<GameStarterEventProcess>();
builder.RegisterEntryPoint<KeyDetectorEventProcess>();
builder.RegisterEntryPoint<UpdateProcess>();
}
}
あとは適宜GameObjectを配置すれば完成です。
この記事が気に入ったらサポートをしてみませんか?