20210415のUnityに関する記事は7件です。

[Unity] 現在のECS環境でシンプルな Boids Simulation を書く

ずいぶん前に Unity ECS の勉強をしたときにhecomiさんのBoidsシミュレーションを題材に使わせていただいたのですが、UnityのDOTS環境がどんどん変わっていって現行環境に合わせて大規模なリファクタリングが必要になったので、その際の情報を備忘録としてまとめます。 環境 (2021/4/15 時点) Unity 2020.3.4f Hybrid Renderer 0.11.0-preview.42 Entities 0.17.0.preview.41 当方の実装結果 GitHub 現在のECSの書き方 ▽Prefab Entity の準備 public class Bootstrap : MonoBehaviour { public static Bootstrap Instance { get; private set; } public static Param Param { get { return Instance.param; } } // Inspector で Prefab を設定しておく [SerializeField] GameObject prefab_obj; // Prefab Entity 保持用の変数 private Entity prefab_entity; // Boid の全体数の管理用 private int n_boid; void Awake() { Instance = this; } public void Start() { var world = World.DefaultGameObjectInjectionWorld; var manager = world.EntityManager; // convert prefab_obj -> prefab_entity prefab_entity = GameObjectConversionUtility.ConvertGameObjectHierarchy( prefab_obj, GameObjectConversionSettings.FromWorld(world, null) ); // add user defined component manager.AddComponent<BoidPrefabType>(prefab_entity); // Prefabであることを示す空のComponent manager.AddComponent<Scale>(prefab_entity); manager.AddComponent<Velocity>(prefab_entity); manager.AddComponent<Acceleration>(prefab_entity); manager.AddComponent<NeighborsEntityBuffer>(prefab_entity); n_boid = 0; } } たびたび Hybrid Renderer が使用する Component が追加されているようで、自力で ArchType を組み立てるやり方だと、適当にアップグレードしたときに描画されなくなります。 また、 Hybrid Renderer も設定(V2を使うか否か)によって動かない場合があり、原因の特定に手間取りました。 公式サンプルもそうですが、Hybrid ECS用のAPIを活用してGameObjectから必要な ArchType を構築すれば上記の仕様変更にも自動的に追従できることになり、ほんの少しリスクが減ります。 PrefabなのでSystemの操作対象に紛れ込まないよう、空のコンポーネント BoidPrefabType をつけています。 ▽Entityの生成、破棄 public class Bootstrap : MonoBehaviour { public static Bootstrap Instance { get; private set; } public static Param Param { get { return Instance.param; } } [SerializeField] public float boidScale = 1.0f; [SerializeField] public Param param; [SerializeField] GameObject prefab_obj; private Entity prefab_entity; private int n_boid; private Unity.Mathematics.Random random; /* 略 */ void UpdateBoidNum(int n_tgt) { if (n_tgt < 0) return; var manager = World.DefaultGameObjectInjectionWorld.EntityManager; int n_diff = n_tgt - n_boid; if (n_diff > 0) { Debug.Log($"update boids num: add {n_diff} boids."); var scale = this.boidScale; var initSpeed = this.param.initSpeed; for (int i = 0; i < n_diff; i++) { var entity = manager.Instantiate(prefab_entity); // BoidPrefabType を BoidType に付け替え manager.RemoveComponent<BoidPrefabType>(entity); manager.AddComponent<BoidType>(entity); // 適当に値を設定 manager.SetComponentData(entity, new Translation { Value = this.random.NextFloat3(1f) }); manager.SetComponentData(entity, new Rotation { Value = quaternion.identity }); manager.SetComponentData(entity, new Scale { Value = scale }); manager.SetComponentData(entity, new Velocity { Value = this.random.NextFloat3Direction() * initSpeed }); manager.SetComponentData(entity, new Acceleration { Value = float3.zero }); } } if (n_diff < 0) { int n_delete = -n_diff; Debug.Log($"update boids num: remove {n_delete} boids."); var entity_query = manager.CreateEntityQuery(new EntityQueryDesc { All = new[] { ComponentType.ReadOnly<BoidType>() } }); var entities = entity_query.ToEntityArray(Allocator.Temp); manager.DestroyEntity(new NativeSlice<Entity>(entities, n_tgt)); entities.Dispose(); } n_boid = n_tgt; } } Entityの生成については元の実装と大して違いはありません。 破棄については、今回は全体の数だけを考えて増減させるため、 BoidType に該当するもの全体の情報にアクセスする必要があり、そのために EntityQuery の生成 -> 対象となる NativeArray<Entity> の取得という流れで処理しています。 このへんは、状況に合わせて公式サンプルの SpawnFromMonoBehaviour , SpawnFromEntity, SpawnAndRemove あたりを参考にするのがよいでしょう。 ▽Entities.ForEach によるジョブ実装例 まず、entity 自身の情報が主な場合、今回の例でいえば MoveSystem の実装は下記のとおり。 [UpdateAfter(typeof(BoidSystemGroup))] public class MoveSystem : SystemBase { protected override void OnUpdate() { // パラメータをローカル変数として置いておき、ラムダ式にキャプチャさせる float dt = Time.DeltaTime; float minSpeed = Bootstrap.Param.maxSpeed; float maxSpeed = Bootstrap.Param.maxSpeed; // ジョブの定義 Dependency = Entities. WithName("MoveJob"). WithAll<BoidType>(). WithBurst(). ForEach((ref Translation pos, ref Rotation rotate, ref Velocity vel, ref Acceleration accel) => { vel.Value += accel.Value * dt; var dir = math.normalize(vel.Value); var speed = math.length(vel.Value); vel.Value = math.clamp(speed, minSpeed, maxSpeed) * dir; pos.Value += vel.Value * dt; var rot = quaternion.LookRotationSafe(dir, new float3(0, 1, 0)); rotate.Value = rot; accel.Value = float3.zero; }).ScheduleParallel(Dependency); } } float や int など単純な struct は適当にOnUpdate() 関数のローカル変数においておけば、同じスコープで書くラムダ式でキャプチャしてくれるので簡単に使えます。 ラムダ式の引数は、C#の仕様かEntitiesの仕様かはわかりませんが、 値渡し(実質 Entity 型のみ)、ref 渡し、in 渡し の順番で並べる必要があります。また、Entity 型以外を値渡しすると、処理中で書き換える引数に ref を、書き換えない引数には in をつけるように警告されます。 Entities と ForEach() の間にある With** 関数でエンティティクエリに属性を追加できます。 主なものはReferenceによれば下表のとおり。 エンティティクエリ 効果 WithName(string) Jobの名前を指定。プロファイラやEntityDebuggerでこの名前が使われる WithBurst Burst Compiler による最適化を適用 WithAll<T1, T2, T3> 処理対象のEntityはすべてのComponentTypeを持つ必要がある WithAny<T1, T2, T3> 処理対象のEntityはいずれかのComponentTypeを持つ必要がある WithNone<T1, T2, T3> 処理対象のEntityはいずれのComponentTypeも含まない必要がある WithChangeFilter<T1, T2> 指定されたComponentが前回のSystem実行時に変更されたものだけを処理する WithSharedComponentFilter(ISharedComponentData) ISharedComponentDataが特定の値のEntityだけを処理する WithEntityQueryOptions(EntityQueryOptions) EntityQueryOptionsに合致するものだけを処理する WithStoreEntityQueryInField(EntityQuery) SystemBase.EntityQuery に ForEach実行時に作成された EntityQuery を保存する。 対象Entityの全体数など、EntityQueryのインターフェイスを使いたい場合に用いる 上表中の <T1, T2, T3> は <T> または <T1, T2> に型パラメータを省略できます。(最大数は表中のとおり) 次に、自身以外の Entity の情報も必要な処理の例として、 CohesionSystem の実装は下記のとおり。 [UpdateInGroup(typeof(BoidSystemGroup))] public class CohesionSystem : SystemBase { private struct CohesionDataContainer { [ReadOnly] public float alignmentWeight; [ReadOnly] public ComponentDataFromEntity<Translation> positionFromGrovalEntity; } protected override void OnUpdate() { var common_data = new CohesionDataContainer { alignmentWeight = Bootstrap.Param.alignmentWeight, positionFromGrovalEntity = GetComponentDataFromEntity<Translation>(true), }; Dependency = Entities. WithName("CohesionJob"). WithAll<BoidType>(). WithBurst(). ForEach( (ref Acceleration accel, in Translation pos, in DynamicBuffer<NeighborsEntityBuffer> neighbors) => { if (neighbors.Length == 0) return; float3 pos_avg = float3.zero; float3 pos0 = pos.Value; float3 acc = accel.Value; for(int i=0; i<neighbors.Length; i++) { pos_avg += common_data.positionFromGrovalEntity[neighbors[i].entity].Value; } pos_avg /= neighbors.Length; acc += (pos_avg - pos0) * common_data.alignmentWeight; accel = new Acceleration { Value = acc }; } ).ScheduleParallel(Dependency); } } ここで、 Entity 経由で他の Boid の Translation を参照するため、全EntityのTranslationへのアクセサを SystemBase.GetComponentDataFromEntity<Translation>(isReadOnly) で取得しています。このSystemでは Translation は書き換えないので isReadOnly = true です。この場合代入先は [ReadOnly] アトリビュートが付加されている必要があり、そのままでは関数内の一時変数には代入できません。 そのため、このデータの代入先として struct CohesionDataContainer の中に [ReadOnly] メンバを用意し、 CohesionDataContainer を一時変数として作ることでラムダ式にキャプチャさせることができます。 ▼IJobEntityBatch の例 (非推奨) IJobEntityBatch を使って CohesionSystem を書くと下記のようになります。 [UpdateInGroup(typeof(BoidSystemGroup))] public class CohesionSystem : SystemBase { // チャンク検索のため EntityQuery が必須 (IJobEntityBatch.Schedule()の引数) private EntityQuery query; protected override void OnCreate() { base.OnCreate(); query = GetEntityQuery(new EntityQueryDesc { All = new[] { ComponentType.ReadOnly<BoidType>(), ComponentType.ReadOnly<Translation>(), ComponentType.ReadOnly<NeighborsEntityBuffer>(), ComponentType.ReadWrite<Acceleration>() } }); } [BurstCompile] public struct CohesionJob : IJobEntityBatch { [ReadOnly] public float alignmentWeight; [ReadOnly] public ComponentDataFromEntity<Translation> positionFromGrovalEntity; // アクセサのハンドルを受け取るためのメンバ。 var で受け取れないのでひたすら面倒 [ReadOnly] public ComponentTypeHandle<Translation> translationHandle; [ReadOnly] public BufferTypeHandle<NeighborsEntityBuffer> neighborsBufferHandle; public ComponentTypeHandle<Acceleration> accelHandle; public void Execute(ArchetypeChunk batchInChunk, int batchIndex) { var pos_array = batchInChunk.GetNativeArray(translationHandle); var neighbors_list = batchInChunk.GetBufferAccessor(neighborsBufferHandle); var acc_array = batchInChunk.GetNativeArray(accelHandle); // チャンク内のイテレーションは自分で書く。 // 1段ネストが増える分手間だしバグ混入の危険も増える for(int i=0; i<batchInChunk.Count; i++) { var neighbors = neighbors_list[i]; if (neighbors.Length == 0) continue; float3 pos_avg = float3.zero; float3 pos0 = pos_array[i].Value; float3 acc = acc_array[i].Value; for (int j = 0; j < neighbors.Length; j++) { pos_avg += positionFromGrovalEntity[neighbors[j].entity].Value; } pos_avg /= neighbors.Length; acc += (pos_avg - pos0) * alignmentWeight; acc_array[i] = new Acceleration { Value = acc }; } // ComponentType のアクセサが NativeArray<> を返すので Dispose する必要がある pos_array.Dispose(); acc_array.Dispose(); } } protected override void OnUpdate() { var param = Bootstrap.Param; var job = new CohesionJob { alignmentWeight = param.alignmentWeight, positionFromGrovalEntity = GetComponentDataFromEntity<Translation>(true), // 必要な ComponentType, BufferType の Handle をいちいち渡す必要がある translationHandle = GetComponentTypeHandle<Translation>(), neighborsBufferHandle = GetBufferTypeHandle<NeighborsEntityBuffer>(), accelHandle = GetComponentTypeHandle<Acceleration>() }; // ここで渡す EntityQuery も自分で組み立てる必要がある Dependency = job.ScheduleParallel(query, 1, Dependency); } } 上記は Entities.ForEach の例と同じ処理ですが、書かなければならないコードの量と複雑さが圧倒的です。 そのため、公式でも 「どうしてもIJobEntityBatchを使う必要がない限りEntities.ForEachを使おう」 と言っています。 参考記事 Original implementation: Unity で Boids シミュレーションを作成して Entity Component System (ECS) を学んでみた Qiita: ECSの簡単な実装と変更点について 既存のUnityプロジェクトをUniversalRenderPipelineに移行する -サードインパクト修復- Official: Entities Hybrid Renderer EntityComponentSystemSamples
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでゲーム開発

今回は「 Carrom 」というボードゲームを題材に Unity と Blender を利用して開発しました。 Carrom とは ・ボードゲーム ・ビリヤードによく似ている ・自分のコイン(ストライカー)を指で弾いて中心のコインにぶつけて  四隅の穴(ポケット)に入れる ルールの設定 ・2人で行う ・交代制 ・青チームは白いコインを、緑チームは茶色のコインを全て落とし  最後に赤いコインを落とすと勝利 ・真ん中の赤いコインは得点が3点 他のコインは1点 その他にも細々としたルールを設定していますが 大きな流れは上記のようになっています。 Background と Object を配置 初めに Background (背景)や Object (コイン、ストライカー)を配置しました。 配置しただけなので動きはない状態です。 スコア機能の作成 スコアTextをゲーム画面に表示させ、 四隅の黒い穴にコインが当たったらスコアText に点数を追加するスクリプトを作成しました。 中央の赤いコインは得点が3点なので Tag( red )で判別させます。 不具合が発生しないか確認し無事実装完了! ...ですが、 開発途中にスコア機能が勝敗に関係ない事に気付きました。 設計書を作成している途中で気づくべきミスだったと反省しています。 せっかく作ったのでスコアは表示させたままにしました。 ストライカーの動作処理 今回はクリックした場所にストライカーを飛ばすよう設定しました。 Striker.cs if (Input.GetMouseButtonDown(0)) { Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); Vector3 shotForward = mouseWorldPos - transform.position; shotForward = Vector3.Scale(shotForward, new Vector3(1, 0, 1)) shotForward = shotForward.normalized; rigidbody.AddForce(shotForward * strikerSpeed); } 実装後Unity 内 Main Cameraの Projection を Orthographic に切り替えます。 Perspective ... 遠近法を使った描写 Orthographic ... 平行投射を使った描写 Perspectiveを利用するとゲーム画面に奥行きができます。今回の開発したゲームは遠近法を利用しないものだったので Orthographic を利用しました。 ターン制の実装 対戦相手用のObject(ストライカー)を配置後、ターン制を実装させるための スクリプトを作成しました。 ターン制は真偽値を利用しました。 下記はターン制を簡単に再現したコードです。 GameManager.cs public classGameManager : MonoBehaviour { public GameObject Sphere; public static bool PlayerTurn; void Start() { Sphere.SetActive (true); PlayerTurn = true; } void Update() { if (PlayerTurn == true) { Sphere.SetActive (false); PlayerTurn = false; } } } 相手のスコア機能実装 対戦相手のスコアText を作成し、コインにTagを付けます。 コインの色を Tag で区別し(白は white 、茶色は brown ) 穴に入ったコインの Tag が whiteだった場合は青に1点 brownだった場合は緑に1点 としました。 勝利TextとRETRYボタンの実装 if 自分のコインが全て落ちた && 赤コインが落ちた 場合 勝利Textを表示 としました。 緑用のTextも用意し、全て落ちたコインのTagによって表示させるTextを分岐させています。 上手く機能しますがかっこいいフォントが見つからず一番修正したい部分です。 RETRYボタンを勝利Textと同じタイミングで表示させます。 ボタン押下後、スコアを0にし、コインとストライカーをスタート時と同じ位置に配置させました。 Blenderでコイン作成 配置 Blenderでコインを作成しUnityにエクスポートしました。 配置前(左)と配置後(右)です。コインが変わるだけでゲームの印象がガラリと変わります。 まとめ 個人でのゲーム開発は初めてでしたがとても面白かったです。 色々な気付きや発見、失敗がありました。 技術不足はもちろんですが、設計書に関しても反省がありました。 開発途中に設計書のミスに気付き修正した部分が多く、設計書の荒が課題だと感じました。 今回の学びをこれからのものづくりに活かしていきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mac を買ったので Unity と VS Code で今日からゲーム開発

