Learning Defold #2: Input Focus, Movement, and Pause Handling
Building Vector Relay, one small system at a time: WASD movement, input ownership, and a pause flow that clears stale intent.
In my first Defold post, I explored game objects, per-instance state, URLs, and message passing. This session gave those ideas a practical job: moving a player and deciding who gets to listen to input.
The goal sounded small. Move with WASD. Press P to pause. Press P again to resume.
The interesting part appeared when I held a movement key, opened the pause layer, and released the key while gameplay was blocked. That sequence made me think more carefully about what my script actually knew.
Input focus is a stack
A script with on_input() needs its game object to acquire input focus:
function init(self)
msg.post(".", "acquire_input_focus")
endThe most recently focused listener receives input first. Returning true consumes that action and prevents it from reaching listeners underneath.
The player stays focused underneath. While the pause controller consumes input, the player does not receive those actions.
My first intuition was closer to a queue: the player acquired focus first, so perhaps it would receive input first. Defold’s stack works in the opposite direction (LIFO) last in first out.
That makes a modal layer useful. It can temporarily own input without asking the player to release focus and later acquire it again.
A small setup makes the example repeatable
The examples below are cleaned-up versions of the session logic. They assume two separate game objects in the same collection: player, with a script component named player, and pause, with a script component named controller.
In the input binding file selected by game.project, add these key triggers:
- W →
up - A →
left - S →
down - D →
right - P →
pause
The pause controller starts without input focus. The player acquires focus during initialization. Keeping them on separate game objects matters because focus messages apply to the input-capable components on the receiving game object.
Record intent, then move in update
I split movement into two responsibilities. on_input() records the current directional intent. update() turns that intent into movement.
Here is the complete player example, player.script:
go.property("speed", 250)
local INPUT_LEFT = hash("left")
local INPUT_RIGHT = hash("right")
local INPUT_UP = hash("up")
local INPUT_DOWN = hash("down")
local INPUT_PAUSE = hash("pause")
local MESSAGE_OPEN = hash("open")
local MESSAGE_RESUME = hash("resume")
local function clear_movement(self)
self.left = false
self.right = false
self.up = false
self.down = false
end
function init(self)
self.is_paused = false
self.pause_url = msg.url("/pause#controller")
clear_movement(self)
msg.post(".", "acquire_input_focus")
end
function on_input(self, action_id, action)
if self.is_paused then
return false
end
if action_id == INPUT_PAUSE and action.pressed then
self.is_paused = true
clear_movement(self)
msg.post(self.pause_url, MESSAGE_OPEN)
return true
elseif action_id == INPUT_LEFT then
self.left = not action.released
elseif action_id == INPUT_RIGHT then
self.right = not action.released
elseif action_id == INPUT_UP then
self.up = not action.released
elseif action_id == INPUT_DOWN then
self.down = not action.released
end
return false
end
function on_message(self, message_id, message, sender)
if message_id == MESSAGE_RESUME and sender == self.pause_url then
self.is_paused = false
end
end
function update(self, dt)
if self.is_paused then
return
end
local x = (self.right and 1 or 0) - (self.left and 1 or 0)
local y = (self.up and 1 or 0) - (self.down and 1 or 0)
local direction = vmath.vector3(x, y, 0)
if vmath.length_sqr(direction) == 0 then
return
end
direction = vmath.normalize(direction)
local position = go.get_position()
go.set_position(position + direction * self.speed * dt)
end
function final(self)
msg.post(".", "release_input_focus")
endThis is a transform-based movement experiment. It does not include collision resolution or a physics controller.
The coding standard is deliberately small: four spaces, snake_case names, local helpers, cached hashes for identifiers, and mutable instance state on self. I expose speed as a script property so it can be tuned without editing the movement logic.
The address is explicit in one place. /pause#controller identifies the pause game object and its script component within the current collection's socket. If the scene structure changes, that dependency needs to change too.
Two details make movement consistent
First, I multiply the movement step by dt. At a speed of 250 units per second, an uninterrupted second of movement should cover roughly 250 units regardless of how many updates occur during it.
Second, I normalize the direction before applying speed.
W+D produces (1, 1). Normalization preserves direction while bringing its length back to 1.
Without normalization, diagonal input has a magnitude of approximately 1.414. It would move about 41% faster than a single-axis input.
Get NV’s stories in your inbox
Join Medium for free to get updates from this writer.
The zero-vector guard matters too. W+S cancels the vertical component, and A+D cancels the horizontal component. When both components are zero, there is no direction to normalize.
Pausing exposed stale movement intent
Stopping update() while paused solves only part of the problem. The player may still remember a key that is no longer held.
The failure sequence is easy to reproduce:
- Hold W. The player records
self.up = true. - Press P. The pause controller takes input focus above the player.
- Release W. The pause controller consumes the release.
- Resume. Without clearing intent, the player still remembers W as active.
Input ownership changed, so the player’s stored intent needed an explicit reset.
I clear all four movement flags when entering pause, before posting the open message. The player also ignores input while its paused flag is set.
In this example, clearing again on resume would be redundant: movement flags are already false and cannot change through the player’s input handler during pause. That reasoning depends on this implementation; another system that writes those flags could change it.
Let the pause controller reply to its sender
The player knows where to request a pause. The pause controller can use the message’s sender as the return address, so it does not need a hardcoded player URL.
Here is pause_controller.script:
local INPUT_PAUSE = hash("pause")
local MESSAGE_OPEN = hash("open")
local MESSAGE_RESUME = hash("resume")
function init(self)
self.is_open = false
self.player_url = nil
end
function on_message(self, message_id, message, sender)
if message_id ~= MESSAGE_OPEN or self.is_open then
return
end
self.player_url = sender
self.is_open = true
msg.post(".", "acquire_input_focus")
end
function on_input(self, action_id, action)
if not self.is_open then
return false
end
if action_id == INPUT_PAUSE and action.pressed then
local player_url = self.player_url
self.is_open = false
self.player_url = nil
msg.post(".", "release_input_focus")
msg.post(player_url, MESSAGE_RESUME)
end
return true
end
function final(self)
if self.is_open then
msg.post(".", "release_input_focus")
end
endThe P action used to resume is consumed too. I do not want that same action to fall through to the player and become another pause request.
Messages are posted for delivery by the engine; they are not synchronous calls into the other script. Each controller therefore owns its own state transition.
This is a modal input controller, not a complete pause menu. A visible menu can be connected to the same open and close transitions. Other gameplay scripts, physics, timers, and animations require their own pause policy; this code only stops this player’s movement.
Pressed, held, and released do different jobs
I use action.pressed for toggling pause, because that should happen once per press. For directional actions, the example records any received action that is not a release as active intent:
self.up = not action.releasedThat distinction became useful in the session notes. Releasing W during pause left the player stopped after resume. Keeping W held through pause allowed movement to return when the player received active W input again.
The flag reset does not poll the keyboard or reconstruct a physical key state. It clears the player’s old knowledge. Future input actions update that knowledge again. A key pressed and released entirely while the modal layer consumes input never reaches the player.
Test the transitions, not just the happy path
The original session notes report successful movement and pause checks. The examples here have been edited for consistency, so these are the acceptance checks to rerun against them, rather than a new runtime test report:
- W, A, S, and D each move along the expected axis.
- W+D moves diagonally at the same speed as W alone.
- Releasing W while holding D leaves only rightward movement.
- W+S and A+D cancel their respective axes.
- Holding W, pausing, releasing W, and resuming leaves the player stopped.
- Holding W through pause resumes movement when active input reaches the player again.
- Pressing and releasing a movement key entirely during pause leaves no stale intent.
- Repeated pause/resume cycles do not leak the resume press into a new pause request.
For the speed check, I would compare distance over the same elapsed time, not over the same number of frames. For pause, I would pay particular attention to key releases: they are where the stored state can diverge from what the player is doing.









