Files
godot-docs/tutorials/misc/state_design_pattern.rst

258 lines
9.4 KiB
ReStructuredText

.. _doc_state_design_pattern:
State design pattern
====================
Introduction
------------
Scripting a game can be difficult when there are many states that need to handled, but
only one script can be attached to a node at a time. Instead of creating a state machine
within the player's control script, it would make development simpler if the states were
separated out into different classes.
There are many ways to implement a state machine with Godot, and some other methods are below:
* The player can have a child node for each state, which are called when utilized.
* Enums can be used in conjunction with a match statement.
* The state scripts themselves could be swapped out from a node dynamically at run-time.
This tutorial will focus only on adding and removing nodes which have a state script attached. Each state
script will be an implementation of a different state.
.. note::
There is a great resource explaining the concept of the state design pattern here:
https://gameprogrammingpatterns.com/state.html
Script setup
------------
The feature of inheritance is useful for getting started with this design principle.
A class should be created that describes the base features of the player. For now, a
player will be limited to two actions: **move left**, **move right**. This means
there will be two states: **idle** and **run**.
Below is the generic state, from which all other states will inherit.
.. tabs::
.. code-tab:: gdscript GDScript
# 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
A few notes on the above script. First, this implementation uses a
``setup(change_state, animated_sprite, persistent_state)`` method to assign
references. These references will be instantiated in the parent of this state. This helps with something
in programming known as *cohesion*. The state of the player does not want the responsibility of creating
these variables, but does want to be able to use them. However, this does make the state *coupled* to the
state's parent. This means that the state is highly reliant on whether it has a parent which contains
these variables. So, remember that *coupling* and *cohesion* are important concepts when it comes to code management.
.. note::
See the following page for more details on cohesion and coupling:
https://courses.cs.washington.edu/courses/cse403/96sp/coupling-cohesion.html
Second, there are some methods in the script for moving, but no implementation. The state script
just uses ``pass`` to show that it will not execute any instructions when the methods are called. This is important.
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.
So, now that there is a base state, the two states discussed earlier can be implemented.
.. tabs::
.. code-tab:: gdscript GDScript
# 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()
.. tabs::
.. code-tab:: gdscript GDScript
# 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")
.. note::
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.
.. tabs::
.. code-tab:: gdscript GDScript
# 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!")
This will look for states in a dictionary and return the state if found.
Now that all the states are defined with their own scripts, it is time to figure out
how those references that passed to them will be instantiated. Since these references
will not change it makes sense to call this new script ``persistent_state.gd``.
.. tabs::
.. code-tab:: gdscript GDScript
# 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)
.. note::
The ``persistent_state.gd`` script contains code for detecting input. This was to make the tutorial simple, but it is not usually
best practice to do this.
Project setup
-------------
This tutorial made an assumption that the node it would be attached to contained a child node which is an :ref:`AnimatedSprite <class_AnimatedSprite>`.
There is also the assumption that this :ref:`AnimatedSprite <class_AnimatedSprite>` has at least two animations,
the idle and run animations. Also, the top-level node is assumed to be a :ref:`KinematicBody2D <class_KinematicBody2D>`.
.. image:: img/llama_run.gif
.. note::
The zip file of the llama used in this tutorial is :download:`here <files/llama.zip>`.
The source was from `piskel_llama <https://www.piskelapp.com/p/agxzfnBpc2tlbC1hcHByEwsSBlBpc2tlbBiAgICfx5ygCQw/edit>`_, but
I couldn't find the original creator information on that page...
There is also a good tutorial for sprite animation already. See :ref:`2D Sprite Animation <doc_2d_sprite_animation>`.
So, the only script that must be attached is ``persistent_state.gd``, which should be attached to the top node of the
player, which is a :ref:`KinematicBody2D <class_KinematicBody2D>`.
.. image:: img/state_design_node_setup.png
.. image:: img/state_design_complete.gif
Now the player has utilized the state design pattern to implement its two different states. The nice part of this
pattern is that if one wanted to add another state, then it would involve creating another class that need only
focus on itself and how it changes to another state. Each state is functionally separated and instantiated dynamically.