主题
GameGraph 粒子与音频系统
WasiCore 提供两级粒子和音频 API。选择哪一级取决于游戏架构:
| 层级 | 粒子 | 音频 | 适用场景 |
|---|---|---|---|
| Actor 层 | ActorParticle / ActorBeam | ActorSound | 数编驱动,通过 ActorScope 管理生命周期,与 Unit/Entity 绑定 |
| GameGraph 层 | ParticleSystem / ParticleEmitter | SoundSourceComponent / 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);
#endifv1 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;
#endifActorBeam、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();
#endifNode.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 | 停止发射 |
IsFinished | bool | 是否播放完毕 |
TickSpeed | float (set) | 播放速率倍数 |
Seek(tickTime) | void | 按指定秒数增量推进(不是绝对时间定位;超过 100000 个 0.1 秒步的请求忽略) |
RandomSeed | uint | 初始/复位随机种子(客户端能力 10) |
DeterministicMode | bool | 手动回放模式;不支持时 setter 抛出异常(客户端能力 10) |
Advance(seconds, fixedStep) | bool | 从当前状态按固定步长推进(客户端能力 10) |
Replay(seed, absoluteTime, fixedStep) | bool | 从初始状态重放到绝对播放时刻(客户端能力 10) |
UnitScale | float | 单位缩放 |
SpawnRateFactor | float | 生成速率因子 (0~1) |
EmitterLoops | int (set) | 循环次数,0 = 无限 |
AutoRemoveMode | AutoRemoveMode | 播放完成后自动移除 |
SetEndPoint(Vector3) | void | Beam 终点 |
SetSourcePoint(Vector3) | void | Beam 起点 |
BeamScale | float (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,单发射器DeterministicModesetter 抛出InvalidOperationException,TrySetDeterministicMode返回 false;Advance/Replay返回 false 且不改变状态。未启用确定性回放时,自动播放不受影响。 - 对于不带
EmitterHierarchy的效果,回放只覆盖发射器内部状态。宿主节点应保持静止;Beam 端点、AnimTrail 骨骼轨迹等外部逐帧输入需要按相同步长自行重放。回放会按相同 seed 和步长推进已覆盖的随机源,不承诺跨 GPU/平台逐像素一致。 - 同一资源挂载多个实例时,自定义 burst 的触发标志和粒子灯光的发射累计值由各发射器分别保存;重置或推进一个实例不会消费另一个实例的这些运行状态。显式 seed 或确定性模式下,锥台形状采样也使用该发射器的随机流。
兼容边界见客户端二进制兼容与能力版本。
ParticleSystem 资源属性
| 属性/方法 | 类型 | 说明 |
|---|---|---|
EffectCount | uint | 效果/发射器定义数量 |
SpawnRateFactor | float | 全局生成速率因子 |
IsInfinite | bool | 是否包含无限持续的发射器 |
PredictDuration(forced) | float | 预测持续时间(秒),0 = 无限 |
注意事项
.effect路径相对于资源根目录,来自effect/目录(例如effect/eff_tongyong/huoqiu2/particle.effect)- 自定义粒子如果未上传云但需要随项目发布,应放到项目
res/effect/...,代码中仍从effect/...开始写。不要放到ui/或user_files。 AddToNode后粒子立即开始播放;如需延迟播放,调用后立即StopStop(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;
#endif3D 空间音效
使用 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();
#endifSoundSourceComponent 完整 API
| 属性/方法 | 类型 | 可用端 | 说明 |
|---|---|---|---|
Play(sound) | void | 客户端 | 播放音频 |
Play(sound, frequency) | void | 客户端 | 以指定频率播放 |
Play(sound, frequency, gain) | void | 客户端 | 以指定频率和音量播放 |
Play(sound, frequency, gain, panning) | void | 客户端 | 以指定频率、音量和立体声位置播放 |
Stop() | void | 双端 | 停止播放 |
Seek(time) | void | 双端 | 跳转到指定时间(秒) |
IsPlaying | bool | 双端 | 是否正在播放 |
TimePosition | float | 双端 | 当前播放位置(秒) |
SoundType | string | 双端 | 声音分组("Effect"、"Music" 等) |
Frequency | float | 双端 | 播放频率 (Hz) |
Gain | float | 双端 | 音量 (0.0~1.0) |
Attenuation | float | 双端 | 衰减系数 |
Panning | float | 双端 | 立体声平移 (-1.0 左 ~ 1.0 右) |
AutoRemoveMode | AutoRemoveMode | 双端 | 播放完成后自动移除模式 |
MixOutput | bool | 双端 | 是否混合到音频输出(构造函数默认 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;
}
}
#endifActorSound 自身已提供常用的 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() |