Cave:入门指南
角色和动画
Lesson 13 of 19 • 25 XP
简体中文 (Simplified Chinese)
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 Game 或 Top Down Game,Cave 会生成一个已经能移动、具有可见动画角色并播放基本动作动画的玩家。

本课将展示该设置是如何组织的,以及如何从 Python 控制动画。
你将学习:
- 启动项目中的
Player模板是如何构建的。 - 为什么可见网格是子实体,而不在根玩家实体上。
- Animation Component 的作用。
- 如何从 Python 获得动画器(类似逻辑也适用于 Logic Bricks)。
- 如何按名称播放动画。
- 混合(blending)、图层(layers)和骨骼过滤器(bone filters)是如何工作的。
- 动画回调和插槽在系统中的作用。
玩家模板结构
在启动项目中,Player 是一个实体模板。这意味着玩家设置是可重用的,可以放置在多个场景中。
结构大致如下:

具体子实体可能因所选项目选项不同有所变化,但重要的理念是根 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 MeshProto MatProto 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 支持的回调用例包括:
- 动画开始时。
- 动画结束时。
- 特定动画帧时。
回调对于动画触发游戏玩法或效果非常有用。
示例:
- 播放脚步声。
- 生成踏地尘土效果。
- 攻击帧造成伤害。
- 启动武器拖尾。
- 触发粒子效果。
- 通知动画结束。
动画通常比代码更清楚具体时机。例如,剑只有在挥到目标区域时才造成伤害,动画回调能准确安排这一时机。
脚步回调示例
入门项目可能在走路和跑步动画中包含脚步回调。查看它们是理解动画回调的好方法。
思路简单:
- 动画知道脚触地的时刻。
- 回调放置在该帧。
- 回调播放声音或生成小效果。
动画回调中,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 将道具附加到动画骨骼上。
当你检查初始的 Player 或 Enemy 模板时,可以将此结构作为你的参照:根实体负责游戏玩法,子实体 Mesh 负责视觉和动画。