このチュートリアルでは、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」とでもしておこう。

エディタが表示されたら、先にゲームのディスプレイサイズを設定しておこう。

  1. 「プロジェクト」メニュー>「プロジェクト設定」を開く。
  2. 「window」で検索して、サイドバーの「Display」>「Window」を選択する。
  3. 「Size」セクションで以下の項目の値を変更する。
    • Width: 256
    • Height: 160
    • Test Width: 1024
    • Test Height: 640
      Display>Window>Size
  4. 「Stretch」セクションで以下の項目の値を変更する。
    • Mode: 2d
    • Aspect: keep
      Display>Window>Stretch

続いて、KENNEYのサイトからアセットをダウンロードして利用させてもらおう。今回利用するのは 1-Bit Pack というアセットパックだ。この素晴らしすぎる無料の素材に感謝せずにはいられない。

ダウンロードしたら「Tilesheet」フォルダ内の「colored-transparent_packed.png」ファイルをエディタのファイルシステムドックへドラッグ&ドロップしてプロジェクトにインポートしよう。

ファイルをインポートした直後は画像がぼやけた感じになっているので、これを修正する。

  1. ファイルシステムドックでインポートしたアセットファイルを選択した状態にする
    select the asset
  2. インポートドックで「プリセット」>「2D Pixel」を選択する
    select 2D Pixel
  3. 一番下にある「再インポート」ボタンをクリックする。
    click reinport

これでピクセルアート特有のエッジの効いた画像になる。


World シーンを作る

まず最初のシーンとして、ゲームの世界を用意する。「World」という名前のシーンを作成しよう。

  1. 「シーン」メニュー>「新規シーン」を選択する。
  2. 「ルートノードを生成」にて「その他のノード」を選択する。
  3. 「Node2D」クラスのノードをルートノードとして選択。
  4. ルートノードの名前を「World」に変更する。
  5. 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://World/World.tscn」としてシーンを保存する。
  6. ルートノードに「TileMap」ノードを追加する。

これでシーンツリーは以下のようになったはずだ。
World Scene Tree



TileMap ノードを編集する

TileMap ノードのプロパティを編集する

シーンツリードックで「TileMap」ノードを選択し、インスペクターでプロパティを編集しよう。

  1. 「TileSet」プロパティに「新規 TileSet」リソースを割り当てる。
    TileSet property
  2. 「Cell」>「Size」プロパティの値を(x: 16, y: 16)にする。これから利用するスプライトシートのテクスチャのサイズ(縦横 16px)に合わせて設定した。
    Cell_size property

TileSet リソースを編集する

続けて、「TileSet」プロパティに割り当てたリソース「TileSet」を編集していく。

  1. インスペクターでリソースをクリックする。
    Click TileSet resource
  2. Godot エディタ下部の「TileSet」パネルが開いたら、ファイルシステムドックからスプライトシートのリソース「res://colored-transparent_packed.png」をパネル内の左サイドバーにドラッグして追加する。追加したリソースをクリックして編集モードにしよう。
    TileSet Pannel
  3. 「New Single Tile」を選択する。
    New Single Tile
  4. 「領域(Region)」タブを選択したまま、グリッドスナップを有効にする。
    Enable grid snap
  5. インスペクターで「Snap Options」>「Step」を (x: 16, y: 16)にして、スプライトシートのテクスチャ一つ分と同じサイズにする。
    Snap options
  6. 順番に「草」と「木」のテクスチャの領域を選択して、Single Tile として登録する。
    Region
  7. 「木」のタイルは、「コリジョン(Collision)」タブを選択して、コリジョン形状も設定する。
    Collision tab Collision

以上で TileSet リソースの編集は完了だ。


TileMap を作成する

ここからは先に作成した「TileSet」リソースのタイルを配置して TileMap を作成していく。

  1. シーンツリードックで「TileMap」を選択した状態にする。
  2. 2D ワークスペースのツールバーからグリッドスナップを有効にする。
    Enable Grid Snap
  3. 同じくツールバーの「Snapping Options」をクリックし、「Configure Snap」を選択する。
    Snapping options
  4. 「Configure Snap」の設定パネルが開いたら、「Grid Step」を (x: 16, y: 16) にして「OK」をクリックする。
    Configure Snap

