Skip to content

GameGraph 粒子与音频系统 ​

WasiCore 提供两级粒子和音频 API。选择哪一级取决于游戏架构:

层级粒子音频适用场景
Actor 层ActorParticle / ActorBeamActorSound数编驱动,通过 ActorScope 管理生命周期,与 Unit/Entity 绑定
GameGraph 层ParticleSystem / ParticleEmitterSoundSourceComponent / AudioManager纯代码控制,直接操作 Node,适合 SceneGraph 独立游戏

Actor 层与 GameGraph 层共享底层粒子和音频系统。Actor 层通过框架创建并管理表现对象,GameGraph 层则由项目 C# 代码直接控制 Node 和组件;这不意味着 Actor 内部通过 GameGraph 的 C# Builder 实现。

> 粒子、ActorSound、SoundResource、AudioManager 和按资源播放音频的 API 仅客户端可用。SoundSourceComponent、SoundSource3DComponent、SoundListenerComponent 的组件状态,以及 Stop/Seek、音量、频率、声像和衰减等部分控制可双端使用;服务端不会加载或输出实际声音。

> 与 Urho 心智模型的关系:仍可沿用“加载粒子资源 → 挂载到 Node → 控制一个或多个发射器”的基本思路,但星火粒子系统不是 Urho 原生粒子 API 的逐项复制。.effect 资源、运行时 Builder、Sprite/Mesh/Beam/Ribbon 发射器和具体控制能力应以本页及当前 SDK 声明为准。


ParticleSystem — 粒子系统 ​

基本用法 ​

csharp
#if CLIENT
// 1. 加载 .effect 文件
var effect = ParticleSystem.Load("effect/SampleSpells/WarStomp/WarStomp/particle.effect");

// 2. 挂载到节点(自动开始播放)
var particleNode = sceneRootNode.CreateChild("MyEffect");
effect.AddToNode(particleNode);

// 3. 控制播放
ParticleSystem.Stop(particleNode, killOnDeactivate: false);  // 停止发射,粒子自然消亡
ParticleSystem.Play(particleNode);                           // 重新播放
ParticleSystem.SetTickSpeed(particleNode, 2.0f);             // 2 倍速
ParticleSystem.SetUnitScale(particleNode, 0.5f);             // 缩小到一半

// 4. 查询状态
bool done = ParticleSystem.IsFinished(particleNode);
#endif

资源加载与实例 ​

ParticleSystem.Load(...) 通过资源缓存按“资源类型 + 逻辑路径”加载:同一路径再次加载返回同一个粒子定义资源,不是独立副本。AddToNode(...) 才在目标节点上创建该资源的发射器组件;同一资源可以挂到多个节点,各节点的发射器状态相互独立。需要重置某个已挂载效果时,使用 ParticleSystem.Play(node) 或发射器的 Reset();不要通过修改共享资源来区分不同节点的运行状态。

运行时构造粒子 ​

如果粒子不来自预制 .effect 文件,也可以在客户端运行时用 builder API 直接构造 ParticleSystem。该能力适合 AI 助手按代码动态生成特效、玩法原型、程序化视觉反馈和调试验证;它是纯客户端表现层 API,不参与服务端权威同步。

csharp
#if CLIENT
var particle = ParticleSystem.CreateRuntime();

var sparks = particle.AddSpriteEmitter()
    .Timing(duration: 1.2f, loops: 1, localSpace: true);
sparks.Spawn.Burst(48);
sparks.Lifetime.Range(0.45f, 0.9f);
sparks.Location.Range(new Vector3(-36, -36, 32), new Vector3(36, 36, 92));
sparks.Velocity.Range(new Vector3(-150, -150, 20), new Vector3(150, 150, 190));
sparks.Acceleration.Constant(new Vector3(0, 0, -180));
sparks.Size.Range(new Vector3(8), new Vector3(18), singleRandom: true);
sparks.Size.OverLife(1.0f, 0.05f);
sparks.Rotation.Range(0, 360).RateRange(-240, 240);
sparks.Color.OverLife(Color.FromArgb(255, 255, 220, 90), Color.FromArgb(0, 255, 70, 20));
sparks.Material
    .Texture(ParticleTextureSlot.Diffuse, "effect/effect_new1/effect_jiguang/eff_jiguang02/uv_once_t_flare.png")
    .Blend(ParticleBlendMode.AddAlpha)
    .UseVertexColor();

var beam = particle.AddBeamEmitter(new Vector3(0, 0, 0), new Vector3(0, 0, 260))
    .Timing(duration: 0.4f, loops: 1, localSpace: true);
