Effects and atmosphere#
Particles, light and weather: the things that make a place feel like somewhere.
A campfire#
--!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#
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:
local function puff() self:particles({ preset = "dust", rate = 200 }) self:after(0.15, function() self:particles() end)endA fire that burns out#
--!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#
--!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:
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#
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#
local world = require("world") world:set_weather({ rain = 0.8, fade = 6 })world:set_weather({ rain = 0 }) -- back to sunshineEvery field is optional and every omitted one keeps its value.
| Key | Range | |
|---|---|---|
rain | 0 to 1 | |
snow | 0 to 1 | independent of rain. Both together is sleet |
lightning | 0 to 1 | a frequency, not a trigger |
wind | 0 to 40 | m/s; also pushes dynamic objects |
wind_dir | degrees | |
fade | seconds | belongs to the change, never carried over |
sound | table | your 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:
world:on_start(function() world:set_weather({ rain = 0, snow = 0, wind = 2 })end)A storm that builds#
--!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.
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:
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#
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#
- Particles has every field
Updated 11 October 2026