Keep your place in this quest

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

既然你已经了解了 Cave 中角色和动画的工作方式,我们退一步来理解通常连接一切的脚本系统:Python

在 Cave 中,Python 用于编写游戏玩法行为、编辑器工具、UI 回调、动画回调、时间线事件以及一些太特定而无法作为内置组件存在的小型自定义逻辑。使用 Cave 不需要成为高级 Python 程序员,但理解 Cave 脚本的基本结构非常有用。

本课你将学习:

  • Python 在 Cave 中的用途。
  • Python Script 资产和 Python Component 如何协同工作。
  • start()update() 的作用。
  • 如何从脚本访问当前实体。
  • 如何从实体获取组件。
  • 何时使用 Python Component 或 Python Code Component。

目标不是教你完整的 Python 语言,而是当你打开 Cave 脚本时觉得易于理解。

哪里学习 Python?

如果你不会 Python 编程,Uniday Studio 实际上提供了免费的 Python 学习任务。访问 uniday.studio/learn 查看所有选项,或者从这些开始:

本节假设你已经了解这两个学习任务中的内容。


Python 在 Cave 中的用途

在 Cave 中,当游戏对象需要行为时,Python 是你使用的脚本层。

例如,Python 可以用来:

  • 移动玩家或敌人。
  • 打开门。
  • 播放动画。
  • 触发声音。
  • 启动时间线。
  • 切换场景。
  • 更新 UI 元素。
  • 创建自定义编辑器工具。

基本上,你可以用 Python 脚本编写游戏的所有逻辑

这也是启动项目中已有 Python 脚本的原因。例如,默认玩家不仅是带动画的模型,还有读取输入、移动角色、旋转模型和播放正确动画的游戏逻辑。

所以,当你在 Cave 中编写 Python 时,通常不是写孤立代码,而是写控制实体并与该实体附加组件交互的逻辑。

脚本资产和 Python Components

Python 代码通常存放在一个Python Script资产中。

image.png

然后,为了使脚本在场景中运行,你需要向实体添加一个Python Component,并选择这个脚本中应执行的类。

可以这样理解:

组成部分 功能
Python Script 资产 存储代码。
Python Component 在实体上运行脚本中的某个类。
Entity(实体) 被脚本控制的对象。
cave.Component 你编写的实际行为。

例如,你可能有一个名为 Door Controller 的脚本资产。里面有一个继承自 cave.ComponentDoorController 类。然后你给门实体添加一个 Python Component 并选择该类。

这种分离很重要,因为同一脚本可以重用。场景中可以放很多门,每个门都用同一个门控制器脚本,但属性或子对象不同。

一个最小的 Cave Component

一个基本的 Cave Python 组件看起来像这样:

import cave

class MyComponent(cave.Component):
    def start(self, scene):
        print("组件已启动!")

    def update(self):
        pass

几个重要点:

  • import cave 让你的脚本访问 Cave 的 Python API。
  • class MyComponent(cave.Component) 创建一个 Cave 可以运行的组件类。
  • start(self, scene) 组件启动时执行。
  • update(self) 组件激活时每帧执行。

MyComponent 名称可以任意,但实际项目应用清晰名称,如 DoorControllerEnemyAICheckpointPlayerHealth

生命周期方法

Cave 会自动调用一些组件运行时的方法。

常用的有:

方法 何时执行 常用场景
start(self, scene) 组件启动时 获取引用,读取属性,准备变量。
firstUpdate(self) 每个 Entity Component 启动后首次 update 创建依赖其他组件初始化的变量。
update(self) 每帧,场景未暂停时 移动、输入、计时器、状态检测。
pausedUpdate(self) 每帧,场景暂停时 暂停逻辑。
end(self, scene) 组件结束时 必要时清理。

大多数初学脚本用 start()update()。例如,创建移动平台时,start() 存储原始位置,update() 每帧移动平台。

还有 editorUpdatelateUpdate,较为高级,这里不作深入。

访问当前实体

Cave 组件中,self.entity 是拥有该 Python Component 的实体。

这是 Cave 脚本最重要的概念之一。脚本不在场景中独立漂浮,它属于一个实体。

示例:

import cave

class DoorController(cave.Component):
    def start(self, scene):
        self.transform = self.entity.getTransform()
        self.isOpen = False

    def update(self):
        pass

