ステート(状態)設計パターン

はじめに

処理が必要な状態が多数ある場合、ゲームのスクリプトを作成するのは困難ですが、一度に1つのノードに接続できるスクリプトは1つだけです。プレイヤーの制御スクリプト内にステートマシンを作成する代わりに、ステートが異なるクラスに分けられていれば、開発が簡単になります。

Godotでステートマシンを実装する方法は多数ありますが、他にもいくつかの方法があります:

  • プレイヤーは、ステートごとに子ノードを持つことができます。子ノードは、使用されるときに呼び出されます。

  • 列挙型は、matchステートメントと組み合わせて使用できます。

  • ステートスクリプト自体は、実行時にノードから動的に交換できます。

このチュートリアルでは、ステートスクリプトがアタッチされているノードの追加と削除のみに焦点を当てます。各ステートスクリプトは、異なるステートの実装になります。

注釈

ここにステート設計パターンの概念を説明する素晴らしいリソースがあります: https://gameprogrammingpatterns.com/state.html

スクリプトのセットアップ

継承の機能は、この設計原則を始めるのに役立ちます。プレイヤーの基本機能を記述するクラスを作成する必要があります。今のところ、プレイヤーは2つのアクションに制限されます: 左に移動右に移動。これは、idlerun の2つのステートがあることを意味します。

以下は、他のすべてのステートが継承する一般的なステートです。

# state.gd

extends Node2D

class_name State

var change_state
var animated_sprite
var persistent_state
var velocity = 0

# Writing _delta instead of delta here prevents the unused variable warning.
func _physics_process(_delta):
    persistent_state.move_and_slide(persistent_state.velocity, Vector2.UP)

func setup(change_state, animated_sprite, persistent_state):
    self.change_state = change_state
    self.animated_sprite = animated_sprite
    self.persistent_state = persistent_state

func move_left():
    pass

func move_right():
    pass

上記のスクリプトに関するいくつかの注意事項。まず、この実装は setup(change_state, animated_sprite, persistent_state) メソッドを使用して参照を割り当てます。これらの参照は、この状態の親でインスタンス化されます。これは、凝集 として知られるプログラミングの考えに役立ちます。プレイヤーの状態は、これらの変数を作成する責任を負いませんが、これらを使用できるようにしたいと考えています。ただし、これにより、ステートはステートの親に 結合 されます。これは、ステートがこれらの変数を含む親を持っているかどうかに大きく依存していることを意味します。したがって、コード管理に関しては、結合凝集 が重要な概念であることを忘れないでください。

注釈

凝集と結合の詳細については、次のページを参照してください: https://courses.cs.washington.edu/courses/cse403/96sp/couple-cohesion.html

2つ目は、スクリプトには移動のためのメソッドがいくつかありますが、実装はありません。ステートスクリプトは単に pass を使用して、メソッドが呼び出されたときに命令を実行しないことを示します。これは重要です。

Third, the _physics_process(delta) method is actually implemented here. This allows the states to have a default _physics_process(delta) implementation where velocity is used to move the player. The way that the states can modify the movement of the player is to use the velocity variable defined in their base class.

Finally, this script is actually being designated as a class named State. This makes refactoring the code easier, since the file path from using the load() and preload() functions in Godot will not be needed.

そのため、基準ステートがあるので、前に説明した2つの状態を実装できます。

# idle_state.gd

extends State

class_name IdleState

func _ready():
    animated_sprite.play("idle")

func _flip_direction():
    animated_sprite.flip_h = not animated_sprite.flip_h

func move_left():
    if animated_sprite.flip_h:
        change_state.call_func("run")
    else:
        _flip_direction()

func move_right():
    if not animated_sprite.flip_h:
        change_state.call_func("run")
    else:
        _flip_direction()
# run_state.gd

extends State

class_name RunState

var move_speed = Vector2(180, 0)
var min_move_speed = 0.005
var friction = 0.32

func _ready():
    animated_sprite.play("run")
    if animated_sprite.flip_h:
        move_speed.x *= -1
    persistent_state.velocity += move_speed

func _physics_process(_delta):
    if abs(persistent_state.velocity.x) < min_move_speed:
         change_state.call_func("idle")
    persistent_state.velocity.x *= friction

func move_left():
    if animated_sprite.flip_h:
        persistent_state.velocity += move_speed
    else:
        change_state.call_func("idle")

func move_right():
    if not animated_sprite.flip_h:
        persistent_state.velocity += move_speed
    else:
        change_state.call_func("idle")

注釈

Since the Run and Idle states extend from State which extends Node2D, the function _physics_process(delta) is called from the bottom-up meaning Run and Idle will call their implementation of _physics_process(delta), then State will call its implementation, then Node2D will call its own implementation and so on. This may seem strange, but it is only relevant for predefined functions such as _ready(), _process(delta), etc. Custom functions use the normal inheritance rules of overriding the base implementation.

There is a roundabout method for obtaining a state instance. A state factory can be used.

# state_factory.gd

class_name StateFactory

var states

func _init():
    states = {
        "idle": IdleState,
        "run": RunState
}

func get_state(state_name):
    if states.has(state_name):
        return states.get(state_name)
    else:
        printerr("No state ", state_name, " in state factory!")

これにより、dictionary内のステートが検索され、見つかった場合はそのステートが返されます。

すべてのステートが独自のスクリプトで定義されたので、渡された参照がどのようにインスタンス化されるかを理解する時が来ました。これらの参照は変更されないため、この新しいスクリプト persistent_state.gd を呼び出すことは理にかなっています。

# persistent_state.gd

extends KinematicBody2D

class_name PersistentState

var state
var state_factory

var velocity = Vector2()

func _ready():
    state_factory = StateFactory.new()
    change_state("idle")

# Input code was placed here for tutorial purposes.
func _process(_delta):
    if Input.is_action_pressed("ui_left"):
        move_left()
    elif Input.is_action_pressed("ui_right"):
        move_right()

func move_left():
    state.move_left()

func move_right():
    state.move_right()

func change_state(new_state_name):
    if state != null:
        state.queue_free()
    state = state_factory.get_state(new_state_name).new()
    state.setup(funcref(self, "change_state"), $AnimatedSprite, self)
    state.name = "current_state"
    add_child(state)

注釈

persistent_state.gd スクリプトには、入力を検出するためのコードが含まれています。これはチュートリアルを簡単にするためでしたが、通常これを行うのはベストプラクティスではありません。

プロジェクトの設定

このチュートリアルでは、接続先のノードに、AnimatedSprite である子ノードが含まれると仮定しました。また、この AnimatedSprite には、少なくとも2つのアニメーション(待機アニメーションと走るアニメーション)があるという仮定があります。また、最上位ノードは KinematicBody2D であると想定されます。

../../_images/llama_run.gif

注釈

The zip file of the llama used in this tutorial is here. The source was from piskel_llama, but I couldn't find the original creator information on that page... There is also a good tutorial for sprite animation already. See 2D Sprite Animation.

したがって、アタッチする必要がある唯一のスクリプトは persistent_state.gd で、これはプレイヤーの最上位ノードにアタッチする必要があります。これは KinematicBody2D です。

../../_images/state_design_node_setup.png ../../_images/state_design_complete.gif

プレイヤーは、ステート設計パターンを使用して、2つの異なるステートを実装しました。このパターンの良い点は、別のステートを追加する場合は、自分自身と他のステートにどのように変化するかにのみに焦点を当てる別のクラスを作成すれば済むということです。各ステートは機能的に分離され、動的にインスタンス化されます。