
迷路のプログラムを書こう
スクリプトの作成
まずは、迷路を生成するプログラムを書きましょう。「ファイルシステム」ドックのres://scn/maze/
フォルダーを右クリックして「新規作成」>「スクリプト」を選びます。
「スクリプト作成」ダイアログが開くので、「テンプレート」のチェックボックスを外し、「パス」をres://scn/maze/maze_util.gd
にして「作成」ボタンを押します。
作成されたmaze_util.gd
をダブルクリックして「Script」ビューで開きます。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)
割り算については少し注意が必要です。次のスクリプトは、変数n
に1
が入ります。GDScriptではPythonと異なり、整数 / 整数
は小数点数ではなく整数になります。
var n = 3 / 2
上のスクリプトを実行すると、「デバッガー」の「エラー」にInteger division, decimal part will be discarded.
(整数除算の場合、小数部分は切り捨てられます)という黄色の警告が出ます。Pythonの割り算と同じと思って書いて、思わぬ値になる人がいるので、このような警告が出ているのだと思います。
この警告をオフにする方法があります。トップメニューの「プロジェクト」>「プロジェクト設定」を選び、「プロジェクト設定」ダイアログを開きます。そして、「一般」タブの「デバッグ」>「GDScript」>「Integer Division」の値を、「Warn」(警告)から「Ignore」(無視)に変更します。これで警告は出なくなります。

次は配列の関数です。
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マス進む位置を追加
-
1マス進む位置
-
道の変数
roads
をシャッフルする
-
4方向の変数
このように、短めの処理で手軽に迷路を作れます。
テスト用のスクリプトの作成
迷路を生成するスクリプトを書きましたが、このプログラムが正しく動作するかを検証する必要があります。gen_maze
関数のあとに、次のようなスクリプトを書きます。
# デバッグ用 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
関数を次のように書き換えます。
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