你好,请选择
语言
关闭

SDK 5.0 版引入了基于实体、组件和系统三个重要因素的新架构(ECS)。 对已经使用旧的 SDK 构建过场景的人来说,可能需要一些时间熟悉。

我们想让旧版向 5.0 版的过渡尽可能顺畅和愉快。或者,如果您是 Decentraland 的新手,我们希望您开始使用我们最新最强的开发人员工具。 因此,请深入本教程,学习有关 ECS 快速介绍,以及如何使用它为 Decentraland 创建交互式 3D 内容。

本教程涵盖的内容

在本教程中,我们将使用 Decentraland SDK 5.0 创建一个简单的场景。 我们将要创作的场景非常简单; 它只是有几个旋转轮子,你可以使它……旋转,这是熟悉 SDK 的许多基本概念一个很好的开始。

我们将看到如何用组件来添加基本形状、材质、3D 模型及单击事件,我们还将介绍一些棘手的概念,包括如何创建 自定义组件(custom components)系统(systems) 如何工作以及如何创建 组件组(component groups)

阅读本文时,您可以创建自己的场景并模仿每个步骤,也可以在这里查看最终代码

创建一个空白场景

如果您希望边阅读时边创建自己的场景,则必须先执行以下操作:

  1. 确保您已经安装最新版本的 Decentraland SDK。 在命令提示符处运行以下命令:npm install -g decentralnd
  2. 创建一个空文件夹,然后导航到该目录并运行以下命令以创建样板代码:dcl init
  3. 打开 game.ts 并删除该文件中的所有默认内容
  4. 最后,在这里下载场景素材。 解压缩并将 / materials/ models 文件夹放在场景目录下。

提示:我们建议在处理 Decentraland 场景时使用 VS Code 等源代码编辑器。 这样通过编辑器提供的智能自动填充等功能,可以更轻松地编写代码。

添加实体和基本组件

让我们通过添加静态内容来开始设计我们的场景。 将以下代码粘贴到 game.ts 文件中。

let stage = new Entity()
stage.add(new GLTFShape("models/Theatre.gltf"))
stage.add(new Transform({
 position: new Vector3(5, 0, 5),
 rotation: Quaternion.Euler(0, 90, 0)
}))
engine.addEntity(stage)

注意:与以前版本的 SDK 不同,不需要将这些代码包含在类或者放在rendersceneDidMount方法中。这些抽象层已经不存在了。现在,场景的主代码就像脚本一样从头到尾执行。

在上面的代码中,我们创建了一个 stage 实体,它具有一个能加载 .glTF 3D 模型的 GLTFShape 组件。 这个3D 模型包括了场景中的所有固定的东西,即除了两个轮子之外的所有东西。 该 3D 模型还嵌入了一个碰撞模型

我们还在 stage 实体中添加了一个 Transform 组件,用以确定实体的 位置(position)旋转(rotation)缩放(scale)

最后,我们将 stage 实体添加到引擎中。 这是一个重要的步骤,告诉场景这些实体及其组件不仅仅是在后台抽象处理的东西,我们还希望它们在场景中被渲染和激活。 如果您不向引擎添加实体,它们就可能不存在场景中。

让我们将更多代码粘贴到场景中:

// Define a reusable Cylinder shape component
let CylinderWCollisions = new CylinderShape()
CylinderWCollisions.withCollisions = true

在这里,我们定义了一个 CylinderShape 组件,我们将它设为_collider_,使得用户不能越过或穿过这个实体。 组件用来存储实体的相关信息,但如果只是单纯的信息,那么它就没有什么用。 如果我们想要在我们的场景中存在一个组件所描述的形状的实际“事物”,我们需要创建一个实体并添加组件给它。

// Create the first wheel entity
let wheel1 = new Entity()
wheel1.add(CylinderWCollisions)
wheel1.add(new Transform({
 position: new Vector3(3, 2, 6),
 rotation: Quaternion.Euler(90, 0, 0),
 scale: new Vector3(1, 0.05, 1)
}))
engine.addEntity(wheel1)

// Create a second wheel entity
let wheel2 = new Entity()
wheel2.add(CylinderWCollisions)
wheel2.add(new Transform({
 position: new Vector3(7, 2, 6),
 rotation: Quaternion.Euler(90, 0, 0),
 scale: new Vector3(1, 0.05, 1)
}))
engine.addEntity(wheel2)

在上面的代码中,我们创建了两个新实体,并将 CylinderWCollisions 组件添加到它们上。 我们还为每个 Transform 组件设置了它们的位置。

