Animator.Update的底层流程

Animator.Update的底层流程是动画系统中每帧驱动动画播放和状态切换的核心逻辑。它负责根据当前状态机参数更新动画状态,计算骨骼变换,处理动画混合和过渡,触发事件,并最终输出当前帧的骨骼姿态。

下面是一个典型的Animator.Update底层流程的详细分解和伪代码示例,帮助你理解其内部执行步骤。


Animator.Update 底层流程详解


1. 参数更新(Update Parameters)

  • 读取或接收外部输入(如角色速度、攻击触发等)。
  • 更新状态机参数(bool、int、float、trigger等)。
  • 参数变化可能影响状态机过渡条件。

2. 状态机状态更新(Update State Machine)

  • 根据当前状态和参数,判断是否满足过渡条件。
  • 如果满足,开始状态过渡,记录过渡起始状态、目标状态和过渡时间。
  • 处理过渡时间推进,计算过渡权重。

3. 动画时间推进(Advance Animation Time)

  • 对当前动画剪辑或混合树推进播放时间(考虑播放速度、循环等)。
  • 如果处于过渡状态,同时推进两个状态的动画时间。

4. 采样动画姿态(Sample Animation Poses)

  • 根据当前时间采样动画剪辑关键帧,计算骨骼局部变换。
  • 如果是混合状态,采样多个动画剪辑,准备混合。

5. 动画混合(Blend Animations)

  • 根据过渡权重或混合参数,线性或多维混合多个动画姿态。
  • 处理层级混合(如上半身和下半身分开混合)。

6. 计算骨骼全局变换(Calculate Global Transforms)

  • 从根骨骼开始递归计算每个骨骼的全局变换矩阵。
  • 结合父骨骼变换,得到最终骨骼姿态。

7. 触发动画事件(Trigger Animation Events)

  • 检查当前动画时间是否经过事件触发点。
  • 触发绑定的回调函数或游戏逻辑。

8. 输出骨骼姿态(Output Final Pose)

  • 将计算好的骨骼全局变换传递给渲染系统或GPU。
  • 准备下一帧使用。

Animator.Update 伪代码示例

void Animator::Update(float deltaTime) {
    // 1. 更新参数(外部输入驱动)
    UpdateParameters();

    // 2. 更新状态机状态和过渡
    if (isTransitioning) {
        transitionTime += deltaTime;
        float t = transitionTime / transitionDuration;
        if (t >= 1.0f) {
            currentState = nextState;
            isTransitioning = false;
            ResetAnimationEvents(currentState);
        }
    } else {
        // 检查是否满足过渡条件
        for (auto& transition : currentState->transitions) {
            if (transition->IsConditionMet(parameters)) {
                StartTransition(transition);
                break;
            }
        }
    }

    // 3. 推进动画时间
    currentState->AdvanceTime(deltaTime);
    if (isTransitioning) {
        nextState->AdvanceTime(deltaTime);
    }

    // 4. 采样动画姿态
    Pose poseA = currentState->SamplePose();
    Pose poseB;
    if (isTransitioning) {
        poseB = nextState->SamplePose();
    }

    // 5. 混合动画
    Pose finalPose;
    if (isTransitioning) {
        float t = transitionTime / transitionDuration;
        finalPose = BlendPoses(poseA, poseB, t);
    } else {
        finalPose = poseA;
    }

    // 6. 计算骨骼全局变换
    CalculateGlobalTransforms(finalPose);

    // 7. 触发动画事件
    TriggerAnimationEvents(currentState, deltaTime);
    if (isTransitioning) {
        TriggerAnimationEvents(nextState, deltaTime);
    }

    // 8. 输出骨骼姿态
    OutputPose(finalPose);
}

关键点总结

步骤说明
参数更新读取外部输入,更新状态机参数
状态机更新判断状态切换,处理过渡
时间推进推进动画播放时间,支持循环和速度调整
采样姿态关键帧插值计算骨骼局部变换
动画混合线性或多维混合多个动画状态,支持层级混合
全局变换计算递归计算骨骼全局变换矩阵
事件触发根据动画时间触发绑定事件
输出姿态将最终骨骼变换传递给渲染或GPU

接下来我将更深入地拆解Animator.Update流程中的关键步骤,特别是状态机过渡处理、动画混合算法、骨骼全局变换计算和动画事件触发的具体实现细节,并给出更贴近实际的伪代码示例。


1. 状态机过渡处理细节


1.1 过渡条件检测

  • 每个状态维护一组过渡(Transition),每个过渡有条件(Condition)和目标状态。
  • 条件通常是参数比较(如speed > 0.1isJumping == true)。
  • 过渡检测时,遍历当前状态所有过渡,判断条件是否满足。

1.2 过渡启动

  • 满足条件时,记录过渡起始时间,设置过渡状态。
  • 过渡期间,两个状态动画同时播放,权重从0线性变化到1。

