Pārlūkot izejas kodu

Add HFSM + pushdown automaton demo

Nathan 7 gadi atpakaļ
vecāks
revīzija
5b744fa1e6
42 mainītis faili ar 1784 papildinājumiem un 0 dzēšanām
  1. 20 0
      2d/finite_state_machine/Demo.tscn
  2. 3 0
      2d/finite_state_machine/autoload/global_constants.gd
  3. 77 0
      2d/finite_state_machine/debug/ControlsPanel.tscn
  4. 59 0
      2d/finite_state_machine/debug/Explanations.tscn
  5. 107 0
      2d/finite_state_machine/debug/StatesStackDiplayer.tscn
  6. 18 0
      2d/finite_state_machine/debug/states-stack-displayer.gd
  7. 5 0
      2d/finite_state_machine/debug/top_level_ui.gd
  8. 101 0
      2d/finite_state_machine/default_env.tres
  9. BIN
      2d/finite_state_machine/fonts/SourceCodePro-Black.ttf
  10. BIN
      2d/finite_state_machine/fonts/SourceCodePro-Bold.ttf
  11. 12 0
      2d/finite_state_machine/fonts/source_code_pro_explanations.tres
  12. 12 0
      2d/finite_state_machine/fonts/source_code_pro_explanations_bold.tres
  13. BIN
      2d/finite_state_machine/icon.png
  14. 32 0
      2d/finite_state_machine/icon.png.import
  15. 198 0
      2d/finite_state_machine/player/Player.tscn
  16. BIN
      2d/finite_state_machine/player/body.png
  17. 32 0
      2d/finite_state_machine/player/body.png.import
  18. 25 0
      2d/finite_state_machine/player/bullet/Bullet.tscn
  19. 28 0
      2d/finite_state_machine/player/bullet/bullet.gd
  20. 16 0
      2d/finite_state_machine/player/bullet/bullet_spawner.gd
  21. 17 0
      2d/finite_state_machine/player/health/Health.tscn
  22. 62 0
      2d/finite_state_machine/player/health/health.gd
  23. BIN
      2d/finite_state_machine/player/shadow.png
  24. 32 0
      2d/finite_state_machine/player/shadow.png.import
  25. 60 0
      2d/finite_state_machine/player/state-machine-bad-use.gd
  26. 133 0
      2d/finite_state_machine/player/state-machine.gd
  27. 14 0
      2d/finite_state_machine/player/states/combat/attack.gd
  28. 16 0
      2d/finite_state_machine/player/states/combat/stagger.gd
  29. 12 0
      2d/finite_state_machine/player/states/debug/state-name-displayer.gd
  30. 10 0
      2d/finite_state_machine/player/states/die.gd
  31. 70 0
      2d/finite_state_machine/player/states/motion/in_air/jump.gd
  32. 22 0
      2d/finite_state_machine/player/states/motion/motion.gd
  33. 14 0
      2d/finite_state_machine/player/states/motion/on_ground/idle.gd
  34. 38 0
      2d/finite_state_machine/player/states/motion/on_ground/move.gd
  35. 9 0
      2d/finite_state_machine/player/states/motion/on_ground/on_ground.gd
  36. 29 0
      2d/finite_state_machine/player/states/state.gd
  37. 278 0
      2d/finite_state_machine/player/weapon/Sword.tscn
  38. 107 0
      2d/finite_state_machine/player/weapon/sword.gd
  39. BIN
      2d/finite_state_machine/player/weapon/sword.png
  40. 32 0
      2d/finite_state_machine/player/weapon/sword.png.import
  41. 16 0
      2d/finite_state_machine/player/weapon/weapon_pivot.gd
  42. 68 0
      2d/finite_state_machine/project.godot

+ 20 - 0
2d/finite_state_machine/Demo.tscn

@@ -0,0 +1,20 @@
+[gd_scene load_steps=5 format=2]
+
+[ext_resource path="res://player/Player.tscn" type="PackedScene" id=1]
+[ext_resource path="res://debug/StatesStackDiplayer.tscn" type="PackedScene" id=2]
+[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="Player" parent="." index="0" instance=ExtResource( 1 )]
+
+[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"]
+
+

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

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

+ 77 - 0
2d/finite_state_machine/debug/ControlsPanel.tscn

@@ -0,0 +1,77 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://debug/top_level_ui.gd" type="Script" id=1]
+[ext_resource path="res://fonts/source_code_pro_explanations.tres" type="DynamicFont" id=2]
+
+[node name="ControlsPanel" type="Panel" index="0"]
+
+anchor_left = 1.0
+anchor_top = 0.0
+anchor_right = 1.0
+anchor_bottom = 0.0
+margin_left = -220.0
+margin_bottom = 170.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 0
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 1
+script = ExtResource( 1 )
+
+[node name="Keys" type="Label" parent="." index="0"]
+
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = 10.0
+margin_right = -10.0
+margin_bottom = -10.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/font = ExtResource( 2 )
+text = "Shoot:
+Attack:
+Stagger:
+Jump:
+Sprint:"
+percent_visible = 1.0
+lines_skipped = 0
+max_lines_visible = -1
+_sections_unfolded = [ "BBCode", "custom_fonts" ]
+
+[node name="Keys2" type="Label" parent="." index="1"]
+
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = 10.0
+margin_right = -10.0
+margin_bottom = -10.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/font = ExtResource( 2 )
+text = "R
+F
+X
+Space
+Shift"
+align = 2
+percent_visible = 1.0
+lines_skipped = 0
+max_lines_visible = -1
+_sections_unfolded = [ "BBCode", "custom_fonts" ]
+
+

+ 59 - 0
2d/finite_state_machine/debug/Explanations.tscn

@@ -0,0 +1,59 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://fonts/source_code_pro_explanations_bold.tres" type="DynamicFont" id=1]
+[ext_resource path="res://fonts/source_code_pro_explanations.tres" type="DynamicFont" id=2]
+[ext_resource path="res://debug/top_level_ui.gd" type="Script" id=3]
+
+
+[node name="Explanations" type="RichTextLabel"]
+
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = -370.0
+margin_right = -10.0
+margin_bottom = -730.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/bold_font = ExtResource( 1 )
+custom_fonts/normal_font = ExtResource( 2 )
+bbcode_enabled = true
+bbcode_text = "This example shows how to apply the State programming pattern in GDscript, including Hierarchical States, and a pushdown automaton. 
+
+States are common in games. You can use the pattern to:
+
+1. Separate each behavior and transitions between behaviors, thus make scripts shorter and easier to manage
+2. Respect the Single Responsibility Principle. Each State object represents [b]one[/b] action
+3. Improve your code's structure. Look at the scene tree and FileSystem tab: without looking at the code, you'll know what the Player can or cannot do.
+
+You can read more about States in the excellent [url=http://gameprogrammingpatterns.com/state.html]Game Programming Patterns ebook[/url]."
+visible_characters = -1
+percent_visible = 1.0
+meta_underlined = true
+tab_size = 4
+text = "This example shows how to apply the State programming pattern in GDscript, including Hierarchical States, and a pushdown automaton. 
+
+States are common in games. You can use the pattern to:
+
+1. Separate each behavior and transitions between behaviors, thus make scripts shorter and easier to manage
+2. Respect the Single Responsibility Principle. Each State object represents one action
+3. Improve your code's structure. Look at the scene tree and FileSystem tab: without looking at the code, you'll know what the Player can or cannot do.
+
+You can read more about States in the excellent Game Programming Patterns ebook."
+scroll_active = true
+scroll_following = false
+selection_enabled = false
+override_selected_font_color = false
+script = ExtResource( 3 )
+_sections_unfolded = [ "BBCode", "custom_fonts" ]
+__meta__ = {
+"_edit_lock_": true
+}
+
+

+ 107 - 0
2d/finite_state_machine/debug/StatesStackDiplayer.tscn

@@ -0,0 +1,107 @@
+[gd_scene load_steps=4 format=2]
+
+[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
+use_mipmaps = false
+use_filter = true
+font_data = ExtResource( 2 )
+_sections_unfolded = [ "Font", "Settings" ]
+
+[node name="StatesStackDiplayer" type="Panel"]
+
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_right = 210.0
+margin_bottom = 170.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 0
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 1
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_group_": true
+}
+
+[node name="Title" type="Label" parent="." index="0"]
+
+anchor_left = 0.5
+anchor_top = 0.0
+anchor_right = 0.5
+anchor_bottom = 0.0
+margin_left = -105.0
+margin_right = 105.0
+margin_bottom = 40.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/font = SubResource( 1 )
+text = "Pushown"
+align = 1
+valign = 1
+uppercase = true
+percent_visible = 1.0
+lines_skipped = 0
+max_lines_visible = -1
+_sections_unfolded = [ "custom_fonts" ]
+
+[node name="States" type="Label" parent="." index="1"]
+
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_left = 20.0
+margin_top = 50.0
+margin_right = 190.0
+margin_bottom = 170.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/font = SubResource( 1 )
+text = "Jump
+Test"
+align = 1
+percent_visible = 1.0
+lines_skipped = 0
+max_lines_visible = -1
+_sections_unfolded = [ "custom_fonts" ]
+
+[node name="Numbers" type="Label" parent="." index="2"]
+
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_left = 20.0
+margin_top = 50.0
+margin_right = 190.0
+margin_bottom = 170.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/font = SubResource( 1 )
+text = "1.
+2."
+percent_visible = 1.0
+lines_skipped = 0
+max_lines_visible = -1
+_sections_unfolded = [ "custom_fonts" ]
+
+

+ 18 - 0
2d/finite_state_machine/debug/states-stack-displayer.gd

@@ -0,0 +1,18 @@
+tool
+extends Panel
+
+func _ready():
+	set_as_toplevel(true)
+
+func _on_Player_state_changed(states_stack):
+	var states_names = ''
+	var numbers = ''
+	var index = 0
+	for state in states_stack:
+		states_names += state.get_name() + '\n'
+		numbers += str(index) + '\n'
+		index += 1
+
+	$States.text = states_names
+	$Numbers.text = numbers
+

+ 5 - 0
2d/finite_state_machine/debug/top_level_ui.gd

@@ -0,0 +1,5 @@
+tool
+extends Control
+
+func _ready():
+	set_as_toplevel(true)

+ 101 - 0
2d/finite_state_machine/default_env.tres

@@ -0,0 +1,101 @@
+[gd_resource type="Environment" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+
+radiance_size = 4
+sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 )
+sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 )
+sky_curve = 0.25
+sky_energy = 1.0
+ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 )
+ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 )
+ground_curve = 0.01
+ground_energy = 1.0
+sun_color = Color( 1, 1, 1, 1 )
+sun_latitude = 35.0
+sun_longitude = 0.0
+sun_angle_min = 1.0
+sun_angle_max = 100.0
+sun_curve = 0.05
+sun_energy = 16.0
+texture_size = 2
+
+[resource]
+
+background_mode = 2
+background_sky = SubResource( 1 )
+background_sky_custom_fov = 0.0
+background_color = Color( 0, 0, 0, 1 )
+background_energy = 1.0
+background_canvas_max_layer = 0
+ambient_light_color = Color( 0, 0, 0, 1 )
+ambient_light_energy = 1.0
+ambient_light_sky_contribution = 1.0
+fog_enabled = false
+fog_color = Color( 0.5, 0.6, 0.7, 1 )
+fog_sun_color = Color( 1, 0.9, 0.7, 1 )
+fog_sun_amount = 0.0
+fog_depth_enabled = true
+fog_depth_begin = 10.0
+fog_depth_curve = 1.0
+fog_transmit_enabled = false
+fog_transmit_curve = 1.0
+fog_height_enabled = false
+fog_height_min = 0.0
+fog_height_max = 100.0
+fog_height_curve = 1.0
+tonemap_mode = 0
+tonemap_exposure = 1.0
+tonemap_white = 1.0
+auto_exposure_enabled = false
+auto_exposure_scale = 0.4
+auto_exposure_min_luma = 0.05
+auto_exposure_max_luma = 8.0
+auto_exposure_speed = 0.5
+ss_reflections_enabled = false
+ss_reflections_max_steps = 64
+ss_reflections_fade_in = 0.15
+ss_reflections_fade_out = 2.0
+ss_reflections_depth_tolerance = 0.2
+ss_reflections_roughness = true
+ssao_enabled = false
+ssao_radius = 1.0
+ssao_intensity = 1.0
+ssao_radius2 = 0.0
+ssao_intensity2 = 1.0
+ssao_bias = 0.01
+ssao_light_affect = 0.0
+ssao_color = Color( 0, 0, 0, 1 )
+ssao_quality = 0
+ssao_blur = 3
+ssao_edge_sharpness = 4.0
+dof_blur_far_enabled = false
+dof_blur_far_distance = 10.0
+dof_blur_far_transition = 5.0
+dof_blur_far_amount = 0.1
+dof_blur_far_quality = 1
+dof_blur_near_enabled = false
+dof_blur_near_distance = 2.0
+dof_blur_near_transition = 1.0
+dof_blur_near_amount = 0.1
+dof_blur_near_quality = 1
+glow_enabled = false
+glow_levels/1 = false
+glow_levels/2 = false
+glow_levels/3 = true
+glow_levels/4 = false
+glow_levels/5 = true
+glow_levels/6 = false
+glow_levels/7 = false
+glow_intensity = 0.8
+glow_strength = 1.0
+glow_bloom = 0.0
+glow_blend_mode = 2
+glow_hdr_threshold = 1.0
+glow_hdr_scale = 2.0
+glow_bicubic_upscale = false
+adjustment_enabled = false
+adjustment_brightness = 1.0
+adjustment_contrast = 1.0
+adjustment_saturation = 1.0
+

BIN
2d/finite_state_machine/fonts/SourceCodePro-Black.ttf


BIN
2d/finite_state_machine/fonts/SourceCodePro-Bold.ttf


+ 12 - 0
2d/finite_state_machine/fonts/source_code_pro_explanations.tres

@@ -0,0 +1,12 @@
+[gd_resource type="DynamicFont" load_steps=2 format=2]
+
+[ext_resource path="res://fonts/SourceCodePro-Bold.ttf" type="DynamicFontData" id=1]
+
+[resource]
+
+size = 20
+use_mipmaps = false
+use_filter = true
+font_data = ExtResource( 1 )
+_sections_unfolded = [ "Font", "Settings" ]
+

+ 12 - 0
2d/finite_state_machine/fonts/source_code_pro_explanations_bold.tres

@@ -0,0 +1,12 @@
+[gd_resource type="DynamicFont" load_steps=2 format=2]
+
+[ext_resource path="res://fonts/SourceCodePro-Black.ttf" type="DynamicFontData" id=1]
+
+[resource]
+
+size = 24
+use_mipmaps = false
+use_filter = true
+font_data = ExtResource( 1 )
+_sections_unfolded = [ "Font", "Settings" ]
+

BIN
2d/finite_state_machine/icon.png


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

@@ -0,0 +1,32 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+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]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 198 - 0
2d/finite_state_machine/player/Player.tscn

@@ -0,0 +1,198 @@
+[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/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]
+
+
+[sub_resource type="Animation" id=1]
+
+length = 1.0
+loop = false
+step = 0.1
+
+[sub_resource type="Animation" id=2]
+
+length = 0.6
+loop = false
+step = 0.1
+tracks/0/type = "value"
+tracks/0/path = NodePath("BodyPivot/Body:modulate")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/keys = {
+"times": PoolRealArray( 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.4 ),
+"transitions": PoolRealArray( 1, 1, 1, 1, 1, 1, 1 ),
+"update": 0,
+"values": [ Color( 1, 1, 1, 1 ), Color( 1, 0, 0, 1 ), Color( 1, 1, 1, 1 ), Color( 1, 0, 0, 1 ), Color( 1, 1, 1, 1 ), Color( 1, 0, 0, 1 ), Color( 1, 1, 1, 1 ) ]
+}
+
+[sub_resource type="Animation" id=3]
+
+length = 1.0
+loop = false
+step = 0.1
+
+[sub_resource type="DynamicFont" id=4]
+
+size = 20
+use_mipmaps = false
+use_filter = true
+font_data = ExtResource( 14 )
+_sections_unfolded = [ "Font", "Settings" ]
+
+[node name="Player" type="KinematicBody2D"]
+
+position = Vector2( 628.826, 391.266 )
+input_pickable = false
+collision_layer = 1
+collision_mask = 1
+collision/safe_margin = 0.08
+script = ExtResource( 1 )
+_sections_unfolded = [ "Visibility" ]
+__meta__ = {
+"_edit_horizontal_guides_": [  ]
+}
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="." index="0"]
+
+root_node = NodePath("..")
+autoplay = ""
+playback_process_mode = 1
+playback_default_blend_time = 0.0
+playback_speed = 1.0
+anims/idle = SubResource( 1 )
+anims/stagger = SubResource( 2 )
+anims/walk = SubResource( 3 )
+blend_times = [  ]
+
+[node name="Shadow" type="Sprite" parent="." index="1"]
+
+self_modulate = Color( 1, 1, 1, 0.361098 )
+position = Vector2( 0, -4 )
+texture = ExtResource( 2 )
+_sections_unfolded = [ "Visibility" ]
+
+[node name="BodyPivot" type="Position2D" parent="." index="2"]
+
+[node name="Body" type="Sprite" parent="BodyPivot" index="0"]
+
+position = Vector2( 0, -58.8242 )
+texture = ExtResource( 3 )
+
+[node name="WeaponPivot" type="Position2D" parent="." index="3"]
+
+position = Vector2( 1.17401, -61.266 )
+script = ExtResource( 4 )
+
+[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="CollisionPolygon2D" type="CollisionPolygon2D" parent="." index="4"]
+
+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"]
+
+editor/display_folded = true
+position = Vector2( 1.17401, -61.266 )
+script = ExtResource( 13 )
+_sections_unfolded = [ "Transform" ]
+
+[node name="CooldownTimer" type="Timer" parent="BulletSpawn" index="0"]
+
+process_mode = 1
+wait_time = 0.2
+one_shot = true
+autostart = false
+
+[node name="StateNameDisplayer" type="Label" parent="." index="8"]
+
+editor/display_folded = true
+anchor_left = 0.0
+anchor_top = 0.0
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_left = -109.0
+margin_top = -172.0
+margin_right = 110.0
+margin_bottom = -138.0
+rect_pivot_offset = Vector2( 0, 0 )
+rect_clip_content = false
+mouse_filter = 2
+mouse_default_cursor_shape = 0
+size_flags_horizontal = 1
+size_flags_vertical = 4
+custom_fonts/font = SubResource( 4 )
+text = "Test"
+align = 1
+valign = 1
+uppercase = true
+percent_visible = 1.0
+lines_skipped = 0
+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="animation_finished" from="AnimationPlayer" to="." method="_on_animation_finished"]
+
+[connection signal="attack_finished" from="WeaponPivot/Offset/Sword" to="States/Attack" method="_on_Sword_attack_finished"]
+
+

BIN
2d/finite_state_machine/player/body.png


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

@@ -0,0 +1,32 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+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]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 25 - 0
2d/finite_state_machine/player/bullet/Bullet.tscn

@@ -0,0 +1,25 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://player/bullet/bullet.gd" type="Script" id=1]
+
+
+[sub_resource type="CircleShape2D" id=1]
+
+custom_solver_bias = 0.0
+radius = 12.0
+
+[node name="Bullet" type="KinematicBody2D"]
+
+input_pickable = false
+collision_layer = 2
+collision_mask = 2
+collision/safe_margin = 0.08
+script = ExtResource( 1 )
+_sections_unfolded = [ "Collision", "collision" ]
+SPEED = 1000.0
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="." index="0"]
+
+shape = SubResource( 1 )
+
+

+ 28 - 0
2d/finite_state_machine/player/bullet/bullet.gd

@@ -0,0 +1,28 @@
+extends KinematicBody2D
+
+
+var direction = Vector2()
+export(float) var SPEED = 1000.0
+
+
+func _ready():
+	set_as_toplevel(true)
+
+
+func _physics_process(delta):
+	if is_outside_view_bounds():
+		queue_free()
+
+	var motion = direction * SPEED * delta
+	var collision_info = move_and_collide(motion)
+	if collision_info:
+		queue_free()
+
+
+func is_outside_view_bounds():
+	return position.x > OS.get_screen_size().x or position.x < 0.0 \
+		or position.y > OS.get_screen_size().y or position.y < 0.0
+
+
+func _draw():
+	draw_circle(Vector2(), $CollisionShape2D.shape.radius, Color('#ffffff'))

+ 16 - 0
2d/finite_state_machine/player/bullet/bullet_spawner.gd

@@ -0,0 +1,16 @@
+extends Node2D
+
+var bullet = preload("Bullet.tscn")
+
+func fire(direction):
+	if not $CooldownTimer.is_stopped():
+		return
+
+	$CooldownTimer.start()
+	var new_bullet = bullet.instance()
+	new_bullet.direction = direction
+	add_child(new_bullet)
+
+
+func update(host, delta):
+	return 'previous'

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

@@ -0,0 +1,17 @@
+[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
+
+

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

@@ -0,0 +1,62 @@
+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()

BIN
2d/finite_state_machine/player/shadow.png


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

@@ -0,0 +1,32 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+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]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

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

@@ -0,0 +1,60 @@
+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())

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

@@ -0,0 +1,133 @@
+"""
+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)

+ 14 - 0
2d/finite_state_machine/player/states/combat/attack.gd

@@ -0,0 +1,14 @@
+"""
+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")
+
+
+func _on_Sword_attack_finished():
+	emit_signal("finished", "previous")

+ 16 - 0
2d/finite_state_machine/player/states/combat/stagger.gd

@@ -0,0 +1,16 @@
+"""
+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"
+
+var knockback_direction = Vector2()
+
+func enter(host):
+	host.get_node("AnimationPlayer").play("stagger")
+
+
+func _on_animation_finished(anim_name):
+	assert anim_name == "stagger"
+	emit_signal("finished", "previous")

+ 12 - 0
2d/finite_state_machine/player/states/debug/state-name-displayer.gd

@@ -0,0 +1,12 @@
+extends Label
+
+var start_position = Vector2()
+
+func _ready():
+	start_position = rect_position
+
+func _physics_process(delta):
+	rect_position = $"../BodyPivot".position + start_position
+
+func _on_Player_state_changed(states_stack):
+	text = states_stack[0].get_name()

+ 10 - 0
2d/finite_state_machine/player/states/die.gd

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

+ 70 - 0
2d/finite_state_machine/player/states/motion/in_air/jump.gd

@@ -0,0 +1,70 @@
+extends "../motion.gd"
+
+export(float) var BASE_MAX_HORIZONTAL_SPEED = 400.0
+
+export(float) var AIR_ACCELERATION = 1000.0
+export(float) var AIR_DECCELERATION = 2000.0
+export(float) var AIR_STEERING_POWER = 50.0
+
+export(float) var JUMP_HEIGHT = 120.0
+export(float) var JUMP_DURATION = 0.8
+
+export(float) var GRAVITY = 1600.0
+
+
+var enter_velocity = Vector2()
+
+var max_horizontal_speed = 0.0
+var horizontal_speed = 0.0
+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):
+	var input_direction = get_input_direction()
+	update_look_direction(host, input_direction)
+
+	horizontal_velocity = enter_velocity if input_direction else Vector2()
+	vertical_speed = 600.0
+
+	host.get_node("AnimationPlayer").play("idle")
+
+
+func update(host, delta):
+	var input_direction = get_input_direction()
+	update_look_direction(host, input_direction)
+
+	move_horizontally(host, delta, input_direction)
+	animate_jump_height(host, delta)
+	if height <= 0.0:
+		emit_signal("finished", "previous")
+
+
+func move_horizontally(host, delta, direction):
+	if direction:
+		horizontal_speed += AIR_ACCELERATION * delta
+	else:
+		horizontal_speed -= AIR_DECCELERATION * delta
+	horizontal_speed = clamp(horizontal_speed, 0, max_horizontal_speed)
+
+	var target_velocity = horizontal_speed * direction.normalized()
+	var steering_velocity = (target_velocity - horizontal_velocity).normalized() * AIR_STEERING_POWER
+	horizontal_velocity += steering_velocity
+
+	host.move_and_slide(horizontal_velocity)
+
+
+func animate_jump_height(host, delta):
+	vertical_speed -= GRAVITY * delta
+	height += vertical_speed * delta
+	height = max(0.0, height)
+
+	host.get_node("BodyPivot").position.y = -height

+ 22 - 0
2d/finite_state_machine/player/states/motion/motion.gd

@@ -0,0 +1,22 @@
+# Collection of important methods to handle direction and animation
+extends "../state.gd"
+
+
+func handle_input(host, 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
+	if not direction.x in [-1, 1]:
+		return
+	host.get_node("BodyPivot").set_scale(Vector2(direction.x, 1))

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

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

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

@@ -0,0 +1,38 @@
+extends "on_ground.gd"
+
+export(float) var MAX_WALK_SPEED = 450
+export(float) var MAX_RUN_SPEED = 700
+
+func enter(host):
+	speed = 0.0
+	velocity = Vector2()
+
+	var input_direction = get_input_direction()
+	update_look_direction(host, input_direction)
+	host.get_node("AnimationPlayer").play("walk")
+
+
+func handle_input(host, event):
+	return .handle_input(host, event)
+
+
+func update(host, delta):
+	var input_direction = get_input_direction()
+	if not input_direction:
+		emit_signal("finished", "idle")
+	update_look_direction(host, input_direction)
+
+	speed = MAX_RUN_SPEED if Input.is_action_pressed("run") else MAX_WALK_SPEED
+	var collision_info = move(host, 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):
+	velocity = direction.normalized() * speed
+	host.move_and_slide(velocity, Vector2(), 5, 2)
+	if host.get_slide_count() == 0:
+		return
+	return host.get_slide_collision(0)

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

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

+ 29 - 0
2d/finite_state_machine/player/states/state.gd

@@ -0,0 +1,29 @@
+"""
+Base interface for all states: it doesn't do anything in itself
+but forces us to pass the right arguments to the methods below
+and makes sure every State object had all of these methods.
+"""
+extends Node
+
+signal finished(next_state_name)
+
+# Initialize the state. E.g. change the animation
+func enter(host):
+	return
+
+
+# Clean up the state. Reinitialize values like a timer
+func exit(host):
+	return
+
+
+func handle_input(host, event):
+	return
+
+
+func update(host, delta):
+	return
+
+
+func _on_animation_finished(anim_name):
+	return

+ 278 - 0
2d/finite_state_machine/player/weapon/Sword.tscn

@@ -0,0 +1,278 @@
+[gd_scene load_steps=8 format=2]
+
+[ext_resource path="res://player/weapon/sword.gd" type="Script" id=1]
+[ext_resource path="res://player/weapon/sword.png" type="Texture" id=2]
+
+[sub_resource type="Animation" id=1]
+
+resource_name = "SETUP"
+length = 1.0
+loop = false
+step = 0.1
+tracks/0/type = "value"
+tracks/0/path = NodePath(".:rotation_degrees")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 0,
+"values": [ 0.0 ]
+}
+tracks/1/type = "value"
+tracks/1/path = NodePath(".:scale")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 0,
+"values": [ Vector2( 1, 1 ) ]
+}
+tracks/2/type = "value"
+tracks/2/path = NodePath(".:visible")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ true ]
+}
+tracks/3/type = "value"
+tracks/3/path = NodePath(".:monitoring")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ true ]
+}
+tracks/4/type = "value"
+tracks/4/path = NodePath(".:monitorable")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ false ]
+}
+
+[sub_resource type="Animation" id=2]
+
+resource_name = "attack_circular"
+length = 0.3
+loop = false
+step = 0.05
+tracks/0/type = "value"
+tracks/0/path = NodePath(".:rotation_degrees")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/keys = {
+"times": PoolRealArray( 0, 0.15, 0.2 ),
+"transitions": PoolRealArray( 0.439427, 1, 1 ),
+"update": 0,
+"values": [ -100.0, 100.0, 90.0 ]
+}
+tracks/1/type = "value"
+tracks/1/path = NodePath(".:scale")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/keys = {
+"times": PoolRealArray( 0, 0.05, 0.15, 0.2 ),
+"transitions": PoolRealArray( 1, 2.50795, 1, 1 ),
+"update": 0,
+"values": [ Vector2( 1, 1 ), Vector2( 1, 1.3 ), Vector2( 1, 1 ), Vector2( 1, 1 ) ]
+}
+tracks/2/type = "value"
+tracks/2/path = NodePath(".:visible")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ true ]
+}
+
+[sub_resource type="Animation" id=3]
+
+length = 0.45
+loop = false
+step = 0.05
+tracks/0/type = "value"
+tracks/0/path = NodePath(".:rotation_degrees")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/keys = {
+"times": PoolRealArray( 0, 0.15, 0.2 ),
+"transitions": PoolRealArray( 0.439427, 1, 1 ),
+"update": 0,
+"values": [ -80.0, 85.0, 75.0 ]
+}
+tracks/1/type = "value"
+tracks/1/path = NodePath(".:scale")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/keys = {
+"times": PoolRealArray( 0, 0.05, 0.15, 0.2 ),
+"transitions": PoolRealArray( 1, 2.50795, 1, 1 ),
+"update": 0,
+"values": [ Vector2( 1, 1 ), Vector2( 1, 1.3 ), Vector2( 1, 1 ), Vector2( 1, 1 ) ]
+}
+tracks/2/type = "value"
+tracks/2/path = NodePath(".:visible")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ true ]
+}
+tracks/3/type = "method"
+tracks/3/path = NodePath(".")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/keys = {
+"times": PoolRealArray( 0.1, 0.25 ),
+"transitions": PoolRealArray( 1, 1 ),
+"values": [ {
+"args": [  ],
+"method": "set_attack_input_listening"
+}, {
+"args": [  ],
+"method": "set_ready_for_next_attack"
+} ]
+}
+
+[sub_resource type="Animation" id=4]
+
+resource_name = "attack_medium"
+length = 0.5
+loop = false
+step = 0.05
+tracks/0/type = "value"
+tracks/0/path = NodePath(".:rotation_degrees")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/keys = {
+"times": PoolRealArray( 0.05, 0.25, 0.35 ),
+"transitions": PoolRealArray( 0.439427, 1, 1 ),
+"update": 0,
+"values": [ 95.0, -95.0, -90.0 ]
+}
+tracks/1/type = "value"
+tracks/1/path = NodePath(".:scale")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/keys = {
+"times": PoolRealArray( 0, 0.1, 0.2, 0.25 ),
+"transitions": PoolRealArray( 1, 2.50795, 1, 1 ),
+"update": 0,
+"values": [ Vector2( 1, 1 ), Vector2( 1, 1.3 ), Vector2( 1, 1 ), Vector2( 1, 1 ) ]
+}
+tracks/2/type = "value"
+tracks/2/path = NodePath(".:visible")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ true ]
+}
+
+[sub_resource type="Animation" id=5]
+
+length = 0.01
+loop = false
+step = 0.1
+tracks/0/type = "value"
+tracks/0/path = NodePath(".:visible")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/keys = {
+"times": PoolRealArray( 0 ),
+"transitions": PoolRealArray( 1 ),
+"update": 1,
+"values": [ false ]
+}
+
+[node name="Sword" type="Area2D"]
+
+input_pickable = false
+gravity_vec = Vector2( 0, 1 )
+gravity = 98.0
+linear_damp = 0.1
+angular_damp = 1.0
+monitorable = false
+collision_layer = 16
+collision_mask = 3
+audio_bus_override = false
+audio_bus_name = "Master"
+script = ExtResource( 1 )
+_sections_unfolded = [ "Transform" ]
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="." index="0"]
+
+root_node = NodePath("..")
+autoplay = ""
+playback_process_mode = 1
+playback_default_blend_time = 0.0
+playback_speed = 1.0
+anims/SETUP = SubResource( 1 )
+anims/attack_circular = SubResource( 2 )
+anims/attack_fast = SubResource( 3 )
+anims/attack_medium = SubResource( 4 )
+anims/idle = SubResource( 5 )
+blend_times = [  ]
+_sections_unfolded = [ "Playback Options" ]
+
+[node name="sword" type="Sprite" parent="." index="1"]
+
+position = Vector2( 4, 0 )
+texture = ExtResource( 2 )
+offset = Vector2( 67, 0 )
+_sections_unfolded = [ "Offset", "Transform" ]
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="." index="2"]
+
+build_mode = 0
+polygon = PoolVector2Array( 28.0001, -15.9999, 136, -15.9995, 160, 0, 136, 16.0005, 27.9999, 16.0001 )
+
+

+ 107 - 0
2d/finite_state_machine/player/weapon/sword.gd

@@ -0,0 +1,107 @@
+extends Area2D
+
+signal attack_finished
+
+enum STATES { IDLE, ATTACK }
+var state = null
+
+enum ATTACK_INPUT_STATES { IDLE, LISTENING, REGISTERED }
+var attack_input_state = IDLE
+var ready_for_next_attack = false
+const MAX_COMBO_COUNT = 3
+var combo_count = 0
+
+var attack_current = {}
+var combo = [{
+		'damage': 1,
+		'animation': 'attack_fast',
+		'effect': null
+	},
+	{
+		'damage': 1,
+		'animation': 'attack_fast',
+		'effect': null
+	},
+	{
+		'damage': 3,
+		'animation': 'attack_medium',
+		'effect': null
+	}]
+
+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:
+			hit_objects = []
+			attack_input_state = IDLE
+			ready_for_next_attack = false
+
+	match new_state:
+		IDLE:
+			combo_count = 0
+			$AnimationPlayer.stop()
+			visible = false
+			monitoring = false
+		ATTACK:
+			attack_current = combo[combo_count -1]
+			$AnimationPlayer.play(attack_current['animation'])
+			visible = true
+			monitoring = true
+	state = new_state
+
+
+func _input(event):
+	if not state == ATTACK:
+		return
+	if attack_input_state != LISTENING:
+		return
+	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
+	if body.get_rid().get_id() in hit_objects:
+		return
+	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
+
+	if attack_input_state == REGISTERED and combo_count < MAX_COMBO_COUNT:
+		attack()
+	else:
+		_change_state(IDLE)
+		emit_signal("attack_finished")

BIN
2d/finite_state_machine/player/weapon/sword.png


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

@@ -0,0 +1,32 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+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]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

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

@@ -0,0 +1,16 @@
+extends Position2D
+
+var z_index_start = 0
+
+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:
+		Vector2(0, -1):
+			z_index = z_index_start - 1
+		_:
+			z_index = z_index_start

+ 68 - 0
2d/finite_state_machine/project.godot

@@ -0,0 +1,68 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=3
+
+[application]
+
+config/name="Hierarchical Finite State Machine example"
+run/main_scene="res://Demo.tscn"
+config/icon="res://icon.png"
+
+[autoload]
+
+GlobalConstants="*res://autoload/global_constants.gd"
+
+[display]
+
+window/size/width=1280
+window/size/height=720
+window/size/test_width=1280
+window/size/test_height=720
+window/stretch/mode="2d"
+window/stretch/aspect="keep"
+
+[gdnative]
+
+singletons=[  ]
+
+[input]
+
+move_left=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
+ ]
+move_up=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
+ ]
+move_right=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
+ ]
+move_down=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
+ ]
+fire=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":82,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
+ ]
+run=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777237,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":7,"pressure":0.0,"pressed":false,"script":null)
+ ]
+jump=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
+ ]
+simulate_damage=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":88,"unicode":0,"echo":false,"script":null)
+ ]
+attack=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":70,"unicode":0,"echo":false,"script":null)
+ ]
+
+[rendering]
+
+environment/default_environment="res://default_env.tres"