注意:另一种方法是为每个轮子创建一个单独的 CylinderShape 组件实例,两种方式都可以正常工作。 但是,在多个实体上重用组件通常是节省处理资源的好方法。

您可能已经注意到,在传递旋转值时,我们使用了一个名为 Quaternion 的东西。 Transform 组件以四元数格式存储旋转角度。 四元数角是有着奇怪的四个数字的东西,在计算中非常实用,但在概念上很难理解。 值得庆幸的是,SDK 包含许多辅助函数,使得您不用考虑四元数,例如:Quaternion.Euler()。 这个辅助函数允许我们通过传递欧拉角的值来创建一个四元数对象。 顺便说一下,欧拉角是我们都知道和喜欢的普通角度,就是那些从 0 到 360 并具有 x,y 和 z 轴的角度。

我们现在可以通过打开命令提示符到场景的根目录并运行 dcl start 来运行场景预览。 你会看到场景已经被渲染了! 这就是将 3D 内容带入世界所需的一切!

材质

我们的轮子看起来是白色的,因为我们没有给它们一种材质。 为了能够分辨我们的轮子何时正在旋转,并使它们更有趣,我们将给它们一个纹理。 这可以通过添加 Material 组件到实体来实现:

// Create material
let SpiralMaterial = new Material()
SpiralMaterial.albedoTexture = "materials/hypno-wheel.png"

// Add material to wheels
wheel1.add(SpiralMaterial)
wheel2.add(SpiralMaterial)

在上面的代码片段中,我们定义了一个类型为 Material 的新组件,然后我们将相同的组件添加到我们的轮子实体中。

我们在材质上配置了 albedoTexture 字段。 albedo 纹理是一种能反应不同光照的纹理,因此它会根据其面向的方向出现不同的亮度。

定制组件

事情开始变得更有趣了! 让我们首先决定在旋转每个轮子时我们需要跟踪哪些信息:

  • 一个 true/false 布尔值,以了解轮子是否应该旋转
  • 旋转速度的数字
  • 旋转方向的矢量

为了存储这些信息,我们将定义一个自定义组件并将其命名为 WheelSpin

@Component('wheelSpin')
export class WheelSpin {
 active: boolean = false
 speed: number = 30
 direction: Vector3 = Vector3.Up()
}

一旦定义为类,我们的自定义组件就可以像我们在场景中使用的其他组件一样添加到实体中。 我们将把这个组件添加到我们的轮子实体中。

请注意,我们在其定义中为我们的组件编写了两个名称:wheelSpin(首字母小写)和 WheelSpin(首字母大写)。 第一个是内部名称,在本教程中不用太在意。 第二个名称,即大写的名称,是我们在引用组件时使用的名称。

wheel1.add(new WheelSpin())
wheel2.add(new WheelSpin())

每个轮子都有一个单独的 WheelSpin 类实例,它存储了特定轮子的信息,并且可以随时间变化。 为了更清楚地说明这一点,让我们的轮子在他们的 WheelSpin 组件中保持略有不同的信息:让我们改变 wheel2 上的 direction 值。 wheel1 则保留默认的 direction 值(Vector3.Up()),因为我们并没有改变它。

wheel2.get(WheelSpin).direction = Vector3.Down()

Component groups

组件组

组件组是 SDK 中包含的一个新工具,正确使用时它的功能可以非常强大。 组件组只是一个跟踪场景中符合特定条件的实体的数组。

在这个例子中,我们有兴趣跟踪所有具有 WheelSpin 组件的实体,因为我们可以假设每个具有 WheelSpin 组件的实体都表现得像一个旋转轮。 你稍后会看到它是如何有用的。

下面的行创建一个组件组,列出具有 WheelSpin 组件的实体。

const wheels = engine.getComponentGroup(WheelSpin)

组件组的优点在于,每次添加或删除实体时,或每次实体增加新组件或删除组件时,引擎都会不断更新它们。 在我们的场景中,组件组将自动加入 wheel1wheel2,而无需我们显式添加它们。

点击

我们希望轮子在您单击它时开始旋转,如果您再次单击它们则旋转得更快。

因为 ECS 中的所有内容都由专门的组件决定。 我们为每个轮子添加一个OnClick组件,并将一个函数写入该组件,这样每次单击该实体时都会执行该函数。

wheel1.add(
 new OnClick(e => {
   let spin = wheel1.get(WheelSpin)
   if (!spin.active){
     spin.active = true
   } else {
     spin.speed += 20
   }
   //log("speed: ", spin.speed)
 })
)

wheel2.add(
 new OnClick(e => {
   let spin = wheel2.get(WheelSpin)
   if (!spin.active){
     spin.active = true
   } else {
     spin.speed += 30
   }
   //log("speed: ", spin.speed)
 })
)

OnClick 组件执行的函数会更改存储在轮子中的 WheelSpin 组件中的值。

到目前为止,更改 WheelSpin 组件中的值对用户在场景中看到的内容没有影响,它们完全被忽略了。在这些值真正起作用之前,我们还需要做最后一件事。

在我们这样做之前,让我们从迄今为止所做的工作中获得一点点即时的满足感。 在 WheelSpin 组件中的值更改时,我们至少可以显示出来。为此,让我们删除每个函数最后一行的 // 来取消注释的 log() 语句,这样我们就可以在控制台中看到这个变量。

再次运行场景预览,打开 javascript 控制台(在Chrome中,您可以通过转到 View > Developer > Javascript Console 来执行此操作)并单击轮子几次。 您应该能看到控制台打印出的数字会在您每次点击时不断提高。

让轮子转起来

现在是时候进入真正有趣的部分了! 让我们的轮子转起来!

下面我们介绍实体组件系统架构的另一部分:系统(systems)。 组件是存储数据的地方,实体则将其组件组合为具有自己个性的具体事物。系统随着时间的推移可更改组件中的数据。 如果你想让场景中的任何东西随着时间的推移而改变,你很可能需要系统 systtem 来做到这一点。

所有 system 都有一个 update() 函数,该函数每帧调用一次,因此每秒约调用 30 次。

在我们的系统中,我们希望更新功能每秒执行30次以下任务,对于场景中的每个轮子:

  • 检查车子的 state 是否是 active
  • 如果是,则根据当前转速将其转动一点

以下是 system 处理该问题的代码:

export class RotatorSystem implements ISystem {
 update(dt: number) {
   for (let wheel of wheels.entities) {
     let spin = wheel.get(WheelSpin)
     let transform = wheel.get(Transform)
     if (spin.active){
       transform.rotate(spin.direction, spin.speed * dt)
     }
   }
 }
}

系统的 update() 函数遍历我们创建的组件组wheels中的每个轮子。 对于每个轮子实体,它获取其相关组件,然后执行上面提到的步骤:检查轮子是否处于 active 状态,如果是,则将其旋转。

我们使用 Transform 组件的 .rotate() 方法旋转轮子。 此方法要求提供转动方向( 3D 矢量)和要转动的量。 由于我们每秒调用此函数 30 次,如果我们想要实际看到旋转,因为是逐帧调用一次,我们转动轮子的值应该非常小。

关于 system 代码可能引起注意的是 dt 参数。 dt 代表延迟时间,它表示处理最后一帧的时间。 理想情况下,是每秒 30 帧,在这种情况下dt等于 1/30。 但是如果用户的设备正在努力计算和渲染场景,那么有些帧可能需要更长的时间,并且 dt 的值会更大。

在我们的代码中,我们将每帧轮子旋转值乘以 dt。 如果某一帧需要比平时更长的时间来处理,轮子就会在该帧上多转动一些以进行补偿。 即使在出现性能问题的情况下,相对于dt进行移动是保持运动平稳的好方法。

粘贴上面的代码将 system 定义为类后,我们需要将 system 实例添加到引擎中,以便使用。 您可以通过将此行添加到场景来实现:

engine.addSystem(new RotatorSystem())

后记

终于完成了,现在我们的场景可以正常工作了! 如果你在阅读中想查看一下场景,可以点击这里

进入预览并单击轮子使它们旋转。 如果你继续点击它们,它们会旋转得会越来越快! 请注意,当它们沿不同方向旋转时,我们会从每个方向获得不同的错觉。

实际上这里有个愉快的互动! 只是为了好玩,尝试一下:通过点击几次让其中一个轮子以合适的速度旋转,然后近距离凝视它约 30 秒,然后快速看看你的手掌。 对另一个轮子做同样的事情,看看会出现什么错觉!:)

如果你想更加牢固地掌握本教程中解释的概念,我建议您尝试一下以下的作业:

在场景中添加更多的轮子。 请注意它们的行为与其他轮子一样。 每个都有自己的 WheelSpin 组件实例,并由RotatorSystem 系统单独处理。

附加题:添加另一个实体作为按钮。 每次用户单击此按钮时,都会在随机位置创建新的轮子。

注意,即使轮子数量随时间变化,wheels 组件组也会跟踪它们,使得 RotatorSystem 系统可以正确处理。

要查看更多高级示例,请查看文档中的示例场景页面

开始建设!
我们的 SDK 提供了开发游戏和应用所需的一切
让我们开始吧