これで 2D ワークスペース上に、縦横 16px ごとに区切られたグリッドが表示されていることだろう。「Grid Snap」を有効にしているので、「TileMap」の編集時は、簡単にグリッドに沿ってタイルを配置できるようになっているはずだ。


では 2D ワークスペースにタイルを配置していく。ここではシンプルに、あとで配置するプレイヤーキャラクターが外に出ないように画面の枠を「木」のタイルで囲って、その中に「草」のタイルを敷き詰めよう。
TileMap

これで「TileMap」の編集は完了だ。



Player シーンを作る

ここからはプレイヤーキャラクターのシーンを作成する。このチュートリアルで実際にグリッドベース移動させるためのオブジェクトだ。

  1. 「シーン」メニュー>「新規シーン」を選択する。
  2. 「ルートノードを生成」にて「その他のノード」を選択する。
  3. 「KinematicBody2D」クラスのノードをルートノードとして選択。
  4. ルートノードの名前を「Player」に変更する。
  5. 一旦ここでシーンを保存しておこう。フォルダを作成して、ファイルパスを「res://Player/Player.tscn」としてシーンを保存する。
  6. ルートノードに「Sprite」ノードを追加する。
  7. ルートノードに「CollisionShape2D」ノードを追加する。
  8. ルートノードに「RayCast2D」ノードを追加する。

これで「Player」シーンツリーは以下のようになったはずだ。
Player Scene Tree


Player シーンの各ノードを編集する

Sprite ノード

たくさんのテクスチャをまとめた1枚のスプライトシートから使いたいテクスチャの範囲を指定してスプライトのテクスチャを設定する方法を採用する。

  1. インスペクターにて、「Texture」プロパティにリソースファイル「res://colored-transparent_packed.png」を適用する。
    Sprite node Texture property
  2. 「Offset」>「Centered」プロパティをオフにする。これにより、このノードの位置(「Position」プロパティ)がテクスチャの中央ではなく左上角になり、ちょうどグリッドのマス目に収まるようになる。
    Offset > Centered = on
  3. 「Region」>「Enabled」をオンにする。
    Region > Enabled = on
  4. エディタ下部の「テクスチャ領域」パネルを開く。ここで行うのはスプライトシートの中の利用したいテクスチャの領域を指定する作業だ。
    Region panel
    1. 作業しやすいように、展開アイコンをクリックしてパネルを広げる。
      Expand Region pannel
    2. パネル上部の「snapモード」で「グリッドスナップ」を選択する。
      Region pannel > choose grid snap
    3. パネル上部の「ステップ」を 16px 16px にする。これでグリッドのサイズがスプライトシートのテクスチャ 1 つ分と同じサイズになる。
      Region pannel > input grid step
    4. スプライトシート上でドラッグ操作により「王様」のテクスチャを範囲選択する。
      Select region

CollisionShape2D ノード

このノードで「KinematicBody2D」クラスのルートノードに対してコリジョン形状を設定する。

  1. インスペクターにて、「Shape」プロパティに「新規 RectangleShape2D」リソースを適用する。
    Shape property
  2. 「Sprite」ノードのテクスチャの位置に合わせるため、 「Transform」>「Position」プロパティの値を (x: 8, y: 8) にする。
    Position property
  3. 2Dワークスペースにて、コリジョン形状を「Sprite」ノードのテクスチャのサイズに合わせる。
    CollisionShape in 2D workspace
    インスペクターで直接入力する場合は、「RectangleShape2D」リソースのプロパティを以下のようにする。
    • Extents: (x: 8, y: 8)

RayCast2D ノード

