Hungrit Scripting

world.npc#

Characters that walk on their own. You tell an NPC where to go; it finds the way around walls, up stairs and across bridges, and tells you when it gets there.

A guard that walks to the gate
local world = require("world")local vec = require("vec") local guard = world.npc:spawn{ name = "Guard", pos = vec(40, 0, -12) }guard:walk_to(vec(80, 0, -30))guard:on_arrive(function(ok, why)    print(ok and "at the gate" or ("could not get there: " .. why))end)

It walks like a player#

An NPC moves with the same body a player has. Gravity, the 45 cm step, ramps, moving platforms and walls all behave for it exactly as they do for a person, so an NPC cannot walk through a wall any more than you can. Players and NPCs bump into each other.

It appears in the player list and has a name over its head, like anyone else in the region. It plays the same animations a player's body does.

Finding the way#

When you send it somewhere, the region plans a route over the ground a person could walk on: floors a body fits on, steps no taller than 45 cm, slopes up to 45°. It knows about floors above floors, so it can walk to the second storey of a house by its stairs.

If the destination cannot be reached, because it is inside a solid building or behind a wall with no way round, the NPC walks as close as it can get and reports "unreachable".

If something keeps it from moving for a second, it plans again. After three tries it gives up and reports "stuck".

Creating and finding NPCs#

VerbWhat it does
world.npc:spawn{ pos, name }Creates one and returns it right away. name defaults to "NPC". A pos outside the region answers nil, "outside the region: …".
world.npc:list()Every NPC in the region.
world.npc:get(id)One NPC by id, or nil.

What an NPC can do#

Every verb answers true, or nil, reason when the call itself is wrong. None of them raises an error.

VerbWhat it does
npc:walk_to(pos)Walks there, finding the way.
npc:run_to(pos)Runs there.
npc:follow(player, { distance, run })Stays within distance metres (default 2) of a player, re-planning as they move. It keeps the player's pace, runs only to catch up, waits beside them while they stand, and never gives up on its own: it ends with stop, a new order, or the player leaving the region (on_arrive hears "player left").
npc:stop()Stands still.
npc:face(pos or player)Turns to look at a point or a player.
npc:play_animation(clip, opts)Plays an animation on its body: the same clips, options and handle as players:play_animation.
npc:set_body(model)The body it is drawn in: a path on the world's library ("world/bodies/zombie") or a name in this object's Contents, like players:set_body. nil goes back to the default body.
npc:despawn()Removes it.
npc:on_arrive(fn(ok, reason))Called at the end of every walk. One handler answers every walk until you replace it.
npc:position()Where its feet are now, or nil once it is gone.
npc:is_walking()true while it is walking, running or following.
npc:is_mine()true if this object's scripts made it.
npc.id, npc.nameIts id (in the same space as players) and its name.

reason is one of:

  • "unreachable": it went as close as it could.
  • "stuck": something kept blocking it.
  • "no ground": it was not standing on anything a person could walk on.
  • "player left": the player it was following left the region.

Whose NPC it is#

Any script can read any NPC in the region: its name, where it is, whether it is walking. Only the object whose script made an NPC can give it orders. An order to somebody else's NPC answers nil, "not your NPC".

Following someone#

A dog that follows whoever touches it
local world = require("world")local self = require("self") local dog = world.npc:spawn{ name = "Rex", pos = self:position() } self:on_touch(function(player)    dog:follow(player, { distance = 2, run = true })end)

A patrol#

A guard walking between three posts
local world = require("world")local vec = require("vec") local posts = { vec(10, 0, -10), vec(30, 0, -10), vec(30, 0, -30) }local guard = world.npc:spawn{ name = "Guard", pos = posts[1] }local next_post = 2 guard:on_arrive(function()    next_post = next_post % #posts + 1    guard:walk_to(posts[next_post])end)guard:walk_to(posts[next_post])

Being hit#

An NPC stands in the same capsule as a player, so world:raycast meets it: the hit says what = "npc", its npc id, its name and the zone it landed on. What a hit does is your game's decision. Only the script that made an NPC can remove it, so a gun tells the spawner with an event:

The zombie spawner: three hits and it goes
local world = require("world")local events = require("events") local zombie = world.npc:spawn{ name = "Zombie", pos = self:position() }local hits = 0 events:listen("npc:shot", function(id)    if id ~= zombie.id then return end    hits += 1    if hits >= 3 then        zombie:despawn()    endend)
Luau
-- in the gun, after a hitif hit and hit.what == "npc" then    events:emit("npc:shot", hit.npc)end

world:find_path#

The route a person would walk, without an NPC: feet positions, start first.

Luau
local world = require("world")local vec = require("vec") local path, reached = world:find_path(vec(10, 0, -10), vec(60, 0, -40))if path then    print(#path .. " corners, reached: " .. tostring(reached))end

reached is false when the destination cannot be reached; the route then ends at the nearest spot that can. It answers nil, reason only when the call itself is wrong: a value that is not a position, or no walkable ground at the start. Like world:raycast it is an immediate read: it needs no permission and costs nothing from your command budget.

How long an NPC lives#

An NPC belongs to the object whose script made it. It goes away when that script is saved again or removed, and when the object is deleted, so saving your script never leaves an extra guard behind.

Limits#

  • A region holds up to 64 NPCs. spawn past that returns the NPC, but it never appears.
  • Creating NPCs needs the same permission as world:spawn: in a social world only the world owner's scripts can do it; in an experience any script can.

Hungrit scripting documentation.

Updated 26 September 2026