beam.Beam.Options(maxBeamCount: 1, speed: 30f, interpolationPoints: 4, alwaysOn: true);
beam.Beam.Tiling(textureTile: 3, textureTileDistance: 100f);
beam.Beam.Taper(ParticleBeamTaperMode.Full, factor: 0.15f);
beam.Beam.Noise(frequency: 4, range: new Vector3(10, 10, 4));
beam.Lifetime.Constant(0.35f);
beam.Size.Constant(24f);
beam.Color.OverLife(Color.FromArgb(230, 120, 220, 255), Color.FromArgb(0, 80, 120, 255));
beam.Material
    .Texture(ParticleTextureSlot.Diffuse, "effect/eff_xujian/effect_emiti_003/uv_once_tex_fx_glow_016.png")
    .Blend(ParticleBlendMode.AddAlpha)
    .UseVertexColor();

var particleNode = sceneRootNode.CreateChild("RuntimeEffect");
particle.AddToNode(particleNode);
ParticleSystem.Play(particleNode);
#endif

v1 builder 覆盖的高价值模块:

类别API
发射器类型AddSpriteEmitter()、AddMeshEmitter(modelPath)、AddBeamEmitter(source, target)、AddRibbonEmitter()
时间与生成Timing(...)、Spawn.Rate(...)、Spawn.Burst(...)、Lifetime.Constant/Range(...)
位置Location.Box/Range(...)、Location.Sphere(...)、Location.Cylinder(...)、Location.Cone(...)、Location.MeshSurface(...)
运动Velocity.Range(...)、Velocity.ScaleOverLife(...)、Velocity.AbsoluteOverLife(...)、Velocity.Orbit(...)、Acceleration.Constant/Range(...)
外观Size.Constant/Range/OverLife(...)、Rotation.Range/RateRange/RateOverLife(...)、Color.OverLife(...)、Facing(...)
材质Material.Texture(...)、Blend(...)、UseVertexColor(...)、UseMask/UseNoise/UseDissolve(...)、SoftParticle(...)、TextureRotation(...)
UV/类型专用SubUV.Grid/Frame/Random(...)、Mesh.Material/Technique/RotationRange/RotationRateRange(...)、Beam.Tiling/Taper/Noise(...)、Ribbon.Options/Behavior/Render/Tessellation(...)

ExportEffect(directory, overwrite) 可以把运行时构造结果保存为 .effect,但它只在 Debug 模式可用。默认写入当前项目的 res/RuntimeParticles 目录;非 Debug 模式会返回 ParticleEffectExportResult.NotDebugMode。

Mesh 发射器默认会使用可渲染的粒子材质;需要更明确外观时,可用 Mesh.Material(...) 指定材质资源,并用 Mesh.Technique(...) 覆盖 technique。生成 mesh 粒子的推荐覆盖 technique 是 Editor/Techniques/Lambert/ParticleLambert.xml,它使用当前已缓存的 Lambert 粒子渲染路径。Ribbon 发射器可通过 Ribbon.Render(...) 调整截面朝向和 sheet 数,通过 Ribbon.Tessellation(...) 调整拖尾细分和平滑程度。

节点级 vs 发射器级控制 ​

ParticleSystem 的静态方法(Play、Stop、SetTickSpeed、SetUnitScale 等)作用于节点上所有发射器。如需对单个发射器独立控制:

csharp
#if CLIENT
// 获取所有发射器
ParticleEmitter[] emitters = ParticleSystem.GetEmitters(particleNode);

// 独立控制第 0 个发射器
emitters[0].TickSpeed = 0.5f;          // 半速
emitters[0].UnitScale = 2.0f;          // 放大
emitters[0].SpawnRateFactor = 0.3f;    // 降低生成率
emitters[0].Stop(killOnDeactivate: true);  // 立即停止并销毁粒子
emitters[0].Reset();                   // 重置并重新开始

// Beam 类型发射器的专用 API
emitters[0].SetSourcePoint(startPos);
emitters[0].SetEndPoint(endPos);
emitters[0].BeamScale = 1.5f;
#endif

ActorBeam、Beam Builder 与端点跟随 ​

ActorBeam 和 ParticleSystem 中的 Beam 发射器使用同一套底层光束粒子能力。它们的职责如下:

入口职责
ActorBeam + GameDataActorBeam通过 Beam 引用 GameDataParticle 光束资源,由框架管理播放、持续时间、两端单位/挂点跟随和生命周期
ParticleSystem加载 .effect 或用 CreateRuntime() 创建粒子资源,再通过 AddToNode(node) 创建运行中的发射器
ParticleEmitterBeamBuilder通过 AddBeamEmitter(...).Beam 配置资源中的端点、插值、噪声、平铺和收束
ParticleEmitter控制挂载后的发射器实例,通过 SetSourcePoint / SetEndPoint 更新运行时光束端点

ParticleEmitterBeamBuilder.Points(source, target) 保存两个坐标值,当前没有接收 Node 并自动跟随的绑定接口。ActorBeam 的单位端点跟随由框架每帧读取挂点位置、换算坐标并更新粒子端点完成;直接使用 GameGraph 时,需要由项目代码完成同样的更新。

下面假设 particle 已通过 AddBeamEmitter(...) 配好一个 Beam 发射器及其材质,beamNode 是专用粒子节点,targetNode 是要跟随的目标或挂点节点:

csharp
#if CLIENT
particle.AddToNode(beamNode);
var emitter = ParticleSystem.GetEmitter(beamNode, 0)!;

// 创建后执行一次,并接入项目的每帧更新,在目标移动后调用。
void UpdateBeamPoints()
{
    // 起点固定在粒子宿主原点,终点跟随目标。
    emitter.SetSourcePoint(Vector3.Zero);
    emitter.SetEndPoint(beamNode.WorldToLocal(targetNode.Position));
}
UpdateBeamPoints();
#endif

Node.Position 是世界坐标,运行时 Beam 端点应转换到粒子宿主 beamNode 的局部空间。两端都跟随节点时,起点同样使用 beamNode.WorldToLocal(sourceNode.Position);挂点有局部偏移时,先通过 targetNode.LocalToWorld(localOffset) 得到世界位置,再转换到 beamNode 局部空间。注意场景偏移,并优先使用无额外旋转、单位缩放的专用粒子节点。

每帧只更新运行时 ParticleEmitter,无需重复调用 Builder、AddToNode 或 Apply();Apply() 会应用资源配置并重置关联发射器。目标离开场景、被销毁或光束结束时,应停止更新并按项目生命周期释放表现对象。ActorBeam 已管理端点失效时的隐藏与恢复,需要这类单位光束行为时可直接使用它,见 ActorBeam。

ParticleEmitter 完整 API ​

属性/方法类型说明
Reset(forceOneShot)void重置并重新发射
Stop(killOnDeactivate)void停止发射
IsFinishedbool是否播放完毕
TickSpeedfloat (set)播放速率倍数
Seek(tickTime)void按指定秒数增量推进(不是绝对时间定位;超过 100000 个 0.1 秒步的请求忽略)
RandomSeeduint初始/复位随机种子(客户端能力 10)
DeterministicModebool手动回放模式;不支持时 setter 抛出异常(客户端能力 10)
Advance(seconds, fixedStep)bool从当前状态按固定步长推进(客户端能力 10)
Replay(seed, absoluteTime, fixedStep)bool从初始状态重放到绝对播放时刻(客户端能力 10)
UnitScalefloat单位缩放
SpawnRateFactorfloat生成速率因子 (0~1)
EmitterLoopsint (set)循环次数,0 = 无限
AutoRemoveModeAutoRemoveMode播放完成后自动移除
SetEndPoint(Vector3)voidBeam 终点
SetSourcePoint(Vector3)voidBeam 起点
BeamScalefloat (set)Beam 缩放

确定性回放(客户端能力 10) ​

粒子默认在挂载时从引擎随机源取得初始状态;同一 .effect、同一目标时刻不能直接复现。需要逐帧比对时,用实例种子挂载或设置 seed,再从 Reset 状态按固定步长推进。此能力仅客户端,最低客户端能力版本为 10;调用前检查 ClientEnvironment.Runtime.Supports(ClientRuntimeFeature.ParticleDeterminism)。

csharp
#if CLIENT
using GameCore.ClientEnvironment;
using GameGraph.ResourceSystem;

if (ClientEnvironment.Runtime.Supports(ClientRuntimeFeature.ParticleDeterminism))
{
    var effect = ParticleSystem.Load("effect/.../particle.effect");
    var node = sceneRoot.CreateChild("ReplayEffect");
    effect.AddToNode(node, randomSeed: 12345u);

    // 绝对时刻:从 Reset 状态重放到 0.8 播放秒;Replay 是 static。
    if (!ParticleSystem.Replay(node, 12345u, absoluteTimeSeconds: 0.8f, fixedStepSeconds: 0.05f))
    {
        // 参数无效、步数超过上限或没有可用发射器
    }
}
#endif