このノードはグリッドベース移動での衝突判定にとても役に立つ。2D ワークスペース上は矢印型のコリジョン形状として表される。その矢印とオブジェクトが重なった場合に衝突を検知する。これを利用して、プレイヤーキャラクターの前方にあるオブジェクトとの衝突を検知させ、その先には進めないようにするなど制御することができる。

  1. インスペクターで、「Enabled」プロパティを On にしておく。これで衝突判定が可能になる。
    RayCast2D Enabled=on
  2. 「Transform」>「Position」プロパティを (x: 8, y: 8) にしておく。これは「Sprite」のテクスチャの中央に位置を合わせるためだ。
    Position property
  3. 「Cast To」プロパティの値を (x: 16, y: 0) にしておく。これは暫定的に初期値を設定しているだけで、実際にプロジェクトを実行したら、スクリプトでプレイヤーキャラクターを移動させるたびに値を変更することになる。
    RayCast2D Cast To 2D ワークスペース上では以下のスクリーンショットのようになったはずだ。
    RayCast2D 2D Workspace
  4. 「Collide With」はデフォルトのまま「Areas」は Off、「Bodies」は On にしておく。さきほど作成したタイルセットの「木」のパネルは物理ボディなので「Bodies」にチェックが入っていれば、衝突が検知される。
    RayCast2D Collide With>Areas=Off, Bodies=On


Player ノードにグリッドベース移動を実装する

インプットマップを設定する

先にキーボードのキー入力でプレイヤーキャラクターを動かせるように、プロジェクト設定のインプットマップにアクションを追加する。

  1. 「プロジェクト」メニュー>「プロジェクト設定」を選択したら「Input Map」タブに切り替える。
  2. 以下の4つの「Action」を追加する。
    • move_right: D キー
    • move_left: A キー
    • move_down: S キー
    • move_up: W キー

*上下左右の矢印キーを割り当ててもOK
Project Settings - Input Map


スクリプトをアタッチして編集する

まずはルートノード「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クラスの組み込み定数(RIGHTDOWNなど)の値は以下の通りだ。

  • 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_leftmove_upなど)と同じ名前のインプットマップアクションだった場合に、そのキーに対する値(例えばキーがmove_rightならVector2.RIGHT)を引数に渡してmoveというメソッドを呼んでいる。このmoveというメソッドはこのあと定義する。

#5: メソッドmoveを定義している。このメソッドを呼ぶときは引数actionに値を渡す必要がある。

まず変数destinationを定義している。値には、辞書型定数inputsから引数actionとマッチするキーに対する値(例えば、actionmove_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 型(truefalse)で返してくれる。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」シーンに追加して、タイルマップ上を移動させてみよう。

  1. 「World.tscn」シーンを開く
  2. ルートノード「World」に「Player.tscn」シーンのインスタンスを追加する。
    Instance child scene
  3. 2D ワークスペースで、画面の中央あたりの適当な位置に「Player」ノードを移動する。
    Move Player node in 2D workspace

以上で作業は完了だ。



グリッドベース移動の動作確認をする

作業が完了したので、実際にグリッドベース移動が問題なくできるかどうか確認してみよう。

特に「Player」の子ノード「RayCast2D」の矢印型コリジョン形状も確認しやすくするため、先に「デバッグ(Debug)」メニューから「Visible Collision Shapes」のチェックを入れて有効にしておこう。
Debug menu - Visible Collision Shapes: On

初めてプロジェクトを実行する場合は Main Scene に「res://World/World.tscn」を選択してほしい。

それでは、プロジェクトを実行して、大草原の中を王様がグリッドベース移動で駆け回ることができるのか見てみよう。
Run project


一回のキー入力で、タイル 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) になっている。

Snap Option - Step Tomb collision setting in TileSet



おわりに

今回のチュートリアルでは2Dゲームにおけるグリッドベース移動の実装について解説した。パズルゲームやシミュレーションゲームなど、これからあなたが取り組むプロジェクトに応用いただける機会があれば幸いだ。

グリッドベース移動のポイントをまとめておこう。

  1. 以下を正確に設定する。
    • リソース「TileSet」で用意したタイルの縦横のサイズ
    • 移動させるオブジェクト(今回は「Player」)のスプライトのサイズと位置
    • 移動させるオブジェクトのコリジョン形状のサイズと位置
    • 2D ワークスペースの Grid Step
    • オブジェクトのノードにアタッチしたスクリプトでの移動距離を表すプロパティの値(今回はgrid_size
  2. 移動させるオブジェクトと他のオブジェクトとの衝突判定には「RayCast2D」を利用する。「RayCast2D」の向きとサイズはスクリプトで制御する。
  3. プレイヤーの操作でオブジェクトを移動させるには、インプットマップのアクションを登録しておき、スクリプトでそれぞれの入力に対して移動する方向を制御する。


参照