AnimNotifyState:通知资源不是播放实例

自动关联目录:AnimNotifyState:通知资源不是播放实例

核心判断:UAnimNotifyState 属于动画资产上的共享配置。来源角色、生成组件、当前时间和清理权都属于某次播放;把任何一项写回通知对象,就会把“资产实例”误当成“播放实例”。

一、具体业务:动作期间生成附件,并让附件动画跟随角色时间

一个持续区间通知在 Begin 时创建附属 USkeletalMeshComponent,挂到角色 Socket,播放附件动画;Tick 把附件位置同步到来源动画;End 销毁附件。这条链路能让内容作者直接在动画时间线上配置临时武器或装饰,业务入口很自然。

真正的难点不是创建组件,而是同一动画资产会被多个角色同时求值。通知 UObject 只有一份,播放实例却有多份;若 Tick 读取通知对象上的 AnimCurrentPosition,后求值的角色会覆盖先求值角色的时间,多个附件被推到同一位置。

二、实际演进:按来源 Mesh 读取时间,这一步是正确的

实现已经针对上述问题增加了逐实例路径:生成附件时保存来源 Mesh 弱指针;Tick 除了比对宿主名和动画名,还要求来源指针一致;时间不再直接取共享字段,而是从该 Mesh 的 UAnimInstance 查找 Montage 段、非同步组 Active Player 或同步组 Active Player。旧逻辑由 CVar 保留,便于灰度回退。

这几项值得保留:

  • 来源弱指针比 Owner 名和动画名更接近真实播放身份;
  • 时间从实际求值该角色的 AnimInstance 读取,而不是从通知资源读取;
  • 异步加载回调使用弱引用,并在提交前复验 Mesh、动画和 World;
  • 新旧路径可切换,降低了修复共享状态时的上线风险。

三、尚未闭合:找到“这个角色”不等于找到“这次播放”

当前时间查询仍按资产名称匹配。若不同包存在同名 Sequence,或同一 Sequence 在一个 AnimInstance 内以不同权重、不同 Slot 同时出现,名称不能确定是哪条播放器记录。只检查当前活动 Montage,也无法表达多 Montage/多实例选择规则。

Montage 时间换算只做“Montage 位置减段起点,再对动画总长取模”。这个公式没有完整表达段裁剪起点、播放倍率、段结束和循环语义;附件会在变速或裁剪段中逐渐错相。应由统一的 Segment 映射把 Track Time 转成 Source Time,而不是在通知里重写一份近似公式。

更关键的是,查询失败又回退到共享 AnimCurrentPosition,等于在最不确定的分支重新引入原问题。正确失败语义应是保持上一帧、隐藏附件、停止同步或结束绑定,不能借用另一播放实例的时间。

Tick 还会遍历宿主的全部 SceneComponent,再用名称和来源指针找自己生成的附件。组件越多、通知越多,成本接近 活动通知数 × 宿主组件数;Begin 明明已经拿到目标组件,却没有保存一个可直接访问的播放绑定。

四、目标设计:Begin 创建绑定,Tick 只消费绑定

struct FNotifyPlaybackBinding {
    FObjectKey SourceMesh;
    uint64 PlaybackEpoch;
    TWeakObjectPtr<USkeletalMeshComponent> DrivenMesh;
    FAnimPlayerHandle TimeSource;   // 指向具体播放器/段,而非资产名
    float LastValidSourceTime;
};

FBindingHandle Begin(const FNotifyConfig& config,
                     USkeletalMeshComponent& source,
                     const FAnimPlaybackRef& playback)
{
    auto driven = CreateAndAttach(config, source);
    return Bind({source, NextEpoch(source), driven,
                 ResolveExactPlayer(playback), 0.0f});
}

void Tick(FBindingHandle h)
{
    auto* b = Find(h);
    if (!b || !b->DrivenMesh.IsValid()) return End(h);

    TOptional<float> t = MapTrackToSourceTime(b->TimeSource);
    if (!t) return ApplyFailurePolicy(*b); // 禁止读共享位置
    b->DrivenMesh->SetPosition(*t, false);
    b->LastValidSourceTime = *t;
}

若附件与角色骨架兼容且目标就是复制同一姿势,优先考虑 Leader Pose/Copy Pose,让 UE 的姿势依赖图负责同步;若附件有独立骨架或独立动画,才保留第二个播放器,并绑定精确时间源。两种方案的差别不能被一个 EnableSyncAllAnim 布尔值掩盖。

五、取舍与验证

方案优点代价/适用边界
共享通知字段实现最短多实例必然串状态,只能用于只读配置
每 Tick 扫 AnimInstance改造小,兼容现有内容玩家记录可能歧义,需精确身份与失败策略
Begin 绑定播放器句柄Tick 为 O(1),身份闭合要处理重绑、播放器失效和引擎版本适配
Leader/Copy Pose相位天然一致要求骨架/姿势语义兼容,不适合独立附件动画

测试必须覆盖同一通知被两个角色同时播放、同一 Sequence 多实例、同名不同资产、Montage 裁剪/变速/循环、来源 Mesh 替换、异步资源晚到和中途销毁。观测至少输出 PlaybackEpoch、SourceMesh、PlayerIdentity、TrackTime、SourceTime、FailureReason。

这段实践最有价值的地方,是已经定位到共享 CDO 状态这一根因,并向逐实例读取迈了一步。下一步不是继续补更多名称判断,而是让“这次通知播放”成为真正的一等运行时对象。