Keep your place in this quest

Log in or sign up for free to subscribe, follow lesson progress, and access more learning content.

现在让我们了解一下 Cave 中的角色和动画是如何工作的。角色不仅仅是关卡中行走的 3D 模型。在真实的游戏设置中,它通常结合了物理、移动逻辑、可见网格、骨骼装配、动画,以及决定播放哪种动画的代码或逻辑积木。

为了实用起见,我们将使用 Cave 启动项目中的默认 Player 模板作为主要参考。如果你创建了 Third Person GameTop Down Game,Cave 会生成一个已经能移动、具有可见动画角色并播放基本动作动画的玩家。

image.png

本课将展示该设置是如何组织的,以及如何从 Python 控制动画。

你将学习:

  • 启动项目中的 Player 模板是如何构建的。
  • 为什么可见网格是子实体,而不在根玩家实体上。
  • Animation Component 的作用。
  • 如何从 Python 获得动画器(类似逻辑也适用于 Logic Bricks)。
  • 如何按名称播放动画。
  • 混合(blending)、图层(layers)和骨骼过滤器(bone filters)是如何工作的。
  • 动画回调和插槽在系统中的作用。

玩家模板结构

在启动项目中,Player 是一个实体模板。这意味着玩家设置是可重用的,可以放置在多个场景中。

结构大致如下:

image.png

具体子实体可能因所选项目选项不同有所变化,但重要的理念是根 Player 实体拥有游戏玩法设置,而其子实体 Mesh 拥有可见的动画角色。

启动的敌人也采用了非常相似的思路。敌人根实体具有角色物理和行为,其子实体 Mesh 包含可见网格和 Animation Component。

根玩家实体

Player 实体代表游戏玩法角色。它通常包含:

  • 一个 Transform Component
  • 一个 Character Component
  • 处理玩家移动、UI、动画辅助及其他游戏逻辑的 Python 组件。
  • 如生命值或可选行为设置等属性。
  • 用于摄像机、UI、可见网格和辅助对象的子实体。

Character Component 尤为重要,因为它处理角色风格的物理:行走、跳跃、碰撞、坡道检测以及针对世界的移动。这意味着根 Player 主要负责游戏玩法和物理。

网格子实体

在玩家模板中,有一个名为 Mesh 的子实体。该子实体通常包含:

  • 一个 Transform Component
  • 一个或多个 Mesh Components
  • 一个 Animation Component

Mesh Component 提供了角色可见的 3D 模型和材质。如果角色使用多种材质,Cave 可以通过多个 Mesh Components 来表示,遵循导入课程中讲解的多材质规则。

Animation Component 用于评估骨骼并播放动画。

简单来说:

实体 主要职责
Player 物理、移动、游戏逻辑、属性。
Player -> Mesh 可见模型、材质、骨骼动画。

这种分离是有意而为之。

为什么 Mesh 是子实体

你可能会疑惑,为什么网格不直接放在根 Player 实体上。

原因是物理角色和可见动画角色通常需要不同的变换。

Player 变换代表游戏内主体。它是移动于世界、与墙体碰撞并携带 Character Component 的实体。子 Mesh 变换代表角色的视觉外观。

这种分离在多种情况下非常有用:

  • 网格可能需要与物理胶囊不同的缩放比例。
  • 网格可能需要一个小偏移以对齐角色碰撞。
  • 网格可能需要独立于移动主体旋转。
  • 网格可能需要面向移动方向,而根实体保持游戏玩法方向。
  • 网格可能需要在玩家侧向移动时保持朝前。

举例来说,假设玩家向左移动。如果你有适合的“向左走”动画,你可能希望角色网格保持朝前,同时动画处理侧向移动。但若没有侧向动画,你或许希望网格朝向左侧旋转,使角色视觉上朝该方向行走。

这两种情况都是合理的。

它们都需要不同的变换:

变换 控制内容
Player 变换 物理位置、游戏实体、世界移动。
Mesh 变换 视觉朝向、缩放、偏移、动画表现。

这就是启动项目中玩家和敌人保持动画网格为子实体的原因。

Animation Component 的作用

Animation Component 是用于在实体上播放骨骼动画的主要组件。

它需要:

  • 一个 Armature
  • 一个默认 Animation
  • 位于同一实体上的有效 Mesh Component(或多个)。

骨骼装配(armature)定义了骨骼结构。动画定义了骨骼随时间的运动。Animation Component 评估最终的动画姿势并应用到可见角色上。

