Skip to content

模型动画系统 ​

技能动画如何跟随服务端施法阶段、重新接近时怎样停止并重建,以及预警 Actor 的清理规则,见指令、施法与技能动画的完整生命周期。

技能动画也支持自定义扩展:继承 GameDataAnimation,在客户端重写 CreateSwingController 并返回自己的 IAnimationSwingController,再将动画数据 Link 配置到技能的 Animation 列表。接口的阶段通知、重建和清理约定见该指南第 6 节“自定义技能动画数据与控制器”。

> 两层 API 关系:WasiCore 提供两条 3D 骨骼动画路径,两者操作同一个底层动画对象: > > | 层级 | 入口 | 适用 | > |------|------|------| > | 本文档:Actor 层 | IActorAnimationPlayable.PlayAnimation / AnimationSequence / AnimationBracket | 数编驱动、Unit 绑定、优先级压制、序列编排、生死元组 | > | GameGraphSkeletalAnimation.md:GameGraph 层 | AnimatedModelComponent / AnimationController / AnimationState | 纯代码控制,直接操作 Node,SceneGraph 独立游戏 / 过场 prop / 装饰物 | > > 两层 API 不是互斥的 —— Actor 节点上同时可以通过扩展方法 actor.GetAnimationController() 等下钻到 GameGraph wrapper 做精细控制。最佳实践:ActorScope 内的对象用 Actor API;其他场景或精细控制用 GameGraph API。详见末尾 "GameGraph 层桥接" 章节。

概述 ​

WasiCore中的模型动画系统提供了一个全面的框架,用于管理和播放游戏Actor上的动画。它由三个主要层次组成:

  1. 引擎层:底层视图动画接口(IViewAnimation、IViewAnimationSequence、IViewAsyncActor)
  2. 游戏层:高级动画管理(Animation、AnimationSequence、AnimationBracket)
  3. Actor集成:提供动画功能的Actor接口(IActorAnimationPlayable)

架构 ​

核心组件 ​

1. 动画类型 ​

系统支持三种主要的动画类型:

  • 普通动画:基础的单一动画播放,带有时间控制
  • 动画序列:复杂的多动画编排,包含事件和Actor生成
  • 动画元组:基于生命周期的动画(出生 → 待机 → 死亡)

2. 关键类 ​

GameCore.ModelAnimation/
├── Animation.cs                    # 核心动画包装器
├── AnimationSequence.cs           # 复杂序列控制器  
├── AnimationBracket.cs            # 元组动画控制器
├── Data/
│   ├── GameDataAnimationSimple.cs    # 普通动画配置
│   ├── GameDataAnimationSequence.cs  # 序列配置
│   └── GameDataAnimationBracket.cs   # 元组配置
├── Enum/
│   ├── BracketStage.cs               # 出生/待机/死亡阶段
│   ├── AnimationEventKey.cs          # 动画事件类型
│   └── SequenceEventKey.cs           # 序列事件类型
└── Struct/
    └── EventArgs.cs                  # 事件参数结构

3. Actor集成 ​

IActorAnimationPlayable接口为Actor提供动画功能:

csharp
public interface IActorAnimationPlayable : IActor
{
    // 播放普通动画
    Animation? PlayAnimation(ResourceType.Animation animationFile, AnimationPlayParam? param = null);
    Animation? PlayAnimation(IGameLink<GameDataAnimationSimple> link);
    
    // 播放复杂序列
    AnimationSequence? PlayAnimationSequence(IGameLink<GameDataAnimationSequence> link);
    AnimationSequence? PlayAnimationSequence(IGameLink<GameDataAnimationSequence> link, bool triggerEvents);
    
    // 动画控制
    bool StopAnimationOfLayer(AnimationLogicLayer logicLayer);
    bool SetBirthStandDeathAnimation(UTF8String birth, UTF8String stand, UTF8String death);
    bool SetAnimationMapping(UTF8String alias, UTF8String animationRaw);
    bool ClearAnimationMapping(UTF8String alias);
    IEnumerable<Animation> GetAnimationInstances();
}

