Skip to content

Chicken Movement

Movement

When playing as the chicken, the player experiences the game in third person. Movement is camera-relative, meaning the chicken will move in the direction the camera is facing. The speed and behavior of the chicken changes based on its current state.

Controls

Keyboard

  • WASD: Basic movement (forward, backward, left, right)
  • Mouse: Controlling the camera, and in turn player movement direction
  • Space: Jump/Glide
  • Shift: Sprint
  • CTRL: Dash
  • F: Switch Weapons

Controller (Xbox buttons for reference)

  • Left Joystick: Basic movement (forward, backward, left, right)
  • Right Joystick: Controlling the camera, and in turn player movement direction
  • A: Jump/Glide
  • L3/Left Click: Sprint
  • B: Dash
  • X: Switch Weapons

States Overview

The chicken player implements a state machine to manage different movement behaviors. Each state has its own logic for handling input, physics, and animations, with basic movement logic implemented in the parent state base_player_state.gd.

Available States

  • IDLE_STATE: Default state when no movement input is detected
  • WALK_STATE: Basic movement at normal speed
  • SPRINT_STATE: Faster movement with stamina consumption
  • JUMP_STATE: Vertical movement when jumping from the ground
  • GLIDE_STATE: Slow descent when falling, with horizontal movement control
  • DASH_STATE: Quick burst of speed in a specific direction
  • HURT_STATE: Temporary state when the player takes damage
  • FALL_STATE: When the player is falling without gliding
  • DEATH_STATE: When the player is dead, shows the death screen

State Decision

Fowl Play is a 3D roguelike arena fighter where players control a chicken in underground fight rings. The movement states were chosen to support the core gameplay loop of arena combat, environmental hazard navigation, and strategic resource management:

  • IDLE_STATE: Serves as a clean transition point between other movement states. Allows the player to stand still for a moment, potentially hiding behind cover within the arena.

  • WALK_STATE: Offers predictable, controlled navigation needed for exploring the fight arena, approaching enemies carefully, and avoiding environmental hazards as mentioned in the pitch document.

  • SPRINT_STATE: Implements the risk-reward philosophy central to the game’s design. By draining stamina, sprinting creates decisions about when to use limited resources for quicker repositioning or escaping threats.

  • JUMP_STATE & GLIDE_STATE: Support vertical exploration and combat strategy in the multi-level arena environment. These states allow players to gain tactical advantages over the various enemy types and navigate over the hazards.

  • DASH_STATE: Provides critical evasive capabilities needed for the fast-paced combat encounters against progressively stronger enemies. The dash’s stamina cost aligns with the game’s resource management mechanics, forcing players to make quick strategic decisions during combat.

  • HURT_STATE: Reflects the punishing nature of underground fighting rings by interrupting player movement flow when taking damage, creating consequences for poor positioning or timing.

  • FALL_STATE: Ensures precise air control mechanics, and serves as a clean transition point between arial and grounded movement states.

Player State Machine

## State machine for the player movement system.
##
## This script manages the different states of the player movement system, for the chicken player.
extends Node
@export var starting_state: BasePlayerState
@export var player: ChickenPlayer
var states: Dictionary[PlayerEnums.PlayerStates, BasePlayerState] = {}
@onready var current_state: BasePlayerState = _get_initial_state()
func _ready() -> void:
if player == null:
push_error(owner.name + ": No player reference set")
# Connect the signal to the transition function
SignalManager.player_transition_state.connect(_transition_to_next_state)
# We wait for the owner to be ready to guarantee all the data and nodes are available.
await owner.ready
# Get all states in the scene tree
for state_node: BasePlayerState in get_children():
states[state_node.STATE_TYPE] = state_node
state_node.setup(player)
print(states)
if current_state:
current_state.enter(current_state.STATE_TYPE)
func _input(event: InputEvent) -> void:
if current_state == null:
push_error(owner.name + ": No state set.")
return
current_state.input(event)
func _process(delta: float) -> void:
if current_state == null:
push_error(owner.name + ": No state set.")
return
current_state.process(delta)
func _physics_process(delta: float) -> void:
if current_state == null:
push_error(owner.name + ": No state set.")
return
current_state.physics_process(delta)
func _transition_to_next_state(target_state: PlayerEnums.PlayerStates, information: Dictionary = {}) -> void:
var previous_state := current_state
previous_state.exit()
current_state = states.get(target_state)
if current_state == null:
push_error(owner.name + ": Trying to transition to state " + str(target_state) + " but it does not exist. Falling back to: " + str(previous_state))
current_state = previous_state
current_state.enter(previous_state.STATE_TYPE, information)
func _get_initial_state() -> BasePlayerState:
return starting_state if starting_state != null else get_child(0)

State Machine Implementation

The State Pattern

The player movement system uses the State pattern to organize different movement behaviors. This allows each state to handle its own physics, input, and transitions independently.

Base Player State

base_player_state.gd provides common functionality shared by all the player states. It extends from base_state.gd, and provides additional typed setup and enter methods. The setup method passes in a reference to the player, so the states can apply movement to the player. The enter method passes in the previous state, and some optional additional information.

