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/ControlsPanel.tscn" type="PackedScene" id=3]
 [ext_resource path="res://debug/Explanations.tscn" type="PackedScene" id=4]
 [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 )]
 [node name="Player" parent="." index="0" instance=ExtResource( 1 )]
 
 
+editor/display_folded = true
+
 [node name="StatesStackDiplayer" parent="." index="1" instance=ExtResource( 2 )]
 [node name="StatesStackDiplayer" parent="." index="1" instance=ExtResource( 2 )]
 
 
 [node name="ControlsPanel" parent="." index="2" instance=ExtResource( 3 )]
 [node name="ControlsPanel" parent="." index="2" instance=ExtResource( 3 )]
 
 
 [node name="Explanations" parent="." index="3" instance=ExtResource( 4 )]
 [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]
 [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]
 [ext_resource path="res://fonts/SourceCodePro-Bold.ttf" type="DynamicFontData" id=2]
 
 
+
 [sub_resource type="DynamicFont" id=1]
 [sub_resource type="DynamicFont" id=1]
 
 
 size = 20
 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
 tool
 extends Panel
 extends Panel
 
 
+onready var fsm_node = get_node("../Player/StateMachine")
+
 func _ready():
 func _ready():
 	set_as_toplevel(true)
 	set_as_toplevel(true)
 
 
-func _on_Player_state_changed(states_stack):
+func _process(delta):
 	var states_names = ''
 	var states_names = ''
 	var numbers = ''
 	var numbers = ''
 	var index = 0
 	var index = 0
-	for state in states_stack:
+	for state in fsm_node.states_stack:
 		states_names += state.get_name() + '\n'
 		states_names += state.get_name() + '\n'
 		numbers += str(index) + '\n'
 		numbers += str(index) + '\n'
 		index += 1
 		index += 1
 
 
 	$States.text = states_names
 	$States.text = states_names
 	$Numbers.text = numbers
 	$Numbers.text = numbers
-

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

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

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

@@ -1,21 +1,20 @@
 [gd_scene load_steps=20 format=2]
 [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://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://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]
 [sub_resource type="Animation" id=1]
 
 
@@ -55,7 +54,7 @@ use_filter = true
 font_data = ExtResource( 14 )
 font_data = ExtResource( 14 )
 _sections_unfolded = [ "Font", "Settings" ]
 _sections_unfolded = [ "Font", "Settings" ]
 
 
-[node name="Player" type="KinematicBody2D"]
+[node name="Player" type="KinematicBody2D" index="0"]
 
 
 position = Vector2( 628.826, 391.266 )
 position = Vector2( 628.826, 391.266 )
 input_pickable = false
 input_pickable = false
@@ -68,7 +67,45 @@ __meta__ = {
 "_edit_horizontal_guides_": [  ]
 "_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("..")
 root_node = NodePath("..")
 autoplay = ""
 autoplay = ""
@@ -80,74 +117,40 @@ anims/stagger = SubResource( 2 )
 anims/walk = SubResource( 3 )
 anims/walk = SubResource( 3 )
 blend_times = [  ]
 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 )
 self_modulate = Color( 1, 1, 1, 0.361098 )
 position = Vector2( 0, -4 )
 position = Vector2( 0, -4 )
-texture = ExtResource( 2 )
+texture = ExtResource( 9 )
 _sections_unfolded = [ "Visibility" ]
 _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"]
 [node name="Body" type="Sprite" parent="BodyPivot" index="0"]
 
 
 position = Vector2( 0, -58.8242 )
 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 )
 position = Vector2( 1.17401, -61.266 )
-script = ExtResource( 4 )
+script = ExtResource( 11 )
 
 
 [node name="Offset" type="Position2D" parent="WeaponPivot" index="0"]
 [node name="Offset" type="Position2D" parent="WeaponPivot" index="0"]
 
 
 position = Vector2( 110, 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
 build_mode = 0
 polygon = PoolVector2Array( -20, 0, -20, -20, 20, -20, 20, 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
 editor/display_folded = true
 position = Vector2( 1.17401, -61.266 )
 position = Vector2( 1.17401, -61.266 )
@@ -161,7 +164,7 @@ wait_time = 0.2
 one_shot = true
 one_shot = true
 autostart = false
 autostart = false
 
 
-[node name="StateNameDisplayer" type="Label" parent="." index="8"]
+[node name="StateNameDisplayer" type="Label" parent="." index="7"]
 
 
 editor/display_folded = true
 editor/display_folded = true
 anchor_left = 0.0
 anchor_left = 0.0
@@ -189,10 +192,12 @@ max_lines_visible = -1
 script = ExtResource( 15 )
 script = ExtResource( 15 )
 _sections_unfolded = [ "Rect", "custom_fonts" ]
 _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]
 [deps]
 
 
 source_file="res://player/body.png"
 source_file="res://player/body.png"
-source_md5="e76fc4b6b913ad7e09352eada55fc124"
-
 dest_files=[ "res://.import/body.png-313f6363670a5852a7b7126ab476d8b1.stex" ]
 dest_files=[ "res://.import/body.png-313f6363670a5852a7b7126ab476d8b1.stex" ]
-dest_md5="b65921b7af7d7cea3cb9567625d04d34"
 
 
 [params]
 [params]
 
 

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

@@ -2,6 +2,10 @@ extends Node2D
 
 
 var bullet = preload("Bullet.tscn")
 var bullet = preload("Bullet.tscn")
 
 
+func _input(event):
+	if event.is_action_pressed("fire"):
+		fire(owner.look_direction)
+
 func fire(direction):
 func fire(direction):
 	if not $CooldownTimer.is_stopped():
 	if not $CooldownTimer.is_stopped():
 		return
 		return
@@ -10,7 +14,3 @@ func fire(direction):
 	var new_bullet = bullet.instance()
 	var new_bullet = bullet.instance()
 	new_bullet.direction = direction
 	new_bullet.direction = direction
 	add_child(new_bullet)
 	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]
 [deps]
 
 
 source_file="res://player/shadow.png"
 source_file="res://player/shadow.png"
-source_md5="ba1c3f4ef3a93dcd980bc4d020c4a22c"
-
 dest_files=[ "res://.import/shadow.png-493c4635eca1ce8bdece629560617dc7.stex" ]
 dest_files=[ "res://.import/shadow.png-493c4635eca1ce8bdece629560617dc7.stex" ]
-dest_md5="7dd3fb33b53c00ce76edd77833bce15d"
 
 
 [params]
 [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():
 func _on_Sword_attack_finished():
 	emit_signal("finished", "previous")
 	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 
 The animation only affects the Body Sprite"s modulate property so 
 it could stack with other animations if we had two AnimationPlayer nodes
 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()
 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):
 func _on_animation_finished(anim_name):
 	assert anim_name == "stagger"
 	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):
 func _physics_process(delta):
 	rect_position = $"../BodyPivot".position + start_position
 	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
 # 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):
 func _on_animation_finished(anim_name):
 	emit_signal("finished", "dead")
 	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
 export(float) var GRAVITY = 1600.0
 
 
-
 var enter_velocity = Vector2()
 var enter_velocity = Vector2()
 
 
 var max_horizontal_speed = 0.0
 var max_horizontal_speed = 0.0
@@ -21,34 +20,30 @@ var horizontal_velocity = Vector2()
 var vertical_speed = 0.0
 var vertical_speed = 0.0
 var height = 0.0
 var height = 0.0
 
 
-
 func initialize(speed, velocity):
 func initialize(speed, velocity):
 	horizontal_speed = speed
 	horizontal_speed = speed
 	max_horizontal_speed = speed if speed > 0.0 else BASE_MAX_HORIZONTAL_SPEED
 	max_horizontal_speed = speed if speed > 0.0 else BASE_MAX_HORIZONTAL_SPEED
 	enter_velocity = velocity
 	enter_velocity = velocity
 
 