使用模式 ​

1. 普通动画播放 ​

csharp
// 播放普通动画
var animation = actor.PlayAnimation(animationResource, new AnimationPlayParam {
    Priority = 30,
    Speed = 1.2f,
    IsLooping = true,
    BlendIn = TimeSpan.FromSeconds(0.3f)
});

// 控制播放
animation.Speed = 0.8f;
animation.Stop(TimeSpan.FromSeconds(0.5f));

2. 动画序列 ​

csharp
// 创建序列配置
var sequenceData = new GameDataAnimationSequence {
    Playbacks = [
        new AnimationPlayback { 
            AnimationRaw = "spell_cast_start", 
            PlaybackDuration = TimeSpan.FromSeconds(1.0) 
        },
        new AnimationPlayback { 
            AnimationRaw = "spell_cast_channel", 
            PlaybackDuration = TimeSpan.FromSeconds(3.0),
            IsLooping = true 
        }
    ],
    SequenceActors = [
        new SequenceActorSpawn {
            Actor = magicCircleActor,
            SpawnOffset = TimeSpan.FromSeconds(0.5),
            Duration = TimeSpan.FromSeconds(2.0)
        }
    ],
    SequenceEvents = [
        new SequenceEvent {
            EventKey = SequenceEventKey.ApplyPlayerState,
            Offset = TimeSpan.FromSeconds(1.5)
        }
    ]
};

// 播放序列
var sequence = actor.PlayAnimationSequence(sequenceDataLink, triggerEvents: true);

// 处理事件
sequence.EventTriggered += (args) => {
    switch (args.EventKey) {
        case SequenceEventKey.ApplyPlayerState:
            ApplySpellEffect(args.Elapsed);
            break;
    }
};

// 控制序列
sequence.Pause();
sequence.SwitchToPlayback(1);
sequence.Resume();

3. 动画元组 ​

csharp
// 配置元组动画
var bracketData = new GameDataAnimationBracket {
    BirthStandDeathAnimation = new BirthStandDeathAnimation {
        BirthAnimation = "enemy_spawn",
        StandAnimation = "enemy_idle", 
        DeathAnimation = "enemy_death"
    },
    ForceOneShot = false,  // 出生 → 待机(循环)→ 死亡
    KillOnFinish = true    // 死亡后销毁actor
};

// 播放元组
var bracket = actor.PlayAnimationBracket(bracketData.CachePlayParam);

动画优先级系统 ​

系统使用 AnimationLogicLayer 和 Priority 共同管理多个动画:

  • 逻辑层:动画系统会先把逻辑层和优先级合成为底层动画 layer,再按 layer 顺序和压制规则决定最终可见动画
  • 优先级值:有效范围为 0-31;同一逻辑层内数值越高,优先级越高;低于 0 按 0 处理,高于 31 按 31 处理
  • 压制:高底层 layer 的全身/根骨骼动画通常会压制低 layer 动画;不同骨骼范围或半身动画可以混合
  • 混合:基于 BlendIn 时间的动画间平滑过渡

框架自动播放的默认待机和移动动画位于 AnimationLogicLayer.Rollback:待机优先级为 1,移动优先级为 2。多数手动播放的触发器/技能动画默认位于 AnimationLogicLayer.Normal,底层 layer 高于 Rollback,因此通常可以覆盖默认待机和移动。

与默认 idle / move 状态机配合 ​

如果需要一个“站立时可见,但单位一移动就自动让位给移动动画”的手动动画,不要放在默认的 AnimationLogicLayer.Normal。Normal 层会高于默认移动层,容易把移动动画也盖住。

这类动画应放到默认回滚层,并使用和默认移动动画相同的优先级:

csharp
_ = new GameDataAnimationSimple(standingPoseLink)
{
    File = "standing_pose"u8,
    LogicLayer = AnimationLogicLayer.Rollback,
    Priority = 2,
    IsLooping = true,
    BlendIn = TimeSpan.FromSeconds(0.1),
};

