4. Character Controller & Movement Systems
# 4. Character Controller & Movement Systems
In this section, we'll explore how to implement a robust character controller and movement systems for our platformer game in Defold. A well-designed character controller is essential for creating responsive and satisfying gameplay.
## Overview of Character Movement
The character controller is responsible for:
1. Processing player input
2. Applying physics and movement calculations
3. Handling collisions
4. Managing character states (idle, running, jumping, etc.)
5. Integrating with animation systems
## Basic Movement Architecture
Our character controller will follow this general flow:
```mermaid
flowchart TD
A[Player Input] --> B[Process Input]
B --> C[Calculate Movement]
C --> D[Apply Physics]
D --> E[Handle Collisions]
E --> F[Update Character State]
F --> G[Play Animations]
G --> H[Render]
H --> A
```
## Core Components
### 1. Input Processing
First, we need to capture and process player input:
```lua
function update(self, dt)
-- Horizontal movement
local move_input = 0
if input.is_key_pressed(hash("left")) then
move_input = move_input - 1
end
if input.is_key_pressed(hash("right")) then
move_input = move_input + 1
end
-- Jump input
local jump = input.is_key_pressed(hash("jump"))
-- Process movement
process_movement(self, dt, move_input, jump)
end
```
### 2. Physics-Based Movement
For a platformer, we'll implement a physics system that handles:
- Horizontal movement with acceleration and deceleration
- Gravity and vertical movement
- Jumping mechanics
Here's a simplified implementation:
```lua
function process_movement(self, dt, move_input, jump)
-- Apply horizontal movement
if move_input ~= 0 then
-- Accelerate
self.velocity.x = self.velocity.x + move_input * self.acceleration * dt
self.velocity.x = math.min(math.max(self.velocity.x, -self.max_speed), self.max_speed)
else
-- Decelerate (friction)
local friction = self.grounded and self.ground_friction or self.air_friction
self.velocity.x = self.velocity.x * (1 - friction * dt)
end
-- Apply gravity
self.velocity.y = self.velocity.y - self.gravity * dt
-- Handle jumping
if jump and self.grounded then
self.velocity.y = self.jump_force
self.grounded = false
end
-- Apply velocity
local new_pos = go.get_position()
new_pos.x = new_pos.x + self.velocity.x * dt
new_pos.y = new_pos.y + self.velocity.y * dt
go.set_position(new_pos)
-- Handle collisions (simplified)
check_collisions(self)
end
```
## Physics Calculations
### Gravity and Jump Physics
For realistic jumping, we use basic physics equations:
$$v_y = v_0 + at$$
Where:
- $v_y$ is the current vertical velocity
- $v_0$ is the initial velocity (jump force)
- $a$ is acceleration due to gravity
- $t$ is time
The height of a jump can be calculated as:
$$h = \frac{v_0^2}{2g}$$
Where:
- $h$ is the maximum jump height
- $v_0$ is the initial velocity (jump force)
- $g$ is the gravity constant
### Variable Jump Height
To achieve variable jump heights based on button press duration:
```lua
function update(self, dt)
-- Other code...
-- For variable jump height
if not input.is_key_pressed(hash("jump")) and self.velocity.y > 0 then
-- Cut the jump short if button released
self.velocity.y = self.velocity.y * 0.5
end
end
```
## Collision Handling
We'll use Defold's built-in collision system:
```lua
function on_message(self, message_id, message, sender)
if message_id == hash("contact_point_response") then
-- Handle collision
if message.group == hash("ground") then
-- Floor collision
if message.normal.y > 0.7 then
self.grounded = true
self.velocity.y = 0
-- Adjust position to avoid sinking
go.set_position(vmath.vector3(go.get_position().x, message.position.y, 0))
-- Wall collision
elseif math.abs(message.normal.x) > 0.7 then
self.velocity.x = 0
end
end
end
end
```
## Character States
We'll implement a state machine to manage different character states:
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Running: Movement Input
Running --> Idle: No Movement
Idle --> Jumping: Jump Input
Running --> Jumping: Jump Input
Jumping --> Falling: Reached Peak
Falling --> Idle: Landed (No Movement)
Falling --> Running: Landed (With Movement)
Jumping --> Idle: Landed (No Movement)
Jumping --> Running: Landed (With Movement)
```
Implementation:
```lua
function update_character_state(self)
local prev_state = self.state
if self.grounded then
if math.abs(self.velocity.x) < 10 then
self.state = "idle"
else
self.state = "running"
end
else
if self.velocity.y > 0 then
self.state = "jumping"
else
self.state = "falling"
end
end
-- State changed - trigger animation
if prev_state ~= self.state then
play_animation(self, self.state)
end
end
```
## Advanced Mechanics
### Coyote Time
To make the platforming more forgiving, we can implement "coyote time" - a brief window after leaving a platform where the player can still jump:
```lua
function update(self, dt)
-- Update grounded state
if self.grounded then
self.coyote_timer = self.coyote_time
else
self.coyote_timer = self.coyote_timer - dt
end
-- Allow jumping during coyote time
if jump and self.coyote_timer > 0 then
self.velocity.y = self.jump_force
self.coyote_timer = 0
end
end
```
### Jump Buffer
Similarly, we can implement a jump buffer to register jump inputs slightly before landing:
```lua
function update(self, dt)
-- Check for jump button press
if input.is_key_pressed(hash("jump")) then
self.jump_buffer_timer = self.jump_buffer_time
else
self.jump_buffer_timer = self.jump_buffer_timer - dt
end
-- If we land and have a buffered jump
if self.grounded and self.jump_buffer_timer > 0 then
self.velocity.y = self.jump_force
self.jump_buffer_timer = 0
end
end
```
## Complete Character Controller Example
Here's a more complete character controller script:
```lua
-- character.script
go.property("max_speed", 250)
go.property("acceleration", 1500)
go.property("ground_friction", 8)
go.property("air_friction", 0.5)
go.property("gravity", 980)
go.property("jump_force", 500)
go.property("coyote_time", 0.1)
go.property("jump_buffer_time", 0.1)
function init(self)
-- Initialize state
self.velocity = vmath.vector3(0, 0, 0)
self.grounded = false
self.state = "idle"
self.facing_right = true
self.coyote_timer = 0
self.jump_buffer_timer = 0
-- Play initial animation
play_animation(self, "idle")
-- Acquire input focus
msg.post(".", "acquire_input_focus")
end
function update(self, dt)
-- Process input
local move_input = 0
if input.is_key_pressed(hash("left")) then
move_input = move_input - 1
end
if input.is_key_pressed(hash("right")) then
move_input = move_input + 1
end
-- Update timers
if self.grounded then
self.coyote_timer = self.coyote_time
else
self.coyote_timer = self.coyote_timer - dt
end
if input.is_key_pressed(hash("jump")) then
self.jump_buffer_timer = self.jump_buffer_time
else
self.jump_buffer_timer = self.jump_buffer_timer - dt
end
-- Process movement
process_movement(self, dt, move_input)
-- Update character state and animation
update_character_state(self)
-- Update facing direction
if move_input < 0 and self.facing_right then
self.facing_right = false
sprite.set_hflip("#sprite", true)
elseif move_input > 0 and not self.facing_right then
self.facing_right = true
sprite.set_hflip("#sprite", false)
end
end
function process_movement(self, dt, move_input)
-- Horizontal movement
if move_input ~= 0 then
self.velocity.x = self.velocity.x + move_input * self.acceleration * dt
self.velocity.x = math.min(math.max(self.velocity.x, -self.max_speed), self.max_speed)
else
local friction = self.grounded and self.ground_friction or self.air_friction
self.velocity.x = self.velocity.x * (1 - friction * dt)
-- Prevent small sliding
if math.abs(self.velocity.x) < 5 then
self.velocity.x = 0
end
end
-- Apply gravity
self.velocity.y = self.velocity.y - self.gravity * dt
-- Handle jumping
if self.jump_buffer_timer > 0 and self.coyote_timer > 0 then
self.velocity.y = self.jump_force
self.jump_buffer_timer = 0
self.coyote_timer = 0
play_sound("jump")
end
-- Variable jump height
if not input.is_key_pressed(hash("jump")) and self.velocity.y > 0 then
self.velocity.y = self.velocity.y * 0.5
end
-- Apply velocity
local new_pos = go.get_position()
new_pos.x = new_pos.x + self.velocity.x * dt
new_pos.y = new_pos.y + self.velocity.y * dt
go.set_position(new_pos)
end
function update_character_state(self)
local prev_state = self.state
if self.grounded then
if math.abs(self.velocity.x) < 10 then
self.state = "idle"
else
self.state = "running"
end
else
if self.velocity.y > 0 then
self.state = "jumping"
else
self.state = "falling"
end
end
if prev_state ~= self.state then
play_animation(self, self.state)
end
end
function play_animation(self, anim_name)
sprite.play_flipbook("#sprite", hash(anim_name))
end
function play_sound(sound_name)
sound.play("#" .. sound_name)
end
function on_message(self, message_id, message, sender)
if message_id == hash("contact_point_response") then
if message.group == hash("ground") then
-- Floor collision
if message.normal.y > 0.7 then
self.grounded = true
self.velocity.y = 0
go.set_position(vmath.vector3(go.get_position().x, message.position.y, 0))
-- Play landing sound if we were falling
if self.state == "falling" then
play_sound("land")
end
-- Wall collision
elseif math.abs(message.normal.x) > 0.7 then
self.velocity.x = 0
end
end
end
end
function on_input(self, action_id, action)
-- Additional input handling if needed
end
```
## Conclusion
A well-implemented character controller is the foundation of any good platformer. By carefully tuning the movement parameters and adding quality-of-life features like coyote time and jump buffering, you can create a character that feels responsive and satisfying to control.
In the next section, we'll explore how to integrate this character controller with animations and visual effects to bring our platformer character to life.