【Godot】コルーチンの使い方

この記事ではコルーチンの使い方を解説します。

コルーチンとは何か

通常の関数は上から下に順番に実行して、それらの一連の処理が終わるまで関数は終了しません。

それに対して、コルーチンでは特定のキーワードを使用することで、関数の途中で処理を中断して、任意のタイミングでその関数を「途中から」再開することができます。関数の一時停止機能のようなものです。

ゲームを作る場合には、メインループというものがあり、そのメインループを止めてしまうとゲーム全体が一時停止してしまいます。そのため通常、ゲームオブジェクトは「状態変数」を定義して一時停止状態なら1秒待つ……というような処理を書く必要があります。

具体的には以下のようなコードです。

// ■コルーチンを使用しない場合

switch(state) {
case 0:
  // 企業ロゴの入場アニメーション
  if(アニメーション終了) {
    timer = 60; // 60フレーム待つ
    state = 1;
  }
  break;

case 1:
  // 少し待つ
  timer--;
  if(timer <= 0) {
    // タイトル画面表示へ
    state = 2;
  }

case 2:
  // タイトル画面表示アニメーション
  if(アニメーション終了) {
    // キー入力受付開始
    state = 3;
  }
  break;

case 4:
  // キー入力の受付
  break;
}

ですが、コルーチンを使うことで状態変数を持たなくても、上から下に連続して処理を記述できます。

# ■コルーチンを使用した場合

# 企業ロゴのアニメーション開始
# 何らかのアニメーション呼び出し

# アニメーション完了をコルーチンでシグナルで待つ
yield($AnimationPlayer, "finished")

# タイマー完了で1秒間停止
yield(get_tree().create_timer(1), "timeout")

# タイトル画面のアニメーション開始
# 何らかのアニメーション呼び出し

# アニメーション完了をコルーチンでシグナルで待つ
yield($AnimationPlayer, "finished")

# キー入力の開始処理へ

Godot 3.x系の場合

どうやら Godot 4以降では、コルーチンを定義するキーワード yield() が await に変更されるようです。

そのため、Godot 3.x 系というカテゴリでまずは紹介します。

yield() の使い方 (引数あり)

Godotでコルーチンを使うためには yield() キーワードを使用します。例えば、以下のコードは 1秒ごとにゲームの経過時間を出力するものです。

extends Node2D

# 経過時間
var past:float = 0

func _ready() -> void:
	while true:
		print("past_time: %f"%past)
		# 1秒ごとに経過時間を出力する
		yield(get_tree().create_timer(1), "timeout")	

func _process(delta: float) -> void:
	# 経過時間を足し込む
	past += delta

このように、yield() 関数の第1引数にオブジェクト、第2引数にシグナルを指定すると、そのシグナルが発行されるまで停止を行うことができます。

引数なしの yield()

先程はオブジェクトとシグナルを指定しましたが、引数なしで yield() を定義すると、処理を一時中断してその関数は中断した状態のオブジェクト (GDScriptFunctionState) を返します。

以下を 上キーを押すと 3回 “up” を出力し、下キーを押すと 3回 “down” を出力する例です。

extends Node2D

# 経過時間
var past:float = 0

# コルーチン
var my_func:GDScriptFunctionState = null

func _print_up_3times() -> void:
	# 3回 "up" を出力する
	for i in range(3):
		print("[%d] up"%i)
		yield()	# 処理を一時中断	

func _print_down_3times() -> void:
	# 3回 "up" を出力する
	for i in range(3):
		print("[%d] down"%i)
		yield()	# 処理を一時中断

func _process(delta: float) -> void:
	if Input.is_action_just_pressed("ui_up"):
		my_func = _print_up_3times()
	elif Input.is_action_just_pressed("ui_down"):
		my_func = _print_down_3times()

	past += delta
	if past > 1:	
		# 1秒ごとに出力する
		past -= 1
		if my_func and my_func.is_valid():
			my_func = my_func.resume()

この場合は、resume() でその関数を呼び出さない限り、処理は継続されません。

それに対して引数ありの yield() は別スレッドが作られるような挙動で resume() の呼び出しに関わらず実行されるような印象です。

引数ありの yield() と引数なしの yield() を混合して使用した場合

特に問題なく動作するようです。

ただ引数ありの yield() は別スレッドを大量に生成するようなものとなりますので、毎フレーム 引数ありの yield() 関数を呼び出すようなことは避けたほうが良さそうです。

Godot 4系の場合

こちらの記事によると、基本的には yield() キーワードが await に置き換わった挙動となるとのことです。

https://zenn.dev/submax/articles/30433a77da3cca

  • GDScriptFunctionState が廃止された
  • シグナルを渡すときは「文字列」はなく「Signal型」になった
  • awaitキーワードを使用した関数の戻り値に、任意の型を指定できるようになった

ただ実際に使ってみないと何とも、という感じですので正式リリースされたら使ってみようと思います。