Browse Source

Rewrite FSM demo to respect the single responsibility principle

Now there's a base state_machine script, the StateMachine is separate from the physics body.
I reworked the code to follow the GDscript guidelines in the official manual.
Nathan Lovato 7 years ago
parent
commit
1ef5373c4f
30 changed files with 272 additions and 453 deletions
  1. 4 3
      2d/finite_state_machine/Demo.tscn
  2. 0 3
      2d/finite_state_machine/autoload/global_constants.gd
  3. 2 1
      2d/finite_state_machine/debug/StatesStackDiplayer.tscn
  4. 4 3
      2d/finite_state_machine/debug/states_stack_displayer.gd
  5. 0 3
      2d/finite_state_machine/icon.png.import
  6. 71 66
      2d/finite_state_machine/player/Player.tscn
  7. 0 3
      2d/finite_state_machine/player/body.png.import
  8. 4 4
      2d/finite_state_machine/player/bullet/bullet_spawner.gd
  9. 0 17
      2d/finite_state_machine/player/health/Health.tscn
  10. 0 62
      2d/finite_state_machine/player/health/health.gd
  11. 25 0
      2d/finite_state_machine/player/player_controller.gd
  12. 34 0
      2d/finite_state_machine/player/player_state_machine.gd
  13. 0 3
      2d/finite_state_machine/player/shadow.png.import
  14. 0 60
      2d/finite_state_machine/player/state-machine-bad-use.gd
  15. 0 133
      2d/finite_state_machine/player/state-machine.gd
  16. 3 10
      2d/finite_state_machine/player/states/combat/attack.gd
  17. 3 4
      2d/finite_state_machine/player/states/combat/stagger.gd
  18. 2 2
      2d/finite_state_machine/player/states/debug/state_name_displayer.gd
  19. 4 4
      2d/finite_state_machine/player/states/die.gd
  20. 11 17
      2d/finite_state_machine/player/states/motion/in_air/jump.gd
  21. 6 9
      2d/finite_state_machine/player/states/motion/motion.gd
  22. 5 7
      2d/finite_state_machine/player/states/motion/on_ground/idle.gd
  23. 12 15
      2d/finite_state_machine/player/states/motion/on_ground/move.gd
  24. 2 2
      2d/finite_state_machine/player/states/motion/on_ground/on_ground.gd
  25. 4 9
      2d/finite_state_machine/player/weapon/sword.gd
  26. 0 3
      2d/finite_state_machine/player/weapon/sword.png.import
  27. 0 1
      2d/finite_state_machine/player/weapon/weapon_pivot.gd
  28. 1 1
      2d/finite_state_machine/project.godot
  29. 4 8
      2d/finite_state_machine/state_machine/state.gd
  30. 71 0
      2d/finite_state_machine/state_machine/state_machine.gd

+ 4 - 3
2d/finite_state_machine/Demo.tscn

@@ -5,16 +5,17 @@
 [ext_resource path="res://debug/ControlsPanel.tscn" type="PackedScene" id=3]
 [ext_resource path="res://debug/Explanations.tscn" type="PackedScene" id=4]
 
-[node name="Demo" type="Node" index="0"]
+[node name="Demo" type="Node"]
 
 [node name="Player" parent="." index="0" instance=ExtResource( 1 )]
 
+editor/display_folded = true
+
 [node name="StatesStackDiplayer" parent="." index="1" instance=ExtResource( 2 )]
 
 [node name="ControlsPanel" parent="." index="2" instance=ExtResource( 3 )]
 
 [node name="Explanations" parent="." index="3" instance=ExtResource( 4 )]
 
-[connection signal="state_changed" from="Player" to="StatesStackDiplayer" method="_on_Player_state_changed"]
-
 
+[editable path="Player"]

+ 0 - 3
2d/finite_state_machine/autoload/global_constants.gd

@@ -1,3 +0,0 @@
-extends Node
-
-enum STATUSES { STATUS_NONE, STATUS_INVINCIBLE, STATUS_POISONED, STATUS_STUNNED }

+ 2 - 1
2d/finite_state_machine/debug/StatesStackDiplayer.tscn

@@ -1,8 +1,9 @@
 [gd_scene load_steps=4 format=2]
 
-[ext_resource path="res://debug/states-stack-displayer.gd" type="Script" id=1]
+[ext_resource path="res://debug/states_stack_displayer.gd" type="Script" id=1]
 [ext_resource path="res://fonts/SourceCodePro-Bold.ttf" type="DynamicFontData" id=2]
 
+
 [sub_resource type="DynamicFont" id=1]
 
 size = 20

+ 4 - 3
2d/finite_state_machine/debug/states-stack-displayer.gd → 2d/finite_state_machine/debug/states_stack_displayer.gd

@@ -1,18 +1,19 @@
 tool
 extends Panel
 
+onready var fsm_node = get_node("../Player/StateMachine")
+
 func _ready():
 	set_as_toplevel(true)
 
-func _on_Player_state_changed(states_stack):
+func _process(delta):
 	var states_names = ''
 	var numbers = ''
 	var index = 0
-	for state in states_stack:
+	for state in fsm_node.states_stack:
 		states_names += state.get_name() + '\n'
 		numbers += str(index) + '\n'
 		index += 1
 
 	$States.text = states_names
 	$Numbers.text = numbers
