Hungrit Docs

players#

Who is in the region right now, where they are, and what they are doing.

Use it to

  • count and find the people in your world
  • react when someone arrives or leaves
  • teleport someone, or change how fast they run and how high they jump
  • play an animation on somebody
  • dress them for this world, and give their own look back later
  • save each person's progress in this world

At a glance#

What you want done, and the verb that does it.

Luau
local players = require("players")

Reads here never touch the region. They come from a mirror the server refreshes on every event and tick, so asking is cheap and never fails.

Two spellings, one call#

Every verb on this page that acts on one person can be written either way:

Luau
players:play_animation(p.id, "world/avatars/anim/dance")p:play_animation("world/avatars/anim/dance")           -- the same call

Use whichever fits what you are holding. If you already have the player, from a handler, from players:get or from a nearby loop, the second reads better and is one thing less to get wrong. If what you have is an id, use the first.

The player form is a forwarder into this module: same permission, same refusal, same everything. There is one implementation, so the two cannot drift.

It applies to play_animation, set_locomotion, set_movement, clear_movement, movement, dress, set_body, undress, teleport, worn and is_wearing, and the screen verbs (show_hud, set_camera, set_viewmodel…) have always been written on the player.

Naming an asset#

Wherever a verb here takes an asset, it takes a name, and the rule is the same one the rest of the API follows:

What you writeWhere it is looked up
"world/avatars/anim/dance"this world's library, the shelf
"dance"this object's own contents
a 64-character hashthe asset itself, for scripts written before names

A slash means the library and nothing else: a world/… path with a typo is refused on the line that wrote it rather than quietly finding something of the same spelling inside the object.

For a file that packs several clips, add #name (or #index): "world/avatars/pack#sacar". Prefer the name, because re-exporting from Blender reorders the actions, and an index then points at another animation with nothing said.

Finding people#

count#

players:count() → number

How many people are here. Never fails, so you can use it straight in maths or text.

Luau
print("players here: " .. players:count())

list#

players:list() → list of players

Everybody, with positions. Never fails.

nearby#

players:nearby(radius) → list of players

Like list(), filtered by distance to the object this script is in.

Luau
for _, p in ipairs(players:nearby(15)) do    p:show_hud(WARNING)end

radius is in metres and must be a real number above zero.

get#

players:get(id) → player, or nil

One person, by their id or by their exact username, with a fresh position.

Luau
self:on_touch(function(p)    local who = players:get(p.id)    if not who then return end    print("they are standing at " .. tostring(who.pos))end)

nil means they are not in this region.

Tip

Why look someone up when a handler already handed them to you? Because the player in a handler carries who they are, not where they are. players:get adds the position, velocity and aim. Both are the same person, and both carry the same screen verbs.


What a player carries#

Who they are#

FieldMeaning
idthe account's permanent id. Never changes. Use this as a key
nametheir display username. They can change it, so never use it as a key
numtheir public number. 0 means an account without one

What their body is doing#

FieldMeaning
gait"idle", "walk", "run", "jump" or "fall"
speedspeed on the ground, m/s, without the vertical part
groundedare their feet on something?
crouchedare they crouched?
turnhow fast they are turning, degrees per second. Positive is to the left
holdingworld objects they are carrying right now, by id

gait is the same word the client is drawing, so a script and the screen never disagree. It is never "land", because landing is a one-frame flourish, not a state a body stays in; watch for the exit from jump or fall instead.

Careful

Do not use gait to swap the leg animation. For that there is set_locomotion, which swaps the clip without taking the decision away from the player's own machine. gait is for game rules: don't aim while running, spend stamina, lose accuracy.

crouched is the stance the simulation settled on, not the key they are holding. Somebody wedged under a low ceiling reads true with nothing pressed. It also decides their capsule height, so it is what a shot is tested against: a crouched player is a shorter target with a lower head.

holding is the region's own list, so it is always right. Keeping your own count in self.store instead drifts the first time somebody disconnects mid-carry.

Where they are#

FieldMeaning
poswhere the player believes they are. The one you want
seenwhat the region actually saw, raw
velvelocity, m/s
lagone-way delay, in seconds, measured by the connection
lookwhere their camera points: a direction

Tip

Use pos, not seen. Every machine draws its own avatar by its own prediction, which is ahead of the region by the time its commands take to arrive. A script acting on the raw position acts on a place the person left lag seconds ago, always, and always to the same side. That is not noise, it is bias, and it is what makes a turret shoot behind a running player.

