【Unity】RPGを作るチュートリアルその8 侵入できないタイルの実装

【Unity】RPGを作るチュートリアルその8 侵入できないタイルの実装

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

第7回では操作キャラクターにカメラを追従させる機能を作成しました。シンプルに対象のゲームオブジェクトを画面中央に収める形で実装しています。あった方が良い機能などは色々とあるのですが、ひとまずはこのシンプルな形でチュートリアルを進めていきます。

今回は移動関連の続きで、侵入できないタイルの機能を作っていきます。今回実装する範囲はタイル側で、NPCなどのいるマスに移動できないようにする部分は次回に回したいと思います。

 

 

制作環境

MacBook Pro 2023 Apple M2 Max

Unity6 (6000.0.30f1) Silicon

 

作業内容と順序

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

 

前回の内容

前回はカメラが操作キャラクターを追従する処理を実装しました。

 

侵入できないタイルの実装方針

今から移動しようとしている先のタイルに移動できるかどうか、という点はRPGでは重要で、現時点で進んでいい場所、進んではいけない場所を制御してストーリー進行に影響が出ないようにする必要があります。

主に壁や柵、扉などの構造物だったり、樽や壺などの装飾品だったりと、RPGで移動する際には通行できない場所があります。このシンプルRPGでもこれらのあるタイルには移動できないようにしていきます。

具体的にどう判断するかですが、以下の手順でやってみたいと思います。

  1. 侵入できないタイルの一覧を保持する定義データをScriptableObjectで作成
  2. 操作キャラクターの移動時、移動先のマスにあるタイルを取得
  3. 1のリストに含まれるタイルなら移動しないでキャラクターの向きだけ変える

まずは侵入できないタイルをリストで定義するようにします。この定義データはScriptableObjectで作成します。シンプルにInspectorウィンドウから侵入禁止のタイルをリストに入れていく形にしましょう。

このファイルはTilemapManagerという管理クラスを作ってそこで参照させます。操作キャラクターの移動用クラスである「PlayerMover」からTilemapManagerを参照するようにして、移動先のマスに含まれるタイルを取得後、侵入できないタイルのリストを確認して、含まれていれば移動中断、含まれていなければ移動処理の開始、といった形で処理させます。後々、このTilemapManagerではマップのインスタンス化などをしてもらうようにします。

また、移動しなかった場合でも、キャラクターの向きだけは変えるようにしたいと思います。方向キーを入力したのに画面上で変化が起こらないと「あれ?」となってしまいますからね。

タイルと侵入可否を紐づけることで、見た目は壁だけど実は通れる、といった隠し通路を実装する場合にちょっと工夫が必要になります。今回の機能ではTilemapに配置したタイルが判定の対象となるので、隠し通路などを実装するならTilemapに配置しないゲームオブジェクトとしてシーン内に配置するようにします。各マップでPrefabを作ろうと思っているので、これを考慮する形で構造を考えたいと思います。

 

侵入できないタイルの定義ファイルの作成

ScriptableObjectを使って、侵入できないタイルを定義するファイルを作成します。Projectウィンドウから「Assets/Scripts」のフォルダに移動してコンテキストメニューを開いて [Create] -> [Scripting] -> [ScriptableObject Script] からScriptableObjectを継承したクラスを作成します。

ScriptableObjectのファイルを作成
ScriptableObjectのファイルを作成

 

名前は [NoEntryTileData] にしました。

名前の変更
名前の変更

 

このファイルを開き、以下のように記載します。

「CreateAssetMenu」の属性をつけることで、このScriptableObjectをコンテキストメニューから作成することができます。Projectウィンドウでコンテキストメニューを開き、[Create] の中に表示されます。また、画面上部のメニューバーの「Assets」のメニューからも同様に [Create] の中に表示されます。

侵入できないタイルの一覧はList形式で定義しています。Inspectorウィンドウからリストの中に侵入できないタイルを追加していくことを想定しています。

スクリプトファイルを保存したら、ScriptableObjectのアセットファイルを作成します。定義データ類はフォルダとしてまとめたいので、Projectウィンドウの「Assets」の下にフォルダを作成します。名前は [Data] にしました。

フォルダを作成
フォルダを作成

 