在默认的启动玩家中,Mesh 子实体使用:

  • Proto Mesh
  • Proto Mat
  • Proto Armature
  • p-idle 作为默认动画(空闲动画)

这些启动资产可让你立即检查一个可工作的动画角色。

骨骼装配、动画与重定向

  • Armature 是角色的骨骼。它包含网格跟随的骨头。
  • Animation 存储了骨骼随时间的运动。

Animation Component 将这些部分连接起来:

组件 含义
网格 可见的角色模型。
骨骼装配 角色内的骨架。
动画 动作,如空闲、行走、奔跑或跌落。
Animation Component 在实体上播放动画的组件。

Cave 还支持动画重定向(Retargeting)。如果播放的动画属于兼容的不同骨骼装配,Animation Component 可以用重定向方式将该动作应用到当前骨骼上。

重定向适合在兼容的角色间重用动画,但仍依赖导入骨骼的质量和兼容性。如果动画看起来扭曲、偏移或异常,请检查源骨骼、导入的骨骼装配和重定向设置。


从 Python 获取动画器

现在开始探索如何通过逻辑为角色添加动画。

在 Cave 项目中,通常做法是将玩家游戏逻辑保存在根 Player 实体上,从该代码访问 Mesh 子实体。默认启动脚本遵循此思路。

基本模式如下:

import cave

class PlayerAnimationExample(cave.Component):
    def start(self, scene: cave.Scene):
        self.mesh : cave.Entity = self.entity.getChild("Mesh")
        self.animator : cave.AnimationComponent = self.mesh.get("Animation")

    def update(self):
        # 示例:播放一个空闲动画。
        self.animator.playByName("p-idle", blend=0.2, loop=True)

在这个例子中:

  • self.entity 是根 Player 实体。
  • getChild("Mesh") 找到可见的子实体。
  • self.mesh.get("Animation") 获取 Animation Component。
  • 变量名叫 animator,这是 Cave 脚本中的常用命名。
  • playByName(...) 按动画资源名称播放动画。

这是控制动画前需要的基础连接。

按名称播放动画

你最常用的方法是 playByName

基本用法如下:

self.animator.playByName("p-walk", blend=0.2, loop=True)

重要参数如下:

参数 含义
anim 要播放的动画资源名称。
blend Cave 过渡到新动画所用的时间(秒)。
loop 动画是否循环播放。
layer 使用哪个动画图层播放动画。

例如:

self.animator.playByName("p-idle", blend=0.2, loop=True)
self.animator.playByName("p-walk", blend=0.2, loop=True)
self.animator.playByName("p-run", blend=0.2, loop=True)
self.animator.playByName("p-fall", blend=0.2, loop=True)

这些调用与默认玩家和敌人的脚本相同。

一个简单的移动动画示例

常见的玩家动画设置是:

  • 站立时播放空闲动画。
  • 移动时播放走路动画。
  • 跑步时播放奔跑动画。
  • 角色未踩地时播放跌落动画。

简化示例如下:

import cave

class SimplePlayerAnimator(cave.Component):
    def start(self, scene: cave.Scene):
        self.character : cave.CharacterComponent = self.entity.get("Character")

        self.mesh : cave.Entity = self.entity.getChild("Mesh")
        self.meshTransform : cave.TransformComponent = self.mesh.getTransform() if self.mesh else None
        self.animator      : cave.AnimationComponent = self.mesh.get("Animation") if self.mesh else None

    def update(self):
        if self.animator is None or self.character is None:
            return

        direction = self.character.getWalkDirection()
        isMoving = direction.length() > 0
        isRunning = False # 用你自己的输入或游戏条件替换这里的值。

if self.character.onGround():
            if isMoving:
                if isRunning:
                    self.animator.playByName("p-run", blend=0.2, loop=True)
                else:
                    self.animator.playByName("p-walk", blend=0.2, loop=True)
            else:
                self.animator.playByName("p-idle", blend=0.2, loop=True)
        else:
            self.animator.playByName("p-fall", blend=0.2, loop=True)

此示例故意设计得很简单。真正的入门控制器还处理输入、移动方向、跳跃、可选的点选行为和网格旋转,但动画的思路是相同的。

让网格朝向移动方向旋转

由于视觉上的 Mesh 实体有自己的变换,您可以将它与根实体 Player 分别旋转。

入门玩家在角色移动时即执行此操作:

