「Godot4.1.3でGodot State Charts 機能拡張プラグインを使う 1/2」という記事の続きです。
前回の記事で作成した状態遷移
前回の記事で実装したStateChartです。
状態遷移図にすると下記のようになると思います。
CompoundStateのRootにAtomicStateの2つの状態、IdleとObservingがあります。黒丸からの矢印はIdleが初期状態であることを示しており、"enemy_entered"イベントをトリガにObserving状態に遷移することを表しています。
本稿では、この状態遷移にObservingからIdleに戻る遷移の追加や、遷移のdelay、並列状態、ガード条件、即時遷移など実装します。
ベースプロジェクト
こちらのGodotプロジェクトをベースにStateChartの機能を追加します。
githubから取得後に下記の操作をすると実行できるようになります。
- Godotにインポート
- AssetLibからState Chartをダウンロード
- プロジェクト設定のプラグインタブでState Chartを有効化する
敵が見張りの監視範囲外に移動したら、Idle状態に遷移する
Observing状態からIdle状態に遷移したいので、遷移元のObservingを選択した状態で、名前を「To Idle」と入力して、Transitionの追加ボタンを押下します。
「To Idle」Transitionノードを選択した状態でインスペクターのTransionのToに遷移先としたい「Idle」状態を設定します。Eventは「enemy_exited」と手入力します。
この設定により、StateChartノードのsend_eventメソッドで"enemy_exited"イベントを送ると、Toで設定した遷移先「Idle」状態に遷移するようになります。
Watchman/Area2Dを選択し、ノード/シグナルから「area_exited(area:Area2D)」シグナルをダブルクリックして、Watchmanのスクリプトに接続します。Watchmanのスクリプトを下記のようにして、StateChartに"enemy_exited"イベントを送信するようにします。
extends Node2D
func _on_area_2d_area_entered(area):
$StateChart.send_event("enemy_entered")
func _on_area_2d_area_exited(area):
$StateChart.send_event("enemy_exited")
実行します。
マウスで敵を移動し、監視領域に入るとObservingになるのは前回確認しました。
敵を監視領域の外に移動すると、Idleに遷移するようになりました。
Watchmanの向きを監視領域に入った敵の方向に変える
StateChartには特定の状態がアクティブの場合に毎周期発行するsignalがあるので、それを利用します。
Observing状態の時に、Watchmanの向きを敵の方向に変えるようにします。
「Observing」ノードを選択して、ノード/シグナルの「state_processing(delta:var)」をダブルクリックして、Watchmanのスクリプトに接続します。
メンバ変数「enemy」を宣言し、_on_area_2d_area_enteredメソッドの引数areaが敵なので、enemyに代入して保存します。observingのstate_processingで「look_at(enemy.global_position)」として、敵の方を見るようにします。
extends Node2D
var enemy = null
func _on_area_2d_area_entered(area):
$StateChart.send_event("enemy_entered")
enemy = area.get_parent()
func _on_area_2d_area_exited(area):
$StateChart.send_event("enemy_exited")
func _on_observing_state_processing(delta):
look_at(enemy.global_position)
実行すると、敵が監視領域に侵入した場合、Watchmanが敵の方向を見続けるようになります。
Idle状態に遷移した場合、Watchmanの向きを正面にする
敵が監視領域から離れた後も、同じ方向を見ているのは疲れそうなので、正面に戻してあげます。
先ほどは敵に合わせて向きをリアルタイムに変更する必要があるため、毎周期処理が実行できるstate_processingシグナルを使用しましたが、今回はIdle状態になったときに1回だけ向きを正面に設定すればよいので、状態に遷移したときに発行されるstate_enteredシグナルを使用します。
「Idle」状態を選択した状態で、ノード/シグナルの「state_entered()」をダブルクリックして、Watchmanのスクリプトに接続します。
rotation_degrees = 90に設定して、正面を向くようにします。enemyも初期化します。
extends Node2D
var enemy = null
func _on_area_2d_area_entered(area):
$StateChart.send_event("enemy_entered")
enemy = area.get_parent()
func _on_area_2d_area_exited(area):
$StateChart.send_event("enemy_exited")
func _on_observing_state_processing(delta):
look_at(enemy.global_position)
func _on_idle_state_entered():
rotation_degrees = 90
enemy = null
実行します。敵が監視領域に入るとWatchmanが敵の方を見るようになりますが、敵が監視領域から離れると正面を向くようになりました。
敵が監視領域から離れても、しばらく敵を見るようにする
状態遷移のTransitionの実行を遅らせることができます。その機能を利用して、敵が管理領域から離れても3秒間は目で追いかけるようにします。
「To Idle」Transionノードを選択した状態で、インスペクターのTransitionの「Delay Seconds」に「3」を設定します。
実行します。敵が管理領域から離れても、しばらくWatchmanが敵を見続けるようになります。
StateChartDebuggerの表示を見ると、状態がObservingのままですが、その下に「>> To Idle(2.41)のように表示され、数字がカウントダウンして、0になるとIdle状態に遷移します。
To Idleのカウントダウン中に、敵をマウスで移動して監視領域に入ってもカウントダウンは進み、Idle状態になります。敵は監視領域内入ったままです。
To Idleのカウントダウン中はまだObserving状態ですが、enemy_enteredイベントを処理しているのがIdle状態のみのためこのような状態になります。
Observing状態にenemy_enteredイベントを受けて自己遷移するTransiton「To Me」を追加すると、敵が再侵入したときにObservingに自己遷移するので、To Idleのカウントダウンが停止します。
敵が監視領域に3回侵入した場合、Watchmanをバーサーク状態にする
実装の仕方はいくつかありますが、並列状態を作って管理します。
状態遷移図にすると下図のようになります(あまり大事ではないので、Observing状態の自己遷移と、enemy_exitedの3秒Delayの表記は省略しました)
今まで使用していたRoot(CompoundState)はAlertStateに名前を変更し、新たにBerserkModeという状態遷移を追加しました。2つの状態遷移をRoot(ParallelState)で管理します。
ポイントとしては、どちらの状態も"enemy_entered"イベントを使います。
実際に状態遷移を変更します。
Root(CompoundState)を名前をAlertStateに変更します。
AlertStateを選択した状態で、名前に「Root」を入力してParallelState追加ボタンを押下します。
RootをStateChartにドラッグして、AlertStateをRootにドラッグします。AlertStateの中は操作しないので閉じました。
Rootを選択した状態で、名前に「BerserkMode」を入力して、CompoundState追加ボタンを押下します。
BerserkModeを選択して、名前「Normal」を入力して、AtomicState追加ボタンを押下します。同様に「Berserk」も追加します。
初期状態を設定する必要があるので、「BerserkMode」を選択してインスペクターのInitial Stateは「Normal」を選択します。
敵が監視領域に3回侵入した場合にNormalからBerserk状態に遷移するので、遷移元のNormalを選択して、名前「To Berserk」を入力してTransition追加ボタンを押下します。
「To Berserk」を選択して、インスペクターのTransitionのToに「Berserk」を選択し、Eventに"enemy_entered"を設定します。
「3回侵入した場合」を実現するため、Guard条件を設定します。Guardの右の「v」をクリックして、「新規ExpressionGuard」を選択します。GDScript形式の評価式を記入して、trueの場合状態が遷移するようになるので、「enemy_entered_count >= 3」と入力します。評価式はGDScriptの表記にします。
「enemy_entered_count」はGDScriptでStateChartに教えます。
メンバ変数として「enemy_entered_count」を追加して、敵が侵入したときのコールバック_on_area_2d_area_entered()が呼ばれたときに、カウントアップしたあと、set_expression_propertyメソッドでStateChartに通知します。
func _on_area_2d_area_entered(area):
enemy_entered_count += 1
$StateChart.set_expression_property("enemy_entered_count", enemy_entered_count)
スクリプトはこのようになります。
extends Node2D
var enemy = null
var enemy_entered_count = 0
func _on_area_2d_area_entered(area):
enemy_entered_count += 1
$StateChart.set_expression_property("enemy_entered_count", enemy_entered_count)
$StateChart.send_event("enemy_entered")
enemy = area.get_parent()
func _on_area_2d_area_exited(area):
$StateChart.send_event("enemy_exited")
func _on_observing_state_processing(delta):
look_at(enemy.global_position)
func _on_idle_state_entered():
rotation_degrees = 90
enemy = null
実行します。
敵が監視領域に1回侵入すると、enemy_entered_count=1が通知され、StateChartDebuggerで値を確認することができます。ガード条件があるので、まだBerserkModeはNormalのままです。
敵が監視領域に3回侵入すると、enemy_entered_count=3になり、ガード条件がtrueになるため、Berserk状態に遷移します。
Berserkしたらアイコンを赤くします。3秒後に元に戻します
3秒後に元の状態に戻すのは、即時遷移でdelayを3秒に設定することで実現できます。
まずBerserk状態になった場合にアイコンを赤くします。
「Berserk」を選択して、ノード/シグナルのstate_entered()をダブルクリックして、Watchmanのスクリプトに接続し、下記のようにします。
func _on_berserk_state_entered():
$Sprite2D.modulate = Color.RED
3秒後にBerserk状態からIdle状態に遷移するようにします。
遷移元の「Berserk」を選択して、名前「To Normal」にしてTransition追加ボタンを押下します。
「To Normal」を選択して、インスペクターのTransitonのToは「Normal」を選択します。Eventは未設定にすることで、即時に遷移することになりますが、3秒待ちたいので、Delay Secondsに3を設定します。
Normal状態で、色を元に戻します。
「Normal」を選択して、ノード/シグナルのstate_entered()をダブルクリックして、Watchmanのスクリプトに接続し、下記のようにします。enemy_entered_countをリセットしてStateChartに通知します。
func _on_normal_state_entered():
$Sprite2D.modulate = Color.WHITE
enemy_entered_count = 0
$StateChart.set_expression_property("enemy_entered_count", enemy_entered_count)
スクリプト全体はこのようになります。
extends Node2D
var enemy = null
var enemy_entered_count = 0
func _on_area_2d_area_entered(area):
enemy_entered_count += 1
$StateChart.set_expression_property("enemy_entered_count", enemy_entered_count)
$StateChart.send_event("enemy_entered")
enemy = area.get_parent()
func _on_area_2d_area_exited(area):
$StateChart.send_event("enemy_exited")
func _on_observing_state_processing(delta):
look_at(enemy.global_position)
func _on_idle_state_entered():
rotation_degrees = 90
enemy = null
func _on_berserk_state_entered():
$Sprite2D.modulate = Color.RED
func _on_normal_state_entered():
$Sprite2D.modulate = Color.WHITE
enemy_entered_count = 0
$StateChart.set_expression_property("enemy_entered_count", enemy_entered_count)
実行します。
敵が3回、監視領域に侵入すると、赤くなりました。
また、Berserk状態に遷移すると同時に即時遷移して、To Normalの3秒カウントダウンが開始されます。
3秒カウントダウンの後、Normal状態に遷移しました。
終わりに
Godot State Chartsを使ってみましたが、Godotのノードとsignalを上手に利用しているので、Godotユーザにはとても使いやすいと思います。また状態一覧がノードで見えるので、管理しやすく開発効率が良くなると印象です。 ゲームを作るときであれば、
- タイトル、設定画面、ゲームなどのシステム状態の遷移
- ゲーム中の開始、ゲーム中、Pause、クリアなどのゲーム状態管理
- Playerの状態管理
- 敵の状態管理
などいろいろ活用できそうです。
マイナス点としては、状態遷移図として表示できないので、規模が大きいと状態間のつながりが把握しにくいことが挙げられますが、Transitionの名前の付け方を工夫すればうまく使えそうな気がしています。
github
ここまで作ったものをgithubに登録しましたが、Godot State Chartsは別途ダウンロードが必要です。
実行する前に必要な手順
- Godotにインポート
- AssetLibからGodot State Chartsをダウンロード
- プロジェクト設定のプラグインタブでState Chartを有効化する
以上です。