默认层级关系是:

动画LogicLayerPriority结果
默认 idleRollback1最底层兜底待机
手动站立表现Rollback2站立时覆盖 idle
默认 moveRollback2移动开始时与手动站立表现同层,播放 move 会淡出同层动画

这适合“站立展示、移动打断”的效果。如果在单位已经移动时再播放这个动画,它也会暂时抢默认 move 的同层权重;这种情况下应先判断单位是否静止,或由自己的状态逻辑决定何时重播/停止。

启用自定义 UnitLocomotionAnimation 控制器后,默认 idle / move 的固定优先级不再是唯一来源。可以通过 GameDataUnitLocomotionAnimationProfile 为 Idle、Move、JumpStart、AirRise、AirFall、Land 等姿态分别设置 LogicLayer 和 Priority,也可以通过 unit.UseUnitLocomotionAnimation(profile) 应用指定 profile。此时应按项目自己的 locomotion profile 来安排手动动画的层级,而不是假设底层默认值一定是 idle=1、move=2。

优先级示例 ​

csharp
// 基础待机动画(低优先级)
var idleAnimation = actor.PlayAnimation(idleAnimationLink, new AnimationPlayParam {
    Priority = 0,
    IsLooping = true
});

// 攻击动画(高优先级)- 将压制待机动画
var attackAnimation = actor.PlayAnimation(attackAnimationLink, new AnimationPlayParam {
    Priority = 30,
    BlendIn = TimeSpan.FromSeconds(0.1f)
});

// 攻击完成时,待机动画自动恢复

事件系统 ​

动画事件 ​

普通动画可以通过引擎的状态变化系统触发事件:

csharp
animation.StateChanged += (stateEvent) => {
    switch (stateEvent) {
        case AnimationStateEvent.Started:
            OnAnimationStarted();
            break;
        case AnimationStateEvent.Finished:
            OnAnimationFinished();
            break;
        case AnimationStateEvent.Removed:
            OnAnimationRemoved();
            break;
    }
};

序列事件 ​

动画序列提供丰富的事件功能:

csharp
sequence.EventTriggered += (args) => {
    // 访问全面的事件上下文
    var sequenceInstance = args.Sequence;
    var eventType = args.EventKey;
    var timingSinceStart = args.Elapsed;
    var activePlaybackIndex = args.PlaybackIndex;
    var activeAnimation = args.Animation;
};

sequence.Removed += () => {
    // 序列完全结束
};

sequence.PlaybackIndexChanged += (newIndex) => {
    // 切换到不同的播放
};

高级功能 ​

1. 动画映射与动画来源 ​

动画来源 ​

模型可以使用两种来源的动画:

  • 模型自带动画:内嵌在模型资产中的动画片段。可以直接用原名播放(如 "attack_01"),无需映射。

  • 共享 Humanoid 动画:独立的动画集(路径格式 anim/{category}/{weapon}/{set}/{name}.ani),可被多个模型复用。通过完整路径(带 .ani)播放,或通过映射后用别名播放。资源统计会尝试从 GameDataUnit.Model / Abilities、GameDataAbility.Animation 和对应 GameDataAnimation 素材中自动推导模型需要烘焙的技能动画,也会收集 GameDataModel.HumanoidSourceAnimations 中的显式动画。编辑器随后针对当前模型骨骼进行重定向烘焙。烘焙前后动画名不变,引擎自动优先使用烘焙版本。

    对 Maker 导入的外部 MDL,不要把上述清单入口理解为首次骨骼适配入口。HumanoidSourceAnimations 只声明待烘焙的动画,不能替代源动作参考模型、骨骼语义映射或模型空间对齐。Maker 模型应先按 官方动作重定向 完成显式适配;只有经过验证的同骨架、同空间模型,或已经生成项目拥有的重定向动画,才适合使用该字段做资源声明。

    自动推导依赖代码结构可被编译期分析器静态追踪,并不保证覆盖 helper、工厂、动态集合修改或运行时条件等所有写法。对于必须稳定进入烘焙列表的动画,建议仍显式配置 HumanoidSourceAnimations;也可以手动维护 ref/model_animation.json,然后在编辑器中只执行 “工具 → 资源统计和动画重定向 → 生成”。若再次执行“统计资源”或“统计资源并生成”,手工 JSON 会被分析结果覆盖。

    编辑器调试时如果原始动画存在但项目 res/anim/<ModelName>/... 下缺少烘焙结果,运行时会给出“原始 Humanoid 动画存在但 baked 动画缺失”的日志提示,此时应先执行全量资源统计和动画烘焙。

