ぬーぶのメモ帳

フリーゲームエンジン「Godot」でプログラム経験のない素人の備忘録 最近は色々な動作を考えて試すのが好き。

【Godot】2DMapでの立体交差を考える

立体交差してるマップ…いいよね



まずこちらをご覧ください

立体的な2Dマップいいですね、見栄えもいいしくぐる時ちょっとワクワクしませんか?
(敵が隠れててイラッとなることもありますが)

日,英語の両方で検索するも手法的なものが出てきません…じゃあどうするか、自分で作るしかないですよね。

見た目は機能するものができました…が
課題も多いです、それでもよろしければ最後までお付き合いください。


今回からGodot4 にバージョンを上げているので3.xの方はスクリプトなど違いがあるのでご注意ください

ノード構成

構成は以下の通りです、

仕組みは非常に単純で上層と下層のタイルマップと、衝突専用のタイルマップをスイッチで切り替えるというものです。

下層の壁のみタイルマップのコリジョン設定しています

上層はZ_indexの設定をプラスにしてプレイヤーが隠れるようにしました

Z_index とは簡単に言うとイラストソフトのレイヤーを想像してください。
値が高ければ手前に、値が低ければ後ろに表示されるようになります、重なれば Z_index の優先順位で表示されます。
 


スクリプト

■プレイヤー

extends CharacterBody2D

var speed = 250

func _physics_process(delta: float) -> void:

	velocity.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
	velocity.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
	
	velocity = speed * velocity.normalized()

	move_and_slide()

操作キャラは特に珍しいことはしてません


colli_map

extends TileMap

# 上層と下層の当たり判定タイルの座標を格納する配列
# それと上層下層の切り替えスイッチ、デフォでは下層スイッチがオン
var l_coll :Array = []
var l_switch_flag := true

var h_coll :Array = []
var h_switch_flag := false


func _ready() -> void:
	# ここで高低の座標を格納させる
	l_coll = get_used_cells_by_id(0, 2, Vector2i(5, 1))
	h_coll = get_used_cells_by_id(0, 2, Vector2i(5, 0))
	
	coll_switch()

# スイッチに応じてタイルを切り替える関数
func coll_switch()-> void:
	clear() # 最初に全てのコリジョンタイルを削除
	if l_switch_flag: # 高低フラグの立っている方を実行
		for l_pos in l_coll: # 配列を順次処理させる
			set_cell(0, l_pos, 2, Vector2i(5, 1))

	if h_switch_flag:
		for h_pos in h_coll:
			set_cell(0, h_pos, 2, Vector2i(5, 0))


# 接続したシグナル達 スイッチの切り替えと対象の Z_index を変更
func _on_l_switch_body_entered(body: Node2D) -> void:
	l_switch_flag = true
	h_switch_flag = false
	body.z_index = 0
	coll_switch()


func _on_h_switch_body_entered(body: Node2D) -> void:
	h_switch_flag = true
	l_switch_flag = false
	body.z_index = 3
	coll_switch()

ポイントになるコマンドは

 ・get_used_cells_by_id(タイルレイヤー, アトラスID, タイルsetの座標)
 ・clear()
 ・set_cell(タイルレイヤー, 座標, アトラスID, タイルsetの座標)

辺りになると思います。



get_used_cells_by_id()指定したタイルがマップ内にある場合、配置座標の全てを配列で返します。
これで上層、下層に分けたコリジョンの配列をそれぞれ作成し配列 l_collh_coll に格納します。

clear() は名前の通り全てのタイルを取り除きます。
今回は作成した関数 coll_switch() 内で使用しています。
最初は上で取得した l_collh_coll で切り替えていましたが、これで初期化した方が早いのでは?と採用してみました。

set_cell() は指定タイルを指定座標に貼り付けます。
つまり上で保存した配列と Area2D をイベントフラグに使い上、下層の当たり判定を切り替えました。



その切り替えの制御を簡単にするためタイルマップノード colli_map に Area2D のスイッチ2つを子として追加。

colli_mapスクリプトを付け、子の Area2D それぞれの シグナル _body_entered(body: Node2D) を接続します。

実際には Area2D と CollisionShape内 を空にして colli_map をシーン化して量産、管理しやすくて良いかもしれませんね。

スクリプトとシグナルを設定した物が下の物になります。



colli_map をコリジョン判定と一緒に表示させた状態での切り替え

階段に設置した2つのArea2Dへ入るたび切り替わっているのがわかると思います。

Area2Dでドアを開いたり、ワープなど良くある手法としていることは同じで非常に単純です。

しかしこの方式ならArea2Dに複数のコリジョンを置いて、複数の出入り口にも対応できるはずです。

(もしかすると同じシェイプであることは重要でないかも)

・川を想定した立体交差、出入り口を複数に

課題も色々

最初にも問題点が多いと書きました。

・Z_indexの管理が面倒
 Z_indexを一つ上げるだけで思った以上に表示に齟齬が発生します、例えば…

このように UI が加わった時 Z_index の優先度に左右されてしまいます。
解決策は UI のZ_index を上げればいいのですが、デフォルトが全て0なものに1つ1つ設定するのは正直面倒です。
もしくは CanvasLayer を使用して常に最前に UI を表示させるのが楽かもしれません。

しかもこれは UI だけでなく、敵、弾、マップオブジェクトなど要素が増えるほど影響するので、あらかじめ自分でZ_indexの階層をどう扱うか決めておく必要がありそうです。
(他の機能で上手いことできるといいのですが)



・敵がハマる
 動き回る敵の場合は切り替えたコリジョンにはまり、動けなくなることがあります。

敵を固定砲台や、立体交差にはそもそも近づけないように専用の当たり判定をつけてもいいかもしれません。

余談ですが、記事外で敵にも専用の当たり判定とスイッチを作成し実験した所、敵が大量にいると切り替えが頻繁に起こりもっと酷いことになりましたw


割愛しますが他にもZ_index、コリジョンの判定から問題になる要素はまだまだあります。
そこを細かく調整していくのはなかなか大変に感じました。

しかし現状でも工夫次第でトップダウンゲームで使えるポテンシャルを感じます。
もしくはエンカウント式のJRPGのマップでは使えそうではないでしょうか?



まとめ
課題はあるもののやはり見栄えは良いのでピンポイントに使用する分は現状でもよさそうです。
z_indexも面倒であまり触れたくないなあと思っているのですが他の方はどうしているか気になります。

まだGodot4にして日が浅いためタイルマップなどで簡単にできる場合なかなか恥ずかしいですが…3.xでならヒントになると思います!
ここまでになります、最後までお付き合いくださりありがとうございました。