Skip to content

单位换模与附属表现重建 ​

已有单位通过服务端 unit.ModelSwap(modelLink, tag) 切换主模型,用 unit.ModelSwapPop(tag) 撤回对应覆盖。换模后,可以在客户端新模型与所需绑点就绪时重建默认表现。需要自定义变身、组合外观或保留特效实例时,也可以同步外观状态,由客户端通过 GameGraph 管理模型子树。

常规路线:换主模型,再重建默认表现 ​

ModelSwap 使用模型覆盖聚合器,后加入的覆盖优先;它不会自动重建技能、Buff 或携带物品的表现。以下方法专门用于重新按数据配置创建表现:

调用清理与重建范围保留的逻辑状态
unit.RebuildDefaultActors()清理单位作用域的全部 Actor,按单位 Cache.ActorArray 重建;默认还逐个重建当前技能、Buff 等附着对象的作用域单位身份、属性、技能和 Buff 实例
unit.RebuildDefaultActors(false)只重建单位作用域,不遍历附着对象同上
ability.RebuildDefaultActors() / buff.RebuildDefaultActors()清理该附着对象作用域的全部 Actor,只按其当前 Cache.ActorArray 重建等级、Buff 层数和持续时间、技能冷却等
itemMod.RebuildHostedActors()清理此物品记录的全部 HostedActors,再按当前 Modifications[SlotType].ActorArray 创建槽位、堆叠、属性修改、授予的技能

这些方法双端可调用:客户端只影响本地;服务端向当前可见客户端发送一次重建请求。请求不保存成待执行任务,也不等待模型加载;晚加入或重新可见的对象仍按正常生命周期创建当前表现。服务端调用物品入口时,以持有者的可见性为准,未被有效单位携带时不发送请求。

完整恢复顺序 ​

携带表现有两份关联:物品用 HostedActors 记录自己创建的 Actor,这些 Actor 同时属于持有者的单位作用域。因此单位重建会清理它们,却不会从单位 ActorArray 恢复物品槽位配置。必须先处理单位,再处理物品:

csharp
#if CLIENT
using System.Linq;
using GameCore.Container;
using GameCore.EntitySystem;
using GameCore.ItemSystem;

// 由项目在客户端确认本轮新模型和所需绑点已就绪后调用。
static void RebuildAfterModelReady(Unit unit)
{
    if (!unit.IsValid) return;

    unit.RebuildDefaultActors();
    var inventory = unit.GetComponent<InventoryManager>();
    if (inventory is not null)
    {
        foreach (var item in inventory.Items.OfType<ItemMod>().ToArray())
            item.RebuildHostedActors();
    }
}
#endif

只恢复某一物品时直接调用 itemMod.RebuildHostedActors();它不会清理持有者作用域中的其他 Actor。物品当前未携带、无有效槽位或未启用时,本地调用只清理旧携带表现;不会创建地面物品或品质特效。

完全替换的副作用 ​

重建入口会销毁其清理范围内的全部表现,包括动态加入对应集合的 Actor;只重建数据中配置的默认表现。

  • 旧 Actor 引用失效,循环特效、声音、动画和其他播放进度会重新开始。
  • 运行时覆盖的绑点、偏移、旋转、缩放等实例状态不保留;重建立即清理,不播放淡出。
  • 单位作用域中其他来源的 Actor,例如几何形状表现,也会被清理;项目负责恢复这些非 ActorArray 内容。
  • 仅在物理节点上附着、没有登记到这些作用域或物品集合的表现,不属于重建方法的清理范围;项目负责其生命周期。
  • HostedActors 的重建不重新装备物品,也不重新结算属性或重新授予技能。UpdateModifications(forced: true) 是逻辑更新入口,不能代替表现重建。

何时算新模型就绪 ​

服务端 ModelSwap() 返回、客户端收到外观属性、下一帧以及固定延迟,都不能单独证明新模型绑点已存在。模型资源可能异步加载。当前没有供脚本通用使用的成对“旧模型拆除前 / 新模型就绪后”通知。

项目应按目标模型核对当前实例和实际所需绑点;单独查到一个同名 Socket 还可能查到旧模型。以本次请求的外观版本或目标实例区分新旧,等待期间遇到单位销毁、离开视野或更新的外观请求应取消旧操作。所需节点尚未就绪时继续等待;确定资源或绑点缺失时,按项目约定暂时隐藏或回退根节点。不要把“服务端紧接着调用重建”作为可靠的换模完成协议。

换模实际替换了什么 ​

单位主模型换模保留单位对象与其表现根 Node,卸下旧模型组件,再为新资源创建模型组件及 Prefab 实例。动画系统重新初始化,缩放、状态条绑点和模型粒子等随之重新设置。资源未就绪时,新实例可能稍后才建立。

