SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Godot Engine 2Dゲーム開発入門

【Godot Engine 2Dゲーム制作 Part2】「オリジナル迷路ゲーム」の迷路をつくろう

Godot Engine 2Dゲーム開発入門 第4回

  • X ポスト
  • このエントリーをはてなブックマークに追加

 この連載は、Godot Engineでのゲーム開発を始めて、簡単なミニゲームを作るまでを、順を追ってたどるものです。前回からは、オリジナルの迷路ゲームを作っていきます。前回は、プレイヤーが操作する「Player」シーンを作り、ゲームの中心となる「Maze」シーンを作り、クリア時の演出である「Clear」シーンを作りました。今回はその続きです。ゲームの中心となる迷路を実装して、実際に遊べるようにします。

  • X ポスト
  • このエントリーをはてなブックマークに追加

迷路のプログラムを書こう

スクリプトの作成

 まずは、迷路を生成するプログラムを書きましょう。「ファイルシステム」ドックのres://scn/maze/フォルダーを右クリックして「新規作成」>「スクリプト」を選びます。

 「スクリプト作成」ダイアログが開くので、「テンプレート」のチェックボックスを外し、「パス」をres://scn/maze/maze_util.gdにして「作成」ボタンを押します。

 作成されたmaze_util.gdをダブルクリックして「Script」ビューで開きます。maze_util.gdに、次のように迷路を生成するプログラムを書きます。

maze_util.gd
extends Node

class_name MazeUtil

const MIN = 9
const ROAD = 0
const WALL = 1


# 迷路の生成
static func gen_maze(w: int, h: int) -> Dictionary:
	# 横幅と高さを奇数にする、最小サイズ以上にする
	if w % 2 == 0: w += 1
	if h % 2 == 0: h += 1
	w = max(w, MIN)
	h = max(h, MIN)

	# 迷路用配列の準備	
	var maze_arr = []
	for y in h:
		var arr = []
		arr.resize(w)
		arr.fill(WALL)
		maze_arr.append(arr)

	# スタート位置の決定と、ゴール位置の仮決定
	var x = randi_range(3, w - 4) / 2 * 2 + 1
	var y = randi_range(3, h - 4) / 2 * 2 + 1
	var start = Vector2i(x, y)
	var goal = Vector2i(start)
	maze_arr[start.y][start.x] = ROAD

	var dirs = [Vector2i.LEFT, Vector2i.DOWN, Vector2i.UP, Vector2i.RIGHT];
	var roads = [start]

	while roads.size() > 0:
		dirs.shuffle()
		var pos_now = roads.pop_back()
		for dir in dirs:
			var pos1 = pos_now + dir
			var pos2 = pos_now + dir + dir
			if pos2.x < 1 or pos2.x >= w - 1 or pos2.y < 1 or pos2.y >= h - 1:
				continue
			if maze_arr[pos2.y][pos2.x] == ROAD:
				continue
			maze_arr[pos1.y][pos1.x] = ROAD
			maze_arr[pos2.y][pos2.x] = ROAD
			goal = pos2
			roads.push_back(pos2)
		roads.shuffle()

	return {"start": start, "goal": goal, "maze_arr": maze_arr, "w": w, "h": h}

 俗に言う「穴掘り法」と呼ばれるアルゴリズムの派生です。全てのマスを壁で埋めて、スタート位置から上下左右にランダムに2マスずつ穴を掘り続けます。すでに掘り進んだ場所や、外周には掘り進めません。最終的に掘れなくなったら終わりです。

スクリプトの解説1 文法

 このスクリプトには、GDScript固有の書き方がいくつかあるので、その書き方を紹介します。

 まずはクラス名の定義です。トップレベルにclass_name クラス名と書くことで、プロジェクト内のどこからも、このスクリプトをクラス名で呼び出すことができます。シーンにアタッチしないスクリプトを使う時に便利な方法です。