AnimationMappings ​

GameDataModel.AnimationMappings 为动画创建别名,有两个作用:

  1. 方便引用(用短别名代替原始动画名或完整路径)
  2. 指定框架硬编码别名:框架通过 idle、move、death 等固定别名自动播放对应动画,必须通过映射指定实际动画名
csharp
// 在 GameDataModel 中配置映射
AnimationMappings = [
    new() { AnimationAlias = "idle", AnimationRaw = "idle" },
    new() { AnimationAlias = "move", AnimationRaw = "move_02" },
    new() { AnimationAlias = "attack", AnimationRaw = "attack_01" },
    new() { AnimationAlias = "death", AnimationRaw = "death" },
],

运行时也可以通过 SetAnimationMapping 动态修改映射:

csharp
// 设置映射
actor.SetAnimationMapping("walk", "character_walk_normal");
actor.SetAnimationMapping("run", "character_run_normal");

// 稍后,改变角色的移动风格
actor.SetAnimationMapping("walk", "character_walk_sneaky");
actor.SetAnimationMapping("run", "character_run_sneaky");

2. 动态Actor生成 ​

序列可以精确定时生成临时Actor:

csharp
var sequenceActors = new List<SequenceActorSpawn> {
    new SequenceActorSpawn {
        Actor = sparkEffectActor,
        SpawnOffset = TimeSpan.FromSeconds(0.8),  // 在0.8秒时生成
        Duration = TimeSpan.FromSeconds(1.5)      // 存活1.5秒
    },
    new SequenceActorSpawn {
        Actor = smokeEffectActor,
        SpawnOffset = TimeSpan.FromSeconds(1.0),  // 在1.0秒时生成
        Duration = TimeSpan.FromSeconds(3.0)      // 存活3.0秒
    }
};

3. 播放控制 ​

精细的动画播放控制:

csharp
// 速度操作
animation.Speed = 0.5f;  // 半速
animation.Speed = 2.0f;  // 双倍速

// 偏移控制
animation.PlayingOffset = TimeSpan.FromSeconds(1.5f);  // 跳转到动画的1.5秒处

// 循环控制
animation.IsLooping = true;

// 混合权重淡化
animation.FadeBlendWeight(0.5f, TimeSpan.FromSeconds(1.0f));

性能考虑 ​

1. 动画缓存 ​

系统维护高效的缓存:

  • 动画缓存:ConditionalWeakTable<IViewAnimation, Animation>用于自动清理
  • 参数缓存:CachePlayParam属性避免重复的参数构造
  • 引擎集成:直接的视图层集成最小化开销

2. 内存管理 ​

  • 可释放模式:所有动画对象实现适当的释放
  • 自动清理:当Actor被销毁时动画会清理
  • 弱引用:引擎动画使用弱引用防止内存泄漏

3. 最佳实践 ​

  1. 重用GameData:缓存动画配置而不是创建新的
  2. 适当释放:不再需要时始终释放动画
  3. 事件取消订阅:移除事件处理程序以防止内存泄漏
  4. 优先级管理:使用适当的优先级值避免冲突

调试和故障排除 ​

常见问题 ​

  1. 动画不播放:检查优先级冲突和压制
  2. 内存泄漏:确保适当的事件处理程序清理
  3. 时间问题:验证混入时间和序列偏移
  4. 性能问题:监控动画实例数量