-
-func enter(host):
+func enter():
 	var input_direction = get_input_direction()
 	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()
 	horizontal_velocity = enter_velocity if input_direction else Vector2()
 	vertical_speed = 600.0
 	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()
 	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:
 	if height <= 0.0:
 		emit_signal("finished", "previous")
 		emit_signal("finished", "previous")
 
 
-
-func move_horizontally(host, delta, direction):
+func move_horizontally(delta, direction):
 	if direction:
 	if direction:
 		horizontal_speed += AIR_ACCELERATION * delta
 		horizontal_speed += AIR_ACCELERATION * delta
 	else:
 	else:
@@ -59,12 +54,11 @@ func move_horizontally(host, delta, direction):
 	var steering_velocity = (target_velocity - horizontal_velocity).normalized() * AIR_STEERING_POWER
 	var steering_velocity = (target_velocity - horizontal_velocity).normalized() * AIR_STEERING_POWER
 	horizontal_velocity += steering_velocity
 	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
 	vertical_speed -= GRAVITY * delta
 	height += vertical_speed * delta
 	height += vertical_speed * delta
 	height = max(0.0, height)
 	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
 # 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"):
 	if event.is_action_pressed("simulate_damage"):
 		emit_signal("finished", "stagger")
 		emit_signal("finished", "stagger")
 
 
-
 func get_input_direction():
 func get_input_direction():
 	var input_direction = Vector2()
 	var input_direction = Vector2()
 	input_direction.x = int(Input.is_action_pressed("move_right")) - int(Input.is_action_pressed("move_left"))
 	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"))
 	input_direction.y = int(Input.is_action_pressed("move_down")) - int(Input.is_action_pressed("move_up"))
 	return input_direction
 	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]:
 	if not direction.x in [-1, 1]:
 		return
 		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"
 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()
 	var input_direction = get_input_direction()
 	if input_direction:
 	if input_direction:
 		emit_signal("finished", "move")
 		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_WALK_SPEED = 450
 export(float) var MAX_RUN_SPEED = 700
 export(float) var MAX_RUN_SPEED = 700
 
 
-func enter(host):
+func enter():
 	speed = 0.0
 	speed = 0.0
 	velocity = Vector2()
 	velocity = Vector2()
 
 
 	var input_direction = get_input_direction()
 	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()
 	var input_direction = get_input_direction()
 	if not input_direction:
 	if not input_direction:
 		emit_signal("finished", "idle")
 		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
 	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:
 	if not collision_info:
 		return
 		return
 	if speed == MAX_RUN_SPEED and collision_info.collider.is_in_group("environment"):
 	if speed == MAX_RUN_SPEED and collision_info.collider.is_in_group("environment"):
 		return null
 		return null
 
 
-
-func move(host, speed, direction):
+func move(speed, direction):
 	velocity = direction.normalized() * speed
 	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
-	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 speed = 0.0
 var velocity = Vector2()
 var velocity = Vector2()
 
 
-func handle_input(host, event):
+func handle_input(event):
 	if event.is_action_pressed("jump"):
 	if event.is_action_pressed("jump"):
 		emit_signal("finished", "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 = []
 var hit_objects = []
 
 
-
 func _ready():
 func _ready():
 	$AnimationPlayer.connect('animation_finished', self, "_on_animation_finished")
 	$AnimationPlayer.connect('animation_finished', self, "_on_animation_finished")
 	self.connect("body_entered", self, "_on_body_entered")
 	self.connect("body_entered", self, "_on_body_entered")
 	_change_state(IDLE)
 	_change_state(IDLE)
 
 
-
 func _change_state(new_state):
 func _change_state(new_state):
 	match state:
 	match state:
 		ATTACK:
 		ATTACK:
@@ -57,7 +55,6 @@ func _change_state(new_state):
 			monitoring = true
 			monitoring = true
 	state = new_state
 	state = new_state
 
 
-
 func _input(event):
 func _input(event):
 	if not state == ATTACK:
 	if not state == ATTACK:
 		return
 		return
@@ -66,27 +63,22 @@ func _input(event):
 	if event.is_action_pressed('attack'):
 	if event.is_action_pressed('attack'):
 		attack_input_state = REGISTERED
 		attack_input_state = REGISTERED
 
 
-
 func _physics_process(delta):
 func _physics_process(delta):
 	if attack_input_state == REGISTERED and ready_for_next_attack:
 	if attack_input_state == REGISTERED and ready_for_next_attack:
 		attack()
 		attack()
 
 
-
 func attack():
 func attack():
 	combo_count += 1
 	combo_count += 1
 	_change_state(ATTACK)
 	_change_state(ATTACK)
 
 
-
 # use with AnimationPlayer func track
 # use with AnimationPlayer func track
 func set_attack_input_listening():
 func set_attack_input_listening():
 	attack_input_state = LISTENING
 	attack_input_state = LISTENING
 
 
-
 # use with AnimationPlayer func track
 # use with AnimationPlayer func track
 func set_ready_for_next_attack():
 func set_ready_for_next_attack():
 	ready_for_next_attack = true
 	ready_for_next_attack = true
 
 
-
 func _on_body_entered(body):
 func _on_body_entered(body):
 	if not body.has_node('Health'):
 	if not body.has_node('Health'):
 		return
 		return
@@ -95,7 +87,6 @@ func _on_body_entered(body):
 	hit_objects.append(body.get_rid().get_id())
 	hit_objects.append(body.get_rid().get_id())
 	body.take_damage(self, attack_current['damage'], attack_current['effect'])
 	body.take_damage(self, attack_current['damage'], attack_current['effect'])
 
 
-
 func _on_animation_finished(name):
 func _on_animation_finished(name):
 	if not attack_current:
 	if not attack_current:
 		return
 		return
@@ -105,3 +96,7 @@ func _on_animation_finished(name):
 	else:
 	else:
 		_change_state(IDLE)
 		_change_state(IDLE)
 		emit_signal("attack_finished")
 		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]
 [deps]
 
 
 source_file="res://player/weapon/sword.png"
 source_file="res://player/weapon/sword.png"
-source_md5="17fb5e0e6c6fc2c23b36fdbb53f93b1d"
-
 dest_files=[ "res://.import/sword.png-fc7f0084cdf333c826eda2b33f2ec3cc.stex" ]
 dest_files=[ "res://.import/sword.png-fc7f0084cdf333c826eda2b33f2ec3cc.stex" ]
-dest_md5="68d99f8d04dc7b6a17bb7ce168593030"
 
 
 [params]
 [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')
 	$"..".connect("direction_changed", self, '_on_Parent_direction_changed')
 	z_index_start = z_index
 	z_index_start = z_index
 
 
-
 func _on_Parent_direction_changed(direction):
 func _on_Parent_direction_changed(direction):
 	rotation = direction.angle()
 	rotation = direction.angle()
 	match direction:
 	match direction:

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

@@ -16,7 +16,7 @@ config/icon="res://icon.png"
 
 
 [autoload]
 [autoload]
 
 
-GlobalConstants="*res://autoload/global_constants.gd"
+GlobalConstants="*res://debug/autoload/global_constants.gd"
 
 
 [display]
 [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)
 signal finished(next_state_name)
 
 
 # Initialize the state. E.g. change the animation
 # Initialize the state. E.g. change the animation
-func enter(host):
+func enter():
 	return
 	return
 
 
-
 # Clean up the state. Reinitialize values like a timer
 # Clean up the state. Reinitialize values like a timer
-func exit(host):
+func exit():
 	return
 	return
 
 
-
-func handle_input(host, event):
+func handle_input(event):
 	return
 	return
 
 
-
-func update(host, delta):
+func update(delta):
 	return
 	return
 
 
-
 func _on_animation_finished(anim_name):
 func _on_animation_finished(anim_name):
 	return
 	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()