Skip to content

Sound and music

Where a sound comes from

VerbPlays atLoops?
self:play_soundthis object, following itno
self:soundthis object, following ityes
world:play_sounda fixed point in the regionno

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

luau
self:play_sound("hammer")                 -- inside this object's Contents
self:play_sound("world/sounds/hammer")    -- the world's shared library
self:play_sound("ui_click")               -- a built-in cue, no upload needed

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

luau
self:play_sound("hammer", {
    volume = 0.9,
    pitch = { 0.95, 1.05 },
    range = 40,
    near = 3,
})
OptionWhat it is for
volume0 – 4. 1 is the recorded level
pitch0.25 – 4. A {low, high} pair picks one value per play
rangemetres past which nobody hears it
nearmetres inside which it stops getting louder
falloff"inverse" (real-world) or "linear" (a definite edge)
air0 – 1, how much top end distance eats
distantanother sound that takes over further out
fadeseconds 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

luau
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

luau
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

luau
self:sound("motor", { volume = 0.4, fade = 1.5, channel = "motor" })

-- later
self: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.

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

luau
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,
    })
end

pos 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

luau
--!strict
local 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:

luau
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

Hungrit scripting documentation.