1.3 过渡结束

  • 过渡时间达到设定时长,切换到目标状态,结束过渡。

1.4 伪代码示例

void Animator::CheckTransitions() {
    for (auto& transition : currentState->transitions) {
        if (transition->IsConditionMet(parameters)) {
            StartTransition(transition);
            break;
        }
    }
}

void Animator::StartTransition(Transition* transition) {
    isTransitioning = true;
    transitionTime = 0.0f;
    transitionDuration = transition->duration;
    nextState = transition->targetState;

    // 重置目标状态动画时间
    nextState->ResetTime();
}

2. 动画混合算法


2.1 线性混合(Crossfade)

  • 两个动画姿态按权重线性插值。
  • 对每个骨骼的旋转使用球面线性插值(Slerp),位置和缩放用线性插值。

2.2 多维混合(Blend Trees)

  • 根据多个参数计算权重,混合多个动画。
  • 例如二维混合使用三角插值或双线性插值。

2.3 混合伪代码示例

Pose BlendPoses(const Pose& poseA, const Pose& poseB, float t) {
    Pose result;
    for (int i = 0; i < poseA.bones.size(); ++i) {
        result.bones[i].position = Lerp(poseA.bones[i].position, poseB.bones[i].position, t);
        result.bones[i].rotation = Slerp(poseA.bones[i].rotation, poseB.bones[i].rotation, t);
        result.bones[i].scale = Lerp(poseA.bones[i].scale, poseB.bones[i].scale, t);
    }
    return result;
}

接下来我将进一步细化Animator.Update底层流程中的关键步骤,特别是状态机过渡处理、动画混合算法、骨骼全局变换计算和事件触发机制的实现细节,并给出更具体的伪代码示例,帮助你更深入理解。


1. 状态机过渡处理细节


1.1 过渡条件判断

  • 每个状态包含若干个过渡(Transition),每个过渡有条件(Condition)列表。
  • 条件类型包括:布尔值、触发器、数值比较等。
  • 只有所有条件满足,过渡才会触发。

1.2 过渡启动与时间管理

  • 过渡开始时,记录起始状态、目标状态、过渡时长和当前过渡时间。
  • 过渡期间,两个状态动画同时播放,权重从0线性变化到1。
  • 过渡结束后,切换到目标状态,清理过渡数据。

1.3 伪代码示例

bool Transition::IsConditionMet(const ParameterSet& params) {
    for (auto& cond : conditions) {
        if (!cond.Evaluate(params)) return false;
    }
    return true;
}

void Animator::StartTransition(Transition* transition) {
    isTransitioning = true;
    transitionTime = 0.0f;
    transitionDuration = transition->duration;
    nextState = transition->targetState;
    // 可能需要重置目标状态动画时间
    nextState->ResetTime();
}

2. 动画混合算法细节


2.1 线性混合(Crossfade)

  • 两个Pose对应骨骼逐个插值。
  • 位置用线性插值,旋转用球面线性插值(Slerp)。

2.2 多层混合

  • 支持多个动画层,每层有权重和蒙版。
  • 蒙版定义哪些骨骼受该层影响。
  • 先计算基础层Pose,再叠加高层Pose。

2.3 伪代码示例

Pose BlendPoses(const Pose& poseA, const Pose& poseB, float t) {
    Pose result;
    for (int i = 0; i < poseA.bones.size(); ++i) {
        result.bones[i].position = Lerp(poseA.bones[i].position, poseB.bones[i].position, t);
        result.bones[i].rotation = Slerp(poseA.bones[i].rotation, poseB.bones[i].rotation, t);
    }
    return result;
}

Pose LayeredBlend(const std::vector<Pose>& poses, const std::vector<float>& weights, const std::vector<BoneMask>& masks) {
    Pose basePose = poses[0];
    for (int layer = 1; layer < poses.size(); ++layer) {
        for (int i = 0; i < basePose.bones.size(); ++i) {
            if (masks[layer].IsBoneActive(i)) {
                basePose.bones[i].position = Lerp(basePose.bones[i].position, poses[layer].bones[i].position, weights[layer]);
                basePose.bones[i].rotation = Slerp(basePose.bones[i].rotation, poses[layer].bones[i].rotation, weights[layer]);
            }
        }
    }
    return basePose;
}

3. 骨骼全局变换计算


3.1 局部到全局变换

  • 每个骨骼的全局变换 = 父骨骼全局变换 * 当前骨骼局部变换。
  • 根骨骼的全局变换通常是模型变换矩阵。

3.2 递归计算

  • 从根骨骼开始递归计算所有骨骼的全局变换。
  • 结果用于顶点变形和GPU上传。

3.3 伪代码示例

