Cave:入门指南
角色与动画
Lesson 13 of 19 • 25 XP
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 模型。在实际游戏设置中,它通常结合了物理、运动逻辑、可见网格、骨架、动画以及决定应该播放哪种动画的代码或 Logic Bricks。
为了使其更具实用性,我们将使用 Cave 启动项目中的默认 Player 模板作为主要参考。如果您创建一个 Third Person Game 或 Top Down Game,Cave 会生成一个已经可以移动、具有可见动画角色并播放基本运动动画的玩家。

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

确切的子元素可能会根据您选择的项目选项而有所不同,但重要的想法是根 Player 实体拥有游戏设置,而子 Mesh 实体拥有可见的动画角色。
启动敌人使用了非常相似的理念。敌人的根实体具有角色物理和行为,其子 Mesh 实体包含可见网格和 Animation Component。
根玩家实体
根 Player 实体表示游戏角色。它通常包含:
- 一个
Transform Component。 - 一个
Character Component。 - 用于玩家移动、用户界面、动画助手和其他游戏逻辑的 Python 组件。
- 属性,如健康状态或可选行为设置。
- 用于相机、用户界面、可视网格和辅助对象的子对象。
Character Component 特别重要,因为它处理角色样式的物理:行走、跳跃、碰撞、坡度以及与世界的移动。这意味着根 Player 主要负责游戏性和物理。
网格子实体
在玩家模板中,有一个子实体称为 Mesh。这个子实体通常包含:
- 一个
Transform Component。 - 一个或多个
Mesh Components。 - 一个
Animation Component。
Mesh Component 为角色提供其可见的 3D 模型和材质。如果角色使用多个材质,Cave 可以使用多个 Mesh Components 来表示,遵循在导入课程中解释的多材质规则。
Animation Component 是用来评估骨架并播放动画的。
因此,简单来说:
| 实体 | 主要责任 |
|---|---|
Player |
物理、运动、游戏逻辑、属性。 |
Player -> Mesh |
可见模型、材质、骨架动画。 |
这种分离是有意为之。
为什么网格是子实体
您可能会想,为什么网格不直接放在根 Player 实体上。
原因是物理角色和可见动画角色通常需要不同的变换。
根 Player 的变换表示游戏主体。这就是在世界中移动、与墙壁碰撞并携带 Character Component 的东西。子 Mesh 的变换表示角色的外观。
这种分离在许多情况下是有用的:
- 网格可能需要与物理胶囊不同的缩放。
- 网格可能需要小的偏移,以与角色碰撞对齐。
- 网格可能需要 独立于移动主体旋转。
- 网格可能需要面向运动方向,而根保持游戏方向。
- 网格可能需要在玩家横向移动时保持面向前方。
例如,想象一下玩家向左移动。如果您有一个合适的 "walk left" 动画,您可能希望角色网格保持面向前方,而动画处理横向移动。但是如果您没有横向动画,您可能希望网格向左旋转,以便角色在视觉上向那个方向行走。
这两种情况都是有效的。
它们需要单独的变换:
| 变换 | 控制 |
|---|---|
根 Player 变换 |
物理位置、游戏体、在世界中的移动。 |
子 Mesh 变换 |
可视方向、缩放、偏移、动画呈现。 |
这就是为什么启动玩家和敌人将动画网格保留为子实体。
Animation Component 的作用
Animation Component 是用于在实体上播放骨骼动画的主要组件。
它需要:
- 一个
Armature。 - 一个默认的
Animation。 - 同一实体上的有效 Mesh Component(或多个)。
Armature 定义骨架。动画定义该骨架随时间的移动。Animation Component 评估最终动画姿势并将其应用于可见角色。
在默认启动玩家中,Mesh 子实体使用:
Proto Mesh\rProto Mat\rProto Armature\rp-idle作为默认动画(待机动画)
这些启动资产在那里,以便您可以立即检查一个正常工作的动画角色。
骨架、动画和重新目标
Armature是角色的骨架。它包含网格跟随的骨骼。Animation存储了骨架中骨骼的随时间变化的运动。
Animation Component 将这些部分连接在一起:
| 部件 | 含义 |
|---|---|
| Mesh | 可见角色模型。 |
| Armature | 角色内部的骨架。 |
| Animation | 动作,比如待机、行走、跑步或下落。 |
| Animation Component | 在实体上播放动画的组件。 |
Cave 在可能的情况下还可以重新目标动画。如果播放的动画属于不同的兼容骨架,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 # 替换为您自己的输入或游戏条件。
如果 self.character.onGround():
如果 isMoving:
如果 isRunning:
self.animator.playByName("p-run", blend=0.2, loop=True)
否则:
self.animator.playByName("p-walk", blend=0.2, loop=True)
否则:
self.animator.playByName("p-idle", blend=0.2, loop=True)
否则:
self.animator.playByName("p-fall", blend=0.2, loop=True)
这个例子故意保持简单。真正的初学者控制器还处理输入、移动方向、跳跃、可选的点按行为和网格旋转,但动画的思路是一样的。
面朝移动方向旋转网格
因为视觉 Mesh 实体有自己的变换,你可以将它与根 Player 分开旋转。
当角色移动时,初学者玩家会这样做:
如果 direction.length() > 0 和 self.meshTransform:
self.meshTransform.lookAtSmooth(
self.entity.getTransform().transformDirection(-direction),
6.0 * cave.getDeltaTime()
)
重要的思路不是确切的数学计算。重要的idea是,游戏主体和视觉网格可以单独控制。
这让你可以选择角色该如何面对:
- 面向移动方向。
- 在侧身移动时持续面向前方。
- 平滑旋转到所需方向。
- 在不同的游戏模式下使用不同的视觉方向规则。
这也是初学者角色有一个子 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)
思路是:
| 层 | 常见目的 |
|---|---|
| Layer 0 | 全身运动,如静止、走动、奔跑、跳跃、下落。 |
| Layer 1 | 上半身动作,如攻击、瞄准、重装、持武器。 |
这允许角色在添加动作的同时保持行走。
层权重
每个层都有一个权重,控制该层的影响程度。
你可以从Python中改变它:
self.animator.setLayerWeight(1, 1.0)
你也可以读取它:
weight = self.animator.getLayerWeight(1)
权重为 1.0 意味着层是完全活动的。权重为 0.0 意味着没有可见的影响。
层权重在你想要逐渐淡入或淡出动作层时非常有用,比如慢慢抬起武器、瞄准或融入特殊姿态。
值得提及的是,混合参数只在同一层之间混合动画。我们不会在不同层之间混合播放的动画。因此,如果你在层0播放动画,然后突然在层1播放动画,无论混合值是多少,它都不会在它们之间进行混合。只有在层1上已经执行了另一个动画时,它才会混合。如果你想要在层间混合,你需要自定义逻辑以使用层权重来实现。
骨骼过滤器
骨骼过滤器控制哪些骨骼受一个层影响以及每个骨骼收到多少影响。
这使得上半身动画成为可能。
例如,你可能希望:
- 层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现在可以用于上半身动画。
你的骨骼名称取决于你导入的骨骼。始终检查你的骨架并使用角色的正确骨骼名称。
实际层例子
想象一个第三人称角色可以同时行走和攻击。
一种可能的设置是:
| 层 | 处理内容 |
|---|---|
| Layer 0 | 静止、走动、奔跑、下落。 |
| Layer 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
如果 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也支持后评估回调。这些发生在动画组件评估骨架姿态之后。
这是一个高级功能,但它可以用于:
- 逆向运动学。
- 头部瞄准。
- 武器瞄准。
- 脚步放置。
- 最终姿态调整。
初学者 Player Toolkit 包含使用此概念的脚部IK示例。它获取 Mesh 子项,获取 Animation 组件,然后用:
self.animator.addPostEvaluationCallback(self.postEvaluation)
你不需要马上编写IK系统,但知道Cave让Python在正常动画播放后调整最终姿态是很有用的。
动画插槽组件
Animation Socket Component 允许子实体跟随父动画实体的骨骼。
子实体必须在具有动画组件的父实体下。然后插槽可以选择一个骨骼并根据需要复制位置、旋转和可选的缩放,同时可以应用偏移。
插槽对于将物体附加到动画角色非常有用:
- 手中持剑。
- 手中持枪。
- 手臂上有盾牌。
- 头上戴头盔。
- 背部有背包。
- 道具附加在手上。
例如,如果一个角色持有一把剑,这把剑可以是一个子实体,带有一个 Animation Socket Component,跟随手骨骼。当角色攻击、奔跑或停顿时,剑仍然保持在正确的动画位置上。
你应该记住的事项
- 启动玩家是一个模板,包含根
Player实体和一个子Mesh实体。 - 根
Player通常处理物理、移动、属性和游戏逻辑。 - 子
Mesh通常处理可视模型、材质、骨架和 Animation Component。 - 这种分离使物理体和视觉角色有独立的变换。
- Animation Component 使用骨架和动画资产播放骨骼动画。
- 在 Python 中,通常将 Animation Component 称为
animator。 playByName按名称播放动画资产,并支持混合、循环和图层。- 图层允许多个动画叠加在一起。
- 骨骼过滤器允许图层仅影响骨骼的一部分。
- 动画回调将动画时间与游戏事件连接。
- 动画插槽将道具附加到动画骨骼上。
当你检查启动的 Player 或 Enemy 模板时,可以使用这个结构作为地图:根实体用于游戏玩法,子 Mesh 实体用于视觉效果和动画。