ぬーぶのメモ帳

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

【Godot】簡易的なワールドマップを作ってみた

ステージセレクトをイメージした感じ




見た目は粗末ですが各ステージを選んでそれぞれのシーンに飛ぶことが可能です。
最低限マップとしての機能は果たしてくれると思います。

素材を用意

あくまで実験的にマップを制作したかったので素材も全てPolygon2Dで適当に作成。
Polygon2Dは画像を用意するのが面倒な時等に色付きの素材を簡単に作れるので便利。



Polygon2Dノードを作った後上の緑のプラスを選択。



ポチポチとポリゴンを増やしていく。



最初に作った点へ戻り、ポリゴンを塞ぐと2Dポリゴンの完成。




選択するためのステージとカーソルもサクサク作成。

ステージを格納できるようにする

まずステージのコード…といってもたったの一行。

# stage.gd
export var stage : PackedScene

exportvarの前に挿入するとスクリプト内で作った変数がインスペクタータブ内に表示され、簡単に変更することが可能になります。
PackedScene は シーンファイル(tscnファイル) を指定しています。
つまり stage という変数指定したデータを入れてくださいねという事。

これに シーンファイル を入れておくことで、ワールドマップでステージを決定した時に格納したステージへ切り替えようと考えています。

ステージデータを取得する

次にマップのコードを書く前にシーンのツリー構成をこのような形にしました。

ステージを格納するノードを作成して、そこにステージオブジェクトを全て入れるようにするのがポイント。

# map.gd

onready var stage_list := $stage_container.get_children()

func _ready() -> void:
# 後で消す
	for i in stage_list.size():
		print(stage_list[i].position)

まず上の画像の stage_container から get_children() で子を取得して 変数stage_list に格納します。(データの型はArray)
上手く格納できていればステージオブジェクトを全て取得したことになり、試しに for文で各ステージの座標を表示してみる。

カーソルの仮置き

上手くステージオブジェクトを全て取得できているようなので次にカーソルを仮でステージ1に配置します。

# map.gd

onready var stage_list := $stage_container.get_children()

onready var cursor := $cursor
var cursor_data : int= 0

func _ready() -> void:
	cursor.position = Vector2(stage_list[cursor_data].position.x, stage_list[cursor_data].position.y -20)

カーソルのポジションを取得したシーンオブジェクトの位置に指定することで、ステージを選択してる事を表す事が可能です。

マップにカーソルが上手く配置することができ、後々キーボードの左右の矢印でステージ選択できるように改造します。

ステージ間にラインを引く

次にステージ間で道を表すラインを引こうとして問題が発生、Godotの仕様で下に来るノードほど画面で手前に表示される。(画像編集ソフト的に言えばレイヤーが上になっている)
そのため一番上にあるルートノードの map で線を引いても、マップ素材の海と陸に邪魔されて見ることができません。

そのためステージ間のラインは stage_container にコードを書くことにしました。

# stage_container.gd

onready var stage_list := $get_children()

func _ready() -> void:
	update()

func _draw() -> void:
	# size()で取得される数は4、Arrayは0から始まるので最大値は3
	# そのためsize()-1としないとこの処理ではエラーになる
	var s_end = stage_list.size() - 1
	for i in stage_list.size():
		draw_line(stage_list[i].position, stage_list[clamp(i+1,0, s_end)].position, Color.black, 2.0)

_draw()はGodotに備わっている描画専用の関数で、この中で直線や四角などグラフィック描画を命令する専用の関数になっています。
draw_lineは名前の通り直線を引く指示を与えることができます。

draw_line(線の開始地点 , 線の終了地点 , 線の色 , 線の太さ)

またグラフィックの描画を実行するにはupdate()の命令を実行させる必要があるので_ready()に配置しています。

for文を使って全てのステージ間に描画命令を出した結果が以下になります、これだけでも大分マップらしくなったと思います。


またこの方法だと stage_container 内を全て読み込むのでステージの場所が変わっても、ステージの数が5個6個と増えても道筋を表示してくれます。

ステージの位置を変えた場合


ステージを増やした場合

ステージを選択できるようにする

いよいよステージ選択するためのコードを書き込む、やる事は配列の変数stage_list にアクセスして任意のステージシーンへ移動できるようにします。

# map.gd

onready var stage_list := $stage_container.get_children()

onready var cursor := $cursor
 var cursor_data : int= 0


func _input(event: InputEvent) -> void:
	var left = event.is_action_pressed("ui_left")
	var right = event.is_action_pressed("ui_right")
	
	if event.is_action_pressed("ui_accept"):
		if stage_list[cursor_data].stage != null:
			get_tree().change_scene_to(stage_list[cursor_data].stage)
		else:
			return
		
	elif (left or right):
		cursor_data = int(clamp(cursor_data + (-1 if left else 1), 0, stage_list.size() - 1))
		cursor.position = Vector2(stage_list[cursor_data].position.x, stage_list[cursor_data].position.y -20)

今回 _input() 関数を使用してステージを選択できるようにしようと思います。
_input(event: InputEvent)もGodotで用意されている関数で、何らかのキーが入力されるたび実行される模様(マウスのモーションにも反応はしているもよう)