pos already has that difference projected, bounded in seconds and metres. A stationary player has pos == seen exactly.

look is where they are aiming, not where they are walking. The two coincide only by accident and separate exactly when it matters: someone strafing looks one way and moves another, and a stationary player has no walk direction at all. It is always a real direction, never zero.

lag is measured by the transport, not reported by the client, so nobody can lie about their own latency to be aimed where they will not be.

Every player also carries the screen verbs. show_hud, show_crosshair, set_camera, set_weapon and the rest. They are documented in Player and screen.


Arriving and leaving#

on_enter#

players:on_enter(fn) → handle

Somebody came into the region. The list already includes them.

Luau
players:on_enter(function(p)    print(p.name .. " arrived")end)

on_leave#

players:on_leave(fn) → handle

Somebody left. The list already excludes them.


Acting on somebody#

teleport#

players:teleport(id, to) → true, or nil, reason

Moves a player somewhere else in this region, at once.

Luau
players:teleport(p.id, { x = 20, y = 0.2, z = -20 })

This is the primitive a respawn is made of. Everything else a death needs, such as what a head is worth, how long you stay down and what the scoreboard says, is a game rule a script can already hold. Moving the body was the one part no script could reach.

Moving somebody else needs Act on other people's avatars, which the world's owner holds and may grant; a worn item moving its own wearer needs nothing. Without it you get nil, "permission", which is a real answer and not a crash, so check it. A script its owner did not write can never teleport somebody else. See Permissions.

In-region only. Sending somebody to another world is a different act, and it is not this call.

The player feels no special message: their prediction and the next snapshot disagree by the whole distance, and a correction that large is already treated as a real teleport, so it snaps rather than sliding.

play_animation#

players:play_animation(id, asset, options) → handle, or nil, reason p:play_animation(asset, options) → the same

Starts an independent, whole-body animation layer on somebody. Everyone sees it, and a script layer takes priority over their walking and idling.

Luau
local h, why = p:play_animation("world/avatars/anim/wave", { looped = false })if h then    h:set_speed(1.5)    h:stop()end
OptionMeaning
speed−8 to 8. Zero pauses; negative plays backwards
loopeddefault true
weight0 to 1, how much this layer contributes when layers coexist
maskwhich joints it claims. See below
blend"override" (default) or "additive"
fadeseconds of transition in and out. Default 0.12; 0 cuts dry

mask defaults to "auto", which claims only the joints the clip actually animates, so an aiming pose made on the torso plays on top of walking and running without you having to author one clip per gait. "upper" and "lower" force a half; "all" takes the whole body, which is what an emote wants.

blend defaults to "override", which replaces, and that is what "play this animation on that avatar" normally means. "additive" adds the clip's deviation from the base pose, and is only correct for a clip authored as a deviation: an aim, a lean, a limp.

Up to 8 animation layers per script. Needs this world's avatar animation permission.

The asset is named like every other one. See Naming an asset above. A name that is on neither shelf, or that is there and is not an animation, is refused on the line that wrote it and says which of the two it was.

It can be an animation you uploaded or one you made in the animator, they are the same kind of item here and behave identically. See Poses and animations.

set_locomotion#

players:set_locomotion(id, clips) → true, or nil, reason p:set_locomotion(clips) → the same

Swaps the clips somebody's walking draws, for as long as this script lives.

Luau
p:set_locomotion({    walk = "world/avatars/anim/andar-armado",    run = "world/avatars/anim/correr-armado",})p:set_locomotion({})     -- give them their own back

Each clip is named like every other asset. See Naming an asset.

The idle/walk/run/jump machine stays on the player's own machine, decided per frame, with no round trip. Only its content changes.

Pass the whole table each time: a gait you leave out means "their own animation".

Key
idle walk run jump fall landthe basics
walk_back walk_left walk_right
run_back run_left run_right
crouch_idle crouch_walkthe crouch

The directional clips exist because the body follows the camera, so you can walk sideways and backwards without turning. If one is missing, the base gait is drawn instead (walk_left absent falls back to walk), so ignoring them costs nothing.

The crouch is a second axis over the gait, not a gait of its own: a crouched player moving in any direction draws crouch_walk, because a duck-walk is a duck-walk sideways too and nobody wants to author four of them. Leave both out and the engine bends the knees itself, and the body still comes down to the height everyone else shoots at, it just does not do it in your style.

Up to 16 locomotion overrides per script.

set_movement#

players:set_movement(id, mods) → true, or nil, reason

How fast this one person moves, how high they jump, and whether they may jump at all. This is what a haste potion, a sprint, heavy armour, a stun or a gust of wind is made of.

Luau
p:set_movement({ speed = 1.5, jump = 1.2 })   -- a haste potionp:set_movement({ can_jump = false })          -- …and now the wind pins them downp:clear_movement()                            -- back to the world's own legs

Everything here is a multiplier on the world's own movement, never an absolute speed. A potion that says "you are 50 % faster" keeps meaning that after you retune the world; one that says "you walk at 4.8" silently becomes a nerf the day the base moves to 5. And multiplication is the only way to stack a haste, a sprint and heavy armour where the order you applied them in does not change the answer.

Every key is optional and every omitted one keeps its value, so the two lines above compose: the second one does not undo the haste.

KeyMultipliesNotes
speedwalk, run and crouchone gait with three throttle positions, so boosting them apart makes a crouch outrun a run
jumpjump strengthheight goes with the square: 1.41 jumps twice as high, 2 jumps four times as high
gravitygravitybelow 1 is the moon, meaning higher and slower, where jump alone is higher and no slower
accelacceleration and brakingice is 0.2; a dash that has to commit is 3
air_controlair controlstill capped at full control: it is a fraction
flyflight speed
can_jumpnonefalse refuses the launch and nothing else
can_movenonefalse roots them: a stun, a cutscene, a channelled cast
can_flynonefalse refuses flight, for a world that grants it to a class

Multipliers are clamped to 0 to 10.

Why can_jump is a state and not something you cancel#

You may be looking for a handler that sees the jump and refuses it. There is preventDefault. That cannot be built honestly here.

The jump is decided on the player's own machine, one frame before their input reaches the region. That is what makes movement feel instant, and it is why nobody waits for a round trip to start walking. A refusal that had to travel to your script and back would arrive several trips after the body already left the ground. The player would see themselves jump and then get yanked down, on every jump.

So the refusal is state both machines already hold. Set can_jump = false when the wind picks up; clear it when it drops. From that moment both sides step the same body to the same place, and nobody rubber-bands.

can_move = false is a stun, not a freeze: they still fall, still ride a moving platform, and are still pushed by anything that pushes bodies. Being rooted is not being made of stone.

Careful

One set per player, not one per script. A second call replaces the first outright.

That is deliberate. An engine that added up contributions would have to decide what happens when your potion and your armour disagree, when one of them stops, and in what order they combine, and the right answer to all three is different in every game. Your combat system multiplies its own numbers and sends the product, which it can get right because it is the only thing that knows its own rules.

The buff dies with the script that granted it. Take the potion off, delete the object, re-save the script, and they go back to the world's own movement. A speed boost that outlived its potion would be somebody who never slows down again, in a world whose scripts have no record they were ever buffed. Nothing here follows anybody out of the world either.

Up to 16 buffed players per script. Needs the same avatar permission as the rest of this family.

clear_movement#

players:clear_movement(id) → true, or nil, reason

Puts them back on the world's own movement, all at once.

movement#

players:movement(id) → table, or nil

What this script has in force on them, not the world's own speeds.

Luau
if not p:movement().can_jump then    print("the wind has them pinned")end

An unbuffed player reads all 1s and all trues. That is a real answer, not a failure: nil means only that they are not in the region.

Tip

These travel as 32-bit numbers, so compare with a tolerance rather than == 1.4. A value that is not exactly representable comes back a hair off, the wire being honest, not a bug. 1.5, 1.25 and 2 are exact.


What somebody is wearing#

worn#

players:worn(id) → list, or nil

What they are wearing now.

AnswerMeaning
nilthey are not in this region
an empty listthey are here and wearing nothing

Two different answers, deliberately.

Each item carries:

FieldMeaning
assetthe content id, which is what the creator knows it by
slotwhere it sits: "head", "top", "right_hand"…
kind"body" for a rigged piece, "attachment" for a rigid one
visiblewhether it is currently shown

A hidden item still appears, with visible = false. Whether that counts is your decision.

HUDs never appear here: the server only sends somebody's HUD attachments to that person, and publishing them would leak something nobody else can see.

is_wearing#

players:is_wearing(id, asset) → boolean, or nil p:is_wearing(asset) → the same

