Sound and music#
A click when something is touched, a hum that keeps going, an explosion where something hit, a mood in one corner of the world, and music.
Where a sound comes from#
| Verb | Plays at | Loops? |
|---|---|---|
self:play_sound | this object, following it | no |
self:sound | this object, following it | yes |
world:play_sound | a fixed point in the region | no |
Use self: for anything that belongs to a thing. Use world: for something whose source does not survive it: an impact, an explosion.
Naming a sound#
self:play_sound("hammer") -- inside this object's Contentsself:play_sound("world/sounds/hammer") -- the world's shared libraryself:play_sound("ui_click") -- a built-in cue, no upload neededTip
The slash is the whole difference. A bare name lives inside the object, so it travels with it. Sell it, drop it in another world, and the sound is still there. A world/… name lives in the world you are standing in: one copy fifty objects share, swappable in one place, and nothing an object carries away.
A name that is none of those is refused on that line, rather than becoming silence on somebody else's machine.
A one-off noise#
self:play_sound("hammer", { volume = 0.9, pitch = { 0.95, 1.05 }, range = 40, near = 3,})| Option | What it is for |
|---|---|
volume | 0 to 4. 1 is the recorded level |
pitch | 0.25 to 4. A {low, high} pair picks one value per play |
range | metres past which nobody hears it |
near | metres inside which it stops getting louder |
falloff | "inverse" (real-world) or "linear" (a definite edge) |
air | 0 to 1, how much top end distance eats |
distant | another sound that takes over further out |
fade | seconds to fade in. 0 is a cut |
The three that matter most#
pitch as a pair. Four identical samples a second sound like a machine. A little jitter and the pattern disappears. The region picks the value, so everybody in earshot hears the same one.
near. Without it, a sound you are standing on is infinitely loud.
range is a cost, not just a taste. It is also the radius the server uses to decide who is even told about the sound.
Making distance sound like distance#
self:play_sound("shot", { range = 220, near = 6, air = 0.7, distant = "shot_far",})air eats the top end as distance grows, which is what makes far away sound muffled rather than merely quiet. Distance that is only quietness reads as small, not as far.
distant goes further: it crosses to a different recording. Something 300 metres away is not its own close-up turned down; it is a different sound.
Several variations#
local SHOTS = { "shot_01", "shot_02", "shot_03", "shot_04" } self:play_sound(SHOTS[math.random(#SHOTS)], { pitch = { 0.96, 1.04 } })Four samples plus a little pitch jitter is enough that the ear stops finding the loop.
A continuous sound#
self:sound("motor", { volume = 0.4, fade = 1.5, channel = "motor" }) -- laterself:stop_sound("motor", 0.8)A looping emitter is a property of the object, not an event: it follows the object, and people who arrive later hear it too.
channel names it so one object can run several: a motor and a radio at once. Using the same channel twice replaces rather than stacking, which is what makes a script safe to re-run.
stop_sound with no channel stops the one started without naming one.
Tip
Let it fade. stop_sound defaults to a short ramp rather than to zero: a motor that stops dead sounds like a bug, and a cut in a waveform is an audible click.
A sound where something happened#
local world = require("world") local hit = world:raycast({ from = shot.eye, dir = shot.dir })if hit then world:play_sound("impact", { pos = hit.pos, volume = 0.7, pitch = { 0.9, 1.15 }, range = 45, })endpos is required, and it is pos rather than at because at in this API is always a socket on a body.
world:play_sound cannot loop, and the refusal says so: emitters are addressed by the object they sit on, and this one sits on nothing, so a loop could never be stopped.
Ambience in a zone#
--!strictlocal self = require("self") local inside = 0 self:on_region({ radius = 25, near = function() inside += 1 if inside == 1 then self:sound("wind", { volume = 0.25, fade = 3, channel = "amb" }) end end, far = function() inside -= 1 if inside <= 0 then inside = 0 self:stop_sound("amb", 3) end end,})Starting it only when the first person arrives means an empty region is silent and costs nothing.
Music#
Music is a looping emitter like any other. Give it a wide range and a large near so it does not pan around as the player walks:
self:sound("theme", { volume = 0.35, range = 200, near = 190, fade = 4, channel = "music",})With near almost as large as range, the sound stays even across the whole area instead of appearing to come from a point.
See also#
Updated 17 October 2026