続いて「Data」フォルダに移動して、定義データを作成しましょう。コンテキストメニューを開いて [Create] -> [ScriptableObject] -> [SimpleRpg] -> [NoEntryTileData] からデータのアセットファイルを作成します。

定義データの作成
定義データの作成

 

作成したファイルの名前は [NoEntryTileData] にしました。「しました」というよりはデフォルトネームをそのまま使っているだけです。すみません。

名前の入力
名前の入力

 

作成したファイルを選択した状態でInspectorウィンドウを見ると、以下のように順番を並べ替えられるリストが表示されます。このリストに侵入できないタイルを追加して、ゲームの実行中に参照して移動可能か判断しましょう。

ここにタイルを追加していくよ
ここにタイルを追加していくよ

 

壁用タイルの導入

今まで作ったタイルを侵入できないタイルにするのは分かりにくくなりそうなので、新たに壁用タイル(モック)を作りました。

壁

 

こちらの画像をプロジェクト内にインポートします。他のタイルと同様、「Assets/Images/Tiles」に配置しましょう。

ファイルのインポート
ファイルのインポート

 

このタイミングでタイル用のインポート設定のプリセットを作ってしまいましょう。お花の画像を選択し、Inspectorウィンドウからプリセットを作る……前に、よく見たら圧縮するようになっていたので [None] に変えて [Apply] ボタンをクリックして適用します。

圧縮設定を変更
圧縮設定を変更

 

設定を変更したら、Inspectorウィンドウ右上のプリセットボタンをクリックして、表示されたウィンドウの中から [Create New …] をダブルクリックしてプリセットを作成します。

プリセットの作成
プリセットの作成

 

保存する場所は [Preset] のフォルダで、名前は [TileImporter] にしました。[Save] をクリックして保存しましょう。

保存先の選択
保存先の選択

 

圧縮設定を変えたので、既存のタイル用画像も含めてインポート設定を変更しましょう。Projectウィンドウから「Assets/Images/Tiles」のフォルダ内にある画像を全て選択して、Inspectorウィンドウの右上のプリセットボタンをクリックし、先ほど作成した[TileImporter] をダブルクリックします。

タイル用プリセットを選択
タイル用プリセットを選択

 

プリセットの適用後は、右下にある [Apply] ボタンをクリックして変更を適用します。続いてこの画像を使ったタイルを作りましょう。「Tile Palette」ウィンドウを開き、インポートした壁用の画像をパレットの領域にドラッグ&ドロップします。

パレットにドラッグ&ドロップ
パレットにドラッグ&ドロップ

 

パレットに追加する際にタイルのファイルを作成するので、保存場所を「Assets/Tiles」にします。ファイル名は画像と同じでよいかと思います。

保存先の選択
保存先の選択

 

タイルが作成されると、ドラッグ&ドロップした位置に壁用のタイルが表示されます。この壁を移動可否の確認に使っていくので、配置していきましょう。タイルに透明ピクセルが含まれる場合に下の地面も見えるように、配置先は花などと同じ装飾品のTilemapである「TilemapProps」にしましょうか。

作成されたタイル
作成されたタイル

 

Hierarchyウィンドウで「TilemapProps」のゲームオブジェクトを選択した状態で壁用タイルを配置していきましょう。タイルパレットの上部のプルダウンから配置先を選んでもOKです。配置する場所も任意の場所でOKで、私は以下のように配置してみました。

任意の場所に壁用タイルを配置
任意の場所に壁用タイルを配置

 

壁用タイルをリストに追加

作成した壁用タイルを侵入できないタイルのリストに追加します。Projectウィンドウから「Assets/Data」のフォルダを開き、「NoEntryTileData」を選択します。Inspectorウィンドウから「No Entry Tiles」のリストの [+] ボタンをクリックして項目を追加します。表示された項目の丸ボタンをクリックして、壁用タイルを追加します。

追加ボタンをクリック
追加ボタンをクリック

 

壁用タイルの追加後は以下のようになります。キャラクターの移動時、移動先の座標のタイルにこのリストのものが含まれていたら、移動できないようにします。

タイルの追加後
タイルの追加後

 

Tilemapの管理クラスの作成

定義ファイルの作業が完了したので、Tilemapを管理するクラスを作成していきます。このクラスでは、マップに対応するTilemapのセットを管理したり、移動先のタイルに進めるかの判定を行います。村、フィールド、洞窟など、それぞれのマップに対応する形で「Grid」とその子オブジェクトのTilemapをPrefab化して、マップ間の移動時に切り替えていくイメージです。この辺りの作業は後々行なっていきますが、これから作るTilemapの管理クラスに担当させます。