Humanoid 动画烘焙缺失 ​

技能或脚本播放 dash.ani 等 Humanoid 动画时,编辑器调试环境下要区分两类资源:

  • 原始 Humanoid 动画:例如 anim/.../dash.ani,来自官方动画库。
  • 当前模型的烘焙动画:生成在项目 res/anim/<ModelName>/... 下,用于把共享 Humanoid 动画适配到具体模型骨骼。

如果客户端日志或信息面板提示原始动画存在但 baked 动画缺失,执行编辑器菜单:

text
调试 / 全量资源统计并调试

AI 助手可通过 SCE Editor MCP 调用:

json
{ "tool": "debug_start_with_resource_statistics", "arguments": {} }

只生成资源统计和烘焙动画、不启动调试时,调用:

json
{ "tool": "resource_statistics_generate_animations", "arguments": {} }

执行后检查 ref/model_animation.json 是否包含目标模型和动画,以及 res/anim/<ModelName>/... 是否出现生成结果。若 model_animation.json 中没有目标动画,优先检查 GameDataModel.Asset、单位 Model / Abilities、技能 Animation 和对应 GameDataAnimation 素材是否以资源统计能识别的方式配置。代码结构比较动态时,将动画显式加入 HumanoidSourceAnimations;必要时也可手动编辑 ref/model_animation.json,再只执行编辑器菜单中的“生成”。

调试功能 ​

编程方式调试 ​

csharp
// 检查活跃动画
var activeAnimations = actor.GetAnimationInstances();
foreach (var anim in activeAnimations) {
    Game.Logger.LogInformation("Animation: {Resource}, Priority: {Priority}, Playing: {Playing}",
        anim.AnimationResource, anim.Priority, anim.IsActivePlaying);
}

// 监控序列状态
sequence.PlaybackIndexChanged += (index) => {
    Game.Logger.LogInformation("Sequence switched to playback {Index}", index);
};

动画信息 Overlay 调试工具 ​

框架提供了专门的动画信息可视化调试工具,可以实时显示动画播放状态:

csharp
// 启用动画信息Overlay
AnimationInfoOverlay.EnableOverlay();

功能特色:

  • 实时显示: 鼠标悬停在Actor/Unit上时显示动画详细信息
  • 信息丰富: 显示动画资源、优先级、播放进度、循环状态等
  • 智能跟随: 面板自动跟随目标移动,保持信息可见
  • 死亡动画支持: 支持观察单位死亡动画的完整过程
  • 性能友好: 20FPS更新频率,对游戏性能影响最小

使用方法:

  1. 按 Ctrl + F2 打开调试界面
  2. 启用"Animation Info Overlay"开关
  3. 将鼠标悬停在具有动画的单位上查看信息

更多详细信息请参考 <see cref="调试作弊系统"/>。

与其他系统的集成 ​

1. Actor系统集成 ​

支持动画的对象通过 IActorAnimationPlayable 提供统一接口:

  • ActorModel:具有完整动画支持的3D模型Actor
  • Entity、Unit:自身实现该接口,可直接调用动画扩展方法

2. 实体与动画集成 ​

Entity 和 Unit 自身实现 IActorAnimationPlayable,可直接调用动画扩展方法。Unit 不是 ActorModel,也没有 Unit.GetActor() 方法:

csharp
#if CLIENT
using GameCore.ActorSystem;

unit.PlayAnimation(animationLink);
#endif

3. 游戏数据集成 ​

动画配置存储为游戏数据:

  • GameDataAnimationSimple:基础动画参数
  • GameDataAnimationSequence:复杂序列配置
  • GameDataAnimationBracket:生命周期动画定义

这允许数据驱动的动画系统,其中动画行为可以在不更改代码的情况下修改。

4. 技能系统集成 ​

技能系统通过 GameDataAbilityActive 的 Animation 字段与模型动画系统深度集成,提供技能释放时的动画播放功能。

Animation 字段配置 ​