void CalculateGlobalTransformsRecursive(int boneIndex, const Pose& localPose, std::vector<Matrix4>& globalTransforms, const std::vector<int>& parentIndices) {
    int parentIndex = parentIndices[boneIndex];
    if (parentIndex < 0) {
        globalTransforms[boneIndex] = localPose.bones[boneIndex].ToMatrix();
    } else {
        globalTransforms[boneIndex] = globalTransforms[parentIndex] * localPose.bones[boneIndex].ToMatrix();
    }

    // 递归计算子骨骼
    for (int childIndex : skeleton.GetChildren(boneIndex)) {
        CalculateGlobalTransformsRecursive(childIndex, localPose, globalTransforms, parentIndices);
    }
}

void CalculateGlobalTransforms(const Pose& localPose) {
    CalculateGlobalTransformsRecursive(rootBoneIndex, localPose, globalTransforms, parentIndices);
}

4. 动画事件触发机制


4.1 事件检测

  • 事件绑定在动画的特定时间点(Normalized Time)。
  • 每帧检测动画时间是否跨过事件时间点。

4.2 事件触发状态管理

  • 防止事件重复触发,记录事件是否已触发。
  • 循环动画时,重置事件触发状态。

4.3 伪代码示例

void TriggerAnimationEvents(AnimationState* state, float deltaTime) {
    float prevTime = state->previousTime;
    float currTime = state->currentTime;
    float duration = state->clip->duration;

    for (auto& e : state->clip->events) {
        float eventTime = e.time * duration;
        if (!e.triggered && prevTime < eventTime && currTime >= eventTime) {
            e.callback();
            e.triggered = true;
        }
    }

    // 循环动画时重置事件
    if (currTime < prevTime) {
        for (auto& e : state->clip->events) {
            e.triggered = false;
        }
    }

    state->previousTime = currTime;
}

总结

细节模块关键点
状态机过渡条件判断严格,过渡时间管理,状态切换清理
动画混合位置线性插值,旋转Slerp,多层蒙版混合
全局变换计算递归局部到全局矩阵计算,根骨骼特殊处理
事件触发时间点检测,防止重复触发,循环动画事件重置

### Unity 动作完成后自动暂停的原因分析 在 Unity 中,当动画或动作完成时自动暂停的行为通常是由以下几个原因引起的: 1. **Animator Controller 配置问题** 如果 Animator Controller 的默认状态未设置为循环模式,则可能导致动画播放完毕后停止并保持最后一帧的状态[^1]。 2. **脚本逻辑控制不足** 在某些情况下,如果通过脚本触发动画但未正确处理回调函数(如 `OnStateExit` 或 `AnimationEvent`),可能会导致动画结束后未能及时切换到其他状态或重新启动动画[^4]。 3. **时间轴配置错误** 游戏的时间管理机制可能影响动画的正常运行。例如,当游戏时间线被暂停(如调出菜单时)或者使用了自定义的时间比例(Time.timeScale),这会影响动画的实际播放速度甚至使其完全静止[^2]。 --- ### 解决方案 以下是针对上述问题的具体解决办法: #### 方法一:修改 Animator Controller 设置 确保目标动画的状态已启用循环选项(Loop Time)。可以通过以下步骤实现: 1. 打开 Animator Controller 文件; 2. 选中对应的动画状态; 3. 在 Inspector 窗口中勾选 “Loop Time” 属性。 此操作适用于希望动画无限重复的情况。 #### 方法二:优化脚本逻辑 利用事件驱动的方式,在动画结束时执行特定行为。例如,可以监听 `OnAnimatorMove()` 或者绑定 `AnimationEvent` 来检测动画是否已完成,并采取相应措施。下面是一个简单的示例代码片段: ```csharp using UnityEngine; public class AnimationController : MonoBehaviour { private Animator animator; void Start() { animator = GetComponent<Animator>(); } public void OnAnimationEnd() { // 当前动画结束后的处理逻辑 Debug.Log("Animation has finished."); // 可在此处重置动画或其他操作 animator.SetTrigger("Reset"); } } ``` 注意:需确保在动画剪辑中设置了相应的 Event 节点以调用 `OnAnimationEnd()` 函数。 #### 方法三:调整时间管理系统 确认项目中的时间比例(Time.timeScale)始终维持在一个合理的范围内。特别是在需要暂停整个场景的情况下,应单独隔离动画更新逻辑不受全局时间的影响。例如: ```csharp void Update() { float deltaTime = (Time.timeScale == 0) ? Time.unscaledDeltaTime : Time.deltaTime; transform.position += Vector3.forward * speed * deltaTime; } ``` 这样即使主流程处于停滞状态,动画仍能继续流畅运作。 #### 方法四:手动提取根运动数据 对于复杂的角色移动情况,建议绕过内置的根运动计算功能,改由开发者自行解析 AnimationStream 并应用至模型实例上。这种方法能够有效规避部分底层引擎缺陷带来的干扰。 --- ### 总结 综上所述,Unity 动画或动作停止的问题多源于控制器设定不当、脚本设计疏漏以及外部环境因素共同作用的结果。通过对以上几个方面的逐一排查与修正即可妥善解决问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值