2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Godot4.1.3でGodot State Charts 機能拡張プラグインを使う 2/2

Posted at

「Godot4.1.3でGodot State Charts 機能拡張プラグインを使う 1/2」という記事の続きです。

前回の記事で作成した状態遷移

 前回の記事で実装したStateChartです。
image.png
 状態遷移図にすると下記のようになると思います。
 CompoundStateのRootにAtomicStateの2つの状態、IdleとObservingがあります。黒丸からの矢印はIdleが初期状態であることを示しており、"enemy_entered"イベントをトリガにObserving状態に遷移することを表しています。
image.png

本稿では、この状態遷移にObservingからIdleに戻る遷移の追加や、遷移のdelay、並列状態、ガード条件、即時遷移など実装します。

ベースプロジェクト

 こちらのGodotプロジェクトをベースにStateChartの機能を追加します。

githubから取得後に下記の操作をすると実行できるようになります。

  1. Godotにインポート
  2. AssetLibからState Chartをダウンロード
  3. プロジェクト設定のプラグインタブでState Chartを有効化する

敵が見張りの監視範囲外に移動したら、Idle状態に遷移する

 Observing状態からIdle状態に遷移したいので、遷移元のObservingを選択した状態で、名前を「To Idle」と入力して、Transitionの追加ボタンを押下します。
2023-11-05 (8).png
 「To Idle」Transitionノードを選択した状態でインスペクターのTransionのToに遷移先としたい「Idle」状態を設定します。Eventは「enemy_exited」と手入力します。
2023-11-05 (9).png
 この設定により、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になるのは前回確認しました。
2023-11-05 (10).png
敵を監視領域の外に移動すると、Idleに遷移するようになりました。
2023-11-05 (11).png

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のアイコンを-90°回転します。
2023-11-05 (13).png

 実行すると、敵が監視領域に侵入した場合、Watchmanが敵の方向を見続けるようになります。

image.png

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が敵の方を見るようになりますが、敵が監視領域から離れると正面を向くようになりました。
2023-11-05 (15).png

敵が監視領域から離れても、しばらく敵を見るようにする

 状態遷移のTransitionの実行を遅らせることができます。その機能を利用して、敵が管理領域から離れても3秒間は目で追いかけるようにします。

 「To Idle」Transionノードを選択した状態で、インスペクターのTransitionの「Delay Seconds」に「3」を設定します。

2023-11-05 (16).png

 実行します。敵が管理領域から離れても、しばらくWatchmanが敵を見続けるようになります。
 StateChartDebuggerの表示を見ると、状態がObservingのままですが、その下に「>> To Idle(2.41)のように表示され、数字がカウントダウンして、0になるとIdle状態に遷移します。
2023-11-05 (18).png

 To Idleのカウントダウン中に、敵をマウスで移動して監視領域に入ってもカウントダウンは進み、Idle状態になります。敵は監視領域内入ったままです。
 To Idleのカウントダウン中はまだObserving状態ですが、enemy_enteredイベントを処理しているのがIdle状態のみのためこのような状態になります。
 Observing状態にenemy_enteredイベントを受けて自己遷移するTransiton「To Me」を追加すると、敵が再侵入したときにObservingに自己遷移するので、To Idleのカウントダウンが停止します。
image.png
image.png

敵が監視領域に3回侵入した場合、Watchmanをバーサーク状態にする

 実装の仕方はいくつかありますが、並列状態を作って管理します。

 状態遷移図にすると下図のようになります(あまり大事ではないので、Observing状態の自己遷移と、enemy_exitedの3秒Delayの表記は省略しました)
image.png
 今まで使用していたRoot(CompoundState)はAlertStateに名前を変更し、新たにBerserkModeという状態遷移を追加しました。2つの状態遷移をRoot(ParallelState)で管理します。
 ポイントとしては、どちらの状態も"enemy_entered"イベントを使います。

 実際に状態遷移を変更します。
 Root(CompoundState)を名前をAlertStateに変更します。
 AlertStateを選択した状態で、名前に「Root」を入力してParallelState追加ボタンを押下します。
2023-11-07 (1).png
 RootをStateChartにドラッグして、AlertStateをRootにドラッグします。AlertStateの中は操作しないので閉じました。
image.png
 Rootを選択した状態で、名前に「BerserkMode」を入力して、CompoundState追加ボタンを押下します。
 BerserkModeを選択して、名前「Normal」を入力して、AtomicState追加ボタンを押下します。同様に「Berserk」も追加します。
image.png
 初期状態を設定する必要があるので、「BerserkMode」を選択してインスペクターのInitial Stateは「Normal」を選択します。
image.png
 敵が監視領域に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の表記にします。
 2023-11-07 (2).png
 「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のままです。
2023-11-07 (4).png
 敵が監視領域に3回侵入すると、enemy_entered_count=3になり、ガード条件がtrueになるため、Berserk状態に遷移します。
2023-11-07 (5).png

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追加ボタンを押下します。
image.png
 「To Normal」を選択して、インスペクターのTransitonのToは「Normal」を選択します。Eventは未設定にすることで、即時に遷移することになりますが、3秒待ちたいので、Delay Secondsに3を設定します。
image.png

 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秒カウントダウンが開始されます。
2023-11-07 (6).png
 3秒カウントダウンの後、Normal状態に遷移しました。

2023-11-07 (8).png

終わりに

 Godot State Chartsを使ってみましたが、Godotのノードとsignalを上手に利用しているので、Godotユーザにはとても使いやすいと思います。また状態一覧がノードで見えるので、管理しやすく開発効率が良くなると印象です。 ゲームを作るときであれば、

  • タイトル、設定画面、ゲームなどのシステム状態の遷移
  • ゲーム中の開始、ゲーム中、Pause、クリアなどのゲーム状態管理
  • Playerの状態管理
  • 敵の状態管理

などいろいろ活用できそうです。
 マイナス点としては、状態遷移図として表示できないので、規模が大きいと状態間のつながりが把握しにくいことが挙げられますが、Transitionの名前の付け方を工夫すればうまく使えそうな気がしています。

github

 ここまで作ったものをgithubに登録しましたが、Godot State Chartsは別途ダウンロードが必要です。

実行する前に必要な手順

  1. Godotにインポート
  2. AssetLibからGodot State Chartsをダウンロード
  3. プロジェクト設定のプラグインタブでState Chartを有効化する

 以上です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?