Skip to content

Runtime Particle Builder Skill ​

Use this skill when the task is to generate or tune a particle effect directly in C# code. This is for client-side visual effects and previews, not server-authoritative gameplay state.

System reference: docs/systems/GameGraphAudioParticle.md. API reference: api/client/GameGraph_ResourceSystem.cs. Sample project: map_templates/code_sample/src/RuntimeParticleBuilderSample/.

When To Use ​

  • The user asks AI to create a particle without preparing a .effect file.
  • The effect is procedural, temporary, or part of a GameGraph/code sample.
  • The task needs sprite, mesh, beam, or ribbon emitters configured by code.
  • The user wants Debug export to .effect after previewing a runtime-built effect.

Prefer existing ParticleSystem.Load("effect/.../particle.effect") when the project already has an authored effect asset and only needs playback. Prefer ActorParticle / GameDataActorParticle when the effect belongs to Unit/Ability/Buff data and should be managed by ActorScope. Prefer ActorBeam / GameDataActorBeam for data-driven beams whose endpoints follow units or sockets. ActorBeam and GameGraph Beam emitters share the underlying particle system; ActorBeam manages endpoint following and lifetime through the framework, without using the C# Builder internally.

Core Rules ​

  • Runtime particle builder APIs are client-only. Wrap use in #if CLIENT.
  • Create with ParticleSystem.CreateRuntime(), then add emitters with AddSpriteEmitter(), AddMeshEmitter(...), AddBeamEmitter(...), or AddRibbonEmitter().
  • Attach with particle.AddToNode(node) and control playback with ParticleSystem.Play/Stop/SetTickSpeed/SetUnitScale.
  • ParticleSystem.Load(...) uses the resource cache: the same path returns the same shared particle definition. AddToNode(...) creates independent emitter components for each node. Do not mutate a loaded resource expecting per-instance state.
  • ParticleEmitter.Seek(tickTime) advances relative to the current state, scaled by TickSpeed; it is not an absolute timeline seek. Non-finite, non-positive, or more-than-100000-step (0.1s step) requests are ignored without changing state. For repeatable captures, fix the reset point, step size, emitter mount order and render settings; do not compare different frame rates or seek chunking.
  • ParticleEmitterBeamBuilder.Points(...) configures coordinate values, not Node bindings. For moving endpoints, get the mounted ParticleEmitter and update SetSourcePoint / SetEndPoint each frame; convert target world positions into the particle owner's local space. Stop updates when a target or the effect is removed. Do not call Builder methods or Apply() every frame to follow a Node: Apply() resets associated emitters. See the endpoint-following example in docs/systems/GameGraphAudioParticle.md.
  • ExportEffect(...) is Debug-only. It writes under the current project's res/RuntimeParticles by default and returns ParticleEffectExportResult.NotDebugMode outside Debug.
  • Do not rely on runtime particles for server synchronization. If a server event should trigger a visual, synchronize the gameplay event or replicated node, then create the particle locally on the client.
  • Use texture/resource paths relative to res/, for example effect/custom/spark.png. Do not prefix paths with res/ in code.
  • Prefer one clear source of initial motion. Do not casually combine emitVelocity: true on a shape location with a large Velocity.Range(...); the two velocities add together and can make particles fly much farther than intended.

Deterministic Replay ​

Use deterministic replay for golden-image checks, frame-by-frame VFX review, or aligning art and code on the same particle frame. It requires client capability 10; check ClientEnvironment.Runtime.Supports(ClientRuntimeFeature.ParticleDeterminism) before using the new APIs.

csharp
#if CLIENT
if (ClientEnvironment.Runtime.Supports(ClientRuntimeFeature.ParticleDeterminism))
{
    effect.AddToNode(node, randomSeed: 12345u);
    ParticleSystem.Replay(node, 12345u, absoluteTimeSeconds: 0.8f, fixedStepSeconds: 0.05f);
}
#endif
  • ParticleSystem.Replay, Advance, SetRandomSeed and SetDeterministicMode are static node-level APIs. Do not call Replay on a ParticleSystem instance.
  • Seek remains incremental. Replay resets to the seed's initial state and replays; Advance continues from the current state and requires deterministic mode. Seek also ignores requests over 100000 0.1s steps.
  • Time is playback seconds; actual simulated time = input × current TickSpeed. Fix TickSpeed to 1 for absolute-time captures, or convert explicitly.
  • Inputs must be finite, nonnegative and use a positive step; one call may advance at most 100000 steps (default step 0.05s). Invalid input or precheck failure returns false without changing state.
  • Setting RandomSeed does not recompute existing particles. Use Reset, Play, or Replay to restart from that seed. The getter returns the recorded seed: a legacy AddToNode mount returns its mount seed, but only an explicit set/seed mount/Replay makes it the Reset target. Node-level seeds are derived from emitter component order; adding, removing or reordering emitters requires setting the seed again. Prefer one particle system per node or the emitter-level API when order can change.
  • In deterministic mode, Play and Reset only reset the emitter; they do not resume automatic playback. Call SetDeterministicMode(node, false) (or TrySetDeterministicMode(false) on one emitter) before Play to restore normal updates.
  • First-phase scope excludes effects with EmitterHierarchy: their replay eligibility is false, node-level SetDeterministicMode returns false, the emitter DeterministicMode setter throws, TrySetDeterministicMode returns false, and Advance/Replay return false without changing state. Normal automatic playback is unaffected when deterministic replay is not enabled.
  • For effects without EmitterHierarchy, replay covers emitter state only. Keep the host node static and replay external inputs such as beam endpoints or AnimTrail skeleton transforms at the same step sequence. The same seed, step sequence, TickSpeed and render settings reproduce the covered simulation; pixels are not guaranteed identical across GPUs or platforms.
  • Custom burst flags and particle-light rate accumulators are per emitter, including normal playback. Cone-frustum sampling uses the emitter random stream with an explicit seed or deterministic mode. When checking instance isolation, mount the same resource twice and reset/advance one instance while observing the other; do not change shared resource configuration.

