【Unity】RPGを作るチュートリアルその9 キャラクター同士の衝突確認

【Unity】RPGを作るチュートリアルその9 キャラクター同士の衝突確認

シンプルなRPGをUnityで作るチュートリアルシリーズの9回目です。

第8回ではTilemap上を移動する際に、侵入できないタイルの機能を作りました。壁や大きなオブジェクトなど、操作キャラクターがその上を歩けないようにする制御はRPGでは大切ですね。オブジェクトの他に、海や川など、フィールド上でも侵入できないタイルの指定は大切です。

今回は侵入できないタイルに加えて、キャラクター同士が重ならないように制御を実装していきましょう。操作キャラクターとNPC、NPC同士など、衝突相手に合わせて処理を確認していきます。

 

 

制作環境

MacBook Pro 2023 Apple M2 Max

Unity6 (6000.0.30f1) Silicon

 

作業内容と順序

シンプルなRPGを作る上でどんな作業が必要か、どんな順番で作っていくと良さそうか、別ページで検討しました。基本的にこの流れに沿って進めていきます。

 

前回の内容

前回はTilemap上を移動する際に、侵入できないタイルの機能を実装しました。

 

衝突の検知

キャラクターが移動する際は、侵入できるタイルの確認に加えて、そのタイル上に他のキャラクターがいるかどうかも確認しておくとグッド。同じマスに重なっても良いキャラクター、重ならない方が良いキャラクターなども事前に決めておくとスムーズに実装できるかと思います。

今回のチュートリアルでは仲間が操作キャラクターひとりなのですが、仲間が操作キャラクターの後ろをついてくるRPGなら、それらのキャラクターは重なってもOKなことが多いかもしれません。

NPCに関してはイベントの起動とも関連する関係で、基本的に重ならないことが多いかと思います。地面に置かれているか、操作キャラクターと同じレイヤーで移動しているか、なんて部分も考慮しておくとイメージしやすくなりそうですね。

今回実装したいのは、操作キャラクターとNPCの衝突確認、NPC同士の衝突確認の部分です。移動時に自分自身から移動先にRayを飛ばして、Colliderがあるかどうかで判定してみたいと思います。工程表の段階ではNPCの移動もなしにしちゃおうかなーなんて考えていましたが、せっかくなのでNPCを移動させて衝突しないようにしてみます。こうやって規模が膨らんでいくんですよね……(遠い目)

 

Colliderのアタッチ

操作キャラクターやNPCにColliderをアタッチしていきます。タイルと合うようなColliderにしたいので、BoxCollider2Dをアタッチしましょう。同じ設定でアタッチしたいので、Hierarchyウィンドウで「Player」から「NPC_Goblin」まで複数選択します。

複数選択するのだポッター
複数選択するのだポッター

 

続いてInspectorウィンドウで [Add Component] から [Box Collider 2D] をアタッチします。

Colliderをアタッチ
Colliderをアタッチ

 

アタッチした「Box Collider 2D」では、「Is Trigger」の項目にチェックを入れます。物理的な挙動を行わないのでいらないかなーとは思いつつ、念の為物理的な衝突を行わないようにトリガーとして使うようにしましょう。Colliderの大きさは1にしておくことでタイルのサイズと一致します。

トリガーにする
トリガーにする

 

タグの設定

RayでColliderを検出した際に、相手のタグを使って操作キャラクターかNPCかを判定します。そのため、NPC用のタグを追加しましょう。Hierarchyウィンドウで任意のNPCのゲームオブジェクトを選択し、Inspectorウィンドウのタグ選択のプルダウンから [Add Tag…] を選択します。

タグを追加
タグを追加

 

タグとレイヤーの管理画面が表示されるので、[+] ボタンからタグ追加のホバーウィンドウを表示します。タグの名前として [NPC] を入力して [Save] ボタンをクリックします。

NPC用のタグを追加する
NPC用のタグを追加する

 

タグを追加したら、HierarchyウィンドウでNPC用のゲームオブジェクトを複数選択します。

NPCを複数選択
NPCを複数選択

 

Inspectorウィンドウからタグ選択のプルダウンにて、先ほど追加した [NPC] を選択します。これで操作キャラクターかNPCかの判別はタグでできそうです。

