この記事ではコルーチンの使い方を解説します。
目次
コルーチンとは何か
通常の関数は上から下に順番に実行して、それらの一連の処理が終わるまで関数は終了しません。
それに対して、コルーチンでは特定のキーワードを使用することで、関数の途中で処理を中断して、任意のタイミングでその関数を「途中から」再開することができます。関数の一時停止機能のようなものです。
ゲームを作る場合には、メインループというものがあり、そのメインループを止めてしまうとゲーム全体が一時停止してしまいます。そのため通常、ゲームオブジェクトは「状態変数」を定義して一時停止状態なら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キーワードを使用した関数の戻り値に、任意の型を指定できるようになった
ただ実際に使ってみないと何とも、という感じですので正式リリースされたら使ってみようと思います。