Skip to Content

particles

Create and control game particle effects: particles.create, PATTACH_* attachments, control points, paths and effect lifetime.


Creates particle effects through the client's game particle manager. Call only from callbacks or top-level script code; every Lua call runs on the game thread, which the particle manager requires.

particles.create

particles.create(name, attach?, entity?)

Creates the effect name, a .vpcf resource path such as 'particles/entity/spectator_utility_trail.vpcf'.

  • attach: one of the particles.PATTACH_* constants below; default particles.PATTACH_CUSTOMORIGIN. Only PATTACH_CUSTOMORIGIN and PATTACH_WORLDORIGIN are known to work without an entity.
  • entity: a base_entity_t (the object entitylist.*, entitylist.list() and gameevent keys return), or nil for none. The table below says what each attachment does with it. The effect belongs to the entity: when the entity is destroyed the engine destroys the effect, and the handle's methods quietly do nothing until release(), unload or the next level change drops it. The reference read but ignored this argument.

Returns: a particle handle, or nil for an empty name or while the client has no particle manager. Outside a game the handle is returned but no effect is visible.

Errors: name is not a string; attach is not one of the constants (expected a particles.PATTACH_* value); entity is neither an entity nor nil (expected base_entity_t or nil); an entity with PATTACH_WORLDORIGIN (PATTACH_WORLDORIGIN does not attach to an entity; ...).

The engine places control point 0 from the attachment and the entity; create itself sets no control point. Each value goes to the engine unchanged:

ConstantValueWith an entity, control point 0 is
particles.PATTACH_ABSORIGIN0the entity's origin when the effect is created; it does not follow
particles.PATTACH_ABSORIGIN_FOLLOW1the entity's origin and rotation, every frame
particles.PATTACH_CUSTOMORIGIN2never placed by the entity: set it with p:control_point(0, v). The entity is still control point 0's entity (an effect that wraps a model takes the model from it) and owns the effect
particles.PATTACH_CUSTOMORIGIN_FOLLOW3the entity's origin and rotation, every frame. The game puts its own body effects (the taser hit) on a player this way
particles.PATTACH_POINT4a model attachment when created; create names none, so the engine uses attachment index 0
particles.PATTACH_POINT_FOLLOW5the same attachment, every frame
particles.PATTACH_EYES_FOLLOW6the entity's eye position and eye angles
particles.PATTACH_OVERHEAD_FOLLOW7the overhead point the entity reports, every frame
particles.PATTACH_WORLDORIGIN8not allowed: this type puts control point 0 at the world origin (0, 0, 0) with no entity, so passing one is an error. Without an entity, move the effect with p:control_point
particles.PATTACH_ROOTBONE_FOLLOW9the model's root bone, every frame
particles.PATTACH_RENDERORIGIN_FOLLOW10the entity's origin, every frame
particles.PATTACH_CENTER_FOLLOW13the entity's centre, every frame

An effect may read more control points than 0. particles/blood_impact/impact_taser_bodyfx.vpcf, for example, wraps sparks around the model of control point 0's entity, takes their layout from a snapshot the effect itself puts on control point 1, and ends by itself after 2 seconds. Effects that end by themselves leave the handle valid; create the effect again to replay it.

-- The taser hit effect on your own model, as the game puts it on a hit player. Visible in third person.
local local_pawn = entitylist.local_pawn()
if local_pawn ~= nil then
    local fx = particles.create('particles/blood_impact/impact_taser_bodyfx.vpcf', particles.PATTACH_CUSTOMORIGIN_FOLLOW, local_pawn)
end

particle

MethodReturns / effect
p:name()The name passed to particles.create.
p:is_valid()true while the effect exists; false after release, unload or a level change.
p:control_point(index, vector)Sets control point index (integer >= 0) to vector, applied immediately. What a control point means depends on the effect. No values.
p:path(index, points)Uploads points, a table of vectors, as a path attached to control point index. Fewer than two points do nothing; elements that are not vectors are skipped. Each call replaces the previous path; the points are spaced evenly in creation time from first to last. No values.
p:release()Removes every particle of the effect immediately. A second call does nothing. No values.
tostring(p)particle<name>.

Errors: a negative or non-integer control point index (expected a non-negative control point index); a control_point value that is not a vector (vector, vector* or {x, y, z} expected); a path argument that is not a table.

Particle lifetime

  • Every effect a script created is released when the script unloads or reloads and at every level change (map load or level shutdown).
  • Methods on a released handle do nothing, and is_valid() returns false. A handle never reaches another effect.
  • Dropping or garbage-collecting a handle does not stop the effect. Keep the handle and call release(), or let unload or the level change remove it.
local from = vector(-211, -1567, 10)
local to = vector(-597, -1832, -116)
local points = {}
for i = 0, 23 do
    local t = i / 23
    points[#points + 1] = from + (to - from) * t
end
-- particles.create(name, particles.PATTACH_*, entity)
local trail = particles.create('particles/entity/spectator_utility_trail.vpcf')
if trail ~= nil then
    trail:path(0, points)
    trail:control_point(16, vector(255, 0, 0)) -- color
    trail:control_point(3, vector(2, 1, 1))    -- lifespan, width, alpha
end
-- trail:release()

This draws a red trail along the 24 points. trail:release() removes it; otherwise it stays until the script unloads or the level changes.

On this page