需要手动步进时:

csharp
ParticleSystem.SetRandomSeed(node, 12345u);
ParticleSystem.Play(node);
if (ParticleSystem.SetDeterministicMode(node, true))
{
    ParticleSystem.Advance(node, 0.1f, 0.05f);
    // ...
    ParticleSystem.SetDeterministicMode(node, false);
}

单发射器可使用 emitter.RandomSeed、emitter.DeterministicMode / emitter.TrySetDeterministicMode(...)、emitter.Advance(...) 和 emitter.Replay(...)。Replay 会恢复 seed 对应的初始状态;Advance 从当前状态继续。

  • ParticleSystem.Replay / Advance / SetRandomSeed / SetDeterministicMode 是 static 节点级入口;不要对 ParticleSystem 实例调用 Replay。节点上所有发射器都必须具备确定性回放资格,任一发射器带 EmitterHierarchy 时节点级模式设置/推进/回放返回 false 且不改变状态。
  • Seek 仍是增量推进,不是绝对定位;单次超过 100000 个 0.1 秒步的请求会被忽略。Replay 从 Reset 状态重放。Replay / Advance 的时间为播放秒,实际粒子时间还会乘当前 TickSpeed;绝对回放应把 TickSpeed 固定为 1,或在调用前按倍率换算。
  • Advance / Replay 要求时间有限且非负、步长为正,完整步数不能超过 100000;默认固定步长为 0.05 秒。参数或能力预检失败返回 false,不会先改变状态。
  • RandomSeed 的 setter 会立即改变后续随机抽取,但不会重算已有粒子;需要 Reset / Play / Replay 才从该 seed 的初始状态重新开始。getter 返回当前记录的种子:旧 AddToNode 挂载路径返回 mount seed;只有显式设置(setter、AddToNode(node, seed) 或 Replay)才会把它作为 Reset 的恢复目标。旧 mount seed 下 Reset 仍继续原有随机序列。
  • SetRandomSeed(node, seed) 按节点上的发射器组件顺序派生种子,不自动 Reset;增删或重排发射器后序号会变化,需要重新设置。需要稳定实例身份时,优先在挂载阶段用 AddToNode(node, seed),或使用单发射器入口;同一节点挂多个 ParticleSystem 时要保持顺序稳定。
  • DeterministicMode 会暂停该发射器的自动推进并暂时关闭自动移除;离开后恢复进入前状态。用 TrySetDeterministicMode 可在不支持时返回 false 而不抛异常。
  • 在确定性模式中,Play / Reset 只重置发射器,不会恢复自动播放;要恢复自动推进,先 ParticleSystem.SetDeterministicMode(node, false)(单发射器用 TrySetDeterministicMode(false))再 Play。
  • 首期不支持带 EmitterHierarchy 的效果:这类效果的确定性回放资格为 false;节点级 SetDeterministicMode 返回 false,单发射器 DeterministicMode setter 抛出 InvalidOperationException,TrySetDeterministicMode 返回 false;Advance / Replay 返回 false 且不改变状态。未启用确定性回放时,自动播放不受影响。
  • 对于不带 EmitterHierarchy 的效果,回放只覆盖发射器内部状态。宿主节点应保持静止;Beam 端点、AnimTrail 骨骼轨迹等外部逐帧输入需要按相同步长自行重放。回放会按相同 seed 和步长推进已覆盖的随机源,不承诺跨 GPU/平台逐像素一致。
  • 同一资源挂载多个实例时,自定义 burst 的触发标志和粒子灯光的发射累计值由各发射器分别保存;重置或推进一个实例不会消费另一个实例的这些运行状态。显式 seed 或确定性模式下,锥台形状采样也使用该发射器的随机流。

兼容边界见客户端二进制兼容与能力版本。

ParticleSystem 资源属性 ​

属性/方法类型说明
EffectCountuint效果/发射器定义数量
SpawnRateFactorfloat全局生成速率因子
IsInfinitebool是否包含无限持续的发射器
PredictDuration(forced)float预测持续时间(秒),0 = 无限