追加したタグの選択
追加したタグの選択

 

タグの定義クラスを更新

操作キャラクター用のタグについては定義クラス内で文字列を定義しているので、同じクラス内にNPC用のタグ文字列も定義します。「ObjectTagSettings」のクラスで以下のように変更しました。

NPCのタグ名を定義するフィールドを追加しました。フィールド名は全部大文字なのもちょっとなーと思い、先頭のみ大文字にしています。

 

キャラクターからのRayを実装

キャラクターから移動先にRayを飛ばす機能を実装していきます。さらっと「Ray」と書いてましたが、Rayは光線のことで、指定した位置から指定した方向に光線のようなものを飛ばし、その光線の経路内にあるColliderを検出してくれます。だから、Colliderをアタッチする必要があったんですね。

こちらを使って操作キャラクターの位置からRayを飛ばしていきます。飛ばす方向は入力されたキーを元に判別します。NPCの場合は移動方向をランダムに決定して、その方向を使用する予定です。

Rayによる判定は操作キャラクター、NPCで使いまわせそうなので、それぞれのクラスから継承できるように「CharacterMover」という基底クラスを作って、そちらに実装していきます。

 

基底クラスの作成

Projectウィンドウから「Assets/Scripts」のフォルダを開き、MonoBehaviourのスクリプトファイルを作成します。名前は [CharacterMover] にしました。

スクリプトの作成
スクリプトの作成

 

キャラクターの移動に関しては、「PlayerMover」で実装した以下の部分が共通して使えるので、こちらも基底クラスに移行しましょう。

  • 各フィールド
  • アニメーションの切り替え機能
  • キャラクターの移動機能

移行したものについてはprotectedに変更して継承先でも使えるようにします。必要に応じて動作を変更しそうなものについてはvirtualをつけておきます。

また、Rayを飛ばして、ヒットしたColliderを取得してタグを比較する機能も追加します。衝突扱いになる対象のタグは「Player」と「NPC」で、今の所操作キャラクターもNPCも対象のタグは同じになりそうです。タグの比較を行う部分は別メソッドに切り出して、virtualにすることで継承先で変更できるようにしておきます。

上の内容を含めて書いたスクリプトの全文が以下の通りです。「PlayerMover」に書いた処理の大半をこちらに移動させました。

フィールドに関しては、移動にかかる時間の「_moveTime」をInspectorウィンドウから設定することとして、その他の項目に関してはこのクラス内で取得するようにしています。「TilemapManager」への参照は「PlayerMover」の中では[SerializeField]をつけてInspectorウィンドウからアサインするようにしていましたが、NPCの数だけアサインするとなると、手動によるミスが生まれそうだったので動的に取得するようにしています。

 

参照を取得するのはCheckComponents()のメソッドで、virtualをつけることによって継承先で他の参照を追加できるようにしています。自身のゲームオブジェクトにアタッチされているコンポーネントはGetComponent()で、他のクラスへの参照を取得するのはFindAnyObjectByType()のメソッドを使っています。検索系のメソッドは重いので、初期化のタイミングで済ませています。

MoveCharacter()のメソッドでは、移動先にキャラクターがいるかどうかの確認を追加しています。現在のワールド座標を使うタイミングが早くなったので、処理の順番も入れ替えています。

ExistsOtherCharacter()のメソッドで、移動先にキャラクターがいるかどうかを確認しています。自身のColliderからRayを飛ばして、ヒットしたコライダーがいればリストに格納されるため、そのリストに含まれるColliderのタグを確認しています。どのタグを衝突対象にするかは、継承先で異なるケースも考えてIsCollisionTargetTag()のメソッドに切り出しています。こちらをoverrideすることでタグを変更することができます。操作キャラクターとNPC、どちらも同じタグが対象になりそうですが、将来的に変更するケースを考えています(Tilemapに配置しないイベント用のゲームオブジェクトなど)

 

PlayerMoverの変更

継承するクラスの変更、必要のない処理の削除などを行いました。

フィールド類は基底クラスのものを使い、キー入力の処理だけ残りました。キャラクターの移動に関して、操作キャラクターに特有のものがあればこちらに追加していきましょう。決定キーの入力を検知してイベントを発火させる機能などはおそらくここに入るかと思います。

 

