主题
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
.effectfile. - 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
.effectafter 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 withAddSpriteEmitter(),AddMeshEmitter(...),AddBeamEmitter(...), orAddRibbonEmitter(). - Attach with
particle.AddToNode(node)and control playback withParticleSystem.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 byTickSpeed; 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 mountedParticleEmitterand updateSetSourcePoint/SetEndPointeach 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 orApply()every frame to follow a Node:Apply()resets associated emitters. See the endpoint-following example indocs/systems/GameGraphAudioParticle.md.ExportEffect(...)is Debug-only. It writes under the current project'sres/RuntimeParticlesby default and returnsParticleEffectExportResult.NotDebugModeoutside 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 exampleeffect/custom/spark.png. Do not prefix paths withres/in code. - Prefer one clear source of initial motion. Do not casually combine
emitVelocity: trueon a shape location with a largeVelocity.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);
}
#endifParticleSystem.Replay,Advance,SetRandomSeedandSetDeterministicModeare static node-level APIs. Do not callReplayon aParticleSysteminstance.Seekremains incremental.Replayresets to the seed's initial state and replays;Advancecontinues from the current state and requires deterministic mode.Seekalso ignores requests over 100000 0.1s steps.- Time is playback seconds; actual simulated time = input × current
TickSpeed. FixTickSpeedto 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
RandomSeeddoes not recompute existing particles. UseReset,Play, orReplayto restart from that seed. The getter returns the recorded seed: a legacyAddToNodemount returns its mount seed, but only an explicit set/seed mount/Replaymakes 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,
PlayandResetonly reset the emitter; they do not resume automatic playback. CallSetDeterministicMode(node, false)(orTrySetDeterministicMode(false)on one emitter) beforePlayto restore normal updates. - First-phase scope excludes effects with
EmitterHierarchy: their replay eligibility is false, node-levelSetDeterministicModereturns false, the emitterDeterministicModesetter throws,TrySetDeterministicModereturns false, andAdvance/Replayreturn 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,TickSpeedand 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;
}
#endifBuilder 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(...)andMesh.Technique(...). For generated mesh particles, useEngineRes/Particle/common/materials/defaultMesh.materialwithEditor/Techniques/Lambert/ParticleLambert.xmlunless the project already has a tested custom particle technique. - Ribbon emitters should use
Ribbon.Render(...)andRibbon.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);
}
#endifUse 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:
- Build the project with
Client-Debug. - Copy the fresh
GameEntry.dlltoui/AppBundle/managed/GameEntry.dllif launching outside the editor. - Start client-only debug and capture a screenshot.
- Check the newest client log if textures, models, or exported files do not appear.