class_name BasePlayerState
extends BaseState
@export var STATE_TYPE: PlayerEnums.PlayerStates
var player: ChickenPlayer
var movement_speed: float = 0.0
var previous_state: PlayerEnums.PlayerStates
## Called once to set the player reference
##
## Parameters:
## _player: The player reference to set.
func setup(_player: ChickenPlayer) -> void:
if _player == null:
push_error(owner.name + ": No player reference set" + str(STATE_TYPE))
player = _player
## Called once when entering the state
##
## Parameters:
## _previous_state: The state that was active before this one.
func enter(_previous_state: PlayerEnums.PlayerStates, _information: Dictionary = {}) -> void:
previous_state = _previous_state
# Providing default player movement
func physics_process(_delta: float) -> void:
if movement_speed == 0.0:
push_error("BasePlayerState: movement_speed is null. Please set it in the child class before calling super.")
var direction: Vector3 = get_player_direction( get_player_input_dir())
# Apply horizontal movement
player.velocity.x = direction.x * movement_speed
player.velocity.z = direction.z * movement_speed
func get_player_input_dir() -> Vector2:
# Get 3D movement input
return Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
func get_player_direction(input_dir: Vector2) -> Vector3:
# Calculate camera-relative movement direction
var player_basis: Basis = player.global_basis
return (player_basis.x * input_dir.x + player_basis.z * input_dir.y).normalized()

Example Dash State

dash_state.gd provides a quick burst of movement in a specific direction, consuming stamina. The dash duration and cooldown are set on a timer, so the player cannot infinitely dash. After the initial burst, dash movement is added in the physics_process method until the dash timer runs out.

################################################################################
## State handling player dash movement.
##
## Applies instant burst movement in facing direction with stamina cost.
################################################################################
extends BasePlayerMovementState
var _stamina_cost: int
var _is_dashing: bool = false
var _dash_direction: Vector3
@onready var dash_duration_timer: Timer = $DashDurationTimer
@onready var dash_cooldown_timer: Timer = $DashCooldownTimer
func enter(prev_state: BasePlayerMovementState, information: Dictionary = {}) -> void:
super(prev_state)
_stamina_cost = movement_component.dash_stamina_cost
# Handle state transitions
if not movement_component.dash_available or player.stats.current_stamina < _stamina_cost:
print("Dash available: ", movement_component.dash_available)
SignalManager.player_transition_state.emit(previous_state.state_type, information)
return
player.stats.drain_stamina(_stamina_cost)
movement_component.dash_available = false
_is_dashing = true
_dash_direction = get_player_direction()
if _dash_direction == Vector3.ZERO:
_dash_direction = -player.global_basis.z # Default forward direction
animation_tree.set("parameters/OneShot/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
dash_duration_timer.start()
dash_cooldown_timer.start()
func physics_process(delta: float) -> void:
apply_gravity(delta)
if _is_dashing:
player.velocity = _dash_direction * player.stats.calculate_speed(movement_component.dash_speed_factor)
player.move_and_slide()
return
# Handle state transitions
if get_jump_velocity() > 0 and movement_component.jump_available:
SignalManager.player_transition_state.emit(PlayerEnums.PlayerStates.JUMP_STATE, {})
if not player.is_on_floor():
SignalManager.player_transition_state.emit(PlayerEnums.PlayerStates.FALL_STATE, {})
return
var direction: Vector3 = get_player_direction()
if direction == Vector3.ZERO:
SignalManager.player_transition_state.emit(PlayerEnums.PlayerStates.IDLE_STATE, {})
return
if is_sprinting() and player.stats.current_stamina > 0:
SignalManager.player_transition_state.emit(PlayerEnums.PlayerStates.SPRINT_STATE, {})
return
SignalManager.player_transition_state.emit(PlayerEnums.PlayerStates.WALK_STATE, {})
player.move_and_slide()
func _on_dash_duration_timer_timeout():
_is_dashing = false
func _on_dash_cooldown_timer_timeout():
print("Dash available: ", movement_component.dash_available)
movement_component.dash_available = true
Dash State Enter Method

The enter method in the dash state demonstrates how state parameters are used to manage game mechanics:

Parameters Usage
  • _previous_state: Tracks which state the player was in before dashing, essential for returning to the appropriate state after the dash completes.
  • information dictionary: Used to pass data between states and prevent infinite dash loops.
Information Dictionary

The information dictionary allows states to communicate. In the dash state it specifically:

  1. Checks for repeated dashes: When information.get("dashed", false) is true, the player has already performed a dash in this movement sequence.
  2. Communicates dash history: Sets information.set("dashed", true) when transitioning back to prevent dash chaining.
Preventing Infinite Dashing

The dash state is designed to prevent infinite or chain dashing for several important reasons:

  1. Game Balance: The dash allows players to strategically evade attacks and hazards. Infinite dashing would allow the player to bypass all challenges, breaking the core gameplay loop and difficulty balance.

  2. Resource Management: The stamina cost creates decisions about when to use the dash ability. Without limitations, this strategic element would be lost.

  3. Skill Expression: The cooldown system encourages players to time their dashes effectively rather than spamming the ability, creating a higher skill ceiling for players.

The three main constraints implemented to prevent infinite dashing are:

  • The _dash_available flag and cooldown timer
  • Stamina consumption requirement
  • The “dashed” flag in the information dictionary to prevent immediate re-entry into dash state

Key Components

Movement Calculation

Movement is calculated based on player input and camera orientation:

  1. Input direction is captured using get_player_input_dir().
  2. This direction is transformed to world space using get_player_direction().
  3. The resulting vector is applied to player velocity, scaled by the state’s movement speed.

State Transitions

State transitions are triggered by the SignalManager.player_transition_state signal. When a state determines a transition should occur (e.g., player presses jump), it emits this signal with the target state and optional information.

Implementation Notes

  • Each state should set an appropriate movement_speed value.
  • Override physics_process() in derived states for custom movement behavior.
  • Use enter() and exit() to handle state-specific setup and cleanup.