現時点での動作確認

この段階で一度動作確認をしておきます。操作キャラクターが前回までと同じように移動できること、壁やNPCのいるマスに進めないことを確認します。

 

NPC用の移動スクリプトの作成

動作が確認できたらNPC用の移動スクリプトを作成していきます。Projectウィンドウから「Assets/Scripts」のフォルダを開き、MonoBehaviourのスクリプトファイルを作成します。名前は [NpcMover] にしました。

スクリプトの作成
スクリプトの作成

 

移動の頻度を定義するEnum

このクラスではNPCがランダムに移動する機能を導入します。移動の頻度をInspectorウィンドウから選べるようにしたいので、選択肢としてEnumを定義しましょう。Projectウィンドウから「Assets/Scripts/Enums」のフォルダを開き、空のスクリプトファイルを作成します。名前は [MoveFrequency] にしました。

Enum用のファイル
Enum用のファイル

 

作成したEnumでは以下のように頻度を定義しました。

Neverは移動しない、Rarelyはまれに移動する、Sometimesは時々移動する、Oftenはしばしば移動する、Alwaysはいつも移動する、といったイメージにしました。「しばしば」ってOftenの訳語として聞くのがほどんどのような気がしますね。

それぞれの項目と移動の頻度に関しては、移動してから次の移動までのインターバル秒数をクラス内で定義して、その秒数が経過していたら次の移動を始めるようにします。この秒数に関しては後で動かしてみて調整する想定でいますが、一旦以下のように決めてみます。

選択肢 インターバル秒数
Never -1
Rarely 5
Sometimes 3
Often 1
Always 0

 

Neverの場合は移動関連の処理の前で抜けるようにしましょうか。Alwaysならずっと移動を続けるように、インターバルがない状態にしましょう。

 

NPCの移動機能の実装

NPCの移動に関しては、以下の流れで処理を進めます。

  • 頻度がNeverなら処理を抜ける
  • 頻度に応じたインターバル秒数を取得
  • 経過時間がインターバル秒数を超えていたら移動処理の実行
  • ランダムに移動方向を取得
  • 移動処理(基底クラス)

操作キャラクターがキー入力の判定を行なっていた部分を、自動で方向選択するようになります。これらの処理を踏まえたスクリプトの全文は以下の通りです。

移動頻度に関してはInspectorウィンドウで変更できるようにしています。インターバル時間に関してはSetIntervalTime()のメソッドで移動頻度に応じてセットしています。Start()のタイミングに加えて、ゲームの実行中に切り替えたものを反映させるために、経過時間をリセットするタイミングでも呼び出しています。

Update()からはMoveNpc()のメソッドを呼び出して、この中で経過時間の判定を行い、まだインターバルの時間が経っていなければ処理を抜けています。

経過時間の判定用メソッドとしてIsFinishedInterval()を作成しています。このメソッドでは、移動中フラグがtrueの場合や、移動頻度がNeverの時にはfalseを返すことで、移動処理に進まないようにしています。

GetDirection()ではランダムに移動方向を取得しています。ここはUnityEngineのRandomクラスを使っていつも通り乱数を取得しています。int型として取得する場合、Rangeの第2引数の数値が含まれないため、(0, 4)で範囲を指定しています。久しぶりに使うと忘れがちですね(1敗)

 

動作確認

HierarchyウィンドウでNPCのゲームオブジェクトを複数選択して一括でスクリプトをアタッチします。

NPCを複数選択
NPCを複数選択

 

Inspectorウィンドウで [Add Component] ボタンから [NpcMover] のスクリプトをアタッチします。

スクリプトのアタッチ
スクリプトのアタッチ

 

スクリプトをアタッチした後は、各NPCで任意の移動頻度を選択します。ここまで設定できたらゲームを実行してNPCが移動するかどうかを確認しましょう。

 

気になること

動かしてみると、たまーにNPCが同じマスに入ってしまうことがあります。画像では動かないゴブリンを壁にしてNPCを移動させてみましたが、2人のNPCが同じマスに入ってしまい、ファミコンのゲームのようにスプライトが交互に表示される状態になりました。また、同じマスでColliderが干渉してしまい、抜け出すこともできない状態に。移動頻度をAlwaysにしてみると再現しやすいかと思います。

