Skip to content

Instantly share code, notes, and snippets.

@glinesbdev
Created June 17, 2026 19:46
Show Gist options
  • Select an option

  • Save glinesbdev/71ba4f139c0e86463f0a3eac09a5ecf3 to your computer and use it in GitHub Desktop.

Select an option

Save glinesbdev/71ba4f139c0e86463f0a3eac09a5ecf3 to your computer and use it in GitHub Desktop.
Godot Player State Machine
class_name PlayerAttackState
extends PlayerState
func enter(_opts := {}) -> void:
var player: Player = entity as Player
if player.equipped_weapon == null or player.weapon_hitbox_scene == null:
return
var hitbox := player.weapon_hitbox_scene.instantiate() as WeaponHitbox
get_tree().current_scene.add_child(hitbox)
hitbox.global_position = player.weapon_socket.global_position
hitbox.configure(player.equipped_weapon, player.combatant)
# TODO: Transition back to previous state when process is done
# i.e. animation, hit connect, etc.
machine.transition_to_previous()
class_name PlayerDashState
extends PlayerState
@export var dash_speed: float = 16.0
@export var dash_duration: float = 0.14
@export var turn_speed: float = 30.0
var dash_time_left: float = 0.0
var dash_direction: Vector3 = Vector3.ZERO
func enter(opts := {}) -> void:
dash_time_left = dash_duration
dash_direction = opts.get("direction", Vector3.ZERO)
if dash_direction == Vector3.ZERO:
dash_direction = -(entity as Player).global_transform.basis.z
dash_direction.y = 0.0
dash_direction = dash_direction.normalized()
_turn_visual(dash_direction, turn_speed, 0.0)
func physics_process(delta: float, _input: Dictionary) -> void:
_set_velocity(Vector3(dash_direction.x * dash_speed, dash_direction.y, dash_direction.z * dash_speed))
dash_time_left -= delta
if dash_time_left <= 0.0:
transitioned.emit(IDLE_STATE, {})
class_name PlayerDiedState
extends PlayerState
func enter(_opts := {}) -> void:
machine = null
(entity as Player).queue_free()
func physics_process(_delta: float, _input: Dictionary) -> void:
_set_velocity(Vector3.ZERO)
class_name PlayerIdleState
extends PlayerState
func enter(_opts := {}) -> void:
var player := (entity as Player)
_set_velocity(Vector3(0.0, player.velocity.y, 0.0))
func physics_process(_delta: float, input: Dictionary) -> void:
var dir := _get_movement_direction(input)
if dir != Vector3.ZERO:
transitioned.emit(WALK_STATE, {})
return
if input["run_pressed"]:
transitioned.emit(RUN_STATE, {})
class_name Player
extends CharacterBody3D
@export_group("References")
@export var equipped_weapon: WeaponData
@export var weapon_hitbox_scene: PackedScene
@export var camera_path: NodePath
@export_group("Movement")
@export var speed: float = 5.0
@export var turn_speed: float = 30.0
@export var acceleration_rate: float = 18.0
@export var deceleration_rate: float = 50.0
@onready var combatant: Combatant = $Combatant
@onready var weapon_socket: Node3D = $WeaponSocket
@onready var camera: Camera3D = get_node(camera_path) as Camera3D
@onready var max_speed: float = speed * 2
@onready var visual_root: Node3D = $VisualRoot
@onready var state_machine: PlayerStateMachine = $StateMachine
func _ready() -> void:
combatant.health.died.connect(_on_died)
func _input(event: InputEvent) -> void:
if event.is_action_pressed(PlayerInputs.ATTACK):
state_machine.transition_to("AttackState", {})
func _on_died() -> void:
state_machine.transition_to("DiedState", {})
class_name PlayerState
extends State
const IDLE_STATE = "IdleState"
const WALK_STATE = "WalkState"
const RUN_STATE = "RunState"
const DASH_STATE = "DashState"
const ATTACK_STATE = "AttackState"
const DIED_STATE = "DiedState"
func _set_velocity(vel: Vector3) -> void:
(entity as Player).velocity = vel
func _get_movement_direction(input: Dictionary) -> Vector3:
var input_vector: Vector2 = input["move"]
var cam: Camera3D = (entity as Player).get_viewport().get_camera_3d() as Camera3D
if cam == null:
return Vector3.ZERO
var basis := cam.global_transform.basis
var right := basis.x
var forward := basis.z
right.y = 0.0
forward.y = 0.0
right = right.normalized()
forward = forward.normalized()
var dir := right * input_vector.x + forward * input_vector.y
if dir.length_squared() > 0.0:
dir = dir.normalized()
return dir
func _turn_visual(direction: Vector3, turn_speed: float, delta: float) -> void:
var visual_root := (entity as Player).get_node_or_null("VisualRoot") as Node3D
if visual_root == null: return
var target_y := atan2(direction.x, direction.z)
visual_root.rotation.y = lerp_angle(visual_root.rotation.y, target_y, 1.0 - exp(-turn_speed * delta))
class_name PlayerStateMachine
extends StateMachine
func _physics_process(_delta: float) -> void:
super._physics_process(_delta)
(entity as Player).move_and_slide()
class_name PlayerRunState
extends PlayerState
@export var run_speed: float = 10.0
@export var turn_speed: float = 30.0
func physics_process(delta: float, input: Dictionary) -> void:
var dir := _get_movement_direction(input)
if dir == Vector3.ZERO:
transitioned.emit(IDLE_STATE, {})
return
_set_velocity(Vector3(dir.x * run_speed, dir.y, dir.z * run_speed))
_turn_visual(dir, turn_speed, delta)
if not input["run_pressed"]:
transitioned.emit(WALK_STATE, {})
class_name State
extends Node
signal transitioned(new_state: String, opts: Dictionary)
var entity: Object
var machine: StateMachine
func enter(_opts := {}) -> void:
pass
func exit() -> void:
pass
func process(_delta: float) -> void:
pass
func physics_process(_delta: float, _input: Dictionary) -> void:
pass
func handle_input(_event: InputEvent) -> void:
pass
class_name StateMachine
extends Node
@export var initial_state: NodePath
var current_state: State
var previous_state: State
var states: Dictionary = {}
var player_input_context: Dictionary = {}
var entity: Object
func _ready() -> void:
entity = get_parent()
if not entity: return
for child in get_children():
if child is State:
var state := child as State
state.entity = entity
state.machine = self
state.transitioned.connect(_on_state_transitioned)
states[state.name] = state
current_state = get_node(initial_state) as State
current_state.enter()
func _process(delta: float) -> void:
if current_state: current_state.process(delta)
func _physics_process(delta: float) -> void:
player_input_context = PlayerInputs.build_player_input_context()
if current_state: current_state.physics_process(delta, player_input_context)
func _unhandled_input(event: InputEvent) -> void:
if current_state: current_state.handle_input(event)
func transition_to(state_name: String, opts := {}) -> void:
if not states.has(state_name): return
if current_state: current_state.exit()
previous_state = current_state
current_state = states[state_name]
current_state.enter(opts)
func transition_to_previous() -> void:
current_state = previous_state
previous_state = null
func _on_state_transitioned(new_state: String, opts: Dictionary) -> void:
transition_to(new_state, opts)
class_name PlayerWalkState
extends PlayerState
@export var walk_speed: float = 5.0
@export var turn_speed: float = 30.0
func physics_process(delta: float, input: Dictionary) -> void:
var dir := _get_movement_direction(input)
if dir == Vector3.ZERO:
transitioned.emit(IDLE_STATE, {})
return
_set_velocity(Vector3(dir.x * walk_speed, dir.y, dir.z * walk_speed))
_turn_visual(dir, turn_speed, delta)
if input["run_pressed"]:
transitioned.emit(RUN_STATE, {})
if input["run_just_released"]:
transitioned.emit(DASH_STATE, { "direction": dir })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment