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

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

在本课中,你将学习:

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

目标不是教授整个 Python 语言,目标是让你在打开 Cave 脚本时感到可理解。

在哪里学习 Python?

如果你不知道如何用 Python 编码,Uniday Studio 实际上也提供了免费的 Python 学习任务。请访问 uniday.studio/learn 查看所有选项或从这里开始:

在本节中,我们将假设你已经了解了这两个学习任务中涉及的主题。


Python 在 Cave 中的用途

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

例如,Python 可以用于:

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

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

这就是为什么初始项目已经包含了 Python 脚本。例如,默认的玩家不仅仅是一个带动画的网格。它还具有读取输入、移动角色、旋转网格和播放正确动画的游戏逻辑。

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

脚本资产和 Python 组件

Python 代码通常位于 Python 脚本 资产中。

image.png

然后,为了让脚本在场景中运行,你需要将 Python 组件 添加到一个实体,并选择应执行的脚本中的类。

可以这样理解:

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

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

这种分离很重要,因为相同的脚本可以重复使用。你可以在场景中放置多个门,每个门都使用相同的门控制器脚本,但具有不同的属性或不同的子对象。

最小的 Cave 组件

一个基本的 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) 始终在每个实体组件的 start 方法后,第一次更新时。 创建依赖于其他组件已初始化的变量。
update(self) 每帧运行,如果场景未暂停。 移动、输入、计时器、状态检查。
pausedUpdate(self) 每帧运行,如果场景已暂停。 暂停逻辑。
end(self, scene) 当组件结束时。 如果需要,清理。

大多数初学者脚本使用 start()update()。例如,如果你正在创建一个移动的平台,start() 是存储原始位置的好地方,而 update() 是在每帧移动平台的地方。

我们还有 editorUpdatelateUpdate 方法,但我们在这里不会探讨,因为它们有点高级。

访问当前实体

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

这是 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 实体具有可视角色网格和动画组件。

因此,如果根玩家实体上的脚本想要播放动画,它首先需要获取子网格实体:

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 代码组件 这样的本地代码,默认情况下也为您定义了一个 scene 变量,您可以神奇地"直接使用"它并期望它有效。

一旦您获得场景,您可以使用以下代码通过名称获取特定实体:

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

Cave 在场景类中提供了很多其他方法,供您进行其他场景查询。您可以进行射线投射、球体投射,检查一个接触框或球体进行基本的碰撞查询,甚至可以获取所有实体、所有根实体、具有特定标签的所有实体、具有特定属性的所有实体或具有特定名称的所有实体等。因此,查看 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")

这使得 IntelliSense 可以在外部编辑器(如 Visual Studio Code)中工作,或在 Cave Engine 提供的内置脚本编辑器中为您提供自动完成功能。

对于 Transform Component,由于它是最常见的组件类型之一,您将经常查询它,实体有一个获取主要变换的本机方法:

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 代码组件

除了常规的 Python 组件,Cave 还有一个 Python 代码组件。Python 代码组件对于直接在实体上编写的快速脚本非常有用,而无需首先创建一个单独的 Python 脚本资产。

它具有所有其他方法,区别在于在 Python 代码组件中,您直接在组件内部编写 Python 脚本,它不是模块化的,不能重用。因此,如果您创建该实体的副本,它也将创建其中的脚本副本。但是有时,这可以更快地创建一些简单的逻辑,比如旋转的硬币。

它适用于:

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

它不是大型游戏系统的最佳选择,因为代码更难重用和组织,但适合小代码。

Python 组件与 Python 代码组件

将此作为一个简单的规则:

使用此
Python 组件 行为应被重用、作为资产进行编辑或随着时间增长。
Python 代码组件 行为较小、本地。

例如,可以将可重用的 EnemyAI 作为一个 Python 脚本资产使用的 Python 组件。一个打印某些内容或在按钮按下时调用函数的微小代码可以是一个 Python 代码组件。

一个好的第一个脚本目标

一个好的 Cave 脚本是一些小且可见的东西。

尝试构建以下内容之一:

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

这些示例很小,但它们教会了最重要的工作流程:获取实体,获取组件,改变一些东西,在播放模式中测试,并进行调整。

您应该记住的

在 Cave 中,Python 脚本通常通过 Python 组件附加到实体上。

最重要的初学者模式是:

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

一旦这种模式感觉自然,Cave 中的脚本编写就变得不那么神秘。您不仅是在编写代码。您是在教导实体如何行为。