キャラクター同士が同じタイルに入ってしまう
キャラクター同士が同じタイルに入ってしまう

 

対策として考えられるのが、移動先の座標の予約です。各キャラクターが移動する際に、TilemapManagerに対して移動先の座標を通知して、その座標をリストとして保持しておくことで後から移動しようとするキャラクターが入れないようにします。

移動先の座標はキャラクターの移動に応じて更新されるので、キャラクターのインスタンスIDをもとにした辞書形式で保持するのが良いかもしれません。

上の内容を踏まえて更新した「TilemapManager」の全文が以下の通りです。

 

上記の辞書をフィールドとして追加しました。定義ファイルのフィールドの下に追加するとグッド。

対象のタイルに侵入できるかどうかを確認するメソッドのCanEntryTile()では上記の辞書を確認して、対象のタイルの座標が含まれていれば侵入できないようにします。その判定用のメソッドであるIsPositionUsed()ではcontainsValueを使って、値として座標があるかどうかを確認しています。

ReservePosition()はキャラクターの移動用クラスから呼び出すもので、インスタンスID、移動したい座標を引数に、辞書に登録します。同じインスタンスIDの要素があれば更新、なければ追加を行います。どのキャラクターが先に登録するかの順番は決まっていませんが、先に登録した方が優先されるようになっています。

 

これに対応して、キャラクターの移動に関する基底クラスの「CharacterMover」では、移動先の位置を登録する処理を追加しています。

キャラクターを移動させるMoveCharacter()のメソッド内、実際の移動処理のコルーチンを呼ぶ前に「TilemapManager」のReservePosition()のメソッドを呼び出します。インスタンスIDはGetInstanceID()のメソッドを呼んで取得します。一意のインスタンスIDが返ってくるので、辞書でもキーの重複なしで座標を保持できます。

 

気になること その2

上の重なってしまう問題に加えて、移動しないときにNPCのアニメーションの向きが変わってしまう点も気になります。操作キャラクターの場合はキー入力を画面に反映する意味で、移動しなくてもキャラクターの向きを変えていましたが、NPCの場合は実際に移動した時だけにすることで、移動先の再選択時のブルブルした動きをなくせそうです。

この動作はNPC特有のものなので、「NpcMover」のクラスを修正します。「CharacterMover」のクラスにあるMoveCharacter()のメソッドを「NpcMover」のクラスでoverrideして、アニメーションを変える処理の順番を変更します。

「CharacterMover」にあったMoveCharacter()のメソッドを「NpcMover」のクラスにコピペして、virtualをoverrideに変えました。さらに、_animator.SetInteger()の処理をガード節(returnで抜けている処理)の後に回すことで、移動の処理が行われるときにアニメーションの向きが変更され、移動しない場合は向きを変更しないようにしています。

 

動作確認再び

上の修正を行なったら再び動作確認をしてみます。今度は操作キャラクターもNPCも同じマスに入らなそうですね。向きの変更も移動のタイミングになったので、変にブルブルしたりもなさそうです。私の環境では数分放置してみて重ならなかったので、移動関係の処理は一旦これでOKにします。

 

今回のブランチ

 

まとめ

今回はキャラクター同士の衝突が起こらないようにしました。「タイルの座標を予約する形式ならRaycastは要らなかったのでは?」と一瞬思ったりしましたが、移動用スクリプトをアタッチしない予定の動かない宝箱や扉の場合は予約形式が使えないので、Colliderを検知するためのRaycastはそのまま使えそうです。

次回は定義データの作成に入ります。多分これが一番妄想が捗って楽しい部分だと思います。

 

     

ゲーム開発の攻略チャートを作りました!

CTA-IMAGE

「ゲームを作ってみたいけど、何から手を付けていいか分からない!」


そんなお悩みをお持ちの方向けに、todoがアプリをリリースした経験を中心に、ゲーム作りの手順や考慮すべき点をまとめたe-bookを作成しました。ゲーム作りはそれ自体がゲームのように楽しいプロセスなので、「攻略チャート」と名付けています。


ゲームを作り始めた時にぶつかる壁である「何をしたら良いのか分からない」という悩みを吹き飛ばしましょう!