Created
January 5, 2024 19:21
-
-
Save N-Carter/6b70333c01b1d3206b0d2482453f01a0 to your computer and use it in GitHub Desktop.
A state machine for walking creatures
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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