GameDataAbilityActive 的 Animation 属性:若设置,技能施放时将播放对应动画;若配置多个动画,每次施放会随机选择一个。

基本用法 ​

> 注意:以下示例中的 abilityLink、animationLink 等变量为项目内定义的 GameLink 引用(如通过 ScopeData 或数据编辑器配置),非框架 API 常量。

csharp
// 1. 首先创建动画数据
var attackAnimationLink = new GameLink<GameDataAnimation, GameDataAnimationSimple>("SwordAttack");
var attackAnimation = new GameDataAnimationSimple(attackAnimationLink)
{
    File = "sword_attack"u8,
    Priority = 30,
    LogicLayer = AnimationLogicLayer.Normal,
    Speed = 1.2f,
    BodyPart = AnimationBodyPart.FullBody,
    BlendIn = TimeSpan.Zero, // 短普攻/高攻速技能避免默认混入时间盖过出手动作
};

// 2. 在技能中配置动画
var swordAttackAbility = new GameDataAbilityExecute(abilityLink)
{
    Time = new()
    {
        Preswing = static (_) => TimeSpan.FromSeconds(0.5),
        Backswing = static (_) => TimeSpan.FromSeconds(0.3),
    },
    Effect = damageEffect,
    Animation = [attackAnimationLink], // 关键:配置技能动画
    // ... 其他配置
};

多动画随机选择 ​

技能系统支持为单个技能配置多个动画,每次释放时会随机选择:

csharp
// 创建多个攻击动画变体
var attack1 = new GameLink<GameDataAnimation, GameDataAnimationSimple>("SwordAttack1");
var attack2 = new GameLink<GameDataAnimation, GameDataAnimationSimple>("SwordAttack2");
var attack3 = new GameLink<GameDataAnimation, GameDataAnimationSimple>("SwordAttack3");

// 在技能中配置多个动画
var combatAbility = new GameDataAbilityExecute(abilityLink)
{
    Animation = [attack1, attack2, attack3], // 随机选择播放
    // ... 其他配置
};

动画与技能阶段同步 ​

技能动画会自动与技能的执行阶段同步:

  • Preswing阶段:开始播放配置的动画
  • Cast阶段:动画进入主要播放阶段
  • Backswing阶段:动画收尾阶段
csharp
// 框架自动处理的动画生命周期
protected override void OnAnimationState(OrderStage stage)
{
    switch (stage.InnerValue)
    {
        case EOrderStage.Preswing:
            // 创建并开始播放动画
            break;
        case EOrderStage.Cast:
            // 动画进入主要阶段
            break;
        case EOrderStage.Backswing:
            // 动画收尾
            break;
    }
}

实际应用示例 ​

在项目中为技能配置动画时,使用项目内的 GameLink 引用动画数据:

csharp
// 怪物攻击动画配置(animationLink 为项目内 GameDataAnimationSimple 的 GameLink)
var monsterAttackAnimation = new GameDataAnimationSimple(animationLink)
{
    File = "attack"u8,
    Priority = 30,
    LogicLayer = AnimationLogicLayer.Normal
};

// 怪物攻击技能配置(abilityLink 为项目内 GameDataAbilityExecute 的 GameLink)
var monsterAttackAbility = new GameDataAbilityExecute(abilityLink)
{
    Time = new()
    {
        Preswing = static (_) => TimeSpan.FromSeconds(0.5),
        Backswing = static (_) => TimeSpan.FromSeconds(0.3),
    },
    Effect = damageEffect,
    Animation = [animationLink], // 技能释放时播放攻击动画
    AbilityExecuteFlags = new() { IsAttack = true },
    // ... 其他配置
};

