【Godot】一筆書きゲームの作り方

今回はマス目を移動してすべてのマスを一筆書きで移動するゲームの作り方を紹介します。

とてもシンプルなものでゲームと言えるか微妙なところですが、マス目移動を行うゲームの基本になるかと思います。

一筆書きゲームの作り方

素材データ

今回使用する素材データは以下からダウンロードできます。

  • tileset.png: タイルとして使用する画像
  • player.png: 操作するプレイヤー画像

プロジェクトの作成と素材の登録

プロジェクトを作成し素材データの「tileset.png」「player.png」をプロジェクト(ファイルシステム)に追加しておきます。

メインシーンの作成

ゲームのメインとなるシーンを作成します。2Dシーン(Node2D) を作成して、ノード名を「Main」に変更しておきます。

タイルマップの追加

次にタイルマップノードを追加します。

タイルセットの作成

タイルマップノードのインスペクターから “Tile Set > [空]” をクリックしてタイルセットを作成します。

「新規 TileSet」をクリック。

そして作成された「TileSet」をクリックすると……

エディタの下の方に「タイルセットビュー」が表示されます。

タイルセットに画像を登録する

次にタイルセット画像を登録します。”tileset.png” をドラッグ&ドロップします。

するとタイルセット画像として「tileset.png」が登録されます。

ここからの操作がややわかりにくいのですが、最初に「+新しいシングルタイル」をクリックします。

そうすると「領域」という項目が表示されます。これは画像のどの部分をタイルとして扱うか、という指示となります。

次に矢印アイコンのとなりにあるアイコンをクリックすると 32px 単位でのスナップを有効にし、画像の黒い部分をドラックして暗い部分全体を囲みます。

少し操作がわかりにくいので GIF動画を用意しておきました。

上記手順でタイルセットを作成できたら Ctrl+S などでシーンを保存しておきます。

タイルマップの配置

続けて TileMapノードを選択すると、タイルマップをマウス操作で配置できるようになります。

このタイルマップを横に6マス、縦に3マス配置します。間違ったところに配置した場合は「右クリック」で消去できます。

タイルセットに通過したタイルを追加

先ほど設定したタイルは何もない状態のタイルとして扱います。そのため通過したタイルを別途作成します。

TileMapノードのインスペクターから TileSet をクリックしてタイルセットビューを表示します。

以下の手順で通過したタイルを追加します。

  1. タイルセットビューの “tileset.png” をクリック]
  2. 「+新しいシングルタイル」をクリック
  3. 白い部分をドラッグする
  4. Ctrl+Sなどでシーンを保存する

すると TileMapノードをクリックしたときに、通過したときのタイルが追加されています。

ちなみに、画像ファイル名の末尾に「数字」があります。例えば今回追加したタイルは「tileset.png 1」となっていますが、この数字は “タイルID” を示すものとなります。

プレイヤーの作成

次にこのタイルマップの上を移動するプレイヤーを作成します。新規に2Dシーン(Node2D)を作成して名前を “Player” に変更します。

続けて “player.png” をキャンバスにドラッグ&ドロップしてSpriteとして登録します。

この Spriteですが、インスペクターから以下の設定をしておきます。

  • Offset > Centerd のチェックを外す (左上座標基準で描画する)
  • Transform > Position の値を初期化 (x, y) = (0, 0) しておく

今回は左上座標基準で描画するのが楽なのでこのように設定しました。

プレイヤースクリプトの作成

Playerノード (ルートに作成したNode2D) に以下のスクリプトをアタッチします。

extends Node2D

# 1つのタイルのサイズ
const TILE_SIZE = 64

# マップの広さ
const MAP_WIDTH = 6
const MAP_HEIGHT = 3

var grid_pos = Vector2(0, 0)

func proc(tilemap:TileMap) -> void:
	# 移動処理
	var v = Vector2()
	if Input.is_action_just_pressed("ui_left"):
		v.x = -1
	elif Input.is_action_just_pressed("ui_up"):
		v.y = -1
	elif Input.is_action_just_pressed("ui_right"):
		v.x = 1
	elif Input.is_action_just_pressed("ui_down"):
		v.y = 1
	
	# 移動できるかどうかを判定する
	var next = grid_pos + v
	if _check_movable_pos(next, tilemap) == false:
		return # 移動できない
	
	# 移動可能
	grid_pos = next
	
	# 座標を反映する
	position.x = grid_pos.x * TILE_SIZE
	position.y = grid_pos.y * TILE_SIZE

func _check_movable_pos(pos:Vector2, tilemap:TileMap) -> bool:
	if pos.x < 0:
		return false # 移動できない
	if pos.y < 0:
		return false # 移動できない
	if pos.x >= MAP_WIDTH:
		return false # 移動できない
	if pos.y >= MAP_HEIGHT:
		return false # 移動できない
		
	# 移動可能
	return true	

ただこのスクリプト単体ではプレイヤーを動かすことはできません。

理由は “proc” という独自関数で更新を行うためです。なぜこの関数を定義したのかというと、マップの範囲外をチェクするためにはタイルマップの情報が必要となるためです。

この “proc” は Mainシーン側から呼び出す関数となります。

Mainシーンにスクリプトをアタッチする

ということでMainシーンを開いて以下のスクリプトをアタッチします。

extends Node2D

class_name Main

enum eTile {
	NONE = 0,
	FILL = 1, # 塗りつぶした
}
const map_size = Vector2(6, 3)
const tile_size = Vector2(64, 64)

onready var player = $Player
onready var tilemap = $TileMap


func _process(delta: float) -> void:
	
	# プレイヤー移動処理
	player.proc(tilemap)
	
	# タイルを塗りつぶす
	var pos = player.grid_pos
	tilemap.set_cell(pos.x, pos.y, eTile.FILL)
	
	# クリア判定
	if _check_completed():
		OS.alert("ゲームクリア")
		get_tree().quit()
	
func _check_completed() -> bool:
	for y in range(map_size.y):
		for x in range(map_size.x):
			if tilemap.get_cell(x, y) != eTile.FILL:
				# 塗りつぶしていないタイルがある
				return false

	# すべて塗りつぶした
	return true

プレイヤーシーンを Main に配置する

次に プレイヤーシーンを配置します。

Mainノードを選択してから “Player.tscn” をドラッグ&ドロップで配置すると、Mainノードの直下にぶら下げることができます。

動作確認

プレイヤーが配置できたら実行して動作を確認します。

ひとまず移動と塗りつぶしができるようになりました。

ですが、すでに塗りつぶされている場所への移動ができてしまっています。

そのため Player.gd スクリプトを以下のように修正します。

extends Node2D

# 1つのタイルのサイズ
const TILE_SIZE = 64

# マップの広さ
const MAP_WIDTH = 6
const MAP_HEIGHT = 3

var grid_pos = Vector2(0, 0)

func proc(tilemap:TileMap) -> void:
	# 移動処理
	var v = Vector2()
	if Input.is_action_just_pressed("ui_left"):
		v.x = -1
	elif Input.is_action_just_pressed("ui_up"):
		v.y = -1
	elif Input.is_action_just_pressed("ui_right"):
		v.x = 1
	elif Input.is_action_just_pressed("ui_down"):
		v.y = 1
	
	# 移動できるかどうかを判定する
	var next = grid_pos + v
	if _check_movable_pos(next, tilemap) == false:
		return # 移動できない
	
	# 移動可能
	grid_pos = next
	
	# 座標を反映する
	position.x = grid_pos.x * TILE_SIZE
	position.y = grid_pos.y * TILE_SIZE

func _check_movable_pos(pos:Vector2, tilemap:TileMap) -> bool:
	if pos.x < 0:
		return false # 移動できない
	if pos.y < 0:
		return false # 移動できない
	if pos.x >= MAP_WIDTH:
		return false # 移動できない
	if pos.y >= MAP_HEIGHT:
		return false # 移動できない
	
	# ----------------------------------
	# ここを追加する
	if tilemap.get_cell(pos.x, pos.y) == Main.eTile.FILL:
		return false # すでに塗りつぶしている
	# ----------------------------------
	
	# 移動可能
	return true	

修正したのは、”_check_movable_pos()” 関数の最後あたりの部分です。

実行するとすでに移動したタイルへの移動ができなくなっています。

補足

今回のコードではタイルマップを Main シーンに直接ぶら下げるようにしました、本来は1つのシーンとして定義しておき、シングルトン(自動読み込み)でどこからでもアクセスできるような作りが良いと思います。

完成プロジェクト

今回作成したプロジェクトファイルを添付しておきます。