主题
单位、表现、场景与 GameGraph 的桥接
本文供需要扩展现有 SCE 项目、查询下钻 API 的开发者和 AI 使用。首次了解两种开发方式时,可先读理解星火。本文代码是局部片段,unit、actor、scene 均指项目中已经取得的有效对象;示例中的组件查询可能返回 null。
对象关系:玩法对象、空间节点、组件
| 对象 | 职责 | 与底层对象的关系 |
|---|---|---|
GameDataUnit、技能、物品等数据定义 | 定义内容和规则 | 数据定义不等于运行时 Node,也不代表每条定义会创建一个 Node |
Entity / Unit | 身份、属性、玩法状态、生命周期与单位机制 | 客户端表现和 GameGraph 绑定通过接口连接;高层类型不继承 Node |
Actor、ActorScope | 模型、特效、声音等表现及其组织 | 具体 Actor 可能有客户端表现节点;scope 的绑定节点与 Actor 的表现节点是不同概念 |
SCE Scene | 地图/逻辑场景上下文、加载与切换 | 底层场景就绪时,可取得其 GameGraph SceneGraph 包装 |
GameGraph SceneGraph | 访问底层 Urho Scene | RootNode 提供根节点;包装类型自身不继承 Node |
Node | 位置、旋转、缩放、父子关系、挂载组件 | 同一单位可以关联表现根、骨骼子节点和多种绑定节点 |
INodeComponent | 挂在 Node 上的能力接口 | Node 属性返回所属节点;原生组件包装通常继承 EngineNodeComponent |
技能、Buff、物品不需要一律转成 Node;需要空间表现或物理行为时,再关联相应对象。GameCore 的实体组件与 GameGraph 的 Node 组件也不是同一套组件容器,不能把任意实体组件交给 node.GetComponent<T>()。后者要求 T 实现 INodeComponent。
Entity、Unit 和 ActorModel 都实现 IActorAnimationPlayable,所以它们可以共用动画扩展方法。Unit 不是 ActorModel;不存在 Unit.GetActor() 这个入口。
先确定要访问哪种节点
| 目的 | 入口 | 运行端 / 返回语义 |
|---|---|---|
| 便捷取得单位节点 | unit.GetNode() | 双端;客户端表现优先,服务端使用默认绑定,详见下节 |
| 明确查询单位的某个绑定节点 | unit.GetNode(role, key) 或 unit.GetNodeBinding(role, key)?.Node | 双端;按用途和 key 查询,不回退到表现节点 |
| 取得单位、实体或具体 Actor 的表现节点 | actor.TryGetClientVisualNode(out var node) | 客户端;接收 IActor,只查询当前表现节点 |
| 直接访问支持动画对象的模型 / 控制器 | playable.GetAnimatedModel() / GetAnimationController() | 客户端;接收 IActorAnimationPlayable,查询表现节点上的组件 |
| 取得 SCE 场景底层图 | scene.GetSceneGraph() | 双端;要求底层场景就绪 |
| 取得底层场景根 / 同步挂载根 | scene.GetSceneGraph()?.RootNode / scene.GetSyncRoot() | 双端;当前同步根为底层 Scene 根 |
| 取得客户端表现的动态根 | scene.GetDynamicRootNode() | 客户端;表现动态根,不应假定等于底层 Scene 根 |
桥接扩展方法使用 using GameGraph.NodeSystem;。它们返回已有对象的包装或绑定入口,不会因为“拿到 Node”而自动复制一套模型、接管玩法状态或获得网络权威。
从 Unit 下钻
便捷入口的双端语义
csharp
using GameGraph.NodeSystem;
var node = unit.GetNode();- 客户端:先取单位当前的表现根节点;不存在时查询默认 Root 绑定。该回退不会在客户端自动创建默认绑定。
- 服务端:获取默认 Root 绑定;缺失时会尝试在所属 SCE 场景同步根下创建 replicated 绑定节点,默认由 Unit 驱动。因此这个便捷调用在服务端可能创建节点,不是纯查询。
- 底层场景未就绪、节点未创建或绑定不可用时,仍可能返回
null。不要把“创建 Unit”理解成所有需要的节点和组件已经可用。
客户端得到的表现节点与服务端得到的绑定节点并不因此是同一个复制对象。仅需要查看已经存在的绑定时,使用 GetNodeBinding(),或显式传入 role 的 GetNode(role, key),其 createIfMissing 默认是 false。
访问单位已有的动画与模型
csharp
#if CLIENT
using GameGraph.NodeSystem;
var model = unit.GetAnimatedModel();
var controller = unit.GetAnimationController();
var socket = model?.GetSocket("socket_weapon_r"); // 含骨骼节点和挂点局部变换
#endif这里不需要取得或转换为 ActorModel。这些扩展只查询表现节点;没有对应组件时返回 null,不会自动添加组件。单位可能使用粒子、静态模型或其他表现,不应假定所有单位都有骨骼模型。播放高层动画和访问动画状态见骨骼动画下钻示例。
为单位增加明确用途的绑定节点
需要 hitbox、查询代理或自定义空间对象时,可以使用 UnitNodeBinding。例如,在场景就绪后于服务端创建一个跟随单位的自定义锚点:
csharp
#if SERVER
using GameGraph.NodeSystem;
using System.Numerics;
var binding = unit.EnsureNodeBinding(
UnitNodeBindingRole.Custom,
key: "interaction-anchor",
mode: UnitNodeBindingMode.UnitDriven,
localOffset: new Vector3(0, 0, 100));
var anchor = binding?.Node;
#endif它只建立带用途的空间节点,不会自动创建可见模型、交互逻辑或碰撞体。需要读取同一绑定时使用明确的标识:
csharp
var anchor = unit.GetNode(UnitNodeBindingRole.Custom, key: "interaction-anchor");| 模式 | 谁提供位置与朝向 |
|---|---|
UnitDriven | Unit 提供,绑定节点跟随 |
NodeDriven | Node / 物理侧提供,Unit 跟随 |
Manual | 仅建立绑定与反查关系,自动 transform 同步由调用方另行处理 |
模式不是随意切换移动机制的完整教程。物理角色优先参考单位移动控制器,避免单位移动和物理模拟同时写同一份位置。SCE 场景内绑定到 Unit 的 replicated Node 随该 Unit 的服务端视野可见性对各客户端创建/移除;普通未绑定节点仍按场景同步。
从表现下钻
如果已有一个具体 Actor 或其他 IActor 对象,可以查询它的客户端表现节点:
csharp
#if CLIENT
using GameGraph.NodeSystem;
using GameGraph.NodeSystem.Component.Graphics;
if (actor.TryGetClientVisualNode(out var visualNode) && visualNode is not null)
{
var model = visualNode.GetComponent<AnimatedModelComponent>();
// model 为 null 时,当前节点没有该组件。
}
#endif查询不创建表现,不回退到 Unit 或 ActorScope 的绑定节点,也不证明该 Node 有服务端对应物。对于已知类型,优先使用更明确的组件入口:
| 已有对象 | 专用入口 |
|---|---|
IActorAnimationPlayable,包括 Unit / Entity / ActorModel | GetAnimatedModel()、GetAnimationController() |
ActorParticle | GetParticleEmitters()、GetParticleEmitter(index) |
ActorSound | GetSoundSource() |
ActorText | GetText3D() |
不是所有 Actor 都需要或拥有相同组件。ActorScopeNodeBinding 则是另一种高级绑定能力,用于需要真实 replicated Node 的持久表现;它的 role/key 标识不等于骨骼挂点,也不等于 slot actor 的父子关系。详见服务端创建表现。
从 SCE 场景下钻
在目标场景已经加载完成的逻辑中查询:
csharp
using GameGraph.NodeSystem;
var graph = scene.GetSceneGraph();
var root = graph?.RootNode;
var syncRoot = scene.GetSyncRoot();
#if CLIENT
var visualRoot = scene.GetDynamicRootNode();
#endif底层场景就绪前这些入口可能为空。应检查 scene.Loaded / LoadState,按需等待 scene.OnLoadEnd;固定延迟或游戏开始事件不能证明地图加载完成。Game.LocalScene.IsEngineFallback 为 true 时,也不能据此访问地图节点和地形。
SCE 内置图由框架管理。想在这张地图中补充内容,直接使用其图或节点入口;new SceneGraph(...) 创建的是另一张独立图。SCE 内置图不需要手动 Publish,也不应由业务代码 Dispose。向现有 SCE 场景加入同步节点使用 scene.AddSyncNode(...);独立服务端发布图使用 Publish(player)。两条同步路径见场景系统和SceneGraph 发布与同步。
地形、天气、光照等固定用途节点有更具体的入口,例如 scene.TryGetTerrainNode()、TryGetWeatherNode()、TryGetLightingNode()。优先使用这些入口及对应控制器,详见场景内置节点与环境。
从 Node 取得组件与子节点
node.GetComponent<T>() 只检查当前节点。node.GetComponentInChildren<T>() 默认连同当前节点在子树中深度优先查找,返回首个匹配组件。模型 prefab 经常有组织根节点和承载模型的子节点,因此根节点查询为空不等于整个模型缺失。
csharp
#if CLIENT
using GameGraph.NodeSystem.Component.Graphics;
// instance 是项目已实例化的 prefab 根节点。
var model = instance.GetComponentInChildren<AnimatedModelComponent>();
var modelNode = model?.Node;
var controller = modelNode?.GetComponent<AnimationController>();
#endif同一子树中有多个模型时,应按项目的节点结构定位目标,再查组件;不要假定两个独立的递归查询一定取到同一角色的模型和控制器。GetComponent 是查询;只有确实要添加能力且符合对象控制归属时,才调用 CreateComponent<T>() 或相应添加接口。
从绑定节点回到高层对象
已有 Node 或物理命中结果时,可以通过已建立的绑定回到玩法对象:
csharp
using GameGraph.NodeSystem;
var boundUnit = node.GetBoundUnit();
var unitBinding = node.GetUnitNodeBinding();
var boundScope = node.GetBoundActorScope();这些接口查询当前节点的绑定,不会因为节点位于单位附近或挂在某个父节点下,就自动找到 Unit / scope。普通表现节点、骨骼子节点或未绑定的自定义节点可能返回 null。物理 PhysicsRaycastResult.GetBoundUnit() 也依赖命中刚体节点的 Unit 绑定,不是对所有可见单位模型的通用拾取接口。
修改前确认控制归属
| 要做的事 | 应考虑的归属 |
|---|---|
| 改单位的位置 | 单位移动、绑定模式和物理模拟谁负责写入;客户端表现位置不等于服务端玩法位置 |
| 改动画速度、权重、播放状态 | 高层动画调度与低层控制器操作同一底层状态,高层后续写入可能覆盖修改 |
| 增加模型、组件或物理对象 | 运行端和复制策略;客户端可见并不自动意味着联机同步 |
| 保存节点或组件引用 | 所属场景、单位、表现、可见性或模型可能变化;生命周期变化后重新取得需要的对象 |
| 清理对象 | 下钻取得的框架对象由框架管理;自行创建的内容按其所有者和绑定契约清理,不通过删除单位底层根节点代替高层销毁 |
桥接让已有游戏可以局部扩展,同时保留数据、触发器和高层机制。可靠的混用需要明确每项状态的来源,而不是让两层独立维护互相覆盖的状态。