movement
Build your own movement features on a virtual keyboard: a stick with a direction, plus jump, duck and walk, applied where the player's own keys are.
On this page
movement is a virtual keyboard for a script. It holds one intent at a time: a stick, and the jump, duck and walk buttons. The game applies the intent exactly where the player's own keys are applied, so the anti-aim, the jump features, the edge bug, the ladder helper and the ragebot treat it as the player's input. Use it for auto-peek steps, stop and slide helpers, crouch and hop helpers, and for steering the built-in strafe along a path.
The world is read with the entity and game modules. movement itself returns nothing but true or false.
The intent
An intent is a table passed to movement.intent. There are two ways to describe the stick.
- Stick axes.
forwardandsideare numbers from -1 to 1. A positiveforwardis ahead and a positivesideis to the right. Values are clamped and the stick is cut to a length of 1. - Heading.
headingis a world yaw in degrees, andspeedis 0 to 1. The stick is steered to that heading relative to the view of the command it is applied to, so it stays correct while the view turns. Do not combine a heading withforwardorside.
| field | type | meaning |
|---|---|---|
forward, side | number | the stick axes, -1 to 1, default 0 |
heading | number | a world yaw in degrees. Any finite number is accepted and wrapped. |
speed | number | 0 to 1 with a heading, default 1 |
jump, duck, walk | boolean | the buttons, default false |
mode | string | "add" (default), "set" or "idle", see below |
ttl | number | how many ticks the intent lives, 1 to 6, default 3 |
| mode | what happens |
|---|---|
"add" | your stick is added to the player's, each axis clamped |
"set" | your stick replaces the player's, zero included, so the player holds still when you send zero |
"idle" | the whole intent applies only while the player's own stick is zero |
ttl counts ticks of 16 ms, so an intent lives 16 to 96 ms. A fraction is cut off and other values are clamped.
How the calls behave
- Refreshing. Call
movement.intentevery frame you want the keys. A script that stops calling, stops with an error, is unloaded, or sees the level change hands the keys back within 96 ms. - Allowance. A script may have 4 accepted
movement.intentcalls per frame. A refused call does not use the allowance. The intent table may hold at most 16 entries. - Errors. An argument that is not a table, a field that does not exist, a value of the wrong type, an unknown
mode, or aheadingmixed withforwardorsideraises an error with a fixed text. - Several scripts. The newest call wins. When it lapses, or its script goes, the newest intent of the others that is still alive takes over.
- The ragebot. The ragebot keeps its priority: it moves the player itself when it stops or strafes. The player's own keys return as soon as the ttl runs out.
- Timing. The intent is published on the render frame and applied on the next game command. That is about one frame (3 ms at 300 fps, 17 ms at 60) plus up to one tick behind what the script saw. It suits steps, stops, slides, crouch and hop helpers and steering the built-in strafe. It is not tick-exact.
Cost 40 instructions for movement.intent, 10 for movement.release.
Functions
movement.intent
movement.intent{ forward =, side =, heading =, speed =, jump =, duck =, walk =, mode =, ttl = } -> boolean
Returns true when the intent was taken. It returns false for a number that is not finite, and after 4 accepted calls in one frame.
events.on_paint(function()
movement.intent{ forward = 1, ttl = 2 }
end)
movement.release
movement.release()
Gives the keys back now, without waiting for the ttl. It returns nothing.
Example: auto-peek
Walk one step out while a key is held, and walk back to the start when it is released. The script remembers where the player stood, then steers home with a heading until it is within 4 units.
local key = ui.keybind("peek key", "hold")
local home
events.on_paint(function()
local me = entity.local_player()
if not me or not me:is_alive() then return end
if key:get() then
if not home then
local x, y = me:origin()
home = { x = x, y = y }
end
movement.intent{ forward = 1, ttl = 2 }
elseif home then
local x, y = me:origin()
local dx, dy = home.x - x, home.y - y
if dx * dx + dy * dy < 16 then
home = nil
movement.release()
return
end
movement.intent{ heading = math.deg(math.atan2(dy, dx)), mode = "set", ttl = 2 }
end
end)
Example: slide stop
When the player lets go of the keys, push against the sliding direction. The "idle" mode keeps the helper out of the way while the player steers.
events.on_paint(function()
local me = entity.local_player()
if not me or not me:is_alive() then return end
local vx, vy = me:velocity()
if not vx or vx * vx + vy * vy < 100 then return end
movement.intent{ heading = math.deg(math.atan2(-vy, -vx)), speed = 0.6, mode = "idle", ttl = 2 }
end)
Example: steer along a path
While a key is held, add a stick at a heading you choose to whatever the player is already pressing.
local path = ui.slider("path yaw", -180, 180, 90)
local on = ui.keybind("steer", "hold")
events.on_paint(function()
if on:get() then
movement.intent{ heading = path:get(), mode = "add", ttl = 2 }
end
end)