|
@@ -0,0 +1,257 @@
|
|
|
+.. _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
|
|
|
+
|
|
|
+ # 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
|
|
|
+``_phyics_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(velocity) < 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::
|
|
|
+ The 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 round-about 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):
|
|
|
+ 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 though...
|
|
|
+ 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.
|
|
|
+
|