📌

Godot 4でゲームを作る(async/awaitによるシーン遷移とユニークノード)

2022/12/25に公開

Godot Engine Advent Calendar 2022 25日の記事です。よろしくお願いいたします。

Godot 4がそろそろ(来年?)リリースされますね!

XR Kaigi 2022というイベントで「ゲームエンジンGodot 4の概要とそのVR開発について」という発表をさせていただいた、こりんです(こちら後日YouTubeでアーカイブ動画が観れるようになると思います)。その流れでGodot 4にもう少し慣れようというのと、GDScriptの新機能の確認を兼ねて、簡単な見下ろし画面のシューティングゲームを作ってみることにしました。

ゲームとしてはまだ全然成立していない状態ですので期待せず!

下をクリックするとブラウザで動きます(現在PCのみ対応です)。矢印キーで移動、スペースキーでショットです。
https://korinvr.com/bin/topdownshooterexample/

プロジェクトファイルをGitHubで公開しています。
https://github.com/korinVR/TopDownShooterExample

async/awaitによるシーン遷移

多くのゲームソフトにはタイトル画面やアウトゲーム、インゲームといったシーン遷移があります。

今回作るのは古典的なアーケードスタイルのシーン遷移で、タイトル画面でスタートボタンを押すとゲームが開始し、敵を全滅させると次のレベルへ、全レベルをクリアするとエンディングへ、ゲーム開始時に残機が3あり、残機が0になるとゲームオーバーになりタイトル画面に戻るという流れにしようと考えました。

こうしたシーン遷移の実装は意外と面倒くさく、多くの場合、コアとなるゲームプレイよりも先にアプリケーション全体の動作の流れとして作っておく必要があります。

Godotの場合、AutoLoadのスクリプトやシーンでゲームの状態を保持しつつ条件やシグナルに従ってシーン遷移していく方法がおそらく一般的と思います。が、Unity方面で最近注目(?)されているテクニックの一つとして、async/awaitを使ってゲーム全体の流れを実行順序の通りに上から下に書いていくというものがあります。スクリプトをひとつ見るだけでシーンがどのように遷移するのか直感的に理解でき、不具合が入り込みにくいというメリットがあります。

詳しくは上のきゅぶんずさんの(Unityでの)解説を、というところなのですが、Godotでも似たようなことができるのではと思って試してみました。以下が今回作ったもののメイン部分です。

func _ready() -> void:
	while true:
		var title_scene := TitleScene.instantiate()
		add_child(title_scene)
		await title_scene.game_started
		title_scene.queue_free()
		
		GameState.reset()
		
		for level_index in range(1, 3):
			_load_level(level_index)
			_show_message("LEVEL %d" % level_index)
			
			while true:
				if %Level/%Player == null:
					GameState.decrease_player()
					if GameState.is_gameover():
						break
					
					var player := Player.instantiate()
					var spwan_transform := (%Level/PlayerSpawnPoint as Node3D).global_transform
					player.global_transform = spwan_transform
					%Level.add_child(player)
					
					%PlayerCamera.reset()
					
				if get_tree().get_nodes_in_group("enemy").size() == 0:
					break
				
				await get_tree().process_frame
			
			if GameState.is_gameover():
				_show_message("GAME OVER")
				await get_tree().create_timer(3).timeout
				%Level.queue_free()
				await get_tree().process_frame
				break
			
			_show_message("LEVEL COMPLETE")
			await get_tree().create_timer(3).timeout
			%Level.queue_free()
			await get_tree().process_frame
		
		if !GameState.is_gameover():
			_show_message("ENDING")
			await get_tree().create_timer(3).timeout

_ready関数のみでゲーム全体の流れをループで走らせてしまい、

await title_scene.game_started

でスタートボタンの入力を待ったり、ゲーム中は

await get_tree().process_frame

で1フレームごとにプレイヤーの死亡判定とリスポーン、敵全滅判定を回しています。

ゲームオーバーのメッセージを3秒間表示してその間遷移は発生しないみたいなのも簡単です(イベント駆動で書くとしょっちゅう不具合が入ったりする箇所だと思います)。

_show_message("GAME OVER")
await get_tree().create_timer(3).timeout

GDScriptにはgoto文やラベル付きbreak文がないのでフローによっては少し手間がかかりそうです。複数のシグナルをawaitしたり分岐したりするのにもひと手間かかる感じですね。果たしてこれがGodotでも良い方法なのかどうかはもう少し検討が必要だと思いますが、とりあえず動いてそうって感じでしょうか。

ユニークノード(Unique Node)

Godot 3.5からユニークノードという機能がつきました。シーンツリーにひとつしかないノードに簡単にアクセスできるようにする機能です。たとえば$Main/Level/Playerといった深い階層のノードが%Playerでアクセスできるようになります。

シーンツリーでノードを右クリックして「% Access as Unique Name」をクリックするとノードに%のマークがつきユニークノードになります。

ユニークノードが実装された経緯はこちらのプロポーザルに詳しいです。

そしてこの機能ですが、エディタ上でユニークノードにするだけでなく、実行時にユニークノードにすることもできます。

set_unique_name_in_owner(true)でユニークノードになるのですが、instantiateしたシーンをユニークノードとして参照する場合(in_ownerというAPIの通り)オーナーを設定する必要があります。子シーンの_ready関数で親のオーナーを設定するか、add_childしたシーンに親からオーナーを設定するかどちらかがよさそうです。

func _ready() -> void:
	owner = get_parent()
	set_unique_name_in_owner(true)
add_child(level)
level.owner = self または level.owner = owner
level.set_unique_name_in_owner(true)

今回、読み込んだ各レベルを%Levelで、またプレイヤー機を1つしか登場しないことを前提に%Playerでアクセスできるようにしてみました。以下のように階層を意識することなくアクセスできます。

var direction = (%Player.global_position - global_position).normalized()

ただ、今回は実行時にユニークノードが使えるか試したくてやってみたというのが大きく、実際には、例えば2人同時プレイできるようにしたいという「仕様を一部変更する!」が振ってきたら困るので、Groupsを使ったり、プレイヤーを参照するヘルパ関数を作るほうがいいかもしれません。

ということで突貫工事になって反省していますが、以上、Godot Engine Advent Calendar 2022の記事でした。来年はもっとGodot使っていきたいなと思っています。よろしくお願いいたします!

あとWebエクスポートでちょっとハマりポイントがあったので別途書きたいです。

Discussion