在此脚本中:

  • self.entity 是门实体。
  • self.entity.getTransform() 获取门的 Transform 组件。
  • self.isOpen 脚本用以记录门是否打开。

这一模式你会反复用到:先获取实体,再获取所需组件或子实体,然后在逻辑中使用它们。

获取其他组件

控制实体通常需要获取它的一个或多个组件。

例如,一个玩家脚本可能获取:

  • 用于移动或旋转实体的 Transform 组件。
  • 处理角色移动的 Character 组件。
  • 用于播放动画的子网格实体的 Animation 组件。
  • 用于播放循环声音的 Audio 组件。

示例:

import cave

class SimpleMover(cave.Component):
    def start(self, scene):
        self.transform = self.entity.getTransform()
        self.speed = 2.0

    def update(self):
        self.transform.move(0, 0, self.speed * cave.getDeltaTime(), local=True)

每帧使实体向前移动。

重点是 cave.getDeltaTime(),在 update() 每帧调用,乘以增量时间可让移动速度在帧率变化时保持一致。

读取自定义属性

硬编码数值适合测试,真实游戏对象通常用可编辑属性更合适。

例如,不写死:

self.speed = 2.0

而改为从实体属性读取:

self.speed = self.entity.properties.get("speed", 2.0)

含义是:

  • 如果实体有 speed 属性,就用它。
  • 没有则默认用 2.0

非常适合重用脚本,你可以写一个 SimpleMover,应用到多个实体,每个实体速度不同。

例如:

实体 speed 属性
慢速平台 1.0
快速平台 4.0
移动危害 7.0

脚本相同,行为因实体而异。

另外,你也可以在组件内创建局部可修改变量,而非依赖实体属性。如:

import cave

class PlatformMover(cave.Component):
    # 这是局部变量:
    speed = 2.0

    def start(self, scene: cave.Scene):
        pass

    def update(self):
        events = cave.getEvents()

speed 变量在每个组件实例中局部可修改:

image.png


获取子实体

许多 Cave 对象是由小型层级结构构成。

玩家模板就是例子。根实体 Player 有角色物理和逻辑,子实体 Mesh 有视觉角色模型和 Animation 组件。

所以,根玩家实体上的脚本若想播放动画,先获取子网格实体:

import cave

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

    def update(self):
        self.animator.playByName("p-idle", blend=0.2, loop=True)

这与默认玩家控制器用法一致:

  1. 获取子实体。
  2. 从子实体获取组件。
  3. 需要时使用组件。

理解这一模式后,很多 Cave 脚本就容易读懂了。

getChild 方法有可选的 "recursive" 参数,默认为 True。若为 True,会查询所有子实体,包括孙子实体,直到找到指定名称的实体。

获取场景实体(场景查询)

制作游戏时很可能需要访问场景中其他实体。我们接下来探讨。第一步是获取场景本身,在 Cave 中,你有两种方式:

# 返回当前活动场景:
scene = cave.getScene()

# 返回实体所属的场景:
scene = self.entity.getScene()

为了方便起见,cave.Componentstartend 方法也会接收场景作为参数,因为在这些方法中你很可能会用到它。像 Python Code Component 这样的本地代码组件,默认也定义了一个名为 scene 的变量,你可以直接魔法般地“使用它”,并且能正常工作。

获取场景后,可以通过名称获取特定实体,代码如下:

watchtower = scene.get("Watch Tower 01")

Cave 在场景类中还提供了许多其它方法,方便你进行场景查询。你可以执行射线投射(ray casts)、球体投射(sphere casts)、检查接触盒或球体用于碰撞检测,或者获取所有实体、所有根实体、所有带有特定标签或特定属性的实体,或者指定名称的实体等。所以值得查看 Python API 获取更多详情。

始终记得检查查询是否返回了有效结果。例如:

# 查询不存在的实体:
ent = scene.get("This Entity Doesnt Exist")

if ent is None:
    print("无效的实体!")

获取实体组件

拿到具体实体并确认它存在后,了解如何从实体中获取特定组件是个好主意。

实体有个名为 get 的方法,可以传入组件名称字符串,它会自动在该实体中查找是否有匹配的组件:

animator = self.entity.get("Animation Component")