if direction.length() > 0 and self.meshTransform:
    self.meshTransform.lookAtSmooth(
        self.entity.getTransform().transformDirection(-direction),
        6.0 * cave.getDeltaTime()
    )

重要的不是具体的数学,而是游戏物体主体和视觉网格可以单独控制。

这让你可以选择角色朝向:

  • 面向移动方向。
  • 在侧移时保持朝向前方。
  • 平滑转向目标方向。
  • 针对不同游戏模式使用不同的视觉朝向规则。

这也是入门角色有一个子 Mesh 实体的主要原因之一。

动画混合

动画混合使过渡更平滑。

没有混合时,从一个动画切换到另一个动画会瞬间切换。通过混合,Cave 可以在同一层动画之间平滑过渡。

例如:

# 从这个:
self.animator.playByName("p-idle", blend=0.2, loop=True)

# 到这个:
self.animator.playByName("p-walk", blend=0.2, loop=True)

这里的 blend=0.2 表示 Cave 会在 0.2 秒内平滑过渡到新动画。

这对角色尤其重要。玩家很容易察觉到生硬的动画突变,即使是在原型阶段。

好的初学者混合值通常较小:

  • 0.1 适用于非常快的过渡。
  • 0.2 适用于正常的移动过渡。
  • 0.4 或更高适用于较慢、较沉重的过渡。

你应该通过感觉来调整。

动画层

Cave 支持多层动画。每层可以播放自己的动画,高层可以叠加在低层之上。你可以在任何层运行动画。

默认的移动动画通常运行在第 0 层。

例如:

self.animator.playByName("p-walk", blend=0.2, layer=0, loop=True)

如果你有上半身动作动画,比如攻击、瞄准或装弹,可以在另一层播放:

self.animator.playByName("Attack", blend=0.1, layer=1, loop=False)

概念如下:

常见用途
层 0 全身移动动画,如闲置、走路、跑步、跳跃、掉落。
层 1 上半身动作,如攻击、瞄准、装弹、持械。

这允许角色在行走的同时,上层加一个动作。

层权重

每层都有权重,用于控制该层的影响力。

你可以用 Python 修改它:

self.animator.setLayerWeight(1, 1.0)

也可以读取它:

weight = self.animator.getLayerWeight(1)

权重为 1.0 表示该层完全激活,0.0 表示完全无影响。

层权重在你想淡入淡出某个动作层时非常有用,比如缓慢抬枪、瞄准或混合到特殊姿态。

需要说明的是,blend 参数只混合同一层的动画。我们不会混合不同层播放的动画。如果一层是动画0,突然另一层播放动画1,不管 blend 多大,它们不会相互混合。只有当同一层已经有动画播放时,才会混合。如果你想跨层混合,需要自定义逻辑,利用层权重实现。

骨骼过滤器

骨骼过滤器控制一层影响哪些骨骼及骨骼的影响力。

这使得上半身动画成为可能。

例如,你可能想:

  • 层 0 控制全身。
  • 层 1 只控制脊柱、手臂、手和武器骨骼。

在 Python 中,可以为层创建过滤器,为骨骼及其子骨骼设置影响力:

def setupUpperBodyLayer(self):
    armature = self.animator.armature.get()
    spine = armature.getBone("mixamorig:Spine")

    upperBody = self.animator.createLayerFilter(1)
    upperBody.defaultBlend = 0.0
    upperBody.setToBone(spine, 1.0, recursive=True)

在此示例中:

  • defaultBlend = 0.0 表示该层默认不影响骨骼。
  • setToBone(spine, 1.0, recursive=True) 表示脊柱和其子骨骼完全受影响。
  • 1 现在可以用于上半身动画。

你的骨骼名称取决于导入的骨架。请始终检查骨架,使用正确的骨骼名称。

实际层示例

想象一个第三人称角色,可以同时行走和攻击。

一个可能的设置是:

处理内容
层 0 闲置、走路、跑步、跌落。
层 1 上半身攻击。

代码示例:

import cave

class CombatAnimator(cave.Component):
    def start(self, scene: cave.Scene):
        self.mesh = self.entity.getChild("Mesh")
        self.animator : cave.AnimationComponent = self.mesh.get("Animation") if self.mesh else None

        if self.animator:
            armature = self.animator.armature.get()
            spine = armature.getBone("mixamorig:Spine")

            upperBody = self.animator.createLayerFilter(1)
            upperBody.defaultBlend = 0.0
            upperBody.setToBone(spine, 1.0, recursive=True)
            self.animator.setLayerWeight(1, 1.0)

    def playWalk(self):
        self.animator.playByName("p-walk", blend=0.2, layer=0, loop=True)

    def playAttack(self):
        self.animator.playByName("Attack", blend=0.1, layer=1, loop=False)