あらすじ 「今日Macを買いましたので、これからゲーム開発環境を整えます」風。 手順 HomebrewとChromeは最初に入れておくのがベタベターですね。 下記がインストールの際の依存関係階層図です。 ほぼほぼほぼ順不同です。そのため、この記事では敢えて順不同で書きます。 Homebrew をインストール Chrome をインストール VS Code をインストール 拡張機能 Debugger for Unity をインストール 拡張機能 Mono Debug をインストール 拡張機能 C# をインストール Unity Hub をインストール Unity をインストール .NET Core をインストール Mono をインストール X Code をインストール 各種設定を行う Homebrew いろんなアプリのインストール手順がめんどくさいので、サボるためにHomebrewをインストールします。 https://brew.sh/ に行きます インストール用のワンライナースクリプトをコピーします command+spaceで スポットライト検索を開きます terminalと検索します ターミナルが出てくるのでそのまま Enterで起動します 先ほどのワンライナースクリプトを command+vで貼り付けます Enterでワンライナースクリプトを実行します 「 was successfully installed!」とか表示されれば完了です Chrome Homebrewを使ってインストールします。 先ほど開いたターミナルにカーソルを置きます brew cask install google-chromeと入力します Enterでインストールを実行します 「https://docs.unity3d.com/Manual/index.html」をブックマークに登録します VS Code Homebrewを使ってインストールします。 先ほど開いたターミナルにカーソルを置きます brew cask install visual-studio-codeと入力します Enterでインストールを実行します 下記の3つの拡張機能は必須です。 https://marketplace.visualstudio.com/items?itemName=Unity.unity-debug https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp https://marketplace.visualstudio.com/items?itemName=ms-vscode.mono-debug インストールはもちろんコマンドです。ターミナルで下記を実行します。 code --install-extension Unity.unity-debug code --install-extension ms-vscode.mono-debug ※vscodeのインストール後にターミナルを開きなおしてから行ってください。 ms-dotnettools.csharpはUnity.unity-debugをインストールする時に半自動的にインストールされます。 .Net Core Homebrewを使ってインストールします。 先ほど開いたターミナルにカーソルを置きます brew install dotnetと入力します Enterでインストールを実行します Mono Homebrewを使ってインストールします。 先ほど開いたターミナルにカーソルを置きます brew install monoと入力します Enterでインストールを実行します Unity Hub Homebrewを使ってインストールします。 先ほど開いたターミナルにカーソルを置きます brew cask install google-chromeと入力します Enterでインストールを実行します ※Unity Hub は、素晴らしいことに、"cask"オプション無しでもしっかりインストールできます。エイリアス化してあるんですね。ログをよく見るとcaskを呼び出してます。 Unity Homebrewを使ってインストールはできません。 しーかたがないので、Unity Hubからインストールします。 先ほど Unity Hub をインストールしたことを思い返します command+spaceで スポットライト検索を開きます unityと検索します Unity Hubが出てくるのでそのまま Enterで起動します 左上のアカウントボタンをクリックします サインインボタンをクリックします(アカウントは事前に作成しておきましょう) 「ACTIVATE NEW LICENSE」ボタンをクリックします 私の場合は「Personal」を選びました 「Done」ボタンをクリックした後に再度サインインを求められる場合もあるようです 下記のようになっていればライセンス認証が完了です ちなみに、「MANUAL ACTIVATION」ボタンを選ぶと、「Unity Hub」でライセンス認証用ファイルが生成されます。このファイルを「Unity Hub」経由の手動でUnityに送りつけるとライセンスファイルが返送されてきます。このライセンスファイルを「Unity Hub」に手動で認識させて完了です。 ここまできたら、上部の「← Preferense」から一度戻って、左側のメニューから「Installs」を選びます 「ADD」ボタンをクリックします LTSが付いていて一番最近の西暦年号に近いバージョンを選んでおきます 「NEXT」ボタンをクリックします 必要な項目だけチェックを入れて「NEXT」します Andoid Build Support iOS Build Support 日本語 EULAを流し読みします プログレズバーが進むので、完了っぽくなったらインストール完了です Xcode 残念ながら「Homebrew」ではインストールできないようなので、「App Store」からインストールします。 command+spaceで スポットライト検索を開きます storeと検索します App Storeが出てくるのでそのまま Enterで起動します xcodeで検索するとでてくるので「Get」ボタンをクリックします そうすると「Install」ボタンに切り替わるので、(めんどくさっ!と思いながら)再びクリックします ちなみにこの時、AppleIDでログインさせられるので、ない人は事前に作成しましょう インストールが完了したので、xcodeを起動して、ログインします。 ちなみに 前はbrew installやupgradeの前に、brew 自体のアップデートが必要だったりしましたが、今は自動でやってくれるので必要ありません。また、brew cask installが必要なものは、brew installすると勝手にcaskを追加して実行してくれることが増えました。さらに、以前はhomebrewをインストールする前にxcodeを自前でインストールする必要がありました。これも、現在はインストールスクリプトを実行すると自動で行われるので意識しなくて良くなりました。 Excelsior!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【実機あり】MRTK ver2.6.1 の Example を一通り試してみた 2