-

+ 0 - 3
2d/finite_state_machine/icon.png.import

@@ -7,10 +7,7 @@ path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
 [deps]
 
 source_file="res://icon.png"
-source_md5="66cdb591455c91e0e285218a159ee4a1"
-
 dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
-dest_md5="302305b1ae85287055e65e5202170a74"
 
 [params]
 

+ 71 - 66
2d/finite_state_machine/player/Player.tscn

@@ -1,21 +1,20 @@
 [gd_scene load_steps=20 format=2]
 
-[ext_resource path="res://player/state-machine.gd" type="Script" id=1]
-[ext_resource path="res://player/shadow.png" type="Texture" id=2]
-[ext_resource path="res://player/body.png" type="Texture" id=3]
-[ext_resource path="res://player/weapon/weapon_pivot.gd" type="Script" id=4]
-[ext_resource path="res://player/weapon/Sword.tscn" type="PackedScene" id=5]
-[ext_resource path="res://player/health/Health.tscn" type="PackedScene" id=6]
-[ext_resource path="res://player/states/motion/on_ground/idle.gd" type="Script" id=7]
-[ext_resource path="res://player/states/motion/on_ground/move.gd" type="Script" id=8]
-[ext_resource path="res://player/states/motion/in_air/jump.gd" type="Script" id=9]
-[ext_resource path="res://player/states/combat/stagger.gd" type="Script" id=10]
-[ext_resource path="res://player/states/combat/attack.gd" type="Script" id=11]
-[ext_resource path="res://player/states/die.gd" type="Script" id=12]
+[ext_resource path="res://player/player_controller.gd" type="Script" id=1]
+[ext_resource path="res://player/player_state_machine.gd" type="Script" id=2]
+[ext_resource path="res://player/states/motion/on_ground/idle.gd" type="Script" id=3]
+[ext_resource path="res://player/states/motion/on_ground/move.gd" type="Script" id=4]
+[ext_resource path="res://player/states/motion/in_air/jump.gd" type="Script" id=5]
+[ext_resource path="res://player/states/combat/stagger.gd" type="Script" id=6]
+[ext_resource path="res://player/states/combat/attack.gd" type="Script" id=7]
+[ext_resource path="res://player/states/die.gd" type="Script" id=8]
+[ext_resource path="res://player/shadow.png" type="Texture" id=9]
+[ext_resource path="res://player/body.png" type="Texture" id=10]
+[ext_resource path="res://player/weapon/weapon_pivot.gd" type="Script" id=11]
+[ext_resource path="res://player/weapon/Sword.tscn" type="PackedScene" id=12]
 [ext_resource path="res://player/bullet/bullet_spawner.gd" type="Script" id=13]
 [ext_resource path="res://fonts/SourceCodePro-Bold.ttf" type="DynamicFontData" id=14]
-[ext_resource path="res://player/states/debug/state-name-displayer.gd" type="Script" id=15]
-
+[ext_resource path="res://player/states/debug/state_name_displayer.gd" type="Script" id=15]
 
 [sub_resource type="Animation" id=1]
 
@@ -55,7 +54,7 @@ use_filter = true
 font_data = ExtResource( 14 )
 _sections_unfolded = [ "Font", "Settings" ]
 
-[node name="Player" type="KinematicBody2D"]
+[node name="Player" type="KinematicBody2D" index="0"]
 
 position = Vector2( 628.826, 391.266 )
 input_pickable = false
@@ -68,7 +67,45 @@ __meta__ = {
 "_edit_horizontal_guides_": [  ]
 }
 
-[node name="AnimationPlayer" type="AnimationPlayer" parent="." index="0"]
+[node name="StateMachine" type="Node" parent="." index="0"]
+
+script = ExtResource( 2 )
+START_STATE = NodePath("Idle")
+
+[node name="Idle" type="Node" parent="StateMachine" index="0"]
+
+script = ExtResource( 3 )
+
+[node name="Move" type="Node" parent="StateMachine" index="1"]
+
+script = ExtResource( 4 )
+MAX_WALK_SPEED = 450
+MAX_RUN_SPEED = 700
+
+[node name="Jump" type="Node" parent="StateMachine" index="2"]
+
+script = ExtResource( 5 )
+BASE_MAX_HORIZONTAL_SPEED = 400.0
+AIR_ACCELERATION = 1000.0
+AIR_DECCELERATION = 2000.0
+AIR_STEERING_POWER = 50.0
+JUMP_HEIGHT = 120.0
+JUMP_DURATION = 0.8
+GRAVITY = 1600.0
+
+[node name="Stagger" type="Node" parent="StateMachine" index="3"]
+
+script = ExtResource( 6 )
+
+[node name="Attack" type="Node" parent="StateMachine" index="4"]
+
+script = ExtResource( 7 )
+
+[node name="Die" type="Node" parent="StateMachine" index="5"]
+
+script = ExtResource( 8 )
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="." index="1"]
 
 root_node = NodePath("..")
 autoplay = ""
@@ -80,74 +117,40 @@ anims/stagger = SubResource( 2 )
 anims/walk = SubResource( 3 )
 blend_times = [  ]
 
