123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- .. _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.
|