Skip to content

API Particles

Piggo41 edited this page Oct 16, 2026 · 3 revisions

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.

Registries and references

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.

Particle type fields

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 }

Spawning and playing effects

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.

Effect fields and emitters

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
})

Scripted updates

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.

Limits and compatibility

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.

Clone this wiki locally