ゲーム開発において、処理の「待機」や「一時停止」は頻繁に発生します。例えば、アニメーションの終了を待つ、ネットワーク通信の応答を待つ、あるいは一定時間後に処理を再開するなどです。これらの処理をメインループ(フレーム処理)を止めずに効率的に行うために不可欠なのが、非同期処理 と コルーチン の概念です。
Godot EngineのGDScriptでは、この非同期処理をシンプルかつ強力に扱うためのキーワードとしてawaitが導入されました。本記事では、awaitの基本的な使い方から、その背後にあるコルーチンの仕組み、パフォーマンスの注意点、そしてGodot 3系で使われていたyieldからの移行方法までを解説します。
なぜ非同期処理とawaitが重要なのか
ゲームは常にスムーズに動作し続ける必要があります。もし、何らかの処理(例えば、巨大なファイルの読み込みや複雑な計算)がメインスレッドを長時間占有してしまうと、ゲームはフリーズし、ユーザー体験は著しく損なわれます。これを ブロッキング と呼びます。
awaitを使った非同期処理は、このブロッキングを回避するための強力な武器です。awaitは、関数の実行をその場で一時停止し、待機が完了するまでGodotエンジンに制御を戻します。その間、エンジンは他の処理(描画、入力受付、物理演算など)を続行できるため、ゲームはフリーズしません。そして、待機していたイベント(シグナルの発行など)が発生すると、エンジンは停止した箇所から自動的に関数の実行を再開します。
この「一時停止と再開」の仕組みを持つ関数がコルーチンです。GDScriptでは、関数内でawaitを使った瞬間に、その関数は自動的にコルーチンになります。
awaitの基本的な使い方
awaitは、シグナルまたは他のコルーチンの完了を待機するために使用します。
1. シグナルの待機
最も一般的な使い方は、ノードから発行されるシグナルを待つことです。
# 例: 攻撃アニメーションが終了したらダメージ判定を行う
func _on_attack_button_pressed():
$AnimationPlayer.play("attack")
# AnimationPlayerが持つ`animation_finished`シグナルを待機する
await $AnimationPlayer.animation_finished
# アニメーションが終わったので、ここでダメージ計算などの処理を実行
print("攻撃アニメーション終了!ダメージ判定!")
2. 一定時間を待機する (タ イマー)
指定した時間だけ処理を中断したい場合は、SceneTreeが提供するタイマー生成機能と組み合わせます。
# 例: 3秒後に敵を出現させる
func spawn_enemies_after_delay():
print("ゲーム開始!3秒後に敵が出現します。")
# SceneTreeTimerを作成し、その`timeout`シグナルを待機する
await get_tree().create_timer(3.0).timeout
print("時間です!敵を生成します。")
# ここで敵インスタンスを生成する処理
3. 他のコルーチンの完了を待機する
awaitは、それ自体がコルーチンである他の関数の完了を待つこともできます。
# 非同期でリソースをロードし、完了を待つ関数
func load_level_async() -> void:
print("レベルデータのロードを開始します...")
await get_tree().create_timer(2.0).timeout # ダミーのロード時間
print("レベルデータのロードが完了しました。")
# ゲーム開始シーケンス
func start_game():
# まずUIをフェードインさせる(コルーチン)
await fade_in_ui()
# 次にレベルデータを非同期でロードする(コルーチン)
await load_level_async()
# すべての準備が整ったので、プレイヤーを操作可能にする
print("ゲームスタート!")
func fade_in_ui():
var tween = create_tween()
tween.tween_property($UI/CanvasLayer, "modulate:a", 1.0, 1.0)
await tween.finished
Tweenと
awaitの注意点: Tweenのdurationが0、または開始時点で既に目標値に到達している場合など、Tweenが即座に完了するケースではfinishedシグナルが発行されない可能性があります。このような場合は、awaitが永遠に待機し続けるため、事前に条件チェックを行うか、タイムアウト処理を併用することを検討してください。
awaitとコルーチンの関係
awaitが使われている関数は、自動的に コルーチン として扱われます。コルーチンとは、実行を一時停止し、後で再開できる関数のことです。
コルーチンの特徴:
| 特徴 | 説明 |
|---|---|
| 協調的 | 処理の切り替えは、プログラマがawaitキーワードを使って明示的に行います。 |
| 一時停止と再開 | awaitの箇所で一時停止し、待機対象が完了した時点で自動的に再開します。 |
| スタックの保持 | 一時停止しても、ローカル変数などの実行コンテキスト(スタック)は保持されます。 |
Godot 3のyieldからawaitへの移行
Godot Engine 4.0以降では、非同期処理の記述方法が大きく変更されました。Godot 3系で使われていたyieldキーワードは廃止され、awaitキーワ ードに置き換えられました。
移行の基本パターン
Godot 3.x (yield) | Godot 4.x (await) | 備考 |
|---|---|---|
yield(object, "signal_name") | await object.signal_name | シグナル待機は最もシンプルな移行パターンです。 |
yield(get_tree().create_timer(time), "timeout") | await get_tree().create_timer(time).timeout | タイマー待機も同様にシグナル待機として記述します。 |
yield(func_call(), "completed") | await func_call() | 非同期関数の完了を待つ場合。 |
実践的な移行例
# Godot 3.x (yield)
func wait_and_do_something():
yield(get_tree().create_timer(2.0), "timeout")
print("2秒経ったよ!")
# Godot 4.x (await)
func wait_and_do_something():
await get_tree().create_timer(2.0).timeout
print("2秒経ったよ!")
よくある間違いとベストプラクティス
| よくある間違い | ベストプラクティス |
|---|---|
_process内で毎フレームawaitする | _process内でのawaitは、処理を1フレーム以上停止させ、意図しない動作遅延の原因になります。awaitは、特定のイベントを起点とする一回限りのシーケンス処理に使い、毎フレームのチェックは状態変数(State Machine)などで行うべきです。 |
ノードが削除された後のawait | awaitで待機しているノードが、待機中にqueue_free()などで削除されると、処理が再開されずエラーの原因になります。待機前にis_instance_valid(node)でノードの存在を確認するか、ノードのtree_exitingシグナルも同時に待機するなどの工夫が必要です。 |
シグナル接続とawaitの混同 | awaitは一度きりの待機です。ボタンが押されるたびに何かをしたい場合は、button_downシグナルをconnectして、対応する関数を呼び出すのが適切です。 |
| 戻り値の無視 | シグナルによっては、引数として値を渡すことがあります。var result = await object.signal_nameのように、awaitはシグナルの引数を返します。 |
パフォーマンスに関する注意点と代替パターン
awaitのオーバーヘッド
awaitによるコルーチンの生成と管理には、わずかながらコストがかかります。数百、数千といった大量のオブジェクトが同時にコルーチンを実行すると、スケジュ ーリングのオーバーヘッドが積み重なり、パフォーマンスに影響を与える可能性があります。
代替パターン:Threadクラス
本当に重い処理(例:複雑なAI計算、大規模なデータ処理、ファイルI/O)を行う場合、awaitではメインスレッドがわずかにカクつく可能性があります。このようなCPU負荷の高いタスクは、Threadクラスを使ってバックグラウンドスレッドで実行するのが最適です。
await (コルーチン) | Thread (スレッド) | |
|---|---|---|
| 目的 | メインスレッドでの協調的マルチタスク(待機処理) | バックグラウンドでの並列処理(重い計算) |
| スレッド | メインスレッドのみ | メインスレッドとは別のスレッド |
| 実装 | awaitキーワードを使うだけ | Threadオブジェクトを作成し、関数を呼び出す |
| 注意点 | 処理自体はメインスレッドで行われる | メインスレッドのデータへのアクセスには注意(Mutexが必要な場合も) |
代替パターン:シグナルの直接接続
イベントに対して永続的な応答をしたい場合は、awaitではなくシグナルを直接connectするのが基本です。
# シグナルを接続する方法(推奨されるイベント処理)
func _ready():
$MyButton.button_down.connect(_on_my_button_pressed)
func _on_my_button_pressed():
print("ボタンが押されました!")
まとめ
Godot Engineのawaitキーワードは、非同期処理とコルーチンをGDScriptで扱うための強力なツールです。
| 概念 | キーワード | 役割 |
|---|---|---|
| 非同期処理 | await | メインスレッドをブロックせずに、処理の待機と再開を可能にする。 |
| コルーチン | awaitを含む関数 | 実行を一時停止し、後で再開できる関数。複雑なシーケンスを簡潔に記述できる。 |
Godot 3系からGodot 4系への移行では、yieldからawaitへの置き換えが必須となりますが、awaitはより直感的でモダンな非同期処理の記述を可能にします。