class_name MyClass

 次は、配列を指定サイズと指定の値で埋めて初期化する方法です。[]で配列を作成したあと、resize関数でサイズを変更して、fill関数で要素を埋めます。もう少し簡便な方法があるとよいのですが、現状ではこのような形になっています。

var arr = []
arr.resize(4)
arr.fill(1)

 割り算については少し注意が必要です。次のスクリプトは、変数n1が入ります。GDScriptではPythonと異なり、整数 / 整数は小数点数ではなく整数になります。

var n = 3 / 2

 上のスクリプトを実行すると、「デバッガー」の「エラー」にInteger division, decimal part will be discarded.(整数除算の場合、小数部分は切り捨てられます)という黄色の警告が出ます。Pythonの割り算と同じと思って書いて、思わぬ値になる人がいるので、このような警告が出ているのだと思います。

 この警告をオフにする方法があります。トップメニューの「プロジェクト」>「プロジェクト設定」を選び、「プロジェクト設定」ダイアログを開きます。そして、「一般」タブの「デバッグ」>「GDScript」>「Integer Division」の値を、「Warn」(警告)から「Ignore」(無視)に変更します。これで警告は出なくなります。

Integer Division
Integer Division

 次は配列の関数です。

var arr = [1, 2, 3]
arr.push_back(4)
var n = arr.pop_back()
arr.shuffle()

 push_back関数で末尾に追加、push_front関数で先頭に追加します。append関数は、push_back関数のエイリアスです。

 また、pop_back関数で末尾から取り出し、pop_front関数で先頭から取り出します。

 配列には、ゲームでよく使う「シャッフル」をおこなうshuffle関数が用意されています。その他の関数も確かめておくとよいでしょう。

スクリプトの解説2 処理の流れ

 迷路の生成をおこなう関数の処理の流れを書きます。

  • 横幅と高さの調整
    • 奇数にする(「開始点1マス+穴掘り2マスずつ+末端の壁2マス」で奇数になるため)
    • 最小サイズ以上にする(壁沿いより少し内側に開始点を置きたいため)
  • 迷路用配列の準備
    • 2次元配列を作る
    • 壁(1)で埋める
  • スタート位置の決定と、ゴール位置の仮決定
    • ランダムな整数をrandi_range関数で得る。範囲は3以上、横幅-4未満
    • ランダムな整数を2で割って2を掛けて偶数にする
    • 偶数に1を足して、奇数マスをスタート位置にする
    • ゴール位置は、最後に穴を掘った場所にするので、スタート位置で仮置きする
  • 4方向の変数dirsと、穴掘り位置を記録する道の変数roadsを用意する
    • 4方向は左、下、上、右とする
    • 変数roadsの最初の要素はスタート位置とする
  • 変数roadsのサイズが0より大きい間、処理を続ける
    • 4方向の変数dirsをシャッフルする
    • 道の変数roadsからマスを1つ取り出す
    • 4方向のループ処理
      • 1マス進む位置pos1、2マス進む位置pos2を作る
      • 2マス進む位置が外壁なら飛ばす
      • 2マス進む位置がすでに道なら飛ばす
      • 迷路の1マス進む位置と2マス進む位置を道にする
      • 2マス進む位置をゴール位置にする
      • 道の変数roadsに、2マス進む位置を追加
    • 道の変数roadsをシャッフルする

 このように、短めの処理で手軽に迷路を作れます。

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

 迷路を生成するスクリプトを書きましたが、このプログラムが正しく動作するかを検証する必要があります。gen_maze関数のあとに、次のようなスクリプトを書きます。

maze_util.gd 続き
# デバッグ用
static func maze_info_to_str(info: Dictionary) -> String:
	var res = ""
	var start = info["start"]; var goal = info["goal"]
	var w = info["w"]; var h = info["h"]
	res += "w%s h%s, S(%s, %s), G(%s, %s)\n" % [w, h, start.x, start.y, goal.x, goal.y]
	for y in h:
		var t = ""
		for x in w:
			var cell = info["maze_arr"][y][x]
			if start.x == x and start.y == y: t += "S"
			elif goal.x == x and goal.y == y: t += "G"
			else: t += [" ", "X"][cell]
		res += t + "\n"
	return res


