Godot 4 await構文
この前の続き
Godot4を触ってて見つけたもののうち、紹介しきれてなかったGDScript2.0の新機能、
await
について書きます
※注釈
この記事が書かれた時点(2021/9/11)はGodot4リリース以前であり、この記事の内容はGodotのnightly buildを使って書かれたものです。
リリース時には仕様が変更されている可能性もありますがご了承ください
基本知識
Godotのイベントシステムの基本はsignal
であり、それは3.x系でも4でも変わりなし
Godotの非同期関数は、一旦処理を中断してNodeが特定のsignalをemitするまで待ち、受信したら再開するというCoroutine
パターンらしいです。
今までは GDScriptFunctionState
というClassがあって、実行中の関数から yield
で状態を返し、.resume()
メソッドで処理を再開したり、completed
シグナルを待ち受けて完了を検出したりしていました。
Godot3.x系以前までの非同期(yield)
英単語の意味
yield 明け渡す、移譲する
一旦、関数の処理を中断して呼び出し元に実行状態(どの行まで実行した?とか)を返すイメージ
yield(node, "signal_string_name")
で、指定したnodeが指定したsignalをemitするまで待つ
# 3.x系のyieldを使った非同期
extends Node
signal my_signal(msg)
# yield を使う関数は、戻り値が GDScriptFunctionState オブジェクトになる
func async_func() -> GDScriptFunctionState:
var timer = self.get_tree().create_timer(5.0)
yield(timer, "timeout")
return "5.0sec lapsed"
# GDScriptFunctionState オブジェクトは、コルーチンが完了したら completed(retVal) シグナルを出す
func _ready():
var finish_msg = yield(async_func(), "completed")
print(finish_msg) # "5.0sec lapsed"
これでも非同期プログラミング出来ないことはないですが、不満点もあります
- yieldを使っている関数だと、戻り値の型ラベルによる検査がうまく動いてない
- signalが文字列指定なので typo する危険がある
- そもそも、今どきだとcoroutineをyieldするという考え方が直感的じゃない
もっとJavaScriptのPromise的な抽象化でやりたいです!
というわけで、 Godot4 で yield
と入れ替えになった await
を見てみましょう
Godot4からの非同期(await)
yield
が await
というキーワードに変わった
使い方はだいたい同じだが、強化されている部分もある
以下の2種類の使い方ができるようになった
-
signal
を待つ -
coroutine
を待つ (new!)
# 4系でのawaitを使った非同期
extends Node
func async_func() -> String:
var timer = self.get_tree().create_timer(5.0)
# SceneTreeTimer オブジェクトの timeout シグナルを待つ
await timer.timeout
return "5.0sec lapsed"
func _ready():
# coroutine を待つ
var finish_msg = await async_func()
print(finish_msg) # "5.0sec lapsed"
(zennがまだGDScript2.0に対応してないみたいで、awaitがハイライトされてませんが)
Godot4ではGDScriptFunctionState
というClassは削除されて、await
キーワードを使う関数はcoroutineとして扱われるようになりました。
yield(func_state, "completed")
のように completed シグナルを待つ方式ではなく直接 coroutine の完了を待つことができるようになり、知っているべき前提知識(completed シグナルの存在)やコードのボイラープレート的な記述がこれまでより減ります。
また、await
使っている関数(coroutine)を実行するときにawait
で待ち受けないと以下のようなコンパイルエラーが出るようになりました。
Parse Error: Function "async_func()" is a coroutine, so it must be called with "await".
また、前の記事で書いたとおり、Signal
はクラス定義されたオブジェクトとなっていてメンバ変数のようにアクセスできるようになったため、yield(node, "signal_string_name")
のようにNodeオブジェクト+文字列指定する必要がなくなり、Signal型を渡せば良くなりました。
型によるコンパイルエラーを検出でき、typoによるミスも減ります。
高度な例: HTTPRequest
await
が使えると何が嬉しいの?的な疑問があると思います。
例えば、ネットワーク関連では非同期的な処理を同期的に扱えると嬉しいですね。
ネットワークの世界では非同期な処理がたくさんありますが、それらの結果を同期的に待ったり、順番が重要な場合というのはよくあります。
Webの申し子たるJavaScript(ECMAScript)に async/await [1] 構文が導入されているように、非同期処理を同期的に扱える抽象化というのは直感的なプログラミングをするために重要だというわけです。
await
を使わないで、signalだけでやろうとするとコールバック地獄[2]になってしまいますからね!
# Godot4 における HTTPRequest Node の使用例
extends Node
# HTTPRequest signal:
# request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray)
func _ready():
var req = HTTPRequest.new()
self.add_child(req)
var network_err = req.request("https://httpbin.org/get") # GET request
if network_err != OK:
return
var res = await req.request_completed # request_completed シグナルを待つ
match res:
[OK, 200, _, var body]:
print("OK")
var bodyStr = (body as PackedByteArray).get_string_from_utf8()
print(bodyStr)
[OK, 400, _, _]:
print("Bad Request")
[OK, _, _, _]:
print("Internal Server Error")
_:
print("Other error")
実はGDScriptは、エンジン付属の動的スクリプト言語のくせにいっちょ前にパターンマッチ[3]が使えます。signal
が多値を含むときはArrayで渡ってくるので、それの分解と場合分けに使ってます(Elixirみたいですね)
上の例は https://httpbin.org (HTTPのテストサイト)にアクセスして、ステータスコードが200だったらbodyを文字列として出力するという単純なプログラムです。
まとめ
- 新しく
await
キーワードが追加-
signal
に加えてcoroutine
も待てるように
-
-
yield
キーワードとGDScriptFunctionState
classは削除 - 非同期処理を同期っぽく書くのが更にかんたんに
非同期プログラミングを同期的にする抽象化は特にネットワークと相性がいいので、GodotでマルチプレイヤーゲームとかMMOとかランキング機能とか、オンライン要素のあるゲームを作りたい人にはGodot4及びGDScript2.0へのアップデートは必見なのでは無いでしょうか?
ますますリリースが待ち遠しくなってきた。
-
ECMAScript2017 で採用された構文。async function は関数を暗黙のPromiseに変換する、await は Promise が返されるのを待つ。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/await ↩︎
-
非同期プログラミングにおいて、コールバック方式を採用したことで関数のネストが深くなり、可読性が著しく阻害される様。例えば、JSのサーバーサイド実行環境であるNodeJS(commonJS)では、非同期にPromiseではなくコールバックが採用されているためよく地獄が見られる(ES6以降からトランスパイルすることで回避可能) ↩︎
-
OCaml、Haskell、Elixirなどの関数型言語に実装されている、めっちゃ強化されたSwitch文。関数型言語にインスパイアされたRustなど、モダンなプログラミング言語にはよく搭載されている。 ↩︎
Discussion