このチュートリアルでは、2Dゲームにおける「グリッドベース移動」をどのように実装するのかを説明する。グリッドベース移動というのは、等間隔にグリッド(格子状の線)で区分されたゲームの画面上をキャラクターなどのオブジェクトが 1 グリッド( 1 マス)ずつ移動する動きのことだ。
人気スマホゲーム「パズル&ドラゴンズ」や元祖落ちゲー「テトリス」などのパズルゲームはもちろん、「ファイヤーエムブレムシリーズ」「タクティクスオウガ」などのタクティカルシミュレーションゲームで、この動きを採用しているものは多いだろう。一方、「ファイナルファンタジー」シリーズや「ドラゴンクエスト」シリーズなどの RPG も、ファミコン用やスーパーファミコン用のソフトとして登場した 2D グラフィックの時代は、キャラクターの移動をよく見ると、このグリッドベース移動を行っている。このように、グリッドベース移動はさまざまなジャンルのゲームで使用されており、その汎用性はかなり高いと言える。
この記事では、できるだけグリッドベース移動の実装に絞って解説していく。また、最後にグリッドベース移動を使ったサンプルゲームも紹介するので、こちらもよかったら参考にしてほしい。
なお、このチュートリアルで最後にできあがるプロジェクトのファイルは、GitHubリポジトリ に置いている。.zipファイルをダウンロードしていただき、「End」フォルダ内の「project.godot」ファイルを Godot Engine でインポートしていただければ、直接プロジェクトを確認していただくことも可能だ。
Environment
このチュートリアルは以下の環境で作成しました。
・Godot のバージョン: 3.4.4
・コンピュータのOS: macOS 11.6.5
Memo:
ゲームを作り始めるのに以下の記事もお役立てください。
Godot をダウンロードする
Godot のプロジェクトマネージャー
Godot の言語設定
新規プロジェクトの作成
まずは Godot Engine を立ち上げて、新規プロジェクトを作成してほしい。プロジェクトの名前は「Grid Based Movement Tutorial」とでもしておこう。
エディタが表示されたら、先にゲームのディスプレイサイズを設定しておこう。
- 「プロジェクト」メニュー>「プロジェクト設定」を開く。
- 「window」で検索して、サイドバーの「Display」>「Window」を選択する。
- 「Size」セクションで以下の項目の値を変更する。
- Width: 256
- Height: 160
- Test Width: 1024
- Test Height: 640
- 「Stretch」セクションで以下の項目の値を変更する。
- Mode: 2d
- Aspect: keep
続いて、KENNEYのサイトからアセットをダウンロードして利用させてもらおう。今回利用するのは 1-Bit Pack というアセットパックだ。この素晴らしすぎる無料の素材に感謝せずにはいられない。
ダウンロードしたら「Tilesheet」フォルダ内の「colored-transparent_packed.png」ファイルをエディタのファイルシステムドックへドラッグ&ドロップしてプロジェクトにインポートしよう。
ファイルをインポートした直後は画像がぼやけた感じになっているので、これを修正する。
- ファイルシステムドックでインポートしたアセットファイルを選択した状態にする
- インポートドックで「プリセット」>「2D Pixel」を選択する
- 一番下にある「再インポート」ボタンをクリックする。
これでピクセルアート特有のエッジの効いた画像になる。
World シーンを作る
まず最初のシーンとして、ゲームの世界を用意する。「World」という名前のシーンを作成しよう。
- 「シーン」メニュー>「新規シーン」を選択する。
- 「ルートノードを生成」にて「その他のノード」を選択する。
- 「Node2D」クラスのノードをルートノードとして選択。
- ルートノードの名前を「World」に変更する。
- 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://World/World.tscn」としてシーンを保存する。
- ルートノードに「TileMap」ノードを追加する。
これでシーンツリーは以下のようになったはずだ。
TileMap ノードを編集する
TileMap ノードのプロパティを編集する
シーンツリードックで「TileMap」ノードを選択し、インスペクターでプロパティを編集しよう。
- 「TileSet」プロパティに「新規 TileSet」リソースを割り当てる。
- 「Cell」>「Size」プロパティの値を(x: 16, y: 16)にする。これから利用するスプライトシートのテクスチャのサイズ(縦横 16px)に合わせて設定した。
TileSet リソースを編集する
続けて、「TileSet」プロパティに割り当てたリソース「TileSet」を編集していく。
- インスペクターでリソースをクリックする。
- Godot エディタ下部の「TileSet」パネルが開いたら、ファイルシステムドックからスプライトシートのリソース「res://colored-transparent_packed.png」をパネル内の左サイドバーにドラッグして追加する。追加したリソースをクリックして編集モードにしよう。
- 「New Single Tile」を選択する。
- 「領域(Region)」タブを選択したまま、グリッドスナップを有効にする。
- インスペクターで「Snap Options」>「Step」を (x: 16, y: 16)にして、スプライトシートのテクスチャ一つ分と同じサイズにする。
- 順番に「草」と「木」のテクスチャの領域を選択して、Single Tile として登録する。
- 「木」のタイルは、「コリジョン(Collision)」タブを選択して、コリジョン形状も設定する。
以上で TileSet リソースの編集は完了だ。
TileMap を作成する
ここからは先に作成した「TileSet」リソースのタイルを配置して TileMap を作成していく。
- シーンツリードックで「TileMap」を選択した状態にする。
- 2D ワークスペースのツールバーからグリッドスナップを有効にする。
- 同じくツールバーの「Snapping Options」をクリックし、「Configure Snap」を選択する。
- 「Configure Snap」の設定パネルが開いたら、「Grid Step」を (x: 16, y: 16) にして「OK」をクリックする。
これで 2D ワークスペース上に、縦横 16px ごとに区切られたグリッドが表示されていることだろう。「Grid Snap」を有効にしているので、「TileMap」の編集時は、簡単にグリッドに沿ってタイルを配置できるようになっているはずだ。
では 2D ワークスペースにタイルを配置していく。ここではシンプルに、あとで配置するプレイヤーキャラクターが外に出ないように画面の枠を「木」のタイルで囲って、その中に「草」のタイルを敷き詰めよう。
これで「TileMap」の編集は完了だ。
Player シーンを作る
ここからはプレイヤーキャラクターのシーンを作成する。このチュートリアルで実際にグリッドベース移動させるためのオブジェクトだ。
- 「シーン」メニュー>「新規シーン」を選択する。
- 「ルートノードを生成」にて「その他のノード」を選択する。
- 「KinematicBody2D」クラスのノードをルートノードとして選択。
- ルートノードの名前を「Player」に変更する。
- 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://Player/Player.tscn」としてシーンを保存する。
- ルートノードに「Sprite」ノードを追加する。
- ルートノードに「CollisionShape2D」ノードを追加する。
- ルートノードに「RayCast2D」ノードを追加する。
これで「Player」シーンツリーは以下のようになったはずだ。
Player シーンの各ノードを編集する
Sprite ノード
たくさんのテクスチャをまとめた1枚のスプライトシートから使いたいテクスチャの範囲を指定してスプライトのテクスチャを設定する方法を採用する。
- インスペクターにて、「Texture」プロパティにリソースファイル「res://colored-transparent_packed.png」を適用する。
- 「Offset」>「Centered」プロパティをオフにする。これにより、このノードの位置(「Position」プロパティ)がテクスチャの中央ではなく左上角になり、ちょうどグリッドのマス目に収まるようになる。
- 「Region」>「Enabled」をオンにする。
- エディタ下部の「テクスチャ領域」パネルを開く。ここで行うのはスプライトシートの中の利用したいテクスチャの領域を指定する作業だ。
- 作業しやすいように、展開アイコンをクリックしてパネルを広げる。
- パネル上部の「snapモード」で「グリッドスナップ」を選択する。
- パネル上部の「ステップ」を 16px 16px にする。これでグリッドのサイズがスプライトシートのテクスチャ 1 つ分と同じサイズになる。
- スプライトシート上でドラッグ操作により「王様」のテクスチャを範囲選択する。
- 作業しやすいように、展開アイコンをクリックしてパネルを広げる。
CollisionShape2D ノード
このノードで「KinematicBody2D」クラスのルートノードに対してコリジョン形状を設定する。
- インスペクターにて、「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
- 「Sprite」ノードのテクスチャの位置に合わせるため、 「Transform」>「Position」プロパティの値を (x: 8, y: 8) にする。
- 2Dワークスペースにて、コリジョン形状を「Sprite」ノードのテクスチャのサイズに合わせる。
インスペクターで直接入力する場合は、「RectangleShape2D」リソースのプロパティを以下のようにする。- Extents: (x: 8, y: 8)
RayCast2D ノード
このノードはグリッドベース移動での衝突判定にとても役に立つ。2D ワークスペース上は矢印型のコリジョン形状として表される。その矢印とオブジェクトが重なった場合に衝突を検知する。これを利用して、プレイヤーキャラクターの前方にあるオブジェクトとの衝突を検知させ、その先には進めないようにするなど制御することができる。
- インスペクターで、「Enabled」プロパティを On にしておく。これで衝突判定が可能になる。
- 「Transform」>「Position」プロパティを (x: 8, y: 8) にしておく。これは「Sprite」のテクスチャの中央に位置を合わせるためだ。
- 「Cast To」プロパティの値を (x: 16, y: 0) にしておく。これは暫定的に初期値を設定しているだけで、実際にプロジェクトを実行したら、スクリプトでプレイヤーキャラクターを移動させるたびに値を変更することになる。
2D ワークスペース上では以下のスクリーンショットのようになったはずだ。 - 「Collide With」はデフォルトのまま「Areas」は Off、「Bodies」は On にしておく。さきほど作成したタイルセットの「木」のパネルは物理ボディなので「Bodies」にチェックが入っていれば、衝突が検知される。
Player ノードにグリッドベース移動を実装する
インプットマップを設定する
先にキーボードのキー入力でプレイヤーキャラクターを動かせるように、プロジェクト設定のインプットマップにアクションを追加する。
- 「プロジェクト」メニュー>「プロジェクト設定」を選択したら「Input Map」タブに切り替える。
- 以下の4つの「Action」を追加する。
- move_right: D キー
- move_left: A キー
- move_down: S キー
- move_up: W キー
*上下左右の矢印キーを割り当ててもOK
スクリプトをアタッチして編集する
まずはルートノード「Player」にスクリプトをアタッチする。ファイルパスを「res://Player/Player.gd」としてスクリプトファイルを作成しよう。
作成後、スクリプトエディタが開いたら、スクリプトのコードを以下のように編集してほしい。
extends KinematicBody2D
#1
const inputs = {
"move_right": Vector2.RIGHT,
"move_left": Vector2.LEFT,
"move_down": Vector2.DOWN,
"move_up": Vector2.UP
}
#2
var grid_size = 16
#3
onready var raycast = $RayCast2D
#4
func _unhandled_input(event):
for action in inputs.keys():
if event.is_action_pressed(action):
move(action)
#5
func move(action):
var destination = inputs[action] * grid_size
raycast.cast_to = destination
raycast.force_raycast_update()
if not raycast.is_colliding():
position += destination
スクリプトには、コメントで #1 ~ #5 まで番号を振っておいた。この順番に解説していく。
#1: 辞書型の定数inputs
を定義した。先に設定しておいたインプットマップのアクションと同じ名前の Key に対して、その値にはそれぞれのアクションで移動させたい Vector2 型の方向ベクトル(長さが 1 のベクトル)を指定した形だ。ちなみにVector2
クラスの組み込み定数(RIGHT
やDOWN
など)の値は以下の通りだ。
- Vector2.RIGHT: Vector2(1, 0)
- Vector2.LEFT: Vector2(-1, 0)
- Vector2.DOWN: Vector2(0, 1)
- Vector2.UP: Vector2(0, -1)
#2: プロパティgrid_size
を定義した。値は「TileMap」のタイルのサイズと同じ16
としている。
#3: プロパティraycast
を定義した。これは「RayCast2D」ノードを参照するプロパティだ。
#4: 組み込み関数_unhandled_input
をオーバーライドした。これはキーボードやマウス、ジョイスティックなどからの入力があるとすぐに呼ばれるコールバック関数だ。別の関数_input
と似ているが、ここでは細かい違いは気にしなくて大丈夫だ。
_unhandled_input
の中で、先に定義した辞書型定数inputs
に対してループ処理を行っている。もし入力されたのがinputs
のキー(move_left
やmove_up
など)と同じ名前のインプットマップアクションだった場合に、そのキーに対する値(例えばキーがmove_right
ならVector2.RIGHT
)を引数に渡してmove
というメソッドを呼んでいる。このmove
というメソッドはこのあと定義する。
#5: メソッドmove
を定義している。このメソッドを呼ぶときは引数action
に値を渡す必要がある。
まず変数destination
を定義している。値には、辞書型定数inputs
から引数action
とマッチするキーに対する値(例えば、action
にmove_left
が渡されている場合はVector2.LEFT
)にプロパティgrid_size
を乗算した値が入る。つまり、プレイヤーが「D」キーを入力した場合、その値は Vector2(1, 0) x 16 = Vector2(16, 0) になる。これは右方向の 1 グリッド分の長さを持つベクトルだ。
次に「RayCast2D」ノードのプロパティcast_to
に先に定義した変数destination
の値を渡している。これでプレイヤーが入力したキーに合わせて「RayCast2D」の矢印の向きと長さが置き換わる。この置き換えをただちにアップデートするのが次の行で呼んでいるforce_raycast_update
というメソッドだ。これは「RayCast2D」ノードの組み込みだ。
次の行ではif
構文が記述されている。「RayCast2D」ノードの組み込みメソッドis_colliding
は、現在このノード(矢印)がオブジェクトと衝突しているかどうかを Bool 型(true
かfalse
)で返してくれる。if
の後にnot
があるので、このif
構文の意味としては「もし『RayCast2D』ノードがオブジェクトと衝突していなければ』となる。
上述のif
構文で「RayCast2D」が他のオブジェクトと衝突していなかった場合、「Player」ノードのプロパティposition
の値にdestination
の値が加算される。つまりdestination
の値の分だけプレイヤーキャラクターが移動する、ということになる。例えば、現在の「Player」ノードの位置がVector2(64, 32)
だとして、この時プレイヤーが「S」キーを一度押すと、現在の位置からVector2(0, 16)
だけ移動するので、移動先の位置はVector2(64, 48)
となる。つまり、現在の位置から下方向に 1 グリッド分移動したということだ。
以上でグリッドベース移動の制御が実装できたはずだ。
World シーンに Player シーンのインスタンスを追加する
「Player」シーンが完成したので、そのインスタンスを「World」シーンに追加して、タイルマップ上を移動させてみよう。
- 「World.tscn」シーンを開く
- ルートノード「World」に「Player.tscn」シーンのインスタンスを追加する。
- 2D ワークスペースで、画面の中央あたりの適当な位置に「Player」ノードを移動する。
以上で作業は完了だ。
グリッドベース移動の動作確認をする
作業が完了したので、実際にグリッドベース移動が問題なくできるかどうか確認してみよう。
特に「Player」の子ノード「RayCast2D」の矢印型コリジョン形状も確認しやすくするため、先に「デバッグ(Debug)」メニューから「Visible Collision Shapes」のチェックを入れて有効にしておこう。
初めてプロジェクトを実行する場合は Main Scene に「res://World/World.tscn」を選択してほしい。
それでは、プロジェクトを実行して、大草原の中を王様がグリッドベース移動で駆け回ることができるのか見てみよう。
一回のキー入力で、タイル 1 つ分だけ移動しているのがよくわかる。また、わざと「木」のタイルの方向に移動しようとしても移動できないことも確認できた。
以上で、グリッドベース移動のチュートリアルは完了だ。
サンプルゲーム
今回のグリッドベース移動を利用したサンプルゲームを用意した。プロジェクトファイルは、GitHubリポジトリ に置いているので、そこから .zip ファイルをダウンロードしていただき、「Sample」フォルダ内の「project.godot」ファイルを Godot Engine でインポートしていただければ OK だ。
このサンプルは、いわゆるモグラ叩きゲームだ。ゲームの設定として、プレイヤーは王様を操り、墓から出てくる王国の兵士たちの亡霊に向かって、聖なる指輪をかざして呪われた魂を解放する、というものだ。
操作は以下の通りだ。
- D: 右に移動
- A: 左に移動
- S: 下に移動
- W: 上に移動
- スペース: 亡霊と向かい合わせの場合のみ指輪を掲げて亡霊の魂を解放する
亡霊は 1 秒以内に指輪で成仏させないと王様に呪いをかけてくる。呪われると Life が一つ減る。Life は最初 10 用意されているが、全てなくなったら王様は呪い殺されゲームオーバーになる。
次の亡霊が出現するまでのインターバルは、ゲーム開始時の 2 秒から徐々に短くなり、最短 1 秒まで短くなる。
ちなみに私の最高スコアは 150 だ。これがすごいのか、大したことないのかはわからない。
補足説明
最後にこのサンプルゲームのプロジェクトについて補足しておきたい。
ゲーム中、墓石から亡霊が出現するとき、「KinematicBody2D」クラスでコリジョン形状を設定済みの「Ghost」インスタンスと「TileMap」のコリジョンを設定した墓石のタイルが重なっている状態だ。このとき「Player」インスタンスの「RayCast2D」が墓石より亡霊との衝突判定を優先させるために少し工夫している。
「TileSet」の編集で墓石のタイルを作るときに、タイルの 1/2 のサイズのコリジョンを設定しているのだ。先にインスペクターで「Snap Options」>「Step」を (x: 4, y: 4)にしてからコリジョン形状を設定するとスムーズだ。もちろん「Ghost」のコリジョン形状は (x: 16, y: 16) になっている。
おわりに
今回のチュートリアルでは2Dゲームにおけるグリッドベース移動の実装について解説した。パズルゲームやシミュレーションゲームなど、これからあなたが取り組むプロジェクトに応用いただける機会があれば幸いだ。
グリッドベース移動のポイントをまとめておこう。
- 以下を正確に設定する。
- リソース「TileSet」で用意したタイルの縦横のサイズ
- 移動させるオブジェクト(今回は「Player」)のスプライトのサイズと位置
- 移動させるオブジェクトのコリジョン形状のサイズと位置
- 2D ワークスペースの Grid Step
- オブジェクトのノードにアタッチしたスクリプトでの移動距離を表すプロパティの値(今回は
grid_size
)
- 移動させるオブジェクトと他のオブジェクトとの衝突判定には「RayCast2D」を利用する。「RayCast2D」の向きとサイズはスクリプトで制御する。
- プレイヤーの操作でオブジェクトを移動させるには、インプットマップのアクションを登録しておき、スクリプトでそれぞれの入力に対して移動する方向を制御する。
参照
- KENNEY
- Godot Docs: RayCast2D
- Godot Docs: Using TileMaps
- Godot Docs: TileSet
- KidsCanCode: Grid-based movement
- YouTube: Grid-based movement Godot 3 demo overview
- YouTube: Make your first 2D grid-based game from scratch in Godot