最佳实践 ​

  1. 动画时长与技能时间匹配

    • 确保动画总时长与技能的 Preswing + Backswing 时间大致匹配
    • 避免动画过长或过短导致的不自然表现
    • 技能动画会按总施法时长调速,但不会自动缩短 GameDataAnimationSimple.BlendIn
  2. 使用合适的动画优先级

    • 技能动画通常应使用同一逻辑层内较高优先级(例如 20-31)
    • 确保技能动画能正确覆盖移动等低优先级动画
  3. 混入时间匹配最短技能周期

    • GameDataAnimationSimple.BlendIn 默认约 0.15 秒
    • 短普攻或高攻速普攻的周期如果接近/短于默认混入时间,出手动作会在尚未完全混入时被下一次施放覆盖
    • 对这类动作显式设置 BlendIn = TimeSpan.Zero,或按手感设置为 20-50ms
  4. 考虑动画变体

    • 为重复使用的技能提供多个动画变体
    • 增加游戏的视觉丰富性和随机性
  5. 动画与效果同步

    • 确保视觉效果的时机与动画关键帧匹配
    • 使用动画事件系统进行精确的时机控制

GameGraph 层桥接 ​

Actor 层的 IActorAnimationPlayable.PlayAnimation 等接口与 GameGraph 的 AnimationController 操作同一个底层动画控制器。可以双向控制等价 —— Actor 层做高层逻辑(优先级压制、序列编排),GameGraph 层做精细控制(单条动画的 Time/Weight/Speed、Bone/Socket 访问)。

扩展方法清单 ​

定义在 GameGraph/NodeSystem/ExtensionMethod.cs,需要 using GameGraph.NodeSystem;:

扩展方法返回说明
IActorAnimationPlayable.GetAnimatedModel()AnimatedModelComponent?取底层 AnimatedModel 组件
IActorAnimationPlayable.GetAnimationController()AnimationController?取底层 AnimationController 组件
GameCore.ModelAnimation.Animation.GetAnimationState()AnimationState?取该动画对应的底层运行时状态

典型下钻示例 ​

csharp
#if CLIENT
using GameCore.ActorSystem;
using GameGraph.NodeSystem;
using GameGraph.NodeSystem.Component.Graphics;

// Unit 自身支持动画,可直接获取底层 AnimationController
var ctrl = unit.GetAnimationController();
ctrl?.SetSpeed("Animations/Run.ani", 1.5f);

// Actor 层 PlayAnimation 拿到 Animation,再下钻取 AnimationState
var anim = unit.PlayAnimation(myAnimationLink);
var state = anim?.GetAnimationState();
if (state != null)
{
    state.Weight = 0.5f;
    state.IsInterpolated = false;
}

// 拿到模型组件做骨骼访问
var model = unit.GetAnimatedModel();
var swordSocket = model?.GetSocket("socket_weapon_r");
if (swordSocket.HasValue)
{
    var sword = swordSocket.Value.BoneNode.CreateChild("Sword");
    sword.LocalPosition = swordSocket.Value.LocalPosition;
    // ...
}
#endif

注意事项 ​

  • Actor 的 ModelAnimation 体系还会做优先级压制等额外逻辑,绕过它直接操作底层 controller 可能与 Actor 层的状态机产生冲突
  • 建议只在以下场景使用下钻:
    • 单条动画的精细 Time / Weight / Speed 控制
    • 骨骼访问(挂武器 / 特效到 socket)
    • 已经脱离 Actor 体系的纯 GameGraph 模式(这种情况建议直接用 GameGraph API,不经过 Actor)
  • 不建议通过下钻路径调 Stop / StopAll / Play 等大动作 —— 这些会与 Actor 上层的优先级/序列状态机失去同步

Humanoid 烘焙动画路径 ​

Actor 层 PlayAnimation 会自动解析烘焙动画路径(详见上方“动画映射与动画来源”章节)。GameGraph 层不做此自动解析 —— 如果纯 GameGraph 模式需要 humanoid 烘焙动画,用 animatedModel.ComputeBakedAnimationPath(animName) 显式取得路径(anim/{modelName}/{animName}),再交给 AnimationController.Play。详见 GameGraphSkeletalAnimation.md — 烘焙动画路径解析。

完整 GameGraph 层 API 文档:GameGraphSkeletalAnimation.md