-[node name="Shadow" type="Sprite" parent="." index="1"]
+[node name="Shadow" type="Sprite" parent="." index="2"]
 
 self_modulate = Color( 1, 1, 1, 0.361098 )
 position = Vector2( 0, -4 )
-texture = ExtResource( 2 )
+texture = ExtResource( 9 )
 _sections_unfolded = [ "Visibility" ]
 
-[node name="BodyPivot" type="Position2D" parent="." index="2"]
+[node name="BodyPivot" type="Position2D" parent="." index="3"]
+
+editor/display_folded = true
 
 [node name="Body" type="Sprite" parent="BodyPivot" index="0"]
 
 position = Vector2( 0, -58.8242 )
-texture = ExtResource( 3 )
+texture = ExtResource( 10 )
 
-[node name="WeaponPivot" type="Position2D" parent="." index="3"]
+[node name="WeaponPivot" type="Position2D" parent="." index="4"]
 
+editor/display_folded = true
 position = Vector2( 1.17401, -61.266 )
-script = ExtResource( 4 )
+script = ExtResource( 11 )
 
 [node name="Offset" type="Position2D" parent="WeaponPivot" index="0"]
 
 position = Vector2( 110, 0 )
 
-[node name="Sword" parent="WeaponPivot/Offset" index="0" instance=ExtResource( 5 )]
+[node name="Sword" parent="WeaponPivot/Offset" index="0" instance=ExtResource( 12 )]
 
-[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="." index="4"]
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="." index="5"]
 
 build_mode = 0
 polygon = PoolVector2Array( -20, 0, -20, -20, 20, -20, 20, 0 )
 
-[node name="Health" parent="." index="5" instance=ExtResource( 6 )]
-
-[node name="States" type="Node" parent="." index="6"]
-
-[node name="Idle" type="Node" parent="States" index="0"]
-
-script = ExtResource( 7 )
-
-[node name="Move" type="Node" parent="States" index="1"]
-
-script = ExtResource( 8 )
-MAX_WALK_SPEED = 450
-MAX_RUN_SPEED = 700
-
-[node name="Jump" type="Node" parent="States" index="2"]
-
-script = ExtResource( 9 )
-BASE_MAX_HORIZONTAL_SPEED = 400.0
-AIR_ACCELERATION = 1000.0
-AIR_DECCELERATION = 2000.0
-AIR_STEERING_POWER = 50.0
-JUMP_HEIGHT = 120.0
-JUMP_DURATION = 0.8
-GRAVITY = 1600.0
-
-[node name="Stagger" type="Node" parent="States" index="3"]
-
-script = ExtResource( 10 )
-
-[node name="Attack" type="Node" parent="States" index="4"]
-
-script = ExtResource( 11 )
-
-[node name="Die" type="Node" parent="States" index="5"]
-
-script = ExtResource( 12 )
-
-[node name="BulletSpawn" type="Node2D" parent="." index="7"]
+[node name="BulletSpawn" type="Node2D" parent="." index="6"]
 
 editor/display_folded = true
 position = Vector2( 1.17401, -61.266 )
@@ -161,7 +164,7 @@ wait_time = 0.2
 one_shot = true
 autostart = false
 
-[node name="StateNameDisplayer" type="Label" parent="." index="8"]
+[node name="StateNameDisplayer" type="Label" parent="." index="7"]
 
 editor/display_folded = true
 anchor_left = 0.0
@@ -189,10 +192,12 @@ max_lines_visible = -1
 script = ExtResource( 15 )
 _sections_unfolded = [ "Rect", "custom_fonts" ]
 
-[connection signal="state_changed" from="." to="StateNameDisplayer" method="_on_Player_state_changed"]
+[connection signal="state_changed" from="StateMachine" to="StateNameDisplayer" method="_on_StateMachine_state_changed"]
+
+[connection signal="state_changed" from="StateMachine" to="WeaponPivot/Offset/Sword" method="_on_StateMachine_state_changed"]
 
-[connection signal="animation_finished" from="AnimationPlayer" to="." method="_on_animation_finished"]
+[connection signal="animation_finished" from="AnimationPlayer" to="StateMachine" method="_on_animation_finished"]
 
-[connection signal="attack_finished" from="WeaponPivot/Offset/Sword" to="States/Attack" method="_on_Sword_attack_finished"]
+[connection signal="attack_finished" from="WeaponPivot/Offset/Sword" to="StateMachine/Attack" method="_on_Sword_attack_finished"]
 
 

+ 0 - 3
2d/finite_state_machine/player/body.png.import

@@ -7,10 +7,7 @@ path="res://.import/body.png-313f6363670a5852a7b7126ab476d8b1.stex"
 [deps]
 
 source_file="res://player/body.png"
-source_md5="e76fc4b6b913ad7e09352eada55fc124"
-
 dest_files=[ "res://.import/body.png-313f6363670a5852a7b7126ab476d8b1.stex" ]
-dest_md5="b65921b7af7d7cea3cb9567625d04d34"
 
 [params]
 

+ 4 - 4
2d/finite_state_machine/player/bullet/bullet_spawner.gd

@@ -2,6 +2,10 @@ extends Node2D
 
 var bullet = preload("Bullet.tscn")
 
