この記事はGPT先生と相談しながら書いています。
前回の続き
1.ファイル構成
Roguelike_Test_v0_1
+-- main.tscn/.gd:ゲームのメインシーンとそのスクリプト。
+-- assets:ゲームで使用される各種アセット。
| (地形チップ、マップ画像、プレイヤーチップなど)
+-- src
+-- common
| +-- gamesettings.gd:ゲーム全体の設定や定数を定義するスクリプト。
+-- dungeon
| +-- dungeon.tscn/.gd:ダンジョン生成のメインロジックとシーン。
| +-- dungeon_map.tscn/.gd:ダンジョンマップの表示を扱うシーンとスクリプト。
| +-- dungeon_rooms.tscn/.gd:ダンジョンの部屋配置を管理するシーンとスクリプト。
| +-- mappin.tscn/.gd:マップ上のピンやプレイヤー位置を示すシーンとスクリプト。
+-- player
+-- player.tscn/.gd:プレイヤーの表示と動作を定義するシーンとスクリプト。今回もdungeonフォルダ以下のシーンを作成していきます。
2.Dungeon_roomsシーン
ダンジョンを生成していきます。
ダンジョンの生成ルールを確認します。一応穴掘り法?を参考にしています。
- 1階層は4x4の16マスの中に部屋を13個作ります。(gamesettings.gdで定義)
- 最初の部屋はランダムにマスを選択し作成する。
- 現在の部屋から隣り合っているマスを確認し、部屋が無いマスの中からランダムに1マスを選択し部屋を作成する。
- 現在の部屋と作成した隣の部屋を接続する。(出口情報と出口の先の部屋情報を持たせる。)
- 作成した隣の部屋を現在の部屋とし、3に戻る。
- 連続して部屋を4個作成したら、既に作成されている部屋をランダムに選択し現在の部屋として掘り始める。(1本道になるのを防ぐ為。4はgamesettings.gdで定義)
- 隣り合ってるマスにすでに部屋が存在する等、部屋が作ることができなかった場合も既に作成されている部屋をランダムに選択し現在の部屋として掘り始める。
- 部屋数が最大値(13個)になったら終了。
- 1階層が完成したら、スタートとゴールを設定する。スタートは最初に作った部屋、ゴールはランダムで設定。
- 生成した部屋にオブジェクトを配置する。(現状は壁のみです。)
func create_rooms():
# 部屋を生成する。一応穴掘り法?で掘っていく
# var pos : Vector2
var currentroom = []
var nextroom = []
# 最初の部屋を取得
currentroom = _create_startroom()
# 隣接するマスに部屋を作成する
var tmproomnum = 0 # 部屋生成カウンタ
if _rooms.size() == 1:
tmproomnum = 1 # さきに始点の部屋作ってたら1から始める
while _rooms.size() < _roomnum:
# 次の部屋を取得する
nextroom = _create_next_room(currentroom)
if nextroom.size() < 1:
# 次の部屋が作成できなかったら
# スタート地点を変えて掘りなおす
tmproomnum = 999
else:
# 次の部屋が出来たら
# 部屋と部屋をつなぐ設定をする
_connect_rooms(currentroom, nextroom)
# その部屋から次を掘る
currentroom = nextroom
tmproomnum += 1
# 連続で一定数部屋を生成したら
# 既にある部屋からランダムに選んで、そこからまた部屋を生成する
if tmproomnum >= _roomnum:
# ランダムに生成された部屋を取得する
currentroom = _rooms[randi()%_rooms.size()]
# 部屋の生成が終了したら、スタート地点とゴール地点を設定
# スタートは最初に作成した部屋に設定
_rooms[0].type = Gamesettings.RoomType.Start
# 現在地をスタート地点に設定
_current_room_pos = _rooms[0].pos
# スタート地点の来たことあるフラグを立てる
_rooms[0].visited = true
# ゴールはスタート以外の部屋にランダムに設定
_rooms[randi()%(_rooms.size()-1)+1].type = Gamesettings.RoomType.Goal
# 部屋内のオブジェクトを生成する
for room in _rooms:
room.objects = _set_obj_room(room)
GDScript上のルールをそのままコードにした感じです。というかコードからルールを書きました。
細かな処理は、別関数で処理しています。
- _create_startroom():最初の部屋を作成する。ランダムにマスを選択し部屋を作成し、作成した部屋を返す。
- _create_next_room(currentroom):隣の部屋を作成する。渡した部屋(現在の部屋)から隣り合っているマスからランダムに1つ部屋を作成し、作成した部屋を返す。
- _connect_rooms(currentroom, nextroom):渡した二つの部屋をつなぐ。出口情報と出口の先の部屋情報の作成する。
- _set_obj_room(room):渡した部屋にオブジェクトを配置する。現状は壁で囲むだけです。
func _create_startroom():
# 掘り始めるマスを決める
# 部屋が一つもない場合は、ランダムに部屋を作成し返す
# 既に部屋がある場合は、既存の部屋からランダムに選択し返す
var index = 0
# 部屋が1つも存在しなかったら、ランダムにマスを選択する
if _rooms.size() < 1:
_add_room(Vector2(randi()%_width, randi()%_height), Gamesettings.RoomType.Others)
return _rooms[0]
else:
index = randi()%_rooms.size()
return _rooms[index]
GDScriptここではランダムに1部屋作っているだけです。部屋の追加関数を使用しています。
- _add_room(Vector2(randi()%_width, randi()%_height), Gamesettings.RoomType.Others)
座標と部屋タイプを指定すると、その部屋を作成する。
というか問題を発見してしまいました。
・この関数では最初の部屋を作る
・ルール6,7の時の新しい現在の部屋を取得する
という二つの処理が書かれていますが、create_roomsでは二つ目は使わず直接取得しています。
バグではない?気もしますが、使われない処理が入ってます。
ソースの見直しは大切です。このブログを書く意味もあったということです(笑)
func _add_room(pos:Vector2, type:Gamesettings.RoomType):
# 部屋情報を追加する
var room = {
"pos" : pos, # 部屋の座標
"exits" : [], # 隣の部屋につながっている出口、2次元配列になっている
# 出口がある方向(上下左右の壁)とつながっている部屋の座標を格納
# 壁4つ分設定(出口がある壁の情報のみ登録される)
"type" : type, # いまのところはスタート、ゴール、その他のみ
"visited" : false, # この部屋に訪れたことがあるか?
"objects" : [] # 部屋内のオブジェクトを設定
}
_rooms.append(room)
GDScriptここでは部屋情報を作成し追加しています。
部屋情報は1次元配列で管理しています。マス目状の階層なので2次元配列で管理しようか迷いましたが、ダンジョン生成時のアクセスのしやすから1次元配列にしました。
- exitsは出口情報です。出口の位置(上下左右)と行き先の部屋の情報を持っています。出口が複数ある場合は出口の数分の配列が追加されます。
- visitedはその部屋に訪れたことがあるかのフラグです。Map表示用です。
- objectsは部屋内の物が設定されます。ここは2次元配列になります。
この項目はどうなるかわかりません。モンスターやアイテム、罠とか宝箱などをどこで管理するかがまだまとまっていない為です。今後修正されるかもしれません。
部屋の作成時は座標しか設定していないので。第2引数は要らなかったかもしれません。
func _create_next_room(room):
# 指定された部屋から次の部屋を生成
# 移動できる部屋が複数あれば、ランダムで1つのマスを選び部屋を生成する
# 生成した部屋を返す
# 移動先が無い場合は空を返す
var nextpos : Vector2
var tmptiles = [] # 作成可能な部屋を格納する(全部)
for i in range(4):
nextpos = room.pos + DIRLIST[i]
if is_tile_open(nextpos):
# マスが空いていたら記録する
tmptiles.append(nextpos)
if tmptiles.size() == 0:
# 空いているマスが無ければ空を返す
return []
else:
# 空いていたら、その中からランダムで選択したマスに部屋を作り返す
nextpos = tmptiles[randi()%tmptiles.size()]
_add_room(nextpos, Gamesettings.RoomType.Others)
# 最後に追加した部屋を返す
return _rooms[_rooms.size() - 1]
GDScript隣り合っているマスで部屋が存在しないマスを抽出し、抽出されたマスからランダムで1マスに部屋を作成しています。そして、作成した部屋情報を返します。
func _connect_rooms(room1, room2):
# 隣り合っている二つの部屋がつながっているという情報をそれぞれの部屋に設定する
if room1.pos == room2.pos - DIRLIST[Gamesettings.Direction.LEFT]:
# 1が左側2が右側
room1.exits.append({"direction" : Gamesettings.Direction.LEFT, "pos" : room2.pos})
room2.exits.append({"direction" : Gamesettings.Direction.RIGHT, "pos" : room1.pos})
elif room1.pos == room2.pos - DIRLIST[Gamesettings.Direction.UP]:
# 1が上側2が下側
room1.exits.append({"direction" : Gamesettings.Direction.UP, "pos" : room2.pos})
room2.exits.append({"direction" : Gamesettings.Direction.DOWN, "pos" : room1.pos})
elif room1.pos == room2.pos - DIRLIST[Gamesettings.Direction.RIGHT]:
# 1が右側2が左側
room1.exits.append({"direction" : Gamesettings.Direction.RIGHT, "pos" : room2.pos})
room2.exits.append({"direction" : Gamesettings.Direction.LEFT, "pos" : room1.pos})
elif room1.pos == room2.pos - DIRLIST[Gamesettings.Direction.DOWN]:
# 1が下側2が上側
room1.exits.append({"direction" : Gamesettings.Direction.DOWN, "pos" : room2.pos})
room2.exits.append({"direction" : Gamesettings.Direction.UP, "pos" : room1.pos})
GDScript2つの部屋がどの方向で繋がっているかを判定し、方向と隣の部屋の座標を持たせています。
すでにある部屋から掘り直す処理があり、その場合は出口が複数になるので、その分出口情報は追加されていきます。
func _set_obj_room(room):
# 生成した部屋の中にオブジェクトを設置する
# 今のところは壁のみ
# 2次元配列を返す
var room2d = []
# 部屋と壁を作成
for i in range(Gamesettings.ROOM_HEIGHT):
var tmp = []
for j in range(Gamesettings.ROOM_WIDTH):
if i == 0 or i == Gamesettings.ROOM_HEIGHT - 1:
tmp.append(Gamesettings.RoomObj.WALL)
elif j == 0 or j == Gamesettings.ROOM_WIDTH - 1:
tmp.append(Gamesettings.RoomObj.WALL)
else:
tmp.append(Gamesettings.RoomObj.NON)
room2d.append(tmp)
# 出口を作成する
for exit in room.exits:
if exit.direction == Gamesettings.Direction.LEFT:
room2d[4][0] = exit.direction + 2
elif exit.direction == Gamesettings.Direction.UP:
room2d[0][5] = exit.direction + 2
elif exit.direction == Gamesettings.Direction.RIGHT:
room2d[4][10] = exit.direction + 2
elif exit.direction == Gamesettings.Direction.DOWN:
room2d[8][5] = exit.direction + 2
return room2d
GDScript今は壁しかありません。外周に壁を設定し、出口がある箇所に出口を設置しているだけです。
3.Dungeon_mapシーン
ダンジョンマップです。一応オートマッピングです。
- 1階層分のマップを表示する。
- 訪れたことのある部屋のみ表示する。
extends Node2D
## タイルレイヤー.
enum eTileLayer {
BACKGROUND , # 背景レイヤー
ROOMS, # 部屋レイヤー
EXIT, # 出口用のタイル(複数出口があるときは重ねる)
OBJECT = 6 # マップに表示するオブジェクト(今のところはスタートとゴールのみ)
}
const NONEXIT = Vector2(1, 0)
const DIRTILELIST = [
Vector2(0, 1), # 左向きタイル
Vector2(1, 1), # 上向きタイル
Vector2(2, 1), # 右向きタイル
Vector2(3, 1), # 下向きタイル
Vector2(0, 2), # スタート
Vector2(1, 2) # ゴール
]
@onready var tilemap = $TileMap
func proc(mapdata:Array) -> void:
# 1階層のマップを表示する
for room in mapdata:
# 来たことあるフラグが立っている部屋だけ表示する
if room.visited:
# 出口が無いタイル(無くてもいい)
tilemap.set_cell(eTileLayer.ROOMS, room.pos, 0, NONEXIT)
# 出口付きのタイルを貼る
var exitnum = 0
for exit in room.exits:
tilemap.set_cell(eTileLayer.EXIT + exitnum, room.pos, 0, DIRTILELIST[exit.direction])
exitnum += 1
# スタート地点とゴール地点もマップに表示
if room.type == Gamesettings.RoomType.Start:
tilemap.set_cell(eTileLayer.OBJECT, room.pos, 0, DIRTILELIST[4])
elif room.type == Gamesettings.RoomType.Goal:
tilemap.set_cell(eTileLayer.OBJECT, room.pos, 0, DIRTILELIST[5])
GDScript
- ここでもTileMapを使用しています。
- 見にくいですが、2段目のが各方向の出口が付いているチップです。
- 出口用のレイヤーが4つあり、出口の数分重ね合わせています。
- 出口レイヤーの上にもレイヤーがあり、スタート地点とゴール地点を表示します。
4.Mappinシーン
ダンジョンマップの上に表示する赤点です。
extends Node2D
func dsp_pin(pos):
# マップにピンを表示する(座標を変えるだけ)
# 最後の+はdungeon_mapの座標
position.x = (pos.x * Gamesettings.TILE_SIZE) + 180
position.y = (pos.y * Gamesettings.TILE_SIZE) + 0
GDScript座標を指定すると、そこに移動するだけです。
5.終わりに
プロジェクトはGithubにアップロードしています。
- ライセンスについて
- 本プロジェクトは [MITライセンス](LICENSE) の下で公開されています。
- 使用しているフリー素材
- このプロジェクトにはKenney (https://kenney.nl/) から提供されたアセットが含まれています。これらのアセットはCreative Commons Zero (CC0) ライセンスの下で提供されています。
これらの素晴らしいリソースを提供してくださったクリエイターの皆様に感謝します。