はじめに https://qiita.com/h_koya/items/6f0bb900a0b517d55e74 前回の続きです。 実機でないと確認できない部分をまとめました。 MRTK ver2.6.1 の Example 実機なしで確認できない部分を実機で行いました。 環境 Windows 10 Enterprise Unity 2019.4.1f1 MRTK 2.6.1 Boundary BoundaryVisualization VR用の機能です。 プレイエリアの可視化ができるようです。 ハンドトラッキング HandMenu MRTK ver2.6.1 の HandMenu のデモ動画です!手のひらを表にするとメニューが表示されます。#MRTK #HoloLens2 pic.twitter.com/lGT4oXpQjE— 原口昂弥 (@k_haraguchi55) April 13, 2021 LeapMotionHandTracing Leap Motion Controller が必要です。 持っていないので試せませんでした。 PulseShader PulseShaderHandMesh ハンドメッシュをパルスで確認できます。 FPSが下がって動画が取れなかったです。、、、 ReadingMode MRTK ver2.6.1 の ReadingMode のデモ動画です!ReadingModeの体験ができます。ReadingModeは解像度が向上する代わりに視野角が狭くなります。#MRTK #HoloLens2 pic.twitter.com/BFimV3VnqQ— 原口昂弥 (@k_haraguchi55) April 15, 2021 Solvers SurfaceMagnetismSpatialAwareness MRTK ver2.6.1 の Surface Magnetism + Spatial Awarenessのデモ動画です!認識した空間の表面にオブジェクト配置できます。#MRTK #HoloLens2 pic.twitter.com/y1R4I13pTo— 原口昂弥 (@k_haraguchi55) April 13, 2021 SpatialAwareness SpatialAwarenessMesh MRTK ver2.6.1 の Spatial Awareness Mesh のデモ動画です!空間メッシュを視覚化できます。#MRTK #HoloLens2 pic.twitter.com/6hqsQgeZkc— 原口昂弥 (@k_haraguchi55) April 13, 2021
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】柔軟な入力に対応したタイピングゲームの作り方(改良編)【C#】

はじめに 前回【Unity】柔軟な入力に対応したタイピングゲームの作り方(導入編)【C#】という記事の続きです。 現在のタイピングゲームの要となる「柔軟な入力(「つ」→「tsu」、「ふ」→「fu」)」に対応させます。 WindowsとMacで異なる入力方法 タイピングの入力方法については、以下のURLが参考になります。 Windows:https://www.cc.saga-u.ac.jp/system/CenterSystem/ime_romaji.htm Mac:https://support.apple.com/ja-jp/guide/japanese-input-method/jpim10277/6.2.1/mac/10.15 よくよく確認すると、WindowsとMacでは入力方法が異なることがわかります。(例:Windowsでは「か」を「ca」と入力できるが、Macではできない) なので、WindowsとMacを区別する必要があります。 ちなみにこの機能は省略しても構いません。 TypingManager.cs public class TypingManager : MonoBehaviour { private bool _isWindows; //追加する private bool _isMac; //追加する void Start() { // 以下を追加する if(SystemInfo.operatingSystem.Contains("Windows")) { _isWindows = true; } if(SystemInfo.operatingSystem.Contains("Mac")) { _isMac = true; } // ここまで } } 柔軟な入力に対応させるアルゴリズム「前後比較法」 現在の日本語入力タイピングゲームにおいて、柔軟な入力に対応させるアルゴリズムはいくつか存在します。 今回は「前後比較法(私が勝手に名付けた方法です)」を使います。 「前後比較法」は、今入力すべき文字とその文字の前後を比較することによって「どのひらがなを入力するのか」を判断して、入力してもOKな文字を抽出するアルゴリズムです。 例えば「うちわ(utiwa)」を考えましょう。 まず最初の入力文字が「u」なので、入力するひらがなは「う」であることが判明します。「う」の別入力が「wu」であるので「w」を入力しても構わないことがわかります。 当然ですが、別入力を行いますとローマ字を改良する必要があります。今回の例で「w」を入力するとローマ字表記は「wutiwa」に変化します。 次の入力文字も「u」ですが、前の文字が「w」なので「う」の入力途中であることが判明します。よって今回は「w」を入力することができません。 そして「u」の入力後、入力文字が「t」になります。この入力文字の前が母音で、後の文字が「i」なので「ち」を入力することが判明します。同様に「ち」の別入力は「chi」なので「c」を入力してもOKとなります。 もちろん「c」を入力した後は「wuchiwa」に変形する必要があります。 こんな感じでほかの文字に対応させることができます。 InputKey() を大幅に改造しよう! 上記のアルゴリズムを用いて、InputKey() を大幅に改造します。 TypingManager.cs 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 1; } if (_isWindows && inputChar == 'y' && currentChar == 'i' && prevChar == 'n' && prevChar2 == 'n' && prevChar3 != 'n') { _roman.Insert(_romanIndex, 'y'); return 1; } if (_isWindows && inputChar == 'y' && currentChar == 'i' && prevChar == 'n' && prevChar2 == 'x') { _roman.Insert(_romanIndex, 'y'); return 1; } //「う」の柔軟な入力(「whu」はWindowsのみ) if (inputChar == 'w' && currentChar == 'u' && (prevChar == '\0' || prevChar == 'a' || prevChar == 'i' || prevChar == 'u' || prevChar == 'e' || prevChar == 'o')) { _roman.Insert(_romanIndex, 'w'); return 1; } if (inputChar == 'w' && currentChar == 'u' && prevChar == 'n' && prevChar2 == 'n' && prevChar3 != 'n') { _roman.Insert(_romanIndex, 'w'); return 1; } if (inputChar == 'w' && currentChar == 'u' && prevChar == 'n' && prevChar2 == 'x') { _roman.Insert(_romanIndex, 'w'); return 1; } if (_isWindows && inputChar == 'h' && prevChar2 != 't' && prevChar2 != 'd' && prevChar == 'w' && currentChar == 'u') { _roman.Insert(_romanIndex, 'h'); return 1; } //「か」「く」「こ」の柔軟な入力(Windowsのみ) if (_isWindows && inputChar == 'c' && prevChar != 'k' && currentChar == 'k' && (nextChar == 'a' || nextChar == 'u' || nextChar == 'o')) { _roman[_romanIndex] = 'c'; return 1; } //「く」の柔軟な入力(Windowsのみ) if (_isWindows && inputChar == 'q' && prevChar != 'k' && currentChar == 'k' && nextChar == 'u') { _roman[_romanIndex] = 'q'; return 1; } //「し」の柔軟な入力 if (inputChar == 'h' && prevChar == 's' && currentChar == 'i') { _roman.Insert(_romanIndex, 'h'); return 1; } //「じ」の柔軟な入力 if (inputChar == 'j' && currentChar == 'z' && nextChar == 'i') { _roman[_romanIndex] = 'j'; return 1; } //「しゃ」「しゅ」「しぇ」「しょ」の柔軟な入力 if (inputChar == 'h' && prevChar == 's' && currentChar == 'y') { _roman[_romanIndex] = 'h'; return 1; } //「じゃ」「じゅ」「じぇ」「じょ」の柔軟な入力 if (inputChar == 'z' && prevChar != 'j' && currentChar == 'j' && (nextChar == 'a' || nextChar == 'u' || nextChar == 'e' || nextChar == 'o')) { _roman[_romanIndex] = 'z'; _roman.Insert(_romanIndex + 1, 'y'); return 1; } if (inputChar == 'y' && prevChar == 'j' && (currentChar == 'a' || currentChar == 'u' || currentChar == 'e' || currentChar == 'o')) { _roman.Insert(_romanIndex, 'y'); return 1; } //「し」「せ」の柔軟な入力(Windowsのみ) if (_isWindows && inputChar == 'c' && prevChar != 's' && currentChar == 's' && (nextChar == 'i' || nextChar == 'e')) { _roman[_romanIndex] = 'c'; return 1; } //「ち」の柔軟な入力 if (inputChar == 'c' && prevChar != 't' && currentChar == 't' && nextChar == 'i') { _roman[_romanIndex] = 'c'; _roman.Insert(_romanIndex + 1, 'h'); return 1; } //「ちゃ」「ちゅ」「ちぇ」「ちょ」の柔軟な入力 if (inputChar == 'c' && prevChar != 't' && currentChar == 't' && nextChar == 'y') { _roman[_romanIndex] = 'c'; return 1; } //「cya」=>「cha」 if (inputChar == 'h' && prevChar == 'c' && currentChar == 'y') { _roman[_romanIndex] = 'h'; return 1; } //「つ」の柔軟な入力 if (inputChar == 's' && prevChar == 't' && currentChar == 'u') { _roman.Insert(_romanIndex, 's'); return 1; } //「つぁ」「つぃ」「つぇ」「つぉ」の柔軟な入力 if (inputChar == 'u' && prevChar == 't' && currentChar == 's' && (nextChar == 'a' || nextChar == 'i' || nextChar == 'e' || nextChar == 'o')) { _roman[_romanIndex] = 'u'; _roman.Insert(_romanIndex + 1, 'x'); return 1; } 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 1; } //「てぃ」の柔軟な入力 if (inputChar == 'e' && prevChar == 't' && currentChar == 'h' && nextChar == 'i') { _roman[_romanIndex] = 'e'; _roman.Insert(_romanIndex + 1, 'x'); return 1; } //「でぃ」の柔軟な入力 if (inputChar == 'e' && prevChar == 'd' && currentChar == 'h' && nextChar == 'i') { _roman[_romanIndex] = 'e'; _roman.Insert(_romanIndex + 1, 'x'); return 1; } //「でゅ」の柔軟な入力 if (inputChar == 'e' && prevChar == 'd' && currentChar == 'h' && nextChar == 'u') { _roman[_romanIndex] = 'e'; _roman.Insert(_romanIndex + 1, 'x'); _roman.Insert(_romanIndex + 2, 'y'); return 1; } //「とぅ」の柔軟な入力 if (inputChar == 'o' && prevChar == 't' && currentChar == 'w' && nextChar == 'u') { _roman[_romanIndex] = 'o'; _roman.Insert(_romanIndex + 1, 'x'); return 1; } //「どぅ」の柔軟な入力 if (inputChar == 'o' && prevChar == 'd' && currentChar == 'w' && nextChar == 'u') { _roman[_romanIndex] = 'o'; _roman.Insert(_romanIndex + 1, 'x'); return 1; } //「ふ」の柔軟な入力 if (inputChar == 'f' && currentChar == 'h' && nextChar == 'u') { _roman[_romanIndex] = 'f'; return 1; } //「ふぁ」「ふぃ」「ふぇ」「ふぉ」の柔軟な入力(一部Macのみ) if (inputChar == 'w' && prevChar == 'f' && (currentChar == 'a' || currentChar == 'i' || currentChar == 'e' || currentChar == 'o')) { _roman.Insert(_romanIndex, 'w'); return 1; } if (inputChar == 'y' && prevChar == 'f' && (currentChar == 'i' || currentChar == 'e')) { _roman.Insert(_romanIndex, 'y'); return 1; } 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 1; } if (inputChar == 'u' && prevChar == 'f' && (currentChar == 'a' || currentChar == 'i' || currentChar == 'e' || currentChar == 'o')) { _roman.Insert(_romanIndex, 'u'); _roman.Insert(_romanIndex + 1, 'x'); return 1; } 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 1; } //「ん」の柔軟な入力(「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 1; } 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 1; } //「きゃ」「にゃ」などを分解する 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 1; } //「しゃ」「ちゃ」などを分解する 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 1; } //「しゃ」を「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 1; } //「っ」の柔軟な入力 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 1; } //「っか」「っく」「っこ」の柔軟な入力(Windows限定) if (_isWindows && inputChar == 'c' && currentChar == 'k' && nextChar == 'k' && (nextChar2 == 'a' || nextChar2 == 'u' || nextChar2 == 'o')) { _roman[_romanIndex] = 'c'; _roman[_romanIndex + 1] = 'c'; return 1; } //「っく」の柔軟な入力(Windows限定) if (_isWindows && inputChar == 'q' && currentChar == 'k' && nextChar == 'k' && nextChar2 == 'u') { _roman[_romanIndex] = 'q'; _roman[_romanIndex + 1] = 'q'; return 1; } //「っし」「っせ」の柔軟な入力(Windows限定) if (_isWindows && inputChar == 'c' && currentChar == 's' && nextChar == 's' && (nextChar2 == 'i' || nextChar2 == 'e')) { _roman[_romanIndex] = 'c'; _roman[_romanIndex + 1] = 'c'; return 1; } //「っちゃ」「っちゅ」「っちぇ」「っちょ」の柔軟な入力 if (inputChar == 'c' && currentChar == 't' && nextChar == 't' && nextChar2 == 'y') { _roman[_romanIndex] = 'c'; _roman[_romanIndex + 1] = 'c'; return 1; } //「っち」の柔軟な入力 if (inputChar == 'c' && currentChar == 't' && nextChar == 't' && nextChar2 == 'i') { _roman[_romanIndex] = 'c'; _roman[_romanIndex + 1] = 'c'; _roman.Insert(_romanIndex + 2, 'h'); return 1; } //「l」と「x」の完全互換性 if (inputChar == 'x' && currentChar == 'l') { _roman[_romanIndex] = 'x'; return 1; } if (inputChar == 'l' && currentChar == 'x') { _roman[_romanIndex] = 'l'; return 1; } return 2; } こうすることによって、「し」を「shi」、んを「xn」などと入力することができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】柔軟な入力に対応したタイピングゲームの作り方(導入編)【C#】