See docs/guides/ClientRuntimeCompatibility.md#粒子确定性回放的旧客户端边界 before shipping to clients that may use an older native runtime or a mixed dependency package.

Minimal Pattern ​

csharp
#if CLIENT
using GameCore.ResourceType;
using GameGraph.NodeSystem;
using GameGraph.ResourceSystem;
using System.Numerics;

static Node CreateSparkBurst(Node parent)
{
    var particle = ParticleSystem.CreateRuntime();

    var sparks = particle.AddSpriteEmitter()
        .Timing(duration: 0.9f, loops: 1, localSpace: true);
    sparks.Spawn.Burst(64);
    sparks.Lifetime.Range(0.35f, 0.8f);
    sparks.Location.Range(new Vector3(-28, -28, 30), new Vector3(28, 28, 85));
    sparks.Velocity.Range(new Vector3(-130, -130, 20), new Vector3(130, 130, 180));
    sparks.Acceleration.Constant(new Vector3(0, 0, -180));
    sparks.Size.Range(new Vector3(6), new Vector3(16), singleRandom: true);
    sparks.Size.OverLife(1.0f, 0.05f);
    sparks.Rotation.Range(0, 360).RateRange(-220, 220);
    sparks.Color.OverLife(
        Color.FromArgb(255, 255, 220, 90),
        Color.FromArgb(0, 255, 80, 20));
    sparks.Material
        .Texture(ParticleTextureSlot.Diffuse, (ResourceTexture)"effect/effect_new1/effect_jiguang/eff_jiguang02/uv_once_t_flare.png")
        .Blend(ParticleBlendMode.AddAlpha)
        .UseVertexColor();

    var node = parent.CreateChild("SparkBurst")!;
    particle.AddToNode(node);
    ParticleSystem.Play(node);
    return node;
}
#endif

Builder Checklist ​

  • Timing: Timing(...), Duration(...), Loops(...), Delay(...), LocalSpace(...), Prewarm(...).
  • Spawn: Spawn.Rate(...), Spawn.Burst(...), Spawn.ClearBursts().
  • Lifetime and shape: Lifetime.Constant/Range(...), Size.Constant/Range/OverLife(...), Color.Constant/OverLife(...).
  • Motion: Location.Box/Sphere/Cylinder/Cone/MeshSurface(...), Velocity.Range/ScaleOverLife/AbsoluteOverLife/Orbit(...), Acceleration.Constant/Range(...).
  • Orientation: Facing(...), ScreenAlignment(...), AxisLock(...), Stretched(...), Rotation.Range/RateRange/RateOverLife(...).
  • Material: Material.Texture(...), Blend(...), UseVertexColor(...), UseMask(...), UseNoise(...), UseDissolve(...), SoftParticle(...).
  • Type-specific: SubUV.Grid/Frame/Random(...), Mesh.Model/Options/Material/Technique/RotationRange(...), Beam.Points/Options/Tiling/Taper/Noise(...), Ribbon.Options/Behavior/Render/Tessellation(...).
  • Mesh emitters can override their render material with Mesh.Material(...) and Mesh.Technique(...). For generated mesh particles, use EngineRes/Particle/common/materials/defaultMesh.material with Editor/Techniques/Lambert/ParticleLambert.xml unless the project already has a tested custom particle technique.
  • Ribbon emitters should use Ribbon.Render(...) and Ribbon.Tessellation(...) when the trail looks too flat, too sparse, or too faceted. Keep tessellation values moderate for AI-generated samples unless the user asks for a dense, smooth trail.

Debug Export ​

csharp
#if CLIENT
var result = particle.ExportEffect("RuntimeParticles/MyEffect", overwrite: true);
if (result.IsSuccess)
{
    Game.Logger.LogInformation("Particle exported: {Path}", result.Data);
}
else
{
    Game.Logger.LogWarning("Particle export failed: {Result}", result.Result);
}
#endif

Use export only as a debug convenience. The primary runtime path should still work without an exported .effect file.

Verification ​

For visual checks, use client-only debug when possible:

  1. Build the project with Client-Debug.
  2. Copy the fresh GameEntry.dll to ui/AppBundle/managed/GameEntry.dll if launching outside the editor.
  3. Start client-only debug and capture a screenshot.
  4. Check the newest client log if textures, models, or exported files do not appear.