+func _input(event):
+	if event.is_action_pressed("fire"):
+		fire(owner.look_direction)
+
 func fire(direction):
 	if not $CooldownTimer.is_stopped():
 		return
@@ -10,7 +14,3 @@ func fire(direction):
 	var new_bullet = bullet.instance()
 	new_bullet.direction = direction
 	add_child(new_bullet)
-
-
-func update(host, delta):
-	return 'previous'

+ 0 - 17
2d/finite_state_machine/player/health/Health.tscn

@@ -1,17 +0,0 @@
-[gd_scene load_steps=2 format=2]
-
-[ext_resource path="res://player/health/health.gd" type="Script" id=1]
-
-[node name="Health" type="Node"]
-
-script = ExtResource( 1 )
-max_health = 9
-
-[node name="PoisonTimer" type="Timer" parent="." index="0"]
-
-process_mode = 1
-wait_time = 0.6
-one_shot = false
-autostart = false
-
-

+ 0 - 62
2d/finite_state_machine/player/health/health.gd

@@ -1,62 +0,0 @@
-extends Node
-
-signal health_changed
-signal health_depleted
-signal status_changed
-
-var health = 0
-export(int) var max_health = 9
-
-var status = null
-const POISON_DAMAGE = 1
-var poison_cycles = 0
-
-
-func _ready():
-	health = max_health
-	$PoisonTimer.connect('timeout', self, '_on_PoisonTimer_timeout')
-
-
-func _change_status(new_status):
-	match status:
-		GlobalConstants.STATUS_POISONED:
-			$PoisonTimer.stop()
-
-	match new_status:
-		GlobalConstants.STATUS_POISONED:
-			poison_cycles = 0
-			$PoisonTimer.start()
-	status = new_status
-	emit_signal('status_changed', new_status)
-
-
-func take_damage(amount, effect=null):
-	if status == GlobalConstants.STATUS_INVINCIBLE:
-		return
-	health -= amount
-	health = max(0, health)
-	emit_signal("health_changed", health)
-
-	if not effect:
-		return
-	match effect[0]:
-		GlobalConstants.STATUS_POISONED:
-			_change_status(GlobalConstants.STATUS_POISONED)
-			poison_cycles = effect[1]
-#	print("%s got hit and took %s damage. Health: %s/%s" % [get_name(), amount, health, max_health])
-
-
-func heal(amount):
-	health += amount
-	health = max(health, max_health)
-	emit_signal("health_changed", health)
-#	print("%s got healed by %s points. Health: %s/%s" % [get_name(), amount, health, max_health])
-
-
-func _on_PoisonTimer_timeout():
-	take_damage(POISON_DAMAGE)
-	poison_cycles -= 1
-	if poison_cycles == 0:
-		_change_status(GlobalConstants.STATUS_NONE)
-		return
-	$PoisonTimer.start()

+ 25 - 0
2d/finite_state_machine/player/player_controller.gd

@@ -0,0 +1,25 @@
+"""
+The Player is a KinematicBody2D, in other words a physics-driven object. 
+It can move, collide with the world...
+It HAS a state machine, but the body and the state machine are separate.
+"""
+extends KinematicBody2D
+
+signal direction_changed(new_direction)
+
+var look_direction = Vector2(1, 0) setget set_look_direction
+
+func take_damage(attacker, amount, effect=null):
+	if self.is_a_parent_of(attacker):
+		return
+	$States/Stagger.knockback_direction = (attacker.global_position - global_position).normalized()
+	$Health.take_damage(amount, effect)
+
+func set_dead(value):
+	set_process_input(not value)
+	set_physics_process(not value)
+	$CollisionPolygon2D.disabled = value
+
+func set_look_direction(value):
+	look_direction = value
+	emit_signal("direction_changed", value)

+ 34 - 0
2d/finite_state_machine/player/player_state_machine.gd

@@ -0,0 +1,34 @@
+extends "res://state_machine/state_machine.gd"
+
+func _ready():
+	states_map = {
+		"idle": $Idle,
+		"move": $Move,
+		"jump": $Jump,
+		"stagger": $Stagger,
+		"attack": $Attack,
+	}
+
+func _change_state(state_name):
+	"""
+	The base state_machine interface this node extends does most of the work
+	"""
+	if not _active:
+		return
+	if state_name in ["stagger", "jump", "attack"]:
+		states_stack.push_front(states_map[state_name])
+	if state_name == "jump" and current_state == $Move:
+		$Jump.initialize($Move.speed, $Move.velocity)
+	._change_state(state_name)
+
+func _input(event):
+	"""
+	Here we only handle input that can interrupt states, attacking in this case
+	otherwise we let the state node handle it
+	"""
+	if event.is_action_pressed("attack"):
+		if current_state == $Attack:
+			return
+		_change_state("attack")
+		return
+	current_state.handle_input(event)

+ 0 - 3
2d/finite_state_machine/player/shadow.png.import

@@ -7,10 +7,7 @@ path="res://.import/shadow.png-493c4635eca1ce8bdece629560617dc7.stex"
 [deps]
 
 source_file="res://player/shadow.png"