此示例假设你有一个名为 Attack 的动画资源。如果你的项目使用不同动画名,请使用项目中对应名称。

关键点是移动动画保持在层 0,而攻击动画播放在层 1,并只影响过滤的骨骼。

动画回调

动画可以在特定时间运行 Python 代码。

Cave 支持的回调用例包括:

  • 动画开始时。
  • 动画结束时。
  • 特定动画帧时。

回调对于动画触发游戏玩法或效果非常有用。

示例:

  • 播放脚步声。
  • 生成踏地尘土效果。
  • 攻击帧造成伤害。
  • 启动武器拖尾。
  • 触发粒子效果。
  • 通知动画结束。

动画通常比代码更清楚具体时机。例如,剑只有在挥到目标区域时才造成伤害,动画回调能准确安排这一时机。

脚步回调示例

入门项目可能在走路和跑步动画中包含脚步回调。查看它们是理解动画回调的好方法。

思路简单:

  1. 动画知道脚触地的时刻。
  2. 回调放置在该帧。
  3. 回调播放声音或生成小效果。

动画回调中,Cave 提供了有用的变量,如:

  • entity,所属实体。
  • animator,动画组件。
  • handle,正在播放的动画层或句柄。

一个很简单的脚步回调示例如下:

cave.playSound("Footstep Grass", volume=0.5)

您可以以后扩展此思路,依据地面材质、角色速度或当前状态选择不同的声音。

可复用的动画回调

回调属于动画资源。

这意味着如果多个实体播放同一动画,回调可为每个使用该动画的实体分别执行。

这让回调具有复用性。例如同一走路动画能为每个播放它的角色触发脚步声,前提是回调代码正确使用所属实体。

动画存储时机,实体提供上下文。

高级姿势回调

Cave 也支持通过 Python 的评估后回调。这发生在动画组件计算骨架姿势之后。

这是一个高级功能,但可用于:

  • 逆向动力学(IK)。
  • 头部瞄准。
  • 武器瞄准。
  • 脚部放置。
  • 最终姿势调整。

入门的 Player Toolkit 包含一个使用该概念的脚步 IK 示范。它获取 Mesh 子对象,获取 Animation 组件,然后注册方法:

self.animator.addPostEvaluationCallback(self.postEvaluation)

你不必立刻编写 IK 系统,但知道 Cave 允许 Python 在正常动画播放后调整最终姿势是很有用的。

Animation Socket Component

Animation Socket Component 允许子实体跟随父动画实体的骨骼。

子实体必须在带有动画组件的父实体下,然后插槽可以选择一个骨骼并复制该骨骼的位置、旋转,以及可选的缩放,必要时带有偏移。

插槽对附着物于动画角色非常有用:

  • 手中的剑。
  • 手中的枪。
  • 手臂上的盾牌。
  • 头上的头盔。
  • 脊椎骨上的背包。
  • 附加在手上的道具。

例如,如果角色持有一把剑,剑可以是一个带有 Animation Socket Component 的子实体,跟随手部骨骼。随着角色的攻击、奔跑或待机,剑会保持附着在正确的动画位置上。

你应该记住的内容

  • 初始玩家是一个带有根实体 Player 和子实体 Mesh 的模板。
  • 根实体 Player 通常负责物理、移动、属性和游戏逻辑。
  • 子实体 Mesh 通常负责可见模型、材质、骨骼架和 Animation Component。
  • 这种分离让物理体和视觉角色拥有独立的变换。
  • Animation Component 使用骨架和动画资源播放骨骼动画。
  • 在 Python 中,通常将 Animation Component 称为 animator
  • playByName 通过名称播放动画资源,并支持混合、循环和图层。
  • 图层让多个动画叠加在一起。
  • 骨骼过滤器让图层只影响骨骼的一部分。
  • 动画回调将动画时间点连接到游戏事件。
  • Animation sockets 将道具附加到动画骨骼上。

当你检查初始的 PlayerEnemy 模板时,可以将此结构作为你的参照:根实体负责游戏玩法,子实体 Mesh 负责视觉和动画。