注意事项 ​

  • .effect 路径相对于资源根目录,来自 effect/ 目录(例如 effect/eff_tongyong/huoqiu2/particle.effect)
  • 自定义粒子如果未上传云但需要随项目发布,应放到项目 res/effect/...,代码中仍从 effect/... 开始写。不要放到 ui/ 或 user_files。
  • AddToNode 后粒子立即开始播放;如需延迟播放,调用后立即 Stop
  • Stop(killOnDeactivate: false) 停止发射但现有粒子自然消亡;killOnDeactivate: true 立即清除所有粒子
  • 粒子节点的生命周期需要手动管理(挂在 SceneGraph 下的节点随 SceneGraph.Dispose 一起销毁)
  • Seek 是相对当前状态推进,不是绝对定位;只处理有限正值,单次超过 100000 个 0.1 秒步的请求会被忽略且状态保持不变,避免同步执行过多步。正常调用按 0.1 秒分片,末尾余量(含恰好 0.1 秒)再推进一次,推进量按 TickSpeed 缩放;因此 Seek(total) 与多次 Seek 不保证完全相同的浮点分步结果。需要逐帧复现时,固定重置点、步长、TickSpeed、发射器挂载顺序和渲染配置;不要在自动更新、不同帧率或不同切片方式之间直接比较。
  • 未显式设置 seed 时,粒子初始随机状态由挂载时的引擎随机源决定;显式 seed 后,Reset / Play / Replay 才从该 seed 的初始状态重新开始。仅固定 .effect 路径和目标时间不足以复现同一帧。

SoundSourceComponent — 音频系统 ​

SoundSourceComponent、SoundSource3DComponent 和 SoundListenerComponent 可在双端创建。Stop/Seek、播放状态、音量、频率、声像和 3D 衰减等成员使用共享接口;加载 SoundResource、调用 Play(sound)、访问 AudioManager 和产生实际声音仅在客户端可用。服务端使用这些组件时,应把它们视为可同步或可查询的场景状态,而不是音频输出设备。

基本用法 ​

csharp
#if CLIENT
// using GameCore.ResourceType;

// 1. 创建音源节点和组件
var soundNode = sceneRootNode.CreateChild("GameSound");
var soundSource = soundNode.CreateComponent<SoundSourceComponent>();
soundSource.SoundType = "Effect";  // 声音分组(用于全局音量控制)
soundSource.Gain = 0.8f;

// 2. 加载并播放音频
Sound soundPath = "sound/a2_SFX/Action/MoveAction/Action_Body_FallDown.ogg";
var sound = SoundResource.Load(soundPath);
if (sound != null)
    soundSource.Play(sound);

// 3. 控制播放
soundSource.Stop();
bool playing = soundSource.IsPlaying;
float pos = soundSource.TimePosition;
#endif

3D 空间音效 ​

使用 SoundSource3DComponent 替代 SoundSourceComponent,声音会根据距离监听器的远近衰减:

csharp
#if CLIENT
// 3D 音源
var source3D = soundNode.CreateComponent<SoundSource3DComponent>();
source3D.NearDistance = 100f;   // 100 厘米内满音量
source3D.FarDistance = 5000f;   // 5000 厘米外静音

// 必须设置监听器(通常挂在相机节点上)
var listener = cameraNode.CreateComponent<SoundListenerComponent>();
AudioManager.SetListener(listener);
#endif

俯视镜头与战斗音效验收 ​

长焦俯视镜头可能离玩法平面很远。若监听器直接挂在相机上,所有音源都可能超出 FarDistance,表现为资源加载成功、IsPlaying 为真但听不到声音。按实际场景尺度选择玩法平面附近的监听器,或使用 2D SoundSourceComponent,由项目按玩法距离计算音量、按镜头屏幕左右方向计算 Panning;世界 +X 不一定是屏幕右侧。

连续射击可使用有上限的音源池,并为重要命中、死亡等反馈安排优先级。重开时停止本功能的旧音源,退出时销毁自有节点;修改全局监听器或分组音量的功能需要管理其恢复。

验收时同时检查 MixOutput、Gain、Panning、TimePosition 和实际听感或音频回录。资源非空、播放计数增加或 IsPlaying 为真都不能单独证明实际输出。空闲与战斗区间对照有助于确认录到的是游戏声音。

AudioManager 全局控制 ​

csharp
#if CLIENT
// 按声音分组控制主音量
AudioManager.SetMasterGain("Effect", 0.5f);   // 特效音量 50%
AudioManager.SetMasterGain("Music", 0.3f);    // 音乐音量 30%
float gain = AudioManager.GetMasterGain("Effect");

// 设置/获取活跃监听器
AudioManager.SetListener(listenerComponent);
var current = AudioManager.GetListener();
#endif

SoundSourceComponent 完整 API ​