-source_md5="ba1c3f4ef3a93dcd980bc4d020c4a22c"
-
 dest_files=[ "res://.import/shadow.png-493c4635eca1ce8bdece629560617dc7.stex" ]
-dest_md5="7dd3fb33b53c00ce76edd77833bce15d"
 
 [params]
 

+ 0 - 60
2d/finite_state_machine/player/state-machine-bad-use.gd

@@ -1,60 +0,0 @@
-extends KinematicBody2D
-
-signal state_changed
-
-var look_direction = Vector2()
-
-var current_state = null
-var states_stack = []
-
-onready var states_map = {
-	'idle': $States/Idle,
-	'move': $States/Move,
-	'jump': $States/Jump,
-	'shoot': $States/Shoot,
-}
-
-func _ready():
-	states_stack.push_front($States/Idle)
-	current_state = states_stack[0]
-	_change_state('idle')
-
-
-# Delegate the call to the state
-func _physics_process(delta):
-	var new_state = current_state.update(self, delta)
-	if new_state:
-		_change_state(new_state)
-
-
-func _input(event):
-	var new_state = current_state.handle_input(self, event)
-	if new_state:
-		_change_state(new_state)
-
-
-# Exit the current state, change it and enter the new one
-func _change_state(state_name):
-	# The pushdown mechanism isn't very useful in this example.
-	# If the player stops moving mid-air Jump will still return to move
-#	print('Exiting %s and enterig %s' % [current_state.get_name(), new_state.get_name()])
-	current_state.exit(self)
-
-	# removing state previously pushed on the stack
-	if state_name == 'previous':
-		states_stack.pop_front()
-	elif state_name in ['jump', 'shoot']:
-		states_stack.push_front(states_map[state_name])
-		if state_name == 'jump':
-			$States/Jump.initialize(current_state.speed, current_state.velocity)
-	else:
-		# pushing new state on to the stack
-		var new_state = states_map[state_name]
-		states_stack[0] = new_state
-
-	# We only reinitialize the state when we don't use the pushdown automaton
-	if state_name != 'previous':
-		current_state.enter(self)
-
-	current_state = states_stack[0]
-	emit_signal('state_changed', current_state.get_name())

+ 0 - 133
2d/finite_state_machine/player/state-machine.gd

@@ -1,133 +0,0 @@
-"""
-The point of the State pattern is to separate concerns, to help follow
-the single responsibility principle. Each state describes an action or 
-behavior.
-
-The state machine is the only entity that"s aware of the states. 
-That"s why this script receives strings from the states: it maps 
-these strings to actual state objects: the states (Move, Jump, etc.)
-are not aware of their siblings. This way you can change any of 
-the states anytime without breaking the game.
-"""
-extends KinematicBody2D
-
-signal state_changed
-signal direction_changed(new_direction)
-
-var look_direction = Vector2(1, 0) setget set_look_direction
-
-
-"""
-This example keeps a history of some states so e.g. after taking a hit,
-the character can return to the previous state. The states_stack is 
-an Array, and we use Array.push_front() and Array.pop_front() to add and
-remove states from the history.
-"""
-var states_stack = []
-var current_state = null
-
-onready var states_map = {
-	"idle": $States/Idle,
-	"move": $States/Move,
-	"jump": $States/Jump,
-	"stagger": $States/Stagger,
-	"attack": $States/Attack,
-}
-
-func _ready():
-	for state_node in $States.get_children():
-		state_node.connect("finished", self, "_change_state")
-
-	states_stack.push_front($States/Idle)
-	current_state = states_stack[0]
-	_change_state("idle")
-
-
-# The state machine delegates process and input callbacks to the current state
-# The state object, e.g. Move, then handles input, calculates velocity 
-# and moves what I called its "host", the Player node (KinematicBody2D) in this case.
-func _physics_process(delta):
-	current_state.update(self, delta)
-
-
-func _input(event):
-	"""
-	If you make a shooter game, you may want the player to be able to
-	fire bullets anytime.
-	If that"s the case you don"t want to use the states. They"ll add micro
-	freezes in the gameplay and/or make your code more complex
-	Firing is the weapon"s responsibility (BulletSpawn here) so the weapon should handle it
-	"""
-	if event.is_action_pressed("fire"):
-		$BulletSpawn.fire(look_direction)
-		return
-	elif event.is_action_pressed("attack"):
-		if current_state == $States/Attack:
-			return
-		_change_state("attack")
-		return
-	current_state.handle_input(self, event)
-
-
-func _on_animation_finished(anim_name):
-	"""
-	We want to delegate every method or callback that could trigger 
-	a state change to the state objects. The base script state.gd,
-	that all states extend, makes sure that all states have the same
-	interface, that is to say access to the same base methods, including
-	_on_animation_finished. See state.gd
-	"""
-	current_state._on_animation_finished(anim_name)
-
-
-func _change_state(state_name):
-	"""
-	We use this method to:
-		1. Clean up the current state with its the exit method
-		2. Manage the flow and the history of states
-		3. Initialize the new state with its enter method
-	Note that to go back to the previous state in the states history,
-	the state objects return the "previous" keyword and not a specific
-	state name.
-	"""
-	current_state.exit(self)
-
-	if state_name == "previous":
-		states_stack.pop_front()
-	elif state_name in ["stagger", "jump", "attack"]:
-		states_stack.push_front(states_map[state_name])
-	elif state_name == "dead":
-		queue_free()
-		return
-	else:
-		var new_state = states_map[state_name]
-		states_stack[0] = new_state
-	
-	if state_name == "attack":
-		$WeaponPivot/Offset/Sword.attack()
-	if state_name == "jump":
-		$States/Jump.initialize(current_state.speed, current_state.velocity)
-
-	current_state = states_stack[0]
-	if state_name != "previous":
-		# We don"t want to reinitialize the state if we"re going back to the previous state
-		current_state.enter(self)
-	emit_signal("state_changed", states_stack)
-
-
-func take_damage(attacker, amount, effect=null):
-	if self.is_a_parent_of(attacker):
-		return
-	$States/Stagger.knockback_direction = (attacker.global_position - global_position).normalized()
-	$Health.take_damage(amount, effect)
-
-
-func set_dead(value):
-	set_process_input(not value)
-	set_physics_process(not value)
-	$CollisionPolygon2D.disabled = value
-
-
-func set_look_direction(value):
-	look_direction = value
-	emit_signal("direction_changed", value)

