Repository navigation
API Particles
Lua API / Particles
Register reusable particle types and effects during modInit or modReload. Custom particles display on the
local client. They use the shared texture assets and bm_assets/ texture-pack overrides.
Try the custom particle ring example
with its spark texture folder.
Craft stone + redstone dust and place Particle Ring Block to see rotating, pulsing sprites around its top.
The block emits through onDisplayTick, using Minecraft's sampled nearby-block presentation updates.
Emission has no fixed interval and resumes for existing blocks after world or chunk reload.
| Function | Behavior |
|---|---|
betamoon.particles:add(definition) |
Register a type and return a particle type reference. |
betamoon.particles:get(key) |
Return a type reference, or nil if absent. |
betamoon.particles:getRequired(key) |
Return a type reference; raise an error if absent. |
betamoon.particles.effects:add(definition) |
Register an effect and return an effect reference. |
betamoon.particles.effects:get(key) |
Return an effect reference, or nil if absent. |
betamoon.particles.effects:getRequired(key) |
Return an effect reference; raise an error if absent. |
reference:getKey() |
Return its canonical namespaced key. Available on both reference kinds. |
betamoon.particles:getStats() |
Return detached local counters indexed by script source owner. |
Types use keys such as mymod:particle/spark; effects use mymod:particle_effect/flare. Type and effect references
have distinct types. References expire after successful owner replacement or removal; look up a new reference
when needed. Invalid declarations fail before publication. Failed initialization retains the previous particle
generation. Successful replacement removes the owner's previous live sprites and emitters.
| Field | Supported values and defaults |
|---|---|
key |
Required namespaced particle key. |
texture |
Required registered texture reference/key, relative PNG path, texture descriptor, or atlas descriptor below. |
frameStart |
first (default) or random. Each sprite has its own playback cursor. |
lifetimeTicks |
Integer or {min, max}, inclusive 1–600; default 20. Sampled once at spawn. |
size |
Number or {start, finish} in world units. Start (0,4], finish [0,4]; default 0.2. Linear change over lifetime. |
color |
{r,g,b,a?} or {start = color, finish = color}. Channels 0–1; alpha defaults to 1. Default opaque white. Linear change over lifetime. |
motion |
Optional table with gravity, drag, collision, and conditional bounce; see below. |
facing |
camera (default) or vertical billboard. |
blend |
alpha (default) or additive. Uses depth testing; overlapping transparent sprites do not write depth. |
light |
world (default) or full. Full brightness displays in darkness without lighting the world. |
rotation |
{start?, degreesPerTick?} in degrees around the billboard normal; both default 0. Each value must be finite within ±1,000,000. |
onTick |
Optional function(ctx) for custom per-instance updates; see scripted updates. |
motion.gravity is −1–1 blocks/tick², positive downward (default 0). drag is 0–1 (default 0.98).
collision is none (default), stop, bounce, or die. Bounce requires bounce = 0..1 for restitution.
Motion is limited to ±16 blocks/tick per axis, including acceleration from gravity.
Collision uses a fixed 0.2-block particle body; visual size does not resize it. A stopped sprite can resume when
setVelocity is called. Particles expire when they leave loaded terrain.
local spark = betamoon.particles:add {
key = "mymod:particle/spark",
texture = "effects/spark.png",
lifetimeTicks = 20,
size = { start = 0.3, finish = 0 },
color = { start = {r=1,g=0.8,b=0.2,a=1}, finish = {r=1,g=0.2,b=0,a=0} },
motion = { gravity = 0.01, drag = 0.98, collision = "bounce", bounce = 0.4 },
rotation = { degreesPerTick = 4 }
}For an animated PNG, use the shared vertical-strip texture descriptor. Each sprite advances independently; particle playback pauses with the world regardless of the texture's clock setting.
texture = { asset = "effects/spark_strip.png", animation = { frameTicks = 2 } }An atlas descriptor reads selected zero-based cells from Minecraft's 16×16 atlases. Use index for one cell
or indices for a dense sequence of 1–256 cells; cell values are 0–255. frameTicks is 1–1200, default 1.
Supported atlases are minecraft:particles, minecraft:terrain, and minecraft:items.
texture = { atlas = "minecraft:particles", indices = {48, 49, 50, 51}, frameTicks = 2 }ctx.world:spawnParticle(type, options?) accepts a type reference or registered custom key. Existing vanilla
names, including betamoon.mc.particles.smoke, retain their native behavior.
| Spawn option | Rule |
|---|---|
x, y, z
|
Finite coordinates within ±30,000,000; default callback block center. |
velocityX, velocityY, velocityZ
|
Exact starting velocity, −16–16 blocks/tick; default 0. |
scale |
Multiplier (0,4], default 1; custom particles only. |
color |
RGBA multiplier, default white; custom particles only. |
seed |
Signed 32-bit integer; custom particles only. Reproduces admitted particles' sampled state. |
ctx.world:spawnParticle("mymod:particle/spark", {
x = ctx.x + 0.5, y = ctx.y + 1, z = ctx.z + 0.5,
velocityY = 0.05, seed = 42
})ctx.world:playParticleEffect(effect, options?) accepts an effect reference or key. It returns an effect handle,
or nil, reason: client_unavailable, world_unavailable, attachment_unavailable, or budget_exceeded.
Invalid options or unknown custom keys raise a Lua error. These methods are available in gameplay contexts
and block display-tick contexts. Read-only observational contexts without presentation access reject requests.
Play options are x/y/z, seed, optional attachTo = ctx.entity (or another scoped actor handle), and
offset = {x?,y?,z?} for attachments, with components −64–64. Attached effects follow a loaded actor and stop
when it disappears or changes world. An offset requires an attachment.
| Handle method | Behavior |
|---|---|
handle:isActive() |
Whether future emissions remain. A burst-only effect becomes inactive after its activation tick. |
handle:stop() |
Cancel future emissions. Existing sprites finish their lifetimes. Repeated calls are safe. |
handle:setPosition(x, y, z) |
Reposition an active unattached effect for its next emission tick; reject attached or inactive effects. |
Requests use the active script's owner; callbacks without an active script source use the definition's owner. Keep handles within that owning package. Owner unload/replacement and world changes remove its sprites immediately. Calls from another active script owner reject handle operations.
An effect requires key and emitters (1–8 entries). Optional viewDistance is 1–64 blocks, default 16.
importance is normal (default) or decorative; normal effects are processed before decorative effects.
| Emitter field | Rule |
|---|---|
particle |
Required registered particle type reference/key. |
shape |
Required point, sphere, box, or line descriptor below. |
burstCount |
0–256 on the activation tick, default 0. |
ratePerTick |
0–64, default 0; fractional values accumulate. |
durationTicks |
Required for positive rate, integer 1–1200. |
velocity |
Optional x/y/z numbers or {min,max} ranges, −16–16; default zero. Alternatively outward 0–16 plus optional y; outward cannot combine with x/z. |
scale |
Number or {min,max} in (0,4], default 1. |
color |
RGBA multiplier, default white. |
seedOffset |
Signed 32-bit offset for this emitter's sampling seed, default 0. |
Each emitter needs a positive burst or rate. The combined worst-case burst plus rate emissions cannot exceed 256 on a tick. Bursts and the first rate accumulation occur on the next unpaused client tick after play. Newly admitted sprites begin updating on the following tick.
Shapes use offsets from the effect origin:
| Shape | Fields |
|---|---|
{type="point"} |
Emit at the origin. Outward velocity is zero at that point. |
{type="sphere", radius=...} |
Radius (0,64]; uniform volume, or surface with surface=true. |
{type="box", halfExtents={x=...,y=...,z=...}} |
Each half-extent (0,64]; uniform volume, or area-weighted faces with surface=true. |
{type="line", from={x=...,y=...,z=...}, to={x=...,y=...,z=...}} |
Endpoints −64–64 per axis; uniform along the line. |
local flare = betamoon.particles.effects:add {
key = "mymod:particle_effect/flare",
emitters = {
{ particle = "mymod:particle/spark", shape = {type="sphere",radius=0.3}, burstCount = 12 },
{ particle = "mymod:particle/spark", shape = {type="point"}, ratePerTick = 0.5, durationTicks = 40 }
}
}local handle, reason = ctx.world:playParticleEffect("mymod:particle_effect/flare", {
attachTo = ctx.entity, offset = {y=1}, seed = 42
})onTick(ctx) runs once per unpaused client tick for each live sprite. ctx.age starts at 1,
ctx.lifetime is its sampled lifetime, and ctx.progress is age/lifetime capped at 1.
ctx.memory is an independent transient table for that sprite.
The tick saves the previous pose, advances age, evaluates declared size/color/rotation, calls onTick, then
applies gravity, motion/collision, drag, and lifetime expiry. Rendering interpolates the previous and current
pose. Size/color/rotation overrides last for this tick; gravity, drag, and velocity persist until changed.
onTick = function(ctx)
ctx.memory.turns = (ctx.memory.turns or 0) + 1
ctx.particle:setRotation(math.sin(ctx.progress * math.pi * 4) * 45)
ctx.particle:setSize(0.2 + math.sin(ctx.progress * math.pi) * 0.3)
ctx.particle:setAlpha(1 - ctx.progress)
end| Particle method | Result or accepted value |
|---|---|
getAge(), getLifetime()
|
Tick age and sampled lifetime. |
getRotation(), getSize(), getAlpha()
|
Current degrees, world size, and alpha. |
getPosition(), getVelocity()
|
Detached {x,y,z} snapshots. |
getColor() |
Detached {r,g,b,a} snapshot. |
setRotation(degrees) |
Finite degrees within ±1,000,000. |
setSize(size) |
0–16 world units. |
setAlpha(alpha) |
0–1. |
setColor(color) |
{r,g,b,a?}, channels 0–1; default alpha 1. |
setVelocity(x,y,z) |
Each −16–16 blocks/tick; also resumes a stopped sprite. |
setGravity(gravity) |
−1–1 blocks/tick², positive downward. |
setDrag(drag) |
0–1. |
remove() |
Remove this sprite. |
Particle access expires on callback return; do not retain ctx.particle. A callback error disables updates
for that instance, restores declarative presentation and prior motion settings, and reports its owner. Other
particles continue. A requested removal still takes effect if the callback later fails.
Default custom budgets are 4,096 active sprites per client, 1,024 per script owner, 1,024 new sprites per client
tick, 64 active effect handles per owner, and 128 resident authored sprite textures. .cfg settings
particles.maxActive and particles.maxPerTick accept 1–65,535; defaults remain 4,096 and 1,024 respectively.
The per-script active limit remains 1,024, even when the client-wide budgets are increased. Larger budgets
allow heavier workloads and do not guarantee a particular frame or tick rate.
particles.viewDistance can lower the distance cap, which defaults to 64; ordinary spawn and effect
declarations default to 16.
getStats() reports active, emitted, distanceCulled, budgetDropped, and failedDefinitions per owner.
Counts are local observations. Budget pressure can omit visuals, and seeded sampling does not guarantee the
same admitted visuals on different clients. Texture replacement failures retain the previous usable art.
Custom particles are transient presentation. Multiplayer delivery is unavailable: effect calls report the missing route, and direct custom spawns produce an owner diagnostic. They do not damage actors or illuminate blocks.
- Installation
- Install scripts
- Getting started
- VS Code and LuaLS
- Examples
- Reload and compatibility
- Troubleshooting
- Changelog
Scripts and shared values
- Script lifecycle
- Packaged Lua mods
- Modules
- References and stacks
- Queries
- Overrides
- Minecraft constants
- Chat and positions
- Structured data
- Script configuration
- Particles
Blocks and items
- Multiblocks
- Blocks
- Block state and placement
- Block shapes and rendering
- Block callbacks
- Block ticks and redstone
- Items
- Fluids
- Item callbacks
- Tools and armor
- Materials
- Textures
Entities
- Entities
- Entity access and movement
- Entity lifecycle and data
- Entity health and loot
- Entity inventories and relationships
- Projectiles and pickups
- Entity presentation and sounds
- Living entities and AI
- Entity parts and sensors
Assets and audio
- Assets
- Models and animations
- Built-in Minecraft models
- Model appearances
- Blockbench workflow
- Audio
- Texture-pack overrides
Events and world actions
World generation
World systems
Inventories and screens
Recipes