is_action_pressed() は括弧内に設定したキーが押された場合trueを返す関数。
それにそれぞれ "ui_left"(左矢印キー) "ui_right"(右矢印キー) "ui_accept"(Enterキー)を設定。
"ui_left" "ui_right" "ui_accept" はGodotでのデフォルトキー設定の名称、詳しく見たい場合はGodotの上メニュー プロジェクト→プロジェクト設定→インプットマップ から確認できます。



話を戻すと、上のキーが押された時にリアクションがあるようにコードを書き込む。

func _input(event: InputEvent) -> void:
	var left = event.is_action_pressed("ui_left")
	var right = event.is_action_pressed("ui_right")

まず _input() 内に変数 leftとright を作りそれぞれ指定したキーが入力されるとtrue、違う場合falseを返す関数を格納。

var cursor_data : int= 0

#	if event.is_action_pressed("ui_accept"):
#		if stage_list[cursor_data].stage != null:
#			get_tree().change_scene_to(stage_list[cursor_data].stage)
#		else:
#			return
		
	elif (left or right):
		cursor_data = int(clamp(cursor_data + (-1 if left else 1), 0, stage_list.size() - 1))
#		cursor.position = Vector2(stage_list[cursor_data].position.x, stage_list[cursor_data].position.y -20)

次は少し飛ばして elif(left or right) を見て下さい。
上で設定した変数 leftかright が押された場合実行されます。
内容としては 変数cursor_data 内の数値を 変数 leftかright が押されるたびに上下させるというもの。
clamp(cursor_data + (-1 if left else 1), 0, stage_list.size() - 1) の太字の部分がポイントで、
(true if 条件 else false) 条件を満たすと左、満たさない場合右の結果を実行します。
上を例にすると、変数 leftが押されると 変数cursor_dataは-1、変数 right が押されると+1される仕組みになり、これを「三項演算子」と呼ぶそうです。
(JavaScriptなど記述が違うものもあります)

また clamp() で0からステージ最大数の範囲外に出ないようにし、エラーが起こらないようにしました。



onready var stage_list := $stage_container.get_children()
var cursor_data : int= 0

func _input(event: InputEvent) -> void:
#	var left = event.is_action_pressed("ui_left")
#	var right = event.is_action_pressed("ui_right")

	if event.is_action_pressed("ui_accept"):
		if stage_list[cursor_data].stage != null:
			get_tree().change_scene_to(stage_list[cursor_data].stage)
		else:
			return
		
#	elif (left or right):
#		cursor_data = int(clamp(cursor_data + (-1 if left else 1), 0, stage_list.size() - 1))
		cursor.position = Vector2(stage_list[cursor_data].position.x, stage_list[cursor_data].position.y -20)

変数cursor_dataを能動的に変更できるようになった事で
if event.is_action_pressed("ui_accept") 内が実行できるようになりました。

stage_list[cursor_data]に注目、変数stage_listは配列の形式でステージオブジェクトを全て取得しています。
配列へのアクセスの方法は上を例にするとstage_list[数字] で取り出したいデータにstage_list[0]...1...2...と数字を変えてアクセスする事ができます。

つまりstage_list[cursor_data]とする事で、左右キーで数値の変わる 変数cursor_data が任意のstage_listの配列へアクセスを可能にしています。

if event.is_action_pressed("ui_accept"):
	if stage_list[cursor_data].stage != null:
		get_tree().change_scene_to(stage_list[cursor_data].stage)
	else:
		return

後はエンターキーが押され呼び出したステージオブジェクトの 変数stage が空でない時、格納されたステージ画面に切り替わるようになります。

	# ノードの取得ができていればstage.gdの 変数stage を指定して引き出すことも可能
	get_tree().change_scene_to(stage_list[cursor_data].stage)

ちなみにelseとreturnがなく、かつ 変数stageが空の場合存在しないシーンに飛ばされてしまうので注意。


上のstage_listの配列アクセスを利用しカーソルをステージ上に配置する事で、プレイヤーに選択されているステージを認識させることが可能に。

#	elif (left or right):
#		cursor_data = int(clamp(cursor_data + (-1 if left else 1), 0, stage_list.size() - 1))
		cursor.position = Vector2(stage_list[cursor_data].position.x, stage_list[cursor_data].position.y -20)
完成とおまけ

完成して動作確認をしたものがこちら。

全てのステージへのアクセス、カーソルの表示、最低限選択型のワールドマップになっていると思います。

欠点はマップが一本道であること、例えば十字のマップを作った時はこの方式ではダメです。
他にもマウスクリックに対応させるには、ステージをアンロック方式にするなど改良点はあげればキリはないがこれは簡易マップです。



これをあと少し見栄えを良くする為ラインをもう少し道らしくする、選択されているステージを光らすくらいと考えた結果。

やっている事は別記事のメニューを作る方式と変わらないですが、よりシンプルに作れたと感じています。
あれで散々頭を悩ませた事でシステムメニュー作りへの理解度が少し深まったと思います。