text
Unit 的表现根 Node                   保留
└─ 主模型的子树                      替换
   └─ Prefab / 骨架 / Socket         替换
      └─ 其他 Actor 的表现 Node     原挂接关系受影响

普通绑点表现通过节点父子关系跟随骨骼:骨骼更新后,子节点的世界变换由父节点世界变换与自身局部变换合成,并不需要每帧根据绑点名查找位置。Actor 的实际父节点若就是 Socket,换模前可以读取 actorNode.Parent.Name 得到绑点名;如果中间还有项目自建锚点,就需要项目自己的路径或语义标识。

旧绑点被释放后,Actor 可能仍持有自己的表现 Node,但父节点关系已经失去、节点也可能离开场景。表现消失不等于 Actor 实例已经被销毁。GameGraph 的 Node 包装不因被 C# 引用就保住底层节点;旧 Socket 和骨骼引用不能跨模型替换继续使用。

高级路线:同步外观状态,客户端拥有模型子树 ​

适合需要自己控制模型切换时机、多个模型组合,或保留特效实例的项目。沿用 Unit 的身份、移动和属性同步,把“当前形态 / 外观版本”写进项目自定义单位属性,各客户端从当前状态创建外观。

  1. 在双端定义项目自有 PropertyUnit 扩展,类型一致,例如整数 AppearanceForm。在服务端设置该属性,服务端独立维护相应玩法规则。定义方式见单位属性系统。
  2. 客户端监听 EventUnitPropertyChange,仅处理此外观属性;在 EventUnitCreate 时也读取当前值。客户端创建事件包括单位重新进入本地视野,不能只监听变化而忽略初始状态。属性和节点就绪顺序可能不同,应由同一外观控制器合并当前状态和待应用请求。
  3. 为单位建立项目拥有的模型子树。可以让单位数据不配置主 Model,在保留的单位表现根下实例化自己的 Prefab,或为项目拥有的节点创建 AnimatedModelComponent、配置 Mesh / 材质与 AnimationController。
  4. 外观切换时读取最新状态,准备新模型,迁移或重建子表现,再释放旧子树;快速连续切换只应用最新版本。离开视野、单位销毁时清理自建实例与订阅,重新可见时从当前属性恢复。

只同步外观选择,客户端 Node、组件包装或资源对象本身不能作为单位属性同步。这条路线的模型是本地表现,不会自动替换服务端碰撞、尺寸或命中规则。

例如,项目扩展的整数属性由服务端调用 unit.SetProperty(appearanceProperty, formId) 写入。客户端可以在 EventUnitCreate 处理器中为该单位调用以下方法,先订阅变化,再应用当前值。requestApply 由项目外观控制器提供,负责把整数形态映射到资源并等待节点就绪;返回的触发器应由控制器保存,清理时调用 Destroy()。

csharp
#if CLIENT
using System;
using System.Threading.Tasks;
using Events;
using GameCore.EntitySystem;
using GameCore.Event;

static Trigger<EventUnitPropertyChange> TrackAppearance(
    Unit unit, PropertyUnit appearanceProperty, Action<int> requestApply)
{
    var trigger = unit.Subscribe<EventUnitPropertyChange>((_, e) =>
    {
        if (unit.IsValid && e.Property.Equals(appearanceProperty))
            requestApply(unit.GetProperty<int>(appearanceProperty));
        return Task.CompletedTask;
    });
    requestApply(unit.GetProperty<int>(appearanceProperty));
    return trigger;
}
#endif

区分两种模型组件 ​

unit.GetAnimatedModel() 返回已有骨骼渲染组件的 GameGraph 包装,并不是主模型换模所管理的整个模型组件。调用 SetModel(mesh, createBones: true) 可改网格和骨架,但不等于 ModelSwap():它不会一起切换整套 Prefab、材质、Socket、模型粒子、动画配置和状态条引用。

自行创建的模型组件和子树可以自行替换。对于框架管理的主模型,继续使用 ModelSwap;不要通过销毁借来的模型组件或删除单位根 Node 来模拟完整主模型换模。高级路线需要项目明确拥有的外观子树,避免高层动画和原有模型管理继续写入已被替换的对象。

替换项目拥有的 Prefab ​

以下示例只管理项目自建模型。unit 是有效单位,数据未配置另一套主模型;current 是项目持有的旧实例。该函数在旧模型释放前交给项目处理子表现,准备失败时保留旧实例。

csharp
#if CLIENT
using System;
using GameCore.EntitySystem;
using GameGraph.NodeSystem;
using PrefabPath = GameCore.ResourceType.Prefab;
using PrefabResource = GameGraph.ResourceSystem.Prefab;