# テスト(目視テスト)
static func test_gen_maze() -> void:
	for i in range(MIN - 2, MIN + 4):
		print("[Input] w%s h%s" % [i, i])
		print(maze_info_to_str(gen_maze(i, i)))

 迷路をテキストにするmaze_info_to_str関数と、迷路を生成して目視で確認するためのtest_gen_maze関数です。

 正しく動作するかを確認するために、MIN - 2から生成を開始して、MIN + 4までの横幅、高さの迷路を出力します。

 このまま直接実行できると便利なのですが、GDScriptは基本的にシーンに紐付いて呼び出されます(その他の方法もあります)。

 ここでは、前回作ったmaze.gd_ready関数から呼び出すようにしましょう。_ready関数を次のように書き換えます。

maze.gd
func _ready() -> void:
	MazeUtil.test_gen_maze()

 maze_util.gdで、クラス名をMazeUtilと定義しているので、そのstatic関数test_gen_mazeをこのように呼び出せます。

 実行して出力した結果を掲載します。正しく動作していることが分かります。

[Input] w7 h7
w9 h9, S(3, 3), G(7, 7)
XXXXXXXXX
X   X   X
XXX X XXX
X  S    X
X X X X X
X X X X X
X X X XXX
X X X  GX
XXXXXXXXX

[Input] w8 h8
w9 h9, S(3, 3), G(7, 7)
XXXXXXXXX
X X     X
X X XXX X
X  S  X X
X X XXXXX
X X     X
X X XXX X
X X   XGX
XXXXXXXXX

[Input] w9 h9
w9 h9, S(3, 3), G(7, 7)
XXXXXXXXX
X     X X
XXX XXX X
X  S    X
XXX XXX X
X     X X
X X XXX X
X X   XGX
XXXXXXXXX

[Input] w10 h10
w11 h11, S(7, 7), G(1, 1)
XXXXXXXXXXX
XGX       X
X XXXXX XXX
X         X
XXXXXXX XXX
X X X   X X
X X XXX X X
X      S  X
XXX X X X X
X   X X X X
XXXXXXXXXXX

[Input] w11 h11
w11 h11, S(7, 7), G(1, 1)
XXXXXXXXXXX
XGX     X X
X XXXXX X X
X X       X
X XXXXX XXX
X X     X X
X XXXXX X X
X      S  X
XXXXX X X X
X     X X X
XXXXXXXXXXX

[Input] w12 h12
w13 h13, S(5, 3), G(9, 11)
XXXXXXXXXXXXX
X           X
XXXXX XXXXX X
X    S    X X
XXX X XXX X X
X   X   X X X
XXX X X X X X
X   X X X X X
XXX XXXXXXX X
X       X   X
XXX X X XXX X
X   X X  GX X
XXXXXXXXXXXXX

 テストが終了したら、MazeUtil.test_gen_maze()をコメントアウトして、次のようにしておきます。

func _ready() -> void:
	#MazeUtil.test_gen_maze()
	pass

次のページ
床や壁のシーンを作ろう

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Godot Engine 2Dゲーム開発入門連載記事一覧

もっと読む

この記事の著者

柳井 政和(ヤナイ マサカズ)

クロノス・クラウン合同会社 代表社員http://crocro.com/オンラインソフトを多数公開。プログラムを書いたり、ゲームを作ったり、記事を執筆したり、マンガを描いたり、小説を書いたりしています。「めもりーくりーなー」でオンラインソフト大賞に入賞。最近は、小説家デビューして小説も書いています(『裏切りのプログラム』他)。面白いことなら何でもOKのさすらいの企画屋です。 

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/21372 2025/07/04 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング