Skip to content

Instantly share code, notes, and snippets.

@N-Carter
Created January 5, 2024 19:21
Show Gist options
  • Save N-Carter/6b70333c01b1d3206b0d2482453f01a0 to your computer and use it in GitHub Desktop.
Save N-Carter/6b70333c01b1d3206b0d2482453f01a0 to your computer and use it in GitHub Desktop.
A state machine for walking creatures
class_name Ambulator
extends StateMachine
@export var _speed := 16.0
@export var _jump_impulse := 200.0
@export var _max_speed := 50.0
@export var _gravity_multiplier := 0.1
@export var _snap_length := 20.0
@onready var _gravity : float = ProjectSettings.get_setting("physics/2d/default_gravity")
@onready var _gravity_vector : Vector2 = ProjectSettings.get_setting("physics/2d/default_gravity_vector")
var _velocity : Vector2
@export var _body_path : NodePath
@onready var _body := get_node(_body_path) as CharacterBody2D
@export var _sprite_path : NodePath
@onready var _sprite := get_node(_sprite_path) as AnimatedSprite2D
@export var _raycast_root_path : NodePath
@onready var _raycast_root := get_node(_raycast_root_path) as Node2D
@export var _audio_path : NodePath
@onready var _audio := get_node(_audio_path) as AudioStreamPlayer2D
@onready var _physics_query := PhysicsShapeQueryParameters2D.new()
signal on_fell()
signal on_landed()
# State machine variables:
var _next_state_on_animation_end := _no_op
var _has_animation_frame_changed : bool
var _has_animation_ended : bool
var joystick : Vector2 : get = get_joystick, set = set_joystick
var _is_floored : bool
var is_fleeing : bool : get = get_fleeing, set = set_fleeing
func _ready():
super._ready()
_sprite.play()
_sprite.connect("frame_changed",Callable(self,"_frame_changed"))
_sprite.connect("animation_finished",Callable(self,"_animation_finished"))
set_state(_idle_state)
# Special settings for certain animations:
var frames = _sprite.sprite_frames
frames.set_animation_loop("land", false)
frames.set_animation_loop("throw", false)
# TODOConverter40 looks that snap in Godot 4.0 is float, not vector like in Godot 3 - previous value `Vector2.DOWN * _snap_length`
_body.set_up_direction(Vector2.UP)
_body.set_floor_stop_on_slope_enabled(true)
_body.set_max_slides(4)
_body.set_floor_max_angle(1.0)
_body.floor_snap_length = _snap_length
func set_joystick(value : Vector2) -> void:
joystick = value
if abs(value.x) > 0.01 and _current_state != _walk_state:
set_state(_walk_state)
func get_joystick() -> Vector2:
return joystick
func set_fleeing(value : bool) -> void:
is_fleeing = value
# FIXME: This is a bit scary, but where else could it go?
if _current_state == _walk_state:
_sprite.animation = "run" if is_fleeing else "walk"
func get_fleeing() -> bool:
return is_fleeing
# Call these functions to change the state from outside of the class instead of set_state():
func go_idle() -> void:
set_state(_idle_state)
func go_surprise() -> void:
set_fleeing(true)
set_state(_surprise_state)
##### STATE MACHINE:
func advance(delta):
# joystick = Vector2.ZERO
_is_floored = _body.is_on_floor()
super.advance(delta)
_has_animation_ended = false
_has_animation_frame_changed = false
# STATES:
func _idle_state() -> void:
if _is_new_state:
_sprite.play("idle")
_stand_motion()
if not _is_floored:
set_state(_fall_state)
return
func _walk_state() -> void:
if _is_new_state:
_sprite.play("run" if is_fleeing else "walk")
_correct_flip()
_walk_motion()
if not _is_floored:
set_state(_fall_state)
return
if abs(joystick.x) <= 0.01:
set_state(_idle_state)
return
func _jump_state() -> void:
if _is_new_state:
_sprite.play("surprise") # FIXME: Dogman doesn't have a jump animation yet!
_next_state_on_animation_end = _fall_state
_velocity.y -= _jump_impulse
log_message("Jumped!")
func _fall_state() -> void:
if _is_new_state:
_sprite.play("fall")
on_fell.emit()
_correct_flip()
_fall_motion()
if _is_floored:
set_state(_land_state)
return
func _land_state() -> void:
if _is_new_state:
_sprite.play("land")
_next_state_on_animation_end = _idle_state
on_landed.emit()
_stand_motion()
func _surprise_state() -> void:
if _is_new_state:
_sprite.play("surprise")
_next_state_on_animation_end = _walk_state
_stand_motion()
# STATE MACHINE UTILITY FUNCTIONS:
func _correct_flip() -> void:
if joystick.x < -0.01:
_sprite.flip_h = true
_raycast_root.scale.x = -1.0
elif joystick.x > 0.01:
_sprite.flip_h = false
_raycast_root.scale.x = 1.0
func _face_point(global_point : Vector2) -> void:
_sprite.flip_h = _body.global_position.x > global_point.x
func _stand_motion() -> void:
_velocity += _gravity_vector * (_gravity * _gravity_multiplier)
_velocity.x = 0.0
# Alternative with heavy damping. If you make it too light, it slides constantly.
# _velocity.x = move_toward(_velocity.x, 0.0, _max_speed * 20.0 * _delta)
_body.set_velocity(_velocity)
_body.move_and_slide()
_velocity = _body.velocity
func _walk_motion() -> void:
_velocity += joystick * _speed
_velocity += _gravity_vector * (_gravity * _gravity_multiplier)
_velocity.x = clamp(_velocity.x, -_max_speed, _max_speed)
# _velocity.y = clamp(_velocity.y, -_max_speed, _max_speed)
# move_and_slide multiplies by delta internally, so don't do it here!
_body.set_velocity(_velocity)
_body.move_and_slide()
_velocity = _body.velocity
func _fall_motion() -> void:
var speed_scaled := _speed * 0.5
# _velocity.x += joystick.x * speed_scaled
_velocity.y += joystick.y * (1.0 if joystick.y > 0.0 else 2.0) * speed_scaled
_velocity += _gravity_vector * (_gravity * _gravity_multiplier) # Gravity
_velocity.x += (-1.0 if _sprite.flip_h else 1.0) * speed_scaled * 0.2 # Drift in direction of travel
_body.set_velocity(_velocity)
_velocity = _body.velocity
_velocity *= pow(0.9, _delta * Engine.physics_ticks_per_second)
# SIGNALS:
func _frame_changed():
_has_animation_frame_changed = true
func _animation_finished():
# print("Animation finished (state machine): ", _current_state)
_has_animation_ended = true
if _next_state_on_animation_end != _no_op:
set_state(_next_state_on_animation_end)
_next_state_on_animation_end = _no_op
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment