4. Character Controller & Movement Systems

Defold Platformer Framework
# 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.