Particles, beams, trails, highlights and decals#
Effects are instances that draw something the world does not have geometry for: sparks, flame, a laser, a glow around a crate, a picture on a wall. Put one in the tree and every client draws it. Take it out and it is gone. They draw in play and in the editor viewport. In the Explorer, Insert > Effects lists all twelve.
| Class | Goes inside | What it draws |
|---|---|---|
ParticleEmitter |
a part, a model or an attachment | particles you describe |
Fire |
a part or an attachment | flames |
Smoke |
a part or an attachment | rising puffs |
Sparkles |
a part or an attachment | glints falling out every way |
Beam |
anywhere | a ribbon between two attachments |
Trail |
anywhere | a ribbon left behind two moving attachments |
Highlight |
anything, or set adornee |
a fill and outline over parts, mesh parts, models and characters |
Decal |
a part | an image on one face |
Texture |
a part | an image tiled across one face |
SelectionBox |
anything, or set adornee |
a box around it, always on top |
SelectionSphere |
anything, or set adornee |
a sphere around it, always on top |
Explosion |
the world | a blast that pushes parts and kills bodies |
The Goes inside column is what you parent the effect to, and it decides where the effect draws. An emitter inside a part sends particles out of that part, and a decal inside a part puts an image on that part's face.
Sizes and distances are metres. Colours are 0 to 1. transparency is 0 solid and 1 gone.
Unlike the rest of Moud, spreadAngle and particle rotations are degrees. A spreadAngle of
25 is 25 degrees, and rotSpeedMin = 90 is a quarter turn a second.
Images#
texture on a particle emitter, beam, trail, decal or texture takes:
| Value | Image |
|---|---|
"res://effects/spark.png" |
an image in the place |
"flame", "glint", "big_smoke_0" |
a Minecraft particle, from textures/particle/ |
"minecraft:block/stone" |
any other game texture, by its path under textures/ |
canvas.uri |
an image a script drew or loaded, see Images |
"" |
a soft round dot on emitters, a plain colour on beams and trails |
Game textures keep their pixels sharp. Images in the place are smoothed. An image that cannot be found draws the dot, and is looked for again a second later, so a file you add while the place is running appears without a reload.
Light#
Particles, beams and trails share three numbers that decide how bright they are drawn:
| Property | |
|---|---|
brightness |
multiplies the colour; above 1 glows |
lightEmission |
0 blends normally, 1 adds its light to what is behind it, like fire or a laser |
lightInfluence |
0 is always fully bright, 1 darkens in shadow and at night like a block |
Raise brightness and lightEmission for anything that is meant to be a source of light itself, such
as sparks or a laser. Raise lightInfluence for anything that is meant to sit in the world, such as
dust or falling leaves, so it goes dark at night with everything around it.
None of them light up the world. Add a light next to the effect for that.
A campfire#
Most effects are one add call with a table of properties. A campfire is four of them in the same
part:
local pit = world:add("Part", {
name = "campfire",
size = vec3(1, 0.2, 1),
cframe = cframe(0, 64.1, 0),
color = color(0.25, 0.18, 0.12),
})
pit:add("Fire", { size = 2, heat = 12 })
pit:add("Smoke", { size = 1.5, opacity = 0.25, riseVelocity = 2, color = color(0.35, 0.35, 0.35) })
pit:add("PointLight", { color = color(1, 0.6, 0.3), brightness = 2, range = 10 })
pit:add("ParticleEmitter", {
name = "embers",
texture = "spark_4",
rate = 6,
lifetimeMin = 0.8, lifetimeMax = 1.6,
speedMin = 2, speedMax = 4,
spreadAngle = 25,
acceleration = vec3(0, -3, 0),
drag = 1,
sizeStart = 0.15, sizeEnd = 0,
colorStart = color(1, 0.8, 0.3), colorEnd = color(1, 0.3, 0.1),
brightness = 2,
lightEmission = 1,
})- The
Fireand theSmokedraw the flames and the puffs. Neither one lights anything, which is why thePointLightis there as well. - The emitter sends six embers a second, each living between 0.8 and 1.6 seconds and leaving at
between 2 and 4 metres a second.
spreadAngle = 25lets each one stray up to 25 degrees from straight up, so they rise in a loose cone. accelerationpulls them back down at three metres a second squared, anddragslows them as they go, which is what makes an ember arc over and fade rather than fly off.sizeStartandsizeEndshrink each ember to nothing, whilecolorStartandcolorEndcool it from yellow to red over the same life.
Particle emitters#
A ParticleEmitter sends particles out of what it is inside. From a part they start inside its box.
From an attachment or a model they start at its point. Inside anything else an emitter sends nothing.
| Property | Default | |
|---|---|---|
enabled |
true | off stops the steady flow; particles already out finish, and emit still works |
texture |
"" |
see Images |
rate |
20 | particles a second |
lifetimeMin, lifetimeMax |
5, 10 | seconds; each particle picks between the two |
speedMin, speedMax |
5, 5 | metres a second as it leaves |
spreadAngle |
0 | degrees a particle may stray from emissionDirection, the same in every direction; 180 sends them every way |
emissionDirection |
"top" |
which face they leave from, in the frame of what the emitter is inside |
shapeStyle |
"volume" |
volume starts them anywhere in the part, surface only on its faces |
acceleration |
0, 0, 0 | a pull in world space, metres a second squared; gravity is vec3(0, -9.8, 0) |
drag |
0 | a particle loses half its speed every 1 / drag seconds |
rotationMin, rotationMax |
0, 0 | starting angle, degrees |
rotSpeedMin, rotSpeedMax |
0, 0 | spin, degrees a second |
sizeStart, sizeEnd |
1, 1 | width in metres when born and when it dies |
transparencyStart, transparencyEnd |
0, 0 | |
colorStart, colorEnd |
white, white | |
brightness, lightEmission, lightInfluence |
1, 0, 0 | see Light |
orientation |
"facingCamera" |
see below |
lockedToPart |
false | on, particles move with the part after they leave; off, they stay in the world |
timeScale |
1 | 1 is normal speed, 0 freezes them |
Every curve is a start and an end value, with no points in between. The value blends from one to the
other over the particle's life, so a particle with sizeStart = 1 and sizeEnd = 0 is half a metre
wide when it is halfway through.
Because emissionDirection is read in the frame of whatever the emitter is inside, turning that part
turns the spray. An emitter in a part you tip on its side sprays sideways.
orientation decides which way each particle faces the camera:
orientation |
Each particle |
|---|---|
facingCamera |
turns fully to the camera |
facingCameraWorldUp |
turns to the camera but stays upright |
velocityParallel |
stands along where it is going, turned towards the camera |
velocityPerpendicular |
faces where it is going |
velocityParallel is what makes a spark look like a streak, because the image is stretched along the
direction the particle is travelling.
One emitter holds at most 8192 particles at once. rate times the longest lifetime tells you how many
an emitter keeps alive, so at rate = 20 and lifetimeMax = 10 an emitter carries 200.
Bursts#
emitter:emit(count) sends out count particles at once, 16 when you leave it out, up to 10000.
Set rate = 0 for an emitter that only bursts.
-- server: a crate that bursts when clicked
local crate = world:find("Crate") :: Part
local burst = crate:add("ParticleEmitter", {
rate = 0,
speedMin = 3, speedMax = 7,
spreadAngle = 180,
lifetimeMin = 0.4, lifetimeMax = 0.9,
acceleration = vec3(0, -9.8, 0),
sizeStart = 0.2, sizeEnd = 0,
colorStart = color(1, 0.9, 0.4),
lightEmission = 1,
})
local detector = crate:add("ClickDetector", {})
detector.mouseClick:connect(function(player)
burst:emit(40)
end)rate = 0means the emitter never sends anything by itself. It sits in the crate doing nothing until a click callsemit.spreadAngle = 180sends the 40 particles in every direction rather than out of one face.acceleration = vec3(0, -9.8, 0)pulls them down at the usual gravity, so the burst arcs and falls.- Every particle is gone within 0.9 seconds, which is
lifetimeMax.
How a burst reaches the other players:
- On the server,
emitadds toemitted. That count replicates, and every client emits the difference. A client that reaches the emitter late (joining, or being sent a branch it did not have) starts from the count it arrives with, so it never replays bursts from before it was there. - On a client,
emithappens only on that client.
Leave emitted alone; it only exists to carry bursts across. Writing it yourself makes clients
emit the difference between the old value and the new one.
Fire, smoke and sparkles#
Fire, Smoke and Sparkles are ready-made emitters with fewer knobs. Reach for one when you want
the usual look and do not want to set fifteen properties.
| Class | Properties |
|---|---|
Fire |
enabled, size (0.2 to 30), heat (0 to 25, how fast the flames rise), color, secondaryColor (what the flames fade to), timeScale |
Smoke |
enabled, size (0.1 to 100), opacity, riseVelocity (-25 to 25, negative sinks), color, timeScale |
Sparkles |
enabled, sparkleColor, timeScale |
Turning one off stops new flames, puffs or glints. What is already out finishes, so a fire you switch off burns down over the next moment rather than vanishing.
A laser#
A Beam draws a ribbon from attachment0 to attachment1. Any part or attachment works as either end.
local turret = world:find("Turret") :: Part
local target = world:find("Target") :: Part
turret:add("Beam", {
attachment0 = turret:add("Attachment", { cframe = cframe(0, 0, -0.5) }),
attachment1 = target:add("Attachment", {}),
texture = "res://effects/laser.png",
textureMode = "wrap",
textureLength = 0.5,
textureSpeed = 4,
colorStart = color(1, 0.1, 0.1), colorEnd = color(1, 0.4, 0.2),
transparencyStart = 0, transparencyEnd = 0,
width0 = 0.15, width1 = 0.15,
faceCamera = true,
brightness = 2,
lightEmission = 1,
})- The two attachments are the ends. One sits half a metre out of the front of the turret and the other in the middle of the target, and the beam follows them: move either part and the beam moves with it on the next frame.
textureMode = "wrap"withtextureLength = 0.5repeats the image every half metre, so a long beam and a short one have the same sized markings.textureSpeed = 4scrolls the image four lengths a second from the turret towards the target, which is what makes the laser look like it is flowing.faceCamera = trueturns the ribbon's flat side towards each player, so the beam never goes edge-on and disappears.brightness = 2withlightEmission = 1adds the beam's colour to whatever is behind it, which is what gives it the glow.
| Property | Default | |
|---|---|---|
enabled |
true | |
attachment0, attachment1 |
where it starts and ends; with either missing, nothing draws | |
texture |
"" |
see Images |
textureMode |
"stretch" |
stretch fits the image to the whole beam and scrolls it; wrap repeats it every textureLength metres and scrolls it; static repeats it the same way and holds it still from attachment0 |
textureLength |
1 | stretch: how many times the image repeats; otherwise metres per repeat |
textureSpeed |
1 | image lengths a second the image scrolls, from attachment0 towards attachment1; static ignores it |
colorStart, colorEnd |
white, white | at attachment0 and at attachment1 |
transparencyStart, transparencyEnd |
0.5, 0.5 | |
width0, width1 |
1, 1 | metres at each end |
curveSize0, curveSize1 |
0, 0 | how far the beam bows out of each attachment, along its X axis |
segments |
10 | straight pieces the curve is drawn with, 1 to 1000 |
faceCamera |
false | on, it turns its flat side to the camera; off, it lies flat facing the attachments' up axis |
brightness, lightEmission, lightInfluence |
1, 0, 0 | see Light |
With both curve sizes 0 the beam is straight. Otherwise it is a curve that leaves attachment0 along
its X axis and arrives at attachment1 along its X axis, so turning the attachments shapes it. Raise
segments when a tight curve looks like a run of straight lines.
A sword trail#
A Trail follows two attachments and leaves a ribbon across the gap between them. You put both
attachments on the same moving part, one at each edge of the shape you want the ribbon to be as wide
as.
local blade = world:find("Sword") :: Part
local trail = blade:add("Trail", {
attachment0 = blade:add("Attachment", { cframe = cframe(0, -1, 0) }),
attachment1 = blade:add("Attachment", { cframe = cframe(0, 1, 0) }),
enabled = false,
lifetime = 0.25,
colorStart = color(1, 1, 1), colorEnd = color(0.6, 0.8, 1),
transparencyStart = 0.2, transparencyEnd = 1,
widthScaleEnd = 0.3,
lightEmission = 1,
})
local function swing()
trail:clear()
trail.enabled = true
-- turn the blade here
task.delay(0.3, function() trail.enabled = false end)
end- The attachments are two metres apart along the blade, so the ribbon is as wide as the blade is long.
enabled = falseat creation means the trail lays nothing until a swing starts. Without it the sword would smear a ribbon behind the player as they walk.trail:clear()before each swing wipes whatever is left of the last one, so the first swing and the tenth look the same.lifetime = 0.25means each piece of the ribbon fades out a quarter of a second after it is laid, andwidthScaleEnd = 0.3narrows the ribbon to a third of its width as it goes.
| Property | Default | |
|---|---|---|
enabled |
true | off lays no new points; what is left fades away |
attachment0, attachment1 |
the two edges; with either missing, the trail is wiped | |
texture, textureLength |
"", 1 |
as on a beam |
textureMode |
"stretch" |
stretch fits the image to the whole trail; wrap repeats it by distance and keeps it still against the attachments; static repeats it by distance and leaves each repeat where it was laid in the world |
lifetime |
2 | seconds a piece lasts, up to 20 |
minLength |
0.1 | how far the attachments must move before a new point is laid |
maxLength |
0 | the longest it gets, in metres; 0 is no limit |
colorStart, colorEnd |
white, white | where it is new and where it is about to vanish |
transparencyStart, transparencyEnd |
0, 1 | |
widthScaleStart, widthScaleEnd |
1, 1 | scales the gap between the attachments along its life |
faceCamera |
false | on, it turns its flat side to the camera |
brightness, lightEmission, lightInfluence |
1, 0, 0 | see Light |
trail:clear() wipes what is behind it. Like emit, on the server it adds to clears and every
client wipes; on a client it wipes only there. A client that reaches the trail late starts from the
count it arrives with, so an old wipe never happens twice.
Highlighting what the pointer is on#
A Highlight lays a colour over parts and draws an outline around them. It covers adornee, or its
parent when adornee is empty, and every part inside it, so one highlight covers a whole model or
character.
-- client: glow the crate the pointer is on
for _, crate in world:byTag("crate") do
local detector = crate:find("ClickDetector") :: ClickDetector
local glow = crate:add("Highlight", {
enabled = false,
fillColor = color(1, 0.9, 0.3), fillTransparency = 0.8,
outlineColor = color(1, 0.9, 0.3),
depthMode = "occluded",
})
detector.mouseHoverEnter:connect(function() glow.enabled = true end)
detector.mouseHoverLeave:connect(function() glow.enabled = false end)
end- One highlight is made per crate and left switched off, and the hover events only flip
enabled. Creating the highlight once is cheaper than adding and destroying one on every hover. fillTransparency = 0.8lets most of the crate's own colour through, so the glow reads as a tint rather than as paint.depthMode = "occluded"hides the parts of the glow that are behind something else, so a crate round a corner does not shine through the wall.
The highlight is made on the client, so only that player sees it. The hover events are in Click detectors.
| Property | Default | |
|---|---|---|
enabled |
true | |
adornee |
empty | what it covers; empty uses its parent |
fillColor, fillTransparency |
red, 0.5 | the colour laid over it; 1 leaves the inside as it is |
outlineColor, outlineTransparency |
white, 0 | the line around it; 1 hides the line |
depthMode |
"alwaysOnTop" |
alwaysOnTop shows it through walls, occluded hides what is behind something |
Invisible parts and parts with transparency = 1 are left out.
There is no limit on how many highlights a scene has, but each one is its own pass over the screen.
Switch them off with enabled rather than keeping hundreds on at once.
A sign and a tiled floor#
A Decal stretches an image over one face of its parent part. A Texture is a decal that tiles.
local board = world:add("Part", { name = "sign", size = vec3(2, 1, 0.1), cframe = cframe(3, 65, 0) })
board:add("Decal", { texture = "res://decals/shop.png", face = "front" })
local floor = world:add("Part", { name = "floor", size = vec3(12, 0.2, 12), cframe = cframe(0, 63.9, 0) })
floor:add("Texture", {
texture = "minecraft:block/oak_planks",
face = "top",
studsPerTileU = 1, studsPerTileV = 1,
})The decal fills the sign's whole front face however big the part is. The texture on the floor repeats the plank image once every metre, so the twelve metre floor is covered by 144 copies of it, and widening the floor adds more copies instead of stretching the ones it has.
| Property | Default | |
|---|---|---|
texture |
"" |
see Images; empty draws nothing |
face |
"front" |
front, back, left, right, top or bottom |
color |
white | tints the image |
transparency |
0 | |
zIndex |
1 | higher draws over lower on the same face |
Texture adds:
| Property | Default | |
|---|---|---|
studsPerTileU, studsPerTileV |
1, 1 | metres one copy of the image covers across and down the face |
offsetStudsU, offsetStudsV |
0, 0 | slides the image across and down, in metres |
A decal takes the light of its part and shades like a block face. It only draws inside a part.
Selection boxes and spheres#
A SelectionBox draws the edges of a box around what it is on, and a SelectionSphere draws a
sphere. Both always draw on top of everything and ignore light, so they read as markers over the world
rather than as objects in it.
local box = crate:add("SelectionBox", { color = color(0.2, 0.6, 1), lineThickness = 0.04 })
local ring = world:add("SelectionSphere", { adornee = crate, surfaceTransparency = 0.8 })The box is inside the crate, so it goes around the crate. The sphere is in the world and reaches the
crate through adornee, which is how you mark something you cannot or do not want to add children to.
| Property | Default | |
|---|---|---|
visible |
true | |
adornee |
empty | what it goes around; empty uses its parent |
color, transparency |
blue, 0 | the edges, or the rings of a sphere |
surfaceColor, surfaceTransparency |
blue, 1 | the faces; 1 leaves them empty |
lineThickness |
0.05 | SelectionBox only, metres |
Around a model the box wraps every part inside, turned with the model. The sphere passes through the corners of that box, so it is wider than the model rather than tight to it.
Explosions#
An Explosion goes off. Adding one to the world from a server Script sets a blast at position,
and the explosion takes itself out of the tree afterwards, so you never destroy one yourself. Nothing
goes off while you edit a scene.
-- server: a grenade that goes off three seconds after it is thrown
local function grenade(from, velocity)
local bomb = world:add("Part", {
name = "grenade", shape = "ball", size = vec3(0.4, 0.4, 0.4),
cframe = cframe(from), anchored = false, color = color(0.2, 0.25, 0.2),
})
bomb:applyImpulse(velocity * bomb:getMass())
task.delay(3, function()
if not bomb.parent then return end
local blast = world:add("Explosion", {
position = bomb.worldCframe.position,
blastRadius = 8,
blastPressure = 400000,
destroyJointRadiusPercent = 0.4,
explosionType = "noCraters",
})
blast.hit:connect(function(part, distance)
print("caught", part.name, "at", distance)
end)
bomb:destroy()
end)
end- The bomb is an unanchored ball, so it flies and rolls. Multiplying
velocityby its mass gives it the speed you asked for whatever its density is. See Physics. if not bomb.parent then return endcovers the case where something destroyed the grenade during those three seconds. Without it the next line reads a position off an instance that is gone.positionis taken frombomb.worldCframe.positionat the moment it goes off, which is wherever the grenade rolled to, not where it was thrown from.destroyJointRadiusPercent = 0.4kills anything with a humanoid inside 3.2 metres, which is 40 per cent of the eight metre radius. Things further out are thrown but survive.explosionType = "noCraters"leaves the blocks alone, so the blast does not chew a hole in your map.
| Property | Default | |
|---|---|---|
position |
0, 0, 0 | where it goes off, in world coordinates |
blastRadius |
4 | how far it reaches, in metres; nothing outside is touched |
blastPressure |
500000 | how hard it throws things |
destroyJointRadiusPercent |
1 | the share of the radius that kills |
explosionType |
"craters" |
craters breaks the blocks it reaches, noCraters leaves them alone |
visible |
true | players see the blast and hear it |
hit |
fires with the part it caught and how far that part was |
It goes off on the next tick, not while your script is still running, so connecting hit right after
you add the explosion still catches everything.
What the blast does when it goes off:
- Every part within the radius fires
hit, and the distance is measured from the part's nearest face rather than its middle. Anchored parts and parts that do not collide fire it too; they just are not pushed. - Loose parts and player bodies are thrown straight away from the centre. The push fades to nothing at the edge: at 500000 a part right on the centre leaves at 20 metres a second, and one halfway out at 10.
- Anything with a humanoid closer in than
destroyJointRadiusPercentof the radius dies. AForceFieldinside the body still saves it. - A body is measured from its own position, where it stands, and not from its nearest point.
cratersbreaks blocks within the radius, never further than 24 metres out however big the radius is. It skips blast-resistant blocks like obsidian and bedrock, and nothing drops.hitdoes not fire for the limbs of a character, and the blast does not damage the game's own mobs.
Server and client#
Where you create an effect decides who sees it, and where you put it decides whether it draws at all:
- Effects the server adds show on every client. Effects a client adds show only there.
- Particles and trails move on each client, so two players do not see the same particles in the same places.
- A property you write shows on the next frame. Colour, size, transparency and acceleration also change particles already out; lifetime, speed and direction only reach new ones.
- Nothing inside a
ViewportFramedraws. - Nothing kept out of the world draws: an emitter, beam or trail inside
ServerStorage,ReplicatedStorage, a starter folder or a backpack sends nothing, and drops the particles it already had when it is moved in. Cloning it out into the world starts it clean. See Containers.