属性/方法类型可用端说明
Play(sound)void客户端播放音频
Play(sound, frequency)void客户端以指定频率播放
Play(sound, frequency, gain)void客户端以指定频率和音量播放
Play(sound, frequency, gain, panning)void客户端以指定频率、音量和立体声位置播放
Stop()void双端停止播放
Seek(time)void双端跳转到指定时间(秒)
IsPlayingbool双端是否正在播放
TimePositionfloat双端当前播放位置(秒)
SoundTypestring双端声音分组("Effect"、"Music" 等)
Frequencyfloat双端播放频率 (Hz)
Gainfloat双端音量 (0.0~1.0)
Attenuationfloat双端衰减系数
Panningfloat双端立体声平移 (-1.0 左 ~ 1.0 右)
AutoRemoveModeAutoRemoveMode双端播放完成后自动移除模式
MixOutputbool双端是否混合到音频输出(构造函数默认 true)

注意事项 ​

  • 音频文件可使用 .wav 或 .ogg,路径相对于资源根目录(例如 sound/a2_SFX/Action/MoveAction/Action_Body_FallDown.ogg 或 sound/custom/click.wav)
  • 自定义音频如果未上传云但需要随项目发布,应放到项目 res/sound/...,代码中仍从 sound/... 开始写。不要放到 ui/ 或 user_files。
  • SoundResource.Load 的参数类型是 GameCore.ResourceType.Sound;Sound 支持从 string 和 UTF8String 隐式转换,所以字符串字面量、string 变量和 UTF8String 变量都可以直接传入。
  • 项目 res/sound/... 中的音频随项目包封装,无需额外资源统计。需要客户端预下载的云音频才依赖发布清单:资源统计主要识别 IFile 类型字段,以及参数字面量直接转发到 IFile 字段的轻量 helper。这类资源若仅由 SoundResource.Load(...) 加载,应在 GameDataSound.Asset 或其他 Sound / IFile 类型字段中保留同一路径,并用 Client-Resource 验证。运行时 new GameDataSound 批量注册不能替代静态资源声明。
  • SoundSourceComponent 是 2D 音源(无空间衰减),适合 UI 音效和背景音乐
  • SoundSource3DComponent 是 3D 空间音源,必须配合 SoundListenerComponent 使用
  • 2D 音源(SoundSourceComponent)不需要 SoundListenerComponent 即可播放
  • SoundType 字符串区分大小写,常用值:"Effect"、"Music"、"Ambient"

从 Actor 层桥接到 GameGraph 层 ​

ActorParticle 提供类型明确的粒子发射器下钻扩展。普通代码不需要先取得任意 Actor 的 Node:

csharp
#if CLIENT
// 获取 ActorParticle 的底层粒子发射器
ActorParticle particle = myParticleLink.Data?.CreateActor(scope) as ActorParticle;

// 方式 1:通过扩展方法直接获取发射器
ParticleEmitter[]? emitters = particle.GetParticleEmitters();
int count = particle.GetParticleEmitterCount();
ParticleEmitter? first = particle.GetParticleEmitter(0);

// 如需检查表现节点,可使用明确的客户端 visual-only 探测入口
if (particle.TryGetClientVisualNode(out Node? node) && node != null)
{
    ParticleEmitter[] emitters2 = ParticleSystem.GetEmitters(node);
}

// 典型场景:对 Actor 层粒子做运行时微调
if (emitters != null)
{
    foreach (var e in emitters)
    {
        e.TickSpeed = 1.5f;
        e.UnitScale = 0.8f;
    }
}
#endif

ActorSound 自身已提供常用的 Play/Stop/Volume/Mute/Pause API,通常无需桥接到底层 GameGraph 层。如果确实需要精细控制,可以通过 actorSound.GetSoundSource() 获取底层 SoundSourceComponent,再调整 Frequency、Panning、Attenuation、MixOutput 等参数。


选型速查 ​

需求推荐方案
数编配置的技能/Buff 特效ActorParticle + ActorScope
两端跟随单位/挂点的数编光束ActorBeam + GameDataActorBeam
纯代码配置光束并自行管理 Node 跟随ParticleSystem + Beam Builder + 运行时 ParticleEmitter
数编配置的音效ActorSound + ActorScope
SceneGraph 独立游戏中的粒子ParticleSystem + ParticleEmitter
SceneGraph 独立游戏中的音效SoundSourceComponent + SoundResource
对 Actor 粒子做发射器级微调actor.GetParticleEmitters() 桥接
UI 全局音量控制AudioManager.SetMasterGain()