Hungrit Docs

Effects and atmosphere#

Particles, light and weather: the things that make a place feel like somewhere.

A campfire#

Luau
--!strictlocal self = require("self") self:particles({ preset = "fire" }) self:set({    light = true,    light_color = "#ff9a3c",    light_radius = 9,    light_intensity = 2.2,})

Two lines and it is a fire that lights the ground around it.

The emitter belongs to the object: it follows it and dies with it. Nothing is sent per particle, so it costs the same for one watcher or forty.

Making fire look like fire#

Luau
self:particles({    preset = "fire",    rate = 60,    life = { 0.7, 1.4 },    speed = { 0.7, 1.6 },    size = { 0.16, 0.02 },    glow = 3.5,    softness = 0.3,})

Tip

Fire that does not look like fire is almost always fire with glow = 1. glow is a multiple of how bright white is where the particle sits: at 1 it is merely lit by the world; above 1 it gives off light and blooms.

size is birth-then-death, not a random range: { 0.16, 0.02 } shrinks as it rises. It is the one pair that does not mean min and max.

softness is what stops smoke from cutting the ground in a straight line.

A burst#

There is no burst verb. Turn the emitter on and off again:

Luau
local function puff()    self:particles({ preset = "dust", rate = 200 })    self:after(0.15, function()        self:particles()    end)end

A fire that burns out#

Luau
--!strictlocal self = require("self")local world = require("world") local BURN = 300 local lit_at = tonumber(self.store:get("lit") or "0") or 0 local function light_it()    lit_at = world:clock()    self.store:set("lit", tostring(lit_at))    self:particles({ preset = "fire" })    self:set({ light = true, light_radius = 9, light_intensity = 2.2 })end local function put_out()    lit_at = 0    self.store:set("lit", "0")    self:particles()    self:set({ light = false })end self:on_touch(function()    if lit_at > 0 then put_out() else light_it() endend) self:on_timer(10, function()    if lit_at > 0 and world:clock() - lit_at >= BURN then        put_out()    endend)

The timestamp is what makes this survive a restart: a fire lit ten minutes before the region went down is out when it comes back, because the clock kept running.

A lamp that comes on at dusk#

Luau
--!strictlocal self = require("self")local world = require("world") local DAY = 600local lit = false self:on_timer(10, function()    local night = (world:clock() % DAY) > DAY * 0.5    if night == lit then return end        -- only act when it CHANGES     lit = night    self:set({        light = lit,        emissive = lit and 2.5 or 0,        color = lit and "#ffe9b0" or "#3a3a3a",    })end)

Writing the same properties every ten seconds would spend the region's budget for nothing. Act on the change.

Tip

emissive and light are different. emissive makes the object look bright; light makes it illuminate what is around it. A glowing sign wants the first; a lamp wants both.

Sunbeams in a misty forest#

Place a box over the trees, make it invisible and tick Fog, or do it from a script:

Luau
local self = require("self") self:set({    transparency = 1,    fog = true,    fog_clarity = 60,      -- metres you can see through it    fog_color = "#e8eef5",    fog_glow = 1.2,        -- a little more sun in the beams    fog_wisps = 0.5,})

The beams come from the gaps in the canopy, so a forest that is one solid roof shows none. Lower fog_clarity for a thicker mist; keep it above about 30 m or the forest disappears into it.

Fog that lifts by noon#

Luau
local self = require("self")local world = require("world") local DAY = 600local thick = nil self:on_timer(10, function()    local t = (world:clock() % DAY) / DAY      -- 0 = midnight, 0.5 = noon    local morning = t > 0.2 and t < 0.4    if morning == thick then return end        -- only act when it CHANGES     thick = morning    self:set({ fog_clarity = morning and 40 or 250 })end)

Weather#

Luau
local world = require("world") world:set_weather({ rain = 0.8, fade = 6 })world:set_weather({ rain = 0 })            -- back to sunshine

Every field is optional and every omitted one keeps its value.

KeyRange
rain0 to 1
snow0 to 1independent of rain. Both together is sleet
lightning0 to 1a frequency, not a trigger
wind0 to 40m/s; also pushes dynamic objects
wind_dirdegrees
fadesecondsbelongs to the change, never carried over
soundtableyour own rain and thunder. See below

Snow is a visibility dial: 0.4 is about 190 metres, 0.8 about 23, and 1.0 is a whiteout with no horizon. What closes a street down is the air, not the number of flakes.

Lightning is a frequency. To fire one at a dramatic moment, raise it and lower it again, and you get a strike within a second or two, not on an exact frame.

Careful

Weather belongs to the place, not to your script. It survives the script being re-saved, the object being deleted and the region restarting. A world that should always open in the same weather says so from world:on_start:

Luau
world:on_start(function()    world:set_weather({ rain = 0, snow = 0, wind = 2 })end)

A storm that builds#

Luau
--!strictlocal world = require("world") local STEPS = {    { rain = 0.2, wind = 4,  lightning = 0,   fade = 20 },    { rain = 0.6, wind = 12, lightning = 0.3, fade = 30 },    { rain = 1.0, wind = 22, lightning = 0.8, fade = 25 },    { rain = 0.3, wind = 8,  lightning = 0.1, fade = 40 },    { rain = 0,   wind = 2,  lightning = 0,   fade = 60 },} local i = 0 local self = require("self")self:on_timer(120, function()    i = (i % #STEPS) + 1    world:set_weather(STEPS[i])end)

fade does the work. Each change eases in over tens of seconds, so nobody sees the sky snap.

A storm with your own sound#

The engine synthesises the rain and the thunder, and it is good at it. Noise has no loop point, costs no download, and bends with how hard it is falling and whether you are under a roof. What it cannot be is your storm.

Luau
world:set_weather({    rain = 0.9,    lightning = 0.7,    sound = {        rain = "world/sounds/rain",        wind = "world/sounds/wind",        thunder = {            { sound = "world/sounds/thunder-far", strength = 0.15 },            { sound = "world/sounds/thunder-mid", strength = 0.5 },            { sound = "world/sounds/thunder-near", strength = 0.95 },        },    },})

Thunder is chosen by how hard the bolt hit, so 0 is on the horizon and 1 is overhead, and it is the same falloff the flash was drawn with, so the clap matches the bolt the player just saw. Up to six recordings. There is no gap to fall into: the engine plays whichever is nearest, so one entry answers for the whole storm and no strike is ever silent.

If you have three files and no opinion about which is the far one, a bare list spreads them across the storm in the order given:

Luau
sound = { thunder = { "far", "mid", "near" } }

Everything is per layer. Naming a rain leaves the synthesised thunder alone. Omitting a key keeps its recording; false takes that one away, and sound = false goes back to the synthesised storm entirely.

You supply the sound; the engine keeps the mix. How loud it is follows the weather, and a roof still muffles it, because only the player's own machine knows they just stepped under an awning.

Tip

Publish weather sounds on the world library (world/…). A name out of an object's Contents works, but the world comes back up before any object does, so after a restart the first storm would be the synthesised one until that script runs again.

Wind that moves things#

Luau
self:set({ dynamic = true, mass = 0.4, drag = 3 })

Wind pushes anything dynamic, in proportion to its drag and inversely to its mass. drag = 0 opts out entirely.

Weight alone does not decide who blows away. The force carries the object's area, which is why a heavy ball ignores a gale that carries a light sheet across a field.

It is self-limiting: an object accelerates until it moves with the air and no further, and a settled object the air cannot slide is left asleep, so a courtyard of crates costs nothing.

See also#

Updated 11 October 2026