Movement, collision and input#
This page covers how a body walks, what it collides with, and how you read the keys a player is
pressing. Movement lives on the Humanoid inside every character. Collision is decided by collision
groups you create and name. Input is read through InputAction instances on the client.
Humanoid#
Every character has a humanoid, and it holds the numbers that decide how that body moves, all in
metres and seconds. A player's body moves with these numbers under the player's own control; a body
a place owns is walked by the engine with the same numbers.
local h = body.humanoid
h.walkSpeed = 4.317 h.sprintMultiplier = 1.3 h.sneakMultiplier = 0.3
h.jumpPower = 8.4 h.gravityScale = 1
h.groundAcceleration = 52.2 h.groundDeceleration = 52.2
h.airSpeed = 4.444 h.airAcceleration = 3.64
h.airDrag = 0.1516 h.fallDrag = 0.6676
h.stepHeight = 0.6 h.slopeLimit = 45 h.followSlopes = true
h.coyoteTime = 0.15 h.jumpBuffer = 0.15
h.health = 20 h.maxHealth = 20 h.healthRegen = 0The defaults are Minecraft's own movement converted to metres and seconds, so a place that changes nothing feels like the game. Change one of them and the body moves differently from the next tick on, for every player who can see it.
walkSpeedis how many metres the body covers in a second on the ground.sprintMultiplierandsneakMultiplierscale it while the player sprints or sneaks.jumpPoweris the speed the body leaves the ground at, not a height. How high it gets falls out of that speed and the gravity pulling it back down.groundAccelerationandgroundDecelerationare how hard the body starts and stops on the ground, in metres per second squared. They work like the game's own friction: every tick takes the same share of the speed still to gain or lose, so a start or a stop is sharp and then settles. At the default, about half the walking speed goes in the first tick after the keys are let go.gravityScaleis this body's own share of the pull, on top of the place's. The two multiply: atgame.gravity = 16a humanoid atgravityScale = 1falls half as hard as it does by default, and one atgravityScale = 2falls as hard as ever. Change one body withgravityScale, and everything at once withgame.gravity.stepHeightis how tall a ledge the body walks up without jumping, andslopeLimitis how steep a slope it walks up at all, in degrees.healthandmaxHealthare the body's life. Health reaching 0 puts the body in thedeadstate and firesdied.
State#
state says what the body is doing right now. It is one of standing, running, jumping,
falling, swimming, climbing, flying, seated, platformStanding, dead.
humanoid:getState() reads the same thing.
A player's body gets its state from the player every tick on the server: on a ladder or vine is
climbing, in water off the ground is swimming, creative flight is flying, in the air is
jumping on the way up and falling on the way down, and on the ground is running or standing.
A body without a player is running while it walks, jumping and falling through a hop,
climbing while a path takes it up or down a ladder, and standing otherwise.
Writing state yourself lasts until the next tick, when the engine works it out again and
overwrites you. To put a body in a state and have it stay there, use sit, platformStand or
changeState below.
| Property | |
|---|---|
moveDirection |
read only: the flat unit direction the body moves in, zero when still |
floorMaterial |
read only: the block under the feet, like "minecraft:grass_block", or "air" off the ground |
sit |
sits the body down |
platformStand |
the body stands limp and cannot walk or jump |
autoRotate |
turns a body without a player to face where it walks; on by default |
healthRegen |
health given back each second while alive and below maxHealth; 0 by default |
A player's moveDirection comes from how their body really moves. A body without a player keeps the
direction move gave it, and while it walks to a point with walkTo or moveTo it is the way it
walks.
floorMaterial only sees blocks. Standing on a part reads the block under the part, or "air".
With autoRotate off, a body without a player keeps facing where it was while it walks. It does
nothing for a player's body yet.
A player that sits loses the move control but can still jump, and jumping stands them up. Their
body shows the sitting pose. A body without a player stands up and hops when jump is set, but its
legs do not fold on their own; set body.riding for that. platformStand takes the move and jump
controls from a player, and stops a body without a player walking and jumping, until you set it back
to false.
body.humanoid.sit = true
body.humanoid.seated:connect(function(active)
print(active and "sat down" or "stood up")
end)Events#
Every one of these is a signal you connect a function to, and it runs until you disconnect it or the body is destroyed:
| Event | Fires with | |
|---|---|---|
stateChanged |
the humanoid | whenever state changes |
died |
the humanoid | when the state becomes dead |
healthChanged |
the humanoid | whenever health changes |
arrived |
the humanoid | when a body reaches a point it was sent to; never while move steers it |
damaged |
the amount | after takeDamage took health away |
running |
the speed | the body starts running or changes speed by half a metre a second; 0 when it stops |
jumping |
true or false | entering and leaving jumping |
freeFalling |
true or false | entering and leaving falling |
swimming |
a speed | entering swimming with walkSpeed, leaving with 0 |
climbing |
a speed | entering climbing with walkSpeed, leaving with 0 |
seated |
true or false | entering and leaving seated |
platformStanding |
true or false | entering and leaving platformStanding |
A body without a player runs at its walkSpeed. It climbs while a path takes it up
or down a ladder, and only swims when changeState puts it there.
local h = body.humanoid
h.freeFalling:connect(function(active)
if not active then print("landed on", h.floorMaterial) end
end)
h.damaged:connect(function(amount)
print(body.name, "took", amount)
end)freeFalling fires with true when the body enters falling and with false when it leaves, so the
false is the landing. Reading floorMaterial at that moment tells you what it landed on, which is
enough to pick a footstep sound or hurt a body that hit stone.
Methods#
Every method except getState and getStateEnabled runs in a server Script. A LocalScript
calling one gets an error.
local h = body.humanoid
h:move(vec3(1, 0, 0)) -- keep walking along +X
h:move(vec3(0, 0, -1), true) -- keep walking forward, the way the player faces
h:move(vec3(0, 0, 0)) -- stop
h:moveTo(vec3(20, 65, 4)) -- straight at the point
h:takeDamage(5)
h:changeState("seated")
h:setStateEnabled("jumping", false)
print(h:getState(), h:getStateEnabled("jumping"))move(direction, relativeToCamera?)walks the body in a direction until you call it again with a zero vector. Only the flat part of the direction counts. WithrelativeToCamerathe direction is turned by where the player faces: -Z is forward, +X is right. A player is steered by their own client, and pressing a movement key takes over while it is held. A body without a player walks that way smoothly on its own, andarriveddoes not fire while it does.moveTo(point)walks in a straight line to the point and firesarrived. It does not go around anything;body:walkTo(point)finds a path.takeDamage(amount)takes health away and firesdamaged. It does nothing while the body has aForceFieldinside it. Writinghealthyourself ignores the force field.changeState(state)does what the state means:deadsets health to 0,jumpingjumps,seatedsetssit,platformStandingsetsplatformStand. Any other state only applies to a body without a player, and only until its next tick. A state that is turned off is ignored.setStateEnabled(state, enabled)turns a state off or back on.getStateEnabled(state)reads it.
Turning a state off takes the thing it stands for away from the body:
| State | |
|---|---|
jumping |
the body cannot jump; a player loses the jump control |
running |
the body cannot walk or move; a player loses the move control |
seated |
the body cannot sit, and a sitting body stands up |
dead |
health stays above 0, so the body cannot die |
Turning off any other state is kept but changes nothing about how the body moves yet. The one thing
it does is on a player's body: where the player would be in a state that is off, other than
running or jumping, state reads standing instead. The switches live on the server, so
getStateEnabled on a client always says true.
-- server: a lobby where nobody can die or jump
game.players.spawned:connect(function(player)
local h = (player.character :: Character).humanoid
h:setStateEnabled("dead", false)
h:setStateEnabled("jumping", false)
end)game.players.spawned fires each time a player gets a body, so the two switches are set again after
every respawn. player.character is nil while a player has no body, and :: Character tells the
type checker that inside spawned it is always there.
Force fields#
A ForceField is an instance you put inside a body, and while it is there takeDamage does
nothing to that body.
-- server: three seconds of spawn protection
game.players.spawned:connect(function(player)
local shield = (player.character :: Character):add("ForceField")
task.delay(3, function() shield:destroy() end)
end)Destroying the force field ends the protection, so task.delay above hands the body back to damage
three seconds after it spawned. visible is kept on the class, but nothing draws a force field yet.
A force field only stops takeDamage. A script that writes health directly still kills the body
it is protecting.
Walking#
For bodies a place owns: walkTo and walking walk in a straight line; jump = true hops once.
For walking around obstacles use body:walkTo(point) and body:follow(target), see
Paths.
Pushing a body#
body:applyImpulse(change) adds to a body's velocity, in metres per second. body:setVelocity(velocity)
replaces it. Bodies have no mass, so the same push moves every body the same way.
body:applyImpulse(vec3(0, 30, 0)) -- launch up about 9 m
body:applyImpulse(body.worldCframe.lookVector * 20) -- dash forward
body:setVelocity(vec3(0, 0, 0)) -- stop deadbody.worldCframe.lookVectoris the direction the body faces in the world, so multiplying it by a speed pushes the body the way it is looking.- A server
Scriptcan push any player's body. ALocalScriptcan only push its own, which feels instant because nothing waits for the network. - Only player bodies can be pushed for now; a body without a player raises an error.
- On the ground, walking friction eats a sideways push within a few ticks. Push up a little as well to carry it through the air.
Seats#
A Seat is a part a body sits on. Walk a player's body into a free one and it sits down: the body is
held on the seat's top face, turned the way the seat faces, and it keeps its place while the seat
moves. Jumping stands it up, and so does humanoid.sit = false. A body that just stood up waits one
second before it can sit again, so it does not drop straight back into the seat it left.
-- server
local bench = world:add("Seat", {
name = "bench",
size = vec3(2, 0.4, 1),
cframe = cframe(4, 65.2, 0),
anchored = true,
})
bench.changed:connect(function(property)
if property ~= "occupant" then return end
print(bench.occupant and "someone sat down" or "the bench is free")
end)changed fires with the name of whatever property changed, so the guard clause at the top is what
makes this a seat handler rather than a handler for every property the bench has.
disabled |
nobody sits down on it, and whoever is on it stands up |
occupant |
read only: the Humanoid sitting on it, or nothing |
seat:sit(humanoid) |
sits that body on it and says whether it took |
A new Seat is two metres square, 0.4 high and blue, so one you drop in the editor already looks
like something to sit on.
Only a player's body sits down by walking in, and only a player's body is held on the seat.
seat:sit(humanoid) takes any body: it fills occupant and sets sit, but a body without a player
stays where it is, so put it on the seat yourself.
sit runs in a server Script. It refuses a disabled seat, a seat someone else is on, a dead body,
and a body with the seated state turned off. Sitting a body that is already on another seat takes
it off that one first.
occupant is filled in by the engine and stays on the server, so a LocalScript always reads
nothing.
Driving#
A VehicleSeat is a seat that reads the keys of whoever sits on it.
| Property | Default | |
|---|---|---|
maxSpeed |
25 | how fast the thing it drives should go |
torque |
10 | how hard its motors should push |
turnSpeed |
1 | how fast it should turn |
throttle |
read only: -1 back, 0 still, 1 forward | |
steer |
read only: -1 left, 0 straight, 1 right |
The engine drives nothing with maxSpeed, torque and turnSpeed. They are numbers kept with the
seat so the thing you build around it can read them. throttle and steer come off the sitting
player's own movement keys, and like occupant they are only on the server.
A seat welded or jointed to other parts is part of their assembly, and the player sitting in it owns the whole assembly. Their client simulates the car, wheels and hinges included, so it answers them at once on their own screen, and everyone else sees it move through their reports. The driver's client also holds the driver on the seat; the server stops doing it while that client simulates the seat. When they stand up, the automatic rule picks the owner again. See Assemblies and vehicles.
A car#
A chassis, a seat and four wheels on hinge motors, all in one server script:
-- server/main.luau
local world = game.world
world:add("SpawnLocation", { name = "Start", cframe = cframe(0, 64, 10) })
world:add("Part", { name = "Floor", size = vec3(120, 1, 120), cframe = cframe(0, 63.5, 0) })
local car = world:add("Model", { name = "Car" }) :: Model
local chassis = car:add("Part", {
name = "Chassis", anchored = false, size = vec3(3.6, 0.6, 5.6), color = color(0.85, 0.2, 0.2),
cframe = cframe(0, 65.3, 0),
}) :: Part
car.primaryPart = chassis
local seat = car:add("VehicleSeat", {
name = "Seat", anchored = false, size = vec3(1.4, 0.4, 1.4),
cframe = cframe(0, 65.8, 0.8),
}) :: VehicleSeat
car:add("WeldConstraint", { part0 = chassis, part1 = seat })
type Wheel = { hinge: HingeConstraint, left: boolean }
local wheels: { Wheel } = {}
for _, spot in { vec3(-2.2, 0, -1.9), vec3(2.2, 0, -1.9), vec3(-2.2, 0, 1.9), vec3(2.2, 0, 1.9) } do
local wheel = car:add("Part", {
name = "Wheel", anchored = false, shape = "cylinder", size = vec3(0.6, 1.6, 1.6),
color = color(0.1, 0.1, 0.1), cframe = cframe(spot.x, 64.8, spot.z), friction = 1.2,
}) :: Part
local hinge = car:add("HingeConstraint", {
attachment0 = chassis:add("Attachment", { cframe = cframe(spot.x, -0.5, spot.z) }),
attachment1 = wheel:add("Attachment", {}),
actuatorType = "motor", motorMaxTorque = 400,
}) :: HingeConstraint
table.insert(wheels, { hinge = hinge, left = spot.x < 0 })
end
local SPEED = 14
local TURN = 7
game.stepped:connect(function()
local forward = seat.throttle * SPEED
local turn = seat.steer * TURN
for _, wheel in wheels do
local spin = if wheel.left then forward + turn else forward - turn
wheel.hinge.angularVelocity = -spin
end
end)- The floor is anchored and everything in the car is loose. Nothing in the car is anchored, or the whole car would stay with the server.
- The weld joins the seat to the chassis, and the hinges join the wheels. That makes the chassis, the seat and the four wheels one assembly, so the player who sits down owns all six parts.
- A cylinder lies along its X axis, and a hinge turns about attachment0's X axis. The wheels are 0.6 wide along X and 1.6 round, so they roll the way the car points.
- The chassis's attachment for each wheel sits at the wheel's centre, 0.5 below the chassis's centre. The wheel's own attachment is at its centre, so the wheel turns about its axle.
- The front of a part is -Z. A wheel turning a positive angle about +X rolls towards +Z, so the speed is negated to drive forwards.
game.steppedruns every tick on the server. It readsthrottleandsteerand writes each hinge'sangularVelocity. The left wheels turn faster when you steer right, and slower when you steer left, so the car turns without steering wheels.angularVelocityis written on the server and reaches the driver's client by replication, which runs the motors. The car answers the keys one round trip after they are pressed.friction = 1.2on the wheels gives them more grip than the default of 0.5.
Walk into the seat and drive with the movement keys. Jump to get out.
The hinges are in Physics.
Collision groups#
A CollisionGroup is a named set of things that pass through each other. You create the group, then
put parts and bodies in it by name:
world:add("CollisionGroup", { name = "players", ignores = "players" })
world:add("CollisionGroup", { name = "ghosts", ignores = "walls, players" })
wall.collisionGroup = "walls"
body.collisionGroup = "ghosts"- Every part and body starts in
default. An unknown name counts asdefault. ignoresworks both ways: if either group ignores the other, they pass through each other.- A limb collides as the body it is on unless it names a group of its own.
- The level's blocks are solid to every group.
- Queries can ask as a group:
{ collisionGroup = "ghosts" }.
The first line above is the usual one: a group that ignores itself, so players walk through each other instead of shoving each other off a ledge.
Input actions#
An InputAction is a named action bound to one or more keys. You create it on the client, connect
to began and ended, and ask input:down whether it is held right now:
-- client
local interact = world:add("InputAction", { name = "interact", keys = "e, mousebutton2" })
interact.began:connect(function() end)
interact.ended:connect(function() end)
if input:down("interact") then endkeysis a list of key names separated by commas. Any one of them fires the action.beganruns on the frame the key goes down,endedon the frame it comes back up. Use those for things that happen once, such as opening a door.input:down(name)is true for as long as the key is held. Use that insiderenderSteppedfor things that happen continuously, such as leaning a camera.
A letter, digit or symbol names what the key types on the player's own keyboard, whatever its
layout: "m" is the key marked M on a qwerty, azerty, qwertz or dvorak board alike, and "é" or "<"
work where the layout has them. Switching layouts while playing is picked up within a couple of seconds.
Key names: a–z, 0–9, f1–f12, keypad0–keypad9, keypadenter, space, enter,
tab, escape, backspace, delete, insert, home, end, pageup, pagedown, capslock,
leftshift, rightshift, leftcontrol, rightcontrol, leftalt, rightalt, leftsuper,
rightsuper, up, down, left, right, mousebutton1, mousebutton2, mousebutton3, and the
gamepad names in Camera and input.
The built-in actions follow the player's own key bindings: forward, back, left, right,
jump, sneak, sprint, attack, use, pointer. A screen that is open (like the chat) or a
focused text box owns the keyboard, so actions do not fire while typing. Nor do they while the editor
is open.
Who may write what#
The server writes the tree. A client writing a replicated property is a Luau error, except:
- instances the client made itself,
- properties a class keeps to each client (like how a body is drawn),
- instances its player owns.
The server hands ownership of an instance to a player with setOwner. It takes a body or nil:
-- server: let that player's LocalScripts write the sign's properties
sign:setOwner(body)
sign:setOwner(nil) -- back to the serverWhat the owner's client writes stays on its own copy. Nothing goes up to the server or out to the other players, and the next change the server sends for that property overwrites it. Use a remote to tell the server about a change.
setOwner does not change who simulates a part, so it does not make pushing a crate feel instant.
That is network ownership: a loose part near a player is already simulated by that player's client,
and part:setNetworkOwner(player) hands one over on purpose. See
Who simulates a part.
Ownership carries down a branch: owning a cart means owning what stands on it.
