2
0

state_design_pattern.rst 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. .. _doc_state_design_pattern:
  2. State design pattern
  3. ====================
  4. Introduction
  5. ------------
  6. Scripting a game can be difficult when there are many states that need to handled, but
  7. only one script can be attached to a node at a time. Instead of creating a state machine
  8. within the player's control script, it would make development simpler if the states were
  9. separated out into different classes.
  10. There are many ways to implement a state machine with Godot, and some other methods are below:
  11. * The player can have a child node for each state, which are called when utilized.
  12. * Enums can be used in conjunction with a match statement.
  13. * The state scripts themselves could be swapped out from a node dynamically at run-time.
  14. This tutorial will focus only on adding and removing nodes which have a state script attached. Each state
  15. script will be an implementation of a different state.
  16. .. note::
  17. There is a great resource explaining the concept of the state design pattern here:
  18. https://gameprogrammingpatterns.com/state.html
  19. Script setup
  20. ------------
  21. The feature of inheritance is useful for getting started with this design principle.
  22. A class should be created that describes the base features of the player. For now, a
  23. player will be limited to two actions: **move left**, **move right**. This means
  24. there will be two states: **idle** and **run**.
  25. Below is the generic state, from which all other states will inherit.
  26. .. tabs::
  27. .. code-tab:: gdscript GDScript
  28. # state.gd
  29. extends Node2D
  30. class_name State
  31. var change_state
  32. var animated_sprite
  33. var persistent_state
  34. var velocity = 0
  35. # Writing _delta instead of delta here prevents the unused variable warning.
  36. func _physics_process(_delta):
  37. persistent_state.move_and_slide(persistent_state.velocity, Vector2.UP)
  38. func setup(change_state, animated_sprite, persistent_state):
  39. self.change_state = change_state
  40. self.animated_sprite = animated_sprite
  41. self.persistent_state = persistent_state
  42. func move_left():
  43. pass
  44. func move_right():
  45. pass
  46. A few notes on the above script. First, this implementation uses a
  47. ``setup(change_state, animated_sprite, persistent_state)`` method to assign
  48. references. These references will be instantiated in the parent of this state. This helps with something
  49. in programming known as *cohesion*. The state of the player does not want the responsibility of creating
  50. these variables, but does want to be able to use them. However, this does make the state *coupled* to the
  51. state's parent. This means that the state is highly reliant on whether it has a parent which contains
  52. these variables. So, remember that *coupling* and *cohesion* are important concepts when it comes to code management.
  53. .. note::
  54. See the following page for more details on cohesion and coupling:
  55. https://courses.cs.washington.edu/courses/cse403/96sp/coupling-cohesion.html
  56. Second, there are some methods in the script for moving, but no implementation. The state script
  57. just uses ``pass`` to show that it will not execute any instructions when the methods are called. This is important.
  58. Third, the ``_physics_process(delta)`` method is actually implemented here. This allows the states to have a default
  59. ``_physics_process(delta)`` implementation where ``velocity`` is used to move the player. The way that the states can modify
  60. the movement of the player is to use the ``velocity`` variable defined in their base class.
  61. Finally, this script is actually being designated as a class named ``State``. This makes refactoring the code
  62. easier, since the file path from using the ``load()`` and ``preload()`` functions in Godot will not be needed.
  63. So, now that there is a base state, the two states discussed earlier can be implemented.
  64. .. tabs::
  65. .. code-tab:: gdscript GDScript
  66. # idle_state.gd
  67. extends State
  68. class_name IdleState
  69. func _ready():
  70. animated_sprite.play("idle")
  71. func _flip_direction():
  72. animated_sprite.flip_h = not animated_sprite.flip_h
  73. func move_left():
  74. if animated_sprite.flip_h:
  75. change_state.call_func("run")
  76. else:
  77. _flip_direction()
  78. func move_right():
  79. if not animated_sprite.flip_h:
  80. change_state.call_func("run")
  81. else:
  82. _flip_direction()
  83. .. tabs::
  84. .. code-tab:: gdscript GDScript
  85. # run_state.gd
  86. extends State
  87. class_name RunState
  88. var move_speed = Vector2(180, 0)
  89. var min_move_speed = 0.005
  90. var friction = 0.32
  91. func _ready():
  92. animated_sprite.play("run")
  93. if animated_sprite.flip_h:
  94. move_speed.x *= -1
  95. persistent_state.velocity += move_speed
  96. func _physics_process(_delta):
  97. if abs(persistent_state.velocity.x) < min_move_speed:
  98. change_state.call_func("idle")
  99. persistent_state.velocity.x *= friction
  100. func move_left():
  101. if animated_sprite.flip_h:
  102. persistent_state.velocity += move_speed
  103. else:
  104. change_state.call_func("idle")
  105. func move_right():
  106. if not animated_sprite.flip_h:
  107. persistent_state.velocity += move_speed
  108. else:
  109. change_state.call_func("idle")
  110. .. note::
  111. Since the ``Run`` and ``Idle`` states extend from ``State`` which extends ``Node2D``, the function
  112. ``_physics_process(delta)`` is called from the **bottom-up** meaning ``Run`` and ``Idle`` will call their
  113. implementation of ``_physics_process(delta)``, then ``State`` will call its implementation, then ``Node2D``
  114. will call its own implementation and so on. This may seem strange, but it is only relevant for predefined functions
  115. such as ``_ready()``, ``_process(delta)``, etc. Custom functions use the normal inheritance rules of overriding
  116. the base implementation.
  117. There is a roundabout method for obtaining a state instance. A state factory can be used.
  118. .. tabs::
  119. .. code-tab:: gdscript GDScript
  120. # state_factory.gd
  121. class_name StateFactory
  122. var states
  123. func _init():
  124. states = {
  125. "idle": IdleState,
  126. "run": RunState
  127. }
  128. func get_state(state_name):
  129. if states.has(state_name):
  130. return states.get(state_name)
  131. else:
  132. printerr("No state ", state_name, " in state factory!")
  133. This will look for states in a dictionary and return the state if found.
  134. Now that all the states are defined with their own scripts, it is time to figure out
  135. how those references that passed to them will be instantiated. Since these references
  136. will not change it makes sense to call this new script ``persistent_state.gd``.
  137. .. tabs::
  138. .. code-tab:: gdscript GDScript
  139. # persistent_state.gd
  140. extends KinematicBody2D
  141. class_name PersistentState
  142. var state
  143. var state_factory
  144. var velocity = Vector2()
  145. func _ready():
  146. state_factory = StateFactory.new()
  147. change_state("idle")
  148. # Input code was placed here for tutorial purposes.
  149. func _process(_delta):
  150. if Input.is_action_pressed("ui_left"):
  151. move_left()
  152. elif Input.is_action_pressed("ui_right"):
  153. move_right()
  154. func move_left():
  155. state.move_left()
  156. func move_right():
  157. state.move_right()
  158. func change_state(new_state_name):
  159. if state != null:
  160. state.queue_free()
  161. state = state_factory.get_state(new_state_name).new()
  162. state.setup(funcref(self, "change_state"), $AnimatedSprite, self)
  163. state.name = "current_state"
  164. add_child(state)
  165. .. note::
  166. The ``persistent_state.gd`` script contains code for detecting input. This was to make the tutorial simple, but it is not usually
  167. best practice to do this.
  168. Project setup
  169. -------------
  170. This tutorial made an assumption that the node it would be attached to contained a child node which is an :ref:`AnimatedSprite <class_AnimatedSprite>`.
  171. There is also the assumption that this :ref:`AnimatedSprite <class_AnimatedSprite>` has at least two animations,
  172. the idle and run animations. Also, the top-level node is assumed to be a :ref:`KinematicBody2D <class_KinematicBody2D>`.
  173. .. image:: img/llama_run.gif
  174. .. note::
  175. The zip file of the llama used in this tutorial is :download:`here <files/llama.zip>`.
  176. The source was from `piskel_llama <https://www.piskelapp.com/p/agxzfnBpc2tlbC1hcHByEwsSBlBpc2tlbBiAgICfx5ygCQw/edit>`_, but
  177. I couldn't find the original creator information on that page...
  178. There is also a good tutorial for sprite animation already. See :ref:`2D Sprite Animation <doc_2d_sprite_animation>`.
  179. So, the only script that must be attached is ``persistent_state.gd``, which should be attached to the top node of the
  180. player, which is a :ref:`KinematicBody2D <class_KinematicBody2D>`.
  181. .. image:: img/state_design_node_setup.png
  182. .. image:: img/state_design_complete.gif
  183. Now the player has utilized the state design pattern to implement its two different states. The nice part of this
  184. pattern is that if one wanted to add another state, then it would involve creating another class that need only
  185. focus on itself and how it changes to another state. Each state is functionally separated and instantiated dynamically.