はじめに 最近はPC離れが加速し、モバイルゲームが主流となった今、タイピングゲームは減少傾向です。 しかしながら普段からキーボードを叩いてコーディングを行っている人々は、タイピングゲームを作りたいと思ったことはないでしょうか? また、タイピングゲームの作成に挫折してしまう人々も少なくないと思います。 なぜなら、タイピングゲームは作りにくいと思うからです。 というのも、今のタイピングゲームは、「し」を「shi」、「ち」を「chi」など、柔軟な入力に対応しています。 これらがなくてもタイピングゲームとしては成立しますが、それでは非常に操作性の悪いタイピングゲームとなってしまい、今時公開できるようなものではありません。 また「柔軟な入力に対応していないタイピングゲームなんて公開できないよ!」と思っている開発者も多いかもしれません。 なので今回はUnityで柔軟な入力に対応したタイピングゲームを作る方法を記述します。 今回はUnityの記事ですが、Unity以外の言語やフレームワークでも参考になれば光栄です。 対象読者は、ある程度Unityが触れて、TextMeshProなどが使えることを想定しています。 とりあえずUnityを起動してプロジェクトを作成しよう とにかく、まずUnityのプロジェクトが存在しないことには、何も始まりません。 なので普段通り、Unityのプロジェクトを作成します。(2Dでも3DでもURPでもお好きに) 今回はTextMeshProを使いますので、TextMeshProをインポートします。 そして、タイピングゲームに必ずといっていいほど存在する「日本語表示用のテキスト」と「ローマ字表示用のテキスト」のTextMeshProオブジェクトを作成します。 また、日本語表示用のテキストには、日本語表示に対応したTextMeshPro用のフォントアセットを用意します。 次に「TypingManager.cs」を作成します。 そして、Hierarchy上に「TypingManager」という空のGameObjectを作成して、そこに先ほど作成した「TypingManager.cs」をInspector上でアタッチします。 ここからは延々と「TypingManager.cs」にコードを書くだけです。 最終的には以下のようになります。 完成したコード TypingManager.cs using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; [Serializable] public class Question { public string japanese; public string roman; } public class TypingManager : MonoBehaviour { [SerializeField] private Question[] questions; [SerializeField] private TextMeshProUGUI textJapanese; // ここに日本語表示のTextMeshProをアタッチする。 [SerializeField] private TextMeshProUGUI textRoman; // ここにローマ字表示のTextMeshProをアタッチする。 private readonly List<char> _roman = new List<char>(); private int _romanIndex; private void Start() { InitializeQuestion(); } private void OnGUI() { if (Event.current.type == EventType.KeyDown) { switch (InputKey(GetCharFromKeyCode(Event.current.keyCode))) { case 1: // 正解タイプ時 _romanIndex++; if (_roman[_romanIndex] == '@') // 「@」がタイピングの終わりの判定となる。 { InitializeQuestion(); } else { textRoman.text = GenerateTextRoman(); } break; case 2: // ミスタイプ時 break; } } } void InitializeQuestion() { Question question = questions[UnityEngine.Random.Range(0, questions.Length)]; _roman.Clear(); _romanIndex = 0; char[] characters = question.roman.ToCharArray(); foreach (char character in characters) { _roman.Add(character); } _roman.Add('@'); textJapanese.text = question.japanese; textRoman.text = GenerateTextRoman(); } string GenerateTextRoman() { 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; } int InputKey(char inputChar) { if (inputChar == '\0') { return 0; } if (inputChar == _roman[_romanIndex]) { return 1; } return 2; } 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: //上記以外のキーが押された場合は「null文字」を返す。 return '\0'; } } } これだけだとわかりにくいと思うので、コードを解説します。 コードの解説 何かしらの入力イベントが生じた時に呼ばれるイベント関数 OnGUI() Unityではスクリプトの生成時に最初から存在するUpdate()というのが存在しますが、もっと便利なイベント関数OnGUI()が存在します。 これは何かしらの入力イベントが生じた時に呼び出され、以下のように書くことによってキーボードの入力が発生した時に処理を走らせることができます。 private void OnGUI() { if (Event.current.type == EventType.KeyDown) { // キーが入力された時に処理を実行する } } OnGUI()関数にEvent.current.type == EventType.KeyDownという条件式を書けば、キーの入力時のみtrueとなって、処理が実行されます。 これをタイピングゲームとして機能させるには、以下の通りです。 private void OnGUI() { if (Event.current.type == EventType.KeyDown) { switch (InputKey(GetCharFromKeyCode(Event.current.keyCode))) { case 1: // 正解タイプ時 _romanIndex++; if (_roman[_romanIndex] == '@') // 「@」がタイピングの終わりの判定となる。 { InitializeQuestion(); } else { textRoman.text = GenerateTextRoman(); } break; case 2: // ミスタイプ時 break; } } } ここから InputKey() や GetCharFromKeyCode() InitializeQuestion() GenerateRomanText() ならびに _roman[] _romanIndex について解説していきます。 KeyCodeをcharに変換する関数 GetCharFromKeyCode() キーが入力され OnGUI() が実行されると Event.current.keyCode に入力されたキーコードが格納されます。型は KeyCode です。 この型はタイピングゲームのアルゴリズム実装には不向きですので、KeyCode を char に変換する関数 GetCharFromKeyCode() を実装しました。 以下のようになりました。今回はShift入力は省略しています。 char GetCharFromKeyCode(KeyCode keyCode) { switch (keyCode) { case KeyCode.A: return 'a'; case KeyCode.B: return 'b'; case KeyCode.C: return 'c'; case KeyCode.D: return 'd'; case KeyCode.E: return 'e'; case KeyCode.F: return 'f'; case KeyCode.G: return 'g'; case KeyCode.H: return 'h'; case KeyCode.I: return 'i'; case KeyCode.J: return 'j'; case KeyCode.K: return 'k'; case KeyCode.L: return 'l'; case KeyCode.M: return 'm'; case KeyCode.N: return 'n'; case KeyCode.O: return 'o'; case KeyCode.P: return 'p'; case KeyCode.Q: return 'q'; case KeyCode.R: return 'r'; case KeyCode.S: return 's'; case KeyCode.T: return 't'; case KeyCode.U: return 'u'; case KeyCode.V: return 'v'; case KeyCode.W: return 'w'; case KeyCode.X: return 'x'; case KeyCode.Y: return 'y'; case KeyCode.Z: return 'z'; case KeyCode.Alpha0: return '0'; case KeyCode.Alpha1: return '1'; case KeyCode.Alpha2: return '2'; case KeyCode.Alpha3: return '3'; case KeyCode.Alpha4: return '4'; case KeyCode.Alpha5: return '5'; case KeyCode.Alpha6: return '6'; case KeyCode.Alpha7: return '7'; case KeyCode.Alpha8: return '8'; case KeyCode.Alpha9: return '9'; case KeyCode.Minus: return '-'; case KeyCode.Caret: return '^'; case KeyCode.Backslash: return '\\'; case KeyCode.At: return '@'; case KeyCode.LeftBracket: return '['; case KeyCode.Semicolon: return ';'; case KeyCode.Colon: return ':'; case KeyCode.RightBracket: return ']'; case KeyCode.Comma: return ','; case KeyCode.Period: return '_'; case KeyCode.Slash: return '/'; case KeyCode.Underscore: return '_'; case KeyCode.Backspace: return '\b'; case KeyCode.Return: return '\r'; case KeyCode.Space: return ' '; default: //上記以外のキーが押された場合は「null文字」を返す。 return '\0'; } } (うん、これはひどい・・・でもこれが私の最適解です(笑)) こうすることによって、KeyCode を char に変換しています。 また、Functionキーなどが入力された場合でも OnGUI() が実行され Event.current.keyCode に格納され、上記の関数が実行されますが、その場合はnull文字 \0 を返しています。 タイピングの正誤判定を行う関数 InputKey() タイピングゲームには、キーの入力が正しいか否かを判断する機能が必要になります。 なので、それを行うための関数 InputKey() を実装します。 int InputKey(char inputChar) { if (inputChar == '\0') { return 0; } if (inputChar == _roman[_romanIndex]) { return 1; } return 2; } キーの入力によって OnGUI() 関数が呼ばれ、GetCharFromKeyCode() の戻り値 char が InputKey() の引数となって、int を返します。 正しい入力であれば 1 を返し、ミスタイプであれば 2 を返します。また入力に対応していないキーが入力された場合は 0 を返しています。 後の記事で上記の関数を編集し、柔軟な入力に対応させます。 タイピングの状態を格納するインスタンス変数 _roman _romanIndex タイピングゲームを作るからには、当然タイピング用の入力文の入力状態をコントロールする変数が必要になります。 using System.Collections.Generic; public class TypingManager : MonoBehaviour { private readonly List<char> _roman = new List<char>(); private int _romanIndex; } _roman はタイピングの入力文字の処理に用いられる List<char> のインスタンス変数で、頻繁に Add() Clear() が用いられますので、今回は List<T> となっております。 Unityではスクリプトの生成時に作成されますが、using System.Collections.Generic; を忘れないようにしてください。 そして、_romanIndex は _roman の参照に用いられるだけの int 型のインスタンス変数です。 問題を初期化する関数 InitializeQuestion() と、問題文を格納するクラス配列 questions どんなタイピングゲームにも、問題を初期化する必要がありますので、それを行うための関数 InitializeQuestion() を実装します。 void InitializeQuestion() { Question question = questions[UnityEngine.Random.Range(0, questions.Length)]; _roman.Clear(); _romanIndex = 0; char[] characters = question.roman.ToCharArray(); foreach (char character in characters) { _roman.Add(character); } _roman.Add('@'); textJapanese.text = question.japanese; textRoman.text = GenerateTextRoman(); } Clear() によって _roman の中身を空にし、_romanIndex を 0 に設定し、その後、クラス配列 questions からランダムに一つ取り出して格納されたクラス question の roman プロパティ( string 型)を ToCharArray() によって char[] に変換し、foreach を用いることによって、_roman に次から次へと Add() で文字を追加します。 そして、_roman の最後に @ を Add() します。この @ が「タイピングの終わり」であることを示します。 タイピングゲームには、タイピング用の文字列のリストが必要になります。 よって、Questionクラスを作成して、[Serializable] [SerializeField] でインスペクタ上から文字列を編集できるようにします。 using System; using TMPro; using UnityEngine; [Serializable] public class Question { public string japanese; public string roman; } public class TypingManager : MonoBehaviour { [SerializeField] private Question[] questions; [SerializeField] private TextMeshProUGUI textJapanese; // ここに日本語表示のTextMeshProをアタッチする。 [SerializeField] private TextMeshProUGUI textRoman; // ここにローマ字表示のTextMeshProをアタッチする。 } そしてインスペクタ上で文字列とローマ字表記を入力します。 (必ず日本式(「し」→「si」、「ち」→「ti」)で入力して、「ん」の後に「な行」や「あ行」が来る場合は「nnna」及び「nna」などと入力してください。後ほどの記事で柔軟な入力方法に響きます。  また、諸事情でjapaneseがTitleになっています。) また、_textJapanese _textRoman は、画面に表示するためのTextMeshProオブジェクトを格納するインスタンス変数です。 インスペクタ上でアタッチを行います。 表示用のテキスト情報を生成する関数 GenerateRomanText() タイピングゲームでは当たり前のように、入力前の文字と入力後の文字で色が異なりますので、それを実装するための関数 GenerateRomanText() を実装します。 string GenerateTextRoman() { string text = "<style=typed>"; for (int i = 0; i < _roman.Count; i++) { if (_roman[i] == '@') { break; } if (i == _romanIndex) { text += "</style><style=untyped>"; } text += _roman[i]; } text += "</style>"; return text; } TextMeshProにはタグ機能が搭載されており、特定の部分のみスタイルを変更する <style> タグを使用します。 ただし、このままでは画面上に <style> が表示されてしまいます。なのでUnityエディタ上で「Project Settings」→「TextMesh Pro」の「Settings」→「Default Style Sheet」の「Default Style Sheet (TMP_StyleSheet)」をダブルクリックして、インスペクタ上で以下のようにタグとタグ情報を追加します。 こうすることによって、<style>は表示されなくなり、タイピングの入力前と入力後の文字の色が変わります。 さいごに コードの解説は以上です。 長々としたコード及び記事をお読みいただき、誠にありがとうございます。 タイピングゲームとして動作しましたでしょうか? しかし、これでは表示されているローマ字以外のタイピングができないため、非常に操作性の悪いタイピングゲームとなっているはずです。 次の記事で柔軟な入力に対応させます。 【Unity】柔軟な入力に対応したタイピングゲームの作り方(改良編)【C#】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity Cloud Build のAPIからビルドターゲットを作成する場合の注意点

Unity Cloud Build のAPIからビルドターゲットを作成しようとした時に Request Body の設定で嵌った箇所の備忘録です。 Unityバージョン 説明文では最新バージョンの指定は「latest」とだけ記載されていますが、実際には末尾に対象バージョンを付ける必要がありました。 「latest」だけで実行した場合、以下のエラーメッセージしか表示されず、原因に気づきにくいため注意が必要です。 ビルドスケジュール デフォルトでは「date」の項目が空の状態で存在しており、気付かずに空のまま実行するとエラーになります。 日時を設定しても良いですが、不要な場合は「Edit JSON」から「date」の項目を削除しても解決します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む