static Node ReplaceOwnedModel(
    Unit unit, Node? current, PrefabPath path, Action<Node?, Node> migrateChildren)
{
    if (!unit.TryGetClientVisualNode(out var root) || root is null)
        throw new InvalidOperationException("单位表现根尚未就绪");
    var prefab = PrefabResource.Load(path)
        ?? throw new InvalidOperationException("目标模型资源不可用");
    var next = prefab.CreateInstance(root);
    try
    {
        // 回调先核对新模型所需骨骼 / Socket,再搬移或重建项目自有表现。
        // 回调失败时须自行回滚已经搬移的子节点。
        migrateChildren(current, next);
    }
    catch
    {
        next.Destroy();
        throw;
    }
    current?.Destroy();
    return next;
}
#endif

Prefab.CreateInstance(parent) 必须传父节点,确保底层实例在返回时已有持有者。模型组件通常在 Prefab 子节点上;可以用 GetComponentInChildren<AnimatedModelComponent>() 查询,复杂资源应按明确结构选定主骨骼宿主,避免选中其他子模型。手工组装 .mdl、材质与控制器的例子见骨骼动画指南。项目资源和候选形态的依赖收集见资源清单排查。

子表现、Node 与 Socket 的处理 ​

表现归属旧模型释放前新模型就绪后
允许重新开始的默认 Actor可直接销毁;不要保留旧引用对框架主模型按上文重建;自建子树使用项目自己的创建及挂接逻辑
需要保留实例的普通 Socket Actor临时保存 Actor、绑点名与局部变换,把表现节点搬到稳定根,避免随旧子树脱离场景框架主模型可用 actor.AttachTo(unit, socketName),成功后恢复局部变换;该入口会维护 Actor 的父子关系
项目自有普通 Node保存逻辑挂点和局部变换,暂挂稳定根;或新旧模型同时存在时直接搬移挂到新 Socket 节点,或新骨骼下的 Socket 锚点,再恢复局部变换
骨骼附加模型释放旧 AdditionModelAttachment 挂载句柄在新 Prefab 的明确主骨骼模型上重新 AttachAdditionModel(...)
材质、光束、模型专属表现核对其引用的组件、端点或宿主资源使用该表现类型的专用重建 / 重绑定逻辑,不假定移动根 Node 足够

Node.Parent / AddChild() 只改变场景树,不同步 Actor 自身的父子记账、专用宿主绑定或缩放约定。对于框架主模型,优先调用 Actor.AttachTo()。自建 Prefab 中的 Socket 不会自动注册成 Actor.AttachTo(unit, socketName) 的主模型绑点;可先用 Actor.Detach() 明确解除原挂接,再把表现 Node 挂到项目自己的锚点,项目继续管理该挂接的变换和清理,Actor 的作用域归属仍独立存在。重建默认 Actor 也不会自动将它们路由到这棵自建模型子树。

GameGraph 有两种 Socket 用法:实际 Prefab Socket Node 可在明确的实例子树中按名称查找;AnimatedModelComponent.GetSocket(name) 返回的是骨骼 Node 与挂点局部变换的快照,并不直接返回那个 Socket Node。后一种用法需要在骨骼下建立锚点并应用快照变换:

csharp
#if CLIENT
// model 是新实例中已确认的骨骼组件;child 是项目拥有的普通表现 Node。
var socket = model.GetSocket("socket_weapon_r");
if (socket is { } s)
{
    var anchor = s.BoneNode.CreateChild("WeaponAnchor")
        ?? throw new System.InvalidOperationException("绑点锚点创建失败");
    anchor.LocalPosition = s.LocalPosition;
    anchor.LocalRotation = s.LocalRotation;
    anchor.LocalScale = s.LocalScale;
    anchor.AddChild(child);
    // child 的局部变换在此锚点之上叠加;替换模型前要迁出或销毁。
}
#endif

缺失绑点时明确选择跳过、隐藏或回退根节点。新旧骨架名字相同也应重新取得 Node。仅需本轮换模时保留实例,可以使用短期迁移快照,无须永久记录所有 Actor 的初始绑点。若旧模型已经拆掉,通常无法再从失去的父节点反查原 Socket;现有服务端 ModelSwap 没有通用的脚本前置拦截,因此不能只靠事后扫描保证无损迁移。

这些节点操作使用现有 GameGraph API;采用项目自建外观不要求为此新增客户端能力版本,但实际使用的具体 API 仍须满足目标客户端支持范围。该高级路线由项目自行实现和验收,没有自动接管现有 ModelSwap 的通用迁移器。

相关入口 ​