+ 3 - 10
2d/finite_state_machine/player/states/combat/attack.gd

@@ -1,14 +1,7 @@
-"""
-The stagger state end with the stagger animation from the AnimationPlayer
-The animation only affects the Body Sprite"s modulate property so 
-it could stack with other animations if we had two AnimationPlayer nodes
-"""
-extends "../state.gd"
-
-
-func enter(host):
-	host.get_node("AnimationPlayer").play("idle")
+extends "res://state_machine/state.gd"
 
+func enter():
+	owner.get_node("AnimationPlayer").play("idle")
 
 func _on_Sword_attack_finished():
 	emit_signal("finished", "previous")

+ 3 - 4
2d/finite_state_machine/player/states/combat/stagger.gd

@@ -3,13 +3,12 @@ The stagger state end with the stagger animation from the AnimationPlayer
 The animation only affects the Body Sprite"s modulate property so 
 it could stack with other animations if we had two AnimationPlayer nodes
 """
-extends "../state.gd"
+extends "res://state_machine/state.gd"
 
 var knockback_direction = Vector2()
 
-func enter(host):
-	host.get_node("AnimationPlayer").play("stagger")
-
+func enter():
+	owner.get_node("AnimationPlayer").play("stagger")
 
 func _on_animation_finished(anim_name):
 	assert anim_name == "stagger"

+ 2 - 2
2d/finite_state_machine/player/states/debug/state-name-displayer.gd → 2d/finite_state_machine/player/states/debug/state_name_displayer.gd

@@ -8,5 +8,5 @@ func _ready():
 func _physics_process(delta):
 	rect_position = $"../BodyPivot".position + start_position
 
-func _on_Player_state_changed(states_stack):
-	text = states_stack[0].get_name()
+func _on_StateMachine_state_changed(current_state):
+	text = current_state.get_name()

+ 4 - 4
2d/finite_state_machine/player/states/die.gd

@@ -1,10 +1,10 @@
-extends "state.gd"
+extends "res://state_machine/state.gd"
 
 
 # Initialize the state. E.g. change the animation
-func enter(host):
-	host.set_dead(true)
-	host.get_node("AnimationPlayer").play("die")
+func enter():
+	owner.set_dead(true)
+	owner.get_node("AnimationPlayer").play("die")
 
 func _on_animation_finished(anim_name):
 	emit_signal("finished", "dead")

+ 11 - 17
2d/finite_state_machine/player/states/motion/in_air/jump.gd

@@ -11,7 +11,6 @@ export(float) var JUMP_DURATION = 0.8
 
 export(float) var GRAVITY = 1600.0
 
-
 var enter_velocity = Vector2()
 
 var max_horizontal_speed = 0.0
@@ -21,34 +20,30 @@ var horizontal_velocity = Vector2()
 var vertical_speed = 0.0
 var height = 0.0
 
-
 func initialize(speed, velocity):
 	horizontal_speed = speed
 	max_horizontal_speed = speed if speed > 0.0 else BASE_MAX_HORIZONTAL_SPEED
 	enter_velocity = velocity
 
-
-func enter(host):
+func enter():
 	var input_direction = get_input_direction()
-	update_look_direction(host, input_direction)
+	update_look_direction(input_direction)
 
 	horizontal_velocity = enter_velocity if input_direction else Vector2()
 	vertical_speed = 600.0
 
-	host.get_node("AnimationPlayer").play("idle")
+	owner.get_node("AnimationPlayer").play("idle")
 
-
-func update(host, delta):
+func update(delta):
 	var input_direction = get_input_direction()
-	update_look_direction(host, input_direction)
+	update_look_direction(input_direction)
 
-	move_horizontally(host, delta, input_direction)
-	animate_jump_height(host, delta)
+	move_horizontally(delta, input_direction)
+	animate_jump_height(delta)
 	if height <= 0.0:
 		emit_signal("finished", "previous")
 
-
-func move_horizontally(host, delta, direction):
+func move_horizontally(delta, direction):
 	if direction:
 		horizontal_speed += AIR_ACCELERATION * delta
 	else:
@@ -59,12 +54,11 @@ func move_horizontally(host, delta, direction):
 	var steering_velocity = (target_velocity - horizontal_velocity).normalized() * AIR_STEERING_POWER
 	horizontal_velocity += steering_velocity
 
-	host.move_and_slide(horizontal_velocity)
-
+	owner.move_and_slide(horizontal_velocity)
 
-func animate_jump_height(host, delta):
+func animate_jump_height(delta):
 	vertical_speed -= GRAVITY * delta
 	height += vertical_speed * delta
 	height = max(0.0, height)
 
-	host.get_node("BodyPivot").position.y = -height
+	owner.get_node("BodyPivot").position.y = -height

+ 6 - 9
2d/finite_state_machine/player/states/motion/motion.gd

@@ -1,22 +1,19 @@
 # Collection of important methods to handle direction and animation
-extends "../state.gd"
+extends "res://state_machine/state.gd"
 
-
-func handle_input(host, event):
+func handle_input(event):
 	if event.is_action_pressed("simulate_damage"):
 		emit_signal("finished", "stagger")
 
-
 func get_input_direction():
 	var input_direction = Vector2()
 	input_direction.x = int(Input.is_action_pressed("move_right")) - int(Input.is_action_pressed("move_left"))
 	input_direction.y = int(Input.is_action_pressed("move_down")) - int(Input.is_action_pressed("move_up"))
 	return input_direction
 
-
-func update_look_direction(host, direction):
-	if direction and host.look_direction != direction:
-		host.look_direction = direction
+func update_look_direction(direction):
+	if direction and owner.look_direction != direction:
+		owner.look_direction = direction
 	if not direction.x in [-1, 1]:
 		return
-	host.get_node("BodyPivot").set_scale(Vector2(direction.x, 1))
+	owner.get_node("BodyPivot").set_scale(Vector2(direction.x, 1))

+ 5 - 7
2d/finite_state_machine/player/states/motion/on_ground/idle.gd

@@ -1,14 +1,12 @@
 extends "on_ground.gd"
 
-func enter(host):
-	host.get_node("AnimationPlayer").play("idle")
+func enter():
+	owner.get_node("AnimationPlayer").play("idle")
 
+func handle_input(event):
+	return .handle_input(event)
 
-func handle_input(host, event):
-	return .handle_input(host, event)
-
-
-func update(host, delta):
+func update(delta):
 	var input_direction = get_input_direction()
 	if input_direction:
 		emit_signal("finished", "move")

+ 12 - 15
2d/finite_state_machine/player/states/motion/on_ground/move.gd

@@ -3,36 +3,33 @@ extends "on_ground.gd"
 export(float) var MAX_WALK_SPEED = 450
 export(float) var MAX_RUN_SPEED = 700
 
-func enter(host):
+func enter():
 	speed = 0.0
 	velocity = Vector2()
 
 	var input_direction = get_input_direction()
-	update_look_direction(host, input_direction)
-	host.get_node("AnimationPlayer").play("walk")
+	update_look_direction(input_direction)
+	owner.get_node("AnimationPlayer").play("walk")
 
+func handle_input(event):
+	return .handle_input(event)
 
-func handle_input(host, event):
-	return .handle_input(host, event)
-
-
-func update(host, delta):
+func update(delta):
 	var input_direction = get_input_direction()
 	if not input_direction:
 		emit_signal("finished", "idle")
-	update_look_direction(host, input_direction)
+	update_look_direction(input_direction)
 
 	speed = MAX_RUN_SPEED if Input.is_action_pressed("run") else MAX_WALK_SPEED
-	var collision_info = move(host, speed, input_direction)
+	var collision_info = move(speed, input_direction)
 	if not collision_info:
 		return
 	if speed == MAX_RUN_SPEED and collision_info.collider.is_in_group("environment"):
 		return null
 
-
-func move(host, speed, direction):
+func move(speed, direction):
 	velocity = direction.normalized() * speed
-	host.move_and_slide(velocity, Vector2(), 5, 2)
-	if host.get_slide_count() == 0:
+	owner.move_and_slide(velocity, Vector2(), 5, 2)
+	if owner.get_slide_count() == 0:
 		return
-	return host.get_slide_collision(0)
+	return owner.get_slide_collision(0)

+ 2 - 2
2d/finite_state_machine/player/states/motion/on_ground/on_ground.gd

@@ -3,7 +3,7 @@ extends "../motion.gd"
 var speed = 0.0
 var velocity = Vector2()
 
-func handle_input(host, event):
+func handle_input(event):
 	if event.is_action_pressed("jump"):
 		emit_signal("finished", "jump")
-	return .handle_input(host, event)
+	return .handle_input(event)

+ 4 - 9
2d/finite_state_machine/player/weapon/sword.gd

@@ -30,13 +30,11 @@ var combo = [{
 
 var hit_objects = []
 
-
 func _ready():
 	$AnimationPlayer.connect('animation_finished', self, "_on_animation_finished")
 	self.connect("body_entered", self, "_on_body_entered")
 	_change_state(IDLE)
 
-
 func _change_state(new_state):
 	match state:
 		ATTACK:
@@ -57,7 +55,6 @@ func _change_state(new_state):
 			monitoring = true
 	state = new_state
 
-
 func _input(event):
 	if not state == ATTACK:
 		return
@@ -66,27 +63,22 @@ func _input(event):
 	if event.is_action_pressed('attack'):
 		attack_input_state = REGISTERED
 
-
 func _physics_process(delta):
 	if attack_input_state == REGISTERED and ready_for_next_attack:
 		attack()
 
-
 func attack():
 	combo_count += 1
 	_change_state(ATTACK)
 
-
 # use with AnimationPlayer func track
 func set_attack_input_listening():
 	attack_input_state = LISTENING
 
-
 # use with AnimationPlayer func track
 func set_ready_for_next_attack():
 	ready_for_next_attack = true
 
-
 func _on_body_entered(body):
 	if not body.has_node('Health'):
 		return
@@ -95,7 +87,6 @@ func _on_body_entered(body):
 	hit_objects.append(body.get_rid().get_id())
 	body.take_damage(self, attack_current['damage'], attack_current['effect'])
 
-
 func _on_animation_finished(name):
 	if not attack_current:
 		return
@@ -105,3 +96,7 @@ func _on_animation_finished(name):
 	else:
 		_change_state(IDLE)
 		emit_signal("attack_finished")
+
+func _on_StateMachine_state_changed(current_state):
+	if current_state.name == "Attack":
+		attack()

+ 0 - 3
2d/finite_state_machine/player/weapon/sword.png.import

@@ -7,10 +7,7 @@ path="res://.import/sword.png-fc7f0084cdf333c826eda2b33f2ec3cc.stex"
 [deps]
 
 source_file="res://player/weapon/sword.png"
-source_md5="17fb5e0e6c6fc2c23b36fdbb53f93b1d"
-
 dest_files=[ "res://.import/sword.png-fc7f0084cdf333c826eda2b33f2ec3cc.stex" ]
-dest_md5="68d99f8d04dc7b6a17bb7ce168593030"
 
 [params]
 

+ 0 - 1
2d/finite_state_machine/player/weapon/weapon_pivot.gd

@@ -6,7 +6,6 @@ func _ready():
 	$"..".connect("direction_changed", self, '_on_Parent_direction_changed')
 	z_index_start = z_index
 
-
 func _on_Parent_direction_changed(direction):
 	rotation = direction.angle()
 	match direction:

+ 1 - 1
2d/finite_state_machine/project.godot

@@ -16,7 +16,7 @@ config/icon="res://icon.png"
 
 [autoload]
 
-GlobalConstants="*res://autoload/global_constants.gd"
+GlobalConstants="*res://debug/autoload/global_constants.gd"
 
 [display]
 

+ 4 - 8
2d/finite_state_machine/player/states/state.gd → 2d/finite_state_machine/state_machine/state.gd

@@ -8,22 +8,18 @@ extends Node
 signal finished(next_state_name)
 
 # Initialize the state. E.g. change the animation
-func enter(host):
+func enter():
 	return
 
-
 # Clean up the state. Reinitialize values like a timer
-func exit(host):
+func exit():
 	return
 
-
-func handle_input(host, event):
+func handle_input(event):
 	return
 
-
-func update(host, delta):
+func update(delta):
 	return
 
-
 func _on_animation_finished(anim_name):
 	return

+ 71 - 0
2d/finite_state_machine/state_machine/state_machine.gd

@@ -0,0 +1,71 @@
+"""
+Base interface for a generic state machine
+It handles initializing, setting the machine active or not
+delegating _physics_process, _input calls to the State nodes,
+and changing the current/active state.
+See the PlayerV2 scene for an example on how to use it
+"""
+extends Node
+
+signal state_changed(current_state)
+
+"""
+You must set a starting node from the inspector or on
+the node that inherits from this state machine interface
+If you don't the game will crash (on purpose, so you won't 
+forget to initialize the state machine)
+"""
+export(NodePath) var START_STATE
+var states_map = {}
+
+var states_stack = []
+var current_state = null
+var _active = false setget set_active
+
+func _ready():
+	if not START_STATE:
+		START_STATE = get_child(0).get_path()
+	for child in get_children():
+		child.connect("finished", self, "_change_state")
+	initialize(START_STATE)
+
+func initialize(start_state):
+	set_active(true)
+	states_stack.push_front(get_node(start_state))
+	current_state = states_stack[0]
+	current_state.enter()
+
+func set_active(value):
+	_active = value
+	set_physics_process(value)
+	set_process_input(value)
+	if not _active:
+		states_stack = []
+		current_state = null
+
+func _input(event):
+	current_state.handle_input(event)
+
+func _physics_process(delta):
+	current_state.update(delta)
+
+func _on_animation_finished(anim_name):
+	if not _active:
+		return
+	current_state._on_animation_finished(anim_name)
+
+func _change_state(state_name):
+	if not _active:
+		return
+	current_state.exit()
+	
+	if state_name == "previous":
+		states_stack.pop_front()
+	else:
+		states_stack[0] = states_map[state_name]
+	
+	current_state = states_stack[0]
+	emit_signal("state_changed", current_state)
+	
+	if state_name != "previous":
+		current_state.enter()