The item is named like every other asset. See Naming an asset. It can also be the hash worn() hands back.

The shortcut for the common question. Counts visible items only.

Luau
if players:is_wearing(p.id, MEMBER_BADGE) then    open_the_door()end

nil means they are not in this region, which is not the same as false.


What THIS WORLD puts on somebody#

Three verbs, and one promise that covers all of them: nothing you put on a person is written to their wardrobe. It lives in this region, it dies with the visit, and they walk out as themselves. There is no restore step to write, and therefore none to forget.

Name what you put on them the way you name everything else: a path on this world's library:

Luau
players:set_body(p.id, "world/bodies/mage")players:dress(p.id, "world/roupas/tabarda", "top")

A 64-character content hash still works, for scripts written before the library existed. Prefer the path: it can be read, it can be typed, and it keeps meaning the same garment after you re-upload the file behind it.

set_body#

players:set_body(id, body) → true, or nil, why

The body they wear here. nil gives them their own back.

dress#

players:dress(id, item, slot) → true, or nil, why

One garment, in one slot: top, bottom, outerwear, hair, head, eyes, hands, feet, accessory. nil as the item takes that slot off.

A slot you claim is yours, so what they had there is not drawn, so the tabard goes over the shirt rather than through it. Dressing the same slot twice replaces it; it does not stack.

undress#

players:undress(id) → true, or nil, why

Everything this world put on them comes off at once. Their own appearance was never gone.

When it refuses#

It saysIt means
nothing is published at '…'that path is not on this world's library
'…' is a sound. A body and a garment are MODELSright path, wrong kind of thing
'…' left this world's librarysomebody removed it while your script ran
permissionacting on somebody's avatar is the world owner's to delegate

It refuses on the line that called it, not a tick later, so a costume that silently never appears is indistinguishable from a script that never ran.

The whole world at once

A world that wants everybody in the same body does not need a script for it. Its settings have a page, What visitors may bring, where the owner says whether people keep their own body, clothes, HUDs and animations, and names a body of this world, worn from the instant of the join, before a line of script runs, and named the same way: world/bodies/hero. Use these verbs for the decision about one person: the class they picked, the team they joined.


What somebody has saved#

store#

One person's memory inside this world: their inventory, their progress, whatever your game gives them.

Luau
local players, json = require("players"), require("json") players:on_enter(function(p)    local raw, why = players.store:get(p.id, "save")    if why then return end                    -- not ready, or not allowed    local save = raw and json.decode(raw) or { gold = 0, items = {} }    ...end)

It is the third store, and the three differ by who can see it:

whosewho reads it
self.storeone object'sthat object's scripts
world.storethe region'severy script here
players.storeone person'sthis world's scripts, when their owner holds the data permission

It follows the person. It lives on the central server, not in this world's save, so it is the same drawer whichever machine they land on the next time they play. That is the difference that matters: world.store travels with the world, this travels with the player.

Key it by player.id, the account's permanent id, which never changes.

The three verbs#

Luau
players.store:get(player, key)              -- text | nil, reasonplayers.store:set(player, key, text | nil)  -- true | nil, reasonplayers.store:update(player, key, fn)       -- new value | nil, reason

update is the only correct way to change a value that depends on the old one:

Luau
players.store:update(p.id, "gold", function(current)    return tostring((tonumber(current) or 0) + 10)end)

Only while they are here#

Luau
local v, why = players.store:get(someone_who_left, "gold")-- nil, "that player is not here"

Not a quirk. This world holds the custody of somebody's save only while they are inside it. The moment they walk into another world, that world takes over, and a write from here would be refused anyway. Saying so at the call is better than letting you write data that silently never lands.

Careful

nil, reason and nil, nil are different answers. nil, nil means nothing was saved under that key, meaning a new player. nil, <reason> means the drawer was not reachable. Treating the second as "new player" is how a save gets overwritten with a fresh one, so check why before deciding somebody is new.

Who may#

Both reading and writing need the world's data permission. Unlike the world's scoreboard, which is public because it is already on the wall, what one person has in their inventory is theirs, and a visitor's script has no business reading it.

How much fits#

keysvalue sizetotal per player
12864 KB256 KB

A save per subject, so inventory, progress and settings, not a key per item. 256 KB is on the order of 2500 inventory lines. Values are text: use json to keep a table in one.

See also#

Updated 17 October 2026