Projectウィンドウの「Scritps」フォルダの下に、MonoBehaviourのスクリプトとして [TilemapManager] を作成します。

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

 

TilemapManagerに実装したい機能は以下の通りです。

  • Tilemap3種を保持するフィールド
  • Tilemap上の座標とワールド座標の変換機能
  • 移動先のタイルの移動可否判定

座標変換の機能をこちらに持たせるため、「PlayerMover」に入れていた座標変換の機能をこちらに移植しましょう。また、「PlayerMover」のフィールドにあったTilemapへの参照も、「TilemapManager」のクラス内で持たせます。

スクリプトの全文は以下のようになりました。

 

このスクリプトはHierarchyウィンドウでゲームオブジェクトを作成して、そこにアタッチして使う想定です。他のクラスからこのクラスへの参照を取得し、座標の変換や移動の可否を確認します。現時点ではStart()やUpdate()から呼び出して使う処理もないため、両方とも削除しています。必要になったら追加する予定です。

それぞれ個別に説明していきます。まずはフィールドの部分から。

Tilemapのそれぞれのレイヤーを保持するフィールドと、侵入できないタイル一覧の定義ファイルを保持するフィールドを追加しました。Tilemapのそれぞれのレイヤーに関しては、後々Prefabからインスタンス化した際に自動的にセットするようにしたいと考えていますが、現時点では動作確認のためInspectorウィンドウから参照をアサインできるようにしています。

侵入できないタイル一覧の定義ファイルに関しては、今回作成した定義ファイルを使っていきます。こちらは動的に変化するものではない想定のため、このままInspectorウィンドウから参照をアサインする形でいきましょう。

 

続いて座標の変換用メソッドです。

ワールド座標を引数にTilemap上の論理的なセルの座標に変換するものと、Tilemap上の論理的なセルの座標を引数にワールド座標に変換するものの2種類を用意しました。

セルの座標に変換するのは「Tilemap」クラスのWorldToCell()のメソッドを使い、ワールド座標に変換するのは同じく「Tilemap」クラスのCellToWorld()のメソッドを使います。CellToWorld()による変換後は、アンカーが考慮されない位置、つまりタイルを配置するセルの左下が返されるため、「tileAnchor」の値を加算してセルの中央のワールド座標になるようにしています。

 

最後に移動可否を判定するメソッドです。

他のクラスからCanEntryTile()のメソッドを呼び出して、そのタイルの位置に移動できるかどうかの判定を行います。移動できないタイルは地面、装飾品、キャラクターの上、のそれぞれのレイヤーで存在する可能性があるので、同じセル座標に関して3つのTilemapからタイルを取得し、それが侵入できないタイルのリストに含まれるかどうかを確認しています。

「Tilemap」クラスのGetTile()のメソッドで位置を取得することによってタイルの取得ができます。Nullチェックを入れて3回同じこと書くのもなぁ……と思い、GetTileOnPos()のメソッドとして切り出し、その中で取得を行なっています。

定義ファイルのリストとの照合はシンプルにContains()のメソッドを使って行なっています。取得した各レイヤーのタイルのうち、どれかひとつでもリストに含まれていたら侵入できるかのフラグ「canEntry」をfalseにしてループを抜けます。

 

PlayerMoverの更新

操作キャラクターを移動させる「PlayerMover」の中に入れていた処理を「TilemapManager」に移動させたので、「PlayerMover」側も更新しましょう。移動可否の判定でも「TilemapManager」のCanEntryTile()のメソッドを呼び出すようにします。

全文は以下の通りです。長いので変更点は全文の後で紹介します。

 

変更した部分は以下の通りです。

  • 「using UnityEngine.Tilemaps;」の記載を削除
  • Tilemapへの参照フィールドを削除し、TilemapManagerへの参照フィールドを追加
  • GetCurrentPositionOnTilemap()内でTilemapを直接参照していた部分を「TilemapManager」内のメソッドを呼ぶように変更
  • MovePlayer()内に侵入可否の判定を追加
  • MovePlayer()内でアニメーション切り替えの順序を変更

「TilemapManager」の導入に伴い、「PlayerMover」内でTilemapの機能を呼び出していた部分を変更しました。また、キー入力した方向に移動できなかった場合は移動処理を抜けるため、その手前でアニメーションを切り替えるようにしました。キー入力があったのに画面の見た目が変わらないとユーザが戸惑う可能性があるためです。

 

動作確認の準備

スクリプトを作成したら動作確認の準備をしていきます。今回作成した「TilemapNamager」のクラスは管理用クラスとしてシーン内で使います。そのため、管理用クラスがHierarchyウィンドウ内でどこに配置されているか把握しやすくするため、整理用の空のゲームオブジェクトを作成します。

Hierarchyウィンドウでコンテキストメニューを開き、[Create Empty] から空のゲームオブジェクトを作成します。

空のゲームオブジェクトを作る
空のゲームオブジェクトを作る

 

名前は [Managers] にしました。なんとかManager系のスクリプトをアタッチするゲームオブジェクトはここに格納していきましょう。プロジェクトによっては、ハイフンを使って「Managers————–」のように区切り線を可視化することもあります。今回は名前だけにしましょうか。

名前の変更
名前の変更

 

Hierarchyウィンドウで「Managers」のゲームオブジェクトを選択したら、InspectorウィンドウでTransformの値をリセットしましょう。Manager系のゲームオブジェクトでは自身の位置を参照することはないのですが、親オブジェクトの位置が子オブジェクトの位置に影響する点を考慮して癖としてリセットしています。リセットする際は、コンポーネントの右上にあるメニューボタンから [Reset] を選択しましょう。

Transformのリセット
Transformのリセット

 

続いて「TilemapManager」のスクリプトをアタッチするための空のゲームオブジェクトを作成します。先ほど作成した「Managers」を選択した状態で空のゲームオブジェクトを作成することで、子オブジェクトとします。名前は [TilemapManager] にしました。初期の頃はひとつのゲームオブジェクトに複数のManager系スクリプトをアタッチしていたこともありましたが、どのゲームオブジェクトにどのスクリプトがいるのか分かりにくかったので、1つのゲームオブジェクトにつき1つのManager系スクリプトをアタッチするようにしています。ゲームオブジェクト名もスクリプト名と合わせておくとHierarchyウィンドウ上でも分かりやすいかと思います。

Managersの子オブジェクトに
Managersの子オブジェクトに

 

Hierarchyウィンドウにて、作成した「TilemapManager」のゲームオブジェクトを選択し、Inspectorウィンドウから [TilemapManager] のスクリプトをアタッチします。

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

 

TilemapManagerの各フィールドに参照をアサインします。Tilemapについてはそれぞれ同名のものを、定義ファイルは作成したものをアサインしましょう。今更ですが、「アサイン」は割り当てる意味で使っています。人によって「参照をアサインする」「参照をセットする」「参照を指定する」と異なる言い回しをしていたりしますが、大体同じような操作を指しているので大丈夫だと思います。多分。ちなみに私もブログ内で「アサインする」「セットする」と表記揺れがあるかもしれません。すみません。

参照のアサイン
参照のアサイン

 

続いて、Hierarchyウィンドウで「Player」のゲームオブジェクトを選択し、Inspectorウィンドウから「TilemapManager」への参照をアサインします。

TilemapManagerへの参照をアサイン
TilemapManagerへの参照をアサイン

 

動作確認

これで準備ができたので、動作確認してみましょう。ゲームを実行して、壁のタイルにキャラクターが移動しなければOKです。また、壁に向かってキー入力を行なった際にアニメーションが切り替わることも確認しておきましょう。

頭の中では例の効果音が鳴っています
頭の中では例の効果音が鳴っています

 

これでTilemapに関する侵入可否の部分ができたので、次はキャラクター同士の衝突もしないようにしていきます。

 

今回のブランチ

 

まとめ

今回は侵入できないタイルの実装を行いました。侵入できないタイルのリストは、追加されるマップに合わせて項目を追加していく予定です。より規模の大きいゲームを作っていくなら、Editor拡張を使ってタイルの画像も表示するようにすると、どのタイルが侵入できないのかより分かりやすくなるかと思います。

次回はキャラクター同士の衝突が起こらないようにする部分を実装していきます。

 

     

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

CTA-IMAGE

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


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


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