为了简化操作,如果组件名称以 "Component" 结尾(这很常见),调用时完全可以省略这部分后缀。在多词组件名中,你也可以选择是否保留空格,或者直接连写。

例如,你想从实体获取 Rigid Body Component,虽然它的 Python 名称是 RigidBodyComponent,下面这些写法都有效:

rb = self.entity.get("Rigid Body")
rb = self.entity.get("RigidBody")
rb = self.entity.get("Rigid Body Component")
rb = self.entity.get("RigidBodyComponent")
rb = self.entity.get("RigidBody Component")

由于 Python 是弱类型语言,在 Cave 里给组件变量提供类型提示是一种良好编程习惯,示例如下。注意这种情况下,你需要填写完整的组件名,因为这是 Python 的语义需求:

rb : cave.RigidBodyComponent = self.entity.get("Rigid Body")

这可以让像 Visual Studio Code 这样的外部编辑器,甚至 Cave 内嵌脚本编辑器的智能感知功能正常工作。

对于 Transform Component,作为最常用的组件类型之一,你会频繁查询它,实体自带一个原生方法来获取这个主要的 Transform:

transf = self.entity.getTransform()

调用 self.entity.getTransform() 得到的结果与调用 self.entity.get("Transform") 相同,但前者更快且更优化。

如果实体拥有多个相同类型的组件,它将返回第一个匹配,但有时你可能想得到所有匹配组件。例如,一个多材质网格在 Cave 中表现为一个拥有多个网格组件的实体,你可能想获取所有网格组件。此时可以用 getAll 方法:

meshCmps = self.entity.getAll("Mesh")

# 将所有材质改为发光材质:
for meshCmp in meshCmps:
    meshCmp.material.setAsset("Glowing Material")

获取实体的 Python 组件

现在我们知道了如何获取 Cave 的原生组件,若想从实体获取由你用 Python 自定义编写的组件该怎么办?

这需要用一个特殊方法 getPy,其用法与普通 get 方法完全相同,但它也会返回 Python 组件:

myCmp = self.entity.getPy("MyCustomComponent")

由于内部优化需要才有此特殊方法,确保 Cave 运行尽可能快。

获取自定义 Python 组件后,你可以自由访问其 Python 变量和方法:

myCmp = self.entity.getPy("MyCustomComponent")

# 修改 Python 变量:
myCmp.customValue = 10

# 调用自定义方法:
myCmp.doSomething()
myCmp.applyDamage(10)

Python Code Component

除了常规的 Python Component,Cave 还有一个 Python Code Component。Python Code Component 适合直接在实体上快速编写脚本,而不需要先创建独立的 Python Script 资源。

它拥有其他组件的方法,其区别是脚本直接写在组件里,不是模块化也不可重用。如果你复制实体,脚本也会被复制。但有时这更适合快速创建简单逻辑,比如旋转的金币。

它适合:

  • 快速测试。
  • 小回调。
  • 一次性行为。
  • 原型制作。

它不适合大型游戏玩法系统,因为代码难复用和组织,但对小段代码而言很高效。

Python Component 与 Python Code Component 区别

简单规则如下:

使用情况 何时使用
Python Component 需要复用、作为资源编辑,或者代码将不断增长。
Python Code Component 行为简单、局部。

例如,可复用的 EnemyAI 应该是 Python Script 资源并由 Python Component 使用。快速打印信息或按键触发函数则合适用 Python Code Component。

一个好的首个脚本目标

好的第一个 Cave 脚本应该小且可见。

尝试制作这些之一:

  • 一个向前移动的平台。
  • 场景开始时打开的门。
  • 每隔几秒开关的灯。
  • 播放声音并禁用自身的拾取物。
  • 切换到另一个场景的按钮。

这些示例虽小,但教会你最重要的工作流程:获取实体,获取组件,改变状态,游戏中测试,再调整。

你应该记住的

Cave 中的 Python 脚本通常通过 Python Component 附加到实体上。

最重要的初学模式是:

  1. 使用 start() 获取引用并准备数值。
  2. 使用 update() 每帧执行行为。
  3. 使用 self.entity 访问拥有脚本的实体。
  4. 使用组件移动、动画、播放声音或控制对象。

当这种模式变得自然时,Cave 的脚本编写就没那么神秘了。你不仅仅是在写代码,更是在教实体如何行动。