首批拥有虚拟化身,即可以早期访问 Decentraland。
X
你好,请选择
语言
关闭

这是如何使用新 SDK开发的第二个教程。如果还没有看过第一篇教程,为了熟悉有关基础知识,请先学习第一篇教程。

注意: 此教程中的代码已经升级到 SDK 5.1。

我们的示例场景分成两个部分。 第一部分将介绍如何使用 glTF 动画,然后探索在场景中移动实体的不同方法。 第二部分将向您介绍如何获取用户位置数据,深入研究 glTF 动画,并说明为什么有必要使用多个独立系统。

在此示例中,我们使用在Bitgem 上购买的人物 3D 模型。 这个独特的模型包含一系列精彩的内置动画,显得非常生动。 该模型被其创作者命名为“骷髅王”,但我决定亲切地将其命名为“Gnark”。 我不知道为什么这么叫,应该是觉得他有一张与“Gnark”相似的面孔吧。

在阅读本文时,您可以创建自己的场景并模仿每个步骤,也可以在 github 库中找到最终代码。 第一部分的源码,可以在此查看release 1.0

创建一个空白场景

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

  • 确保您拥有最新版本的 Decentraland SDK。 在命令提示符处运行以下命令:
npm install -g decentralnd
  • 创建一个空文件夹以创建项目,然后进入该目录。
  • 运行以下命令以创建示范代码:
dcl init
  • 打开 game.ts 并删除该文件中的所有内容
  • 此链接下载场景素材。 解压缩并将 /models 文件夹放在场景目录的根目录下。
  • 这个场景使用了四块土地。 预览前,您需要打开 scene.json 文件并指定彼此相邻的四块土地坐标。 例如:
"scene": {
   "parcels": [
     "0,0", "0,1", "1,1", "1,0"
   ],
   "base": "0,0"
 }

提示:我们建议在处理 Decentraland 场景时使用 VS Code 等源代码编辑器。它具有的智能代码自动补全等功能将使代码编写变得更为方便。

添加 3D 模型和动画

首先我们添加以下代码到 game.ts 文件中,将 Gnark 和他的神殿放入到我们的场景中:

// Create temple
const temple = new Entity()
temple.addComponent(new GLTFShape('models/Temple.glb'))
temple.addComponent(new Transform({
    position: new Vector3(16, 0, 16)
}))

// Add temple to engine
engine.addEntity(temple)

// Create Gnark
let gnark = new Entity()
gnark.add(new GLTFShape('models/gnark.gltf'))
gnark.add(new Transform({
    position: new Vector3(8, 0, 8)
}))

// Add Gnark to engine
engine.addEntity(gnark)

让我们添加动画让 Gnark 走动起来!gnark.gltf 文件已经附带了一些我们可以使用的嵌入式动画。 要播放它们,我们需要知道它们在文件中的名称。 如果您不确定 .glTF 中包含哪些动画或者它们的名称,可以使用以下方法:

  • 下载 Visual Studio Code 的 glTF tools 扩展并使用它来打开文件。 您将能够看到所有动画的列表并进行预览。
  • 使用文本编辑器打开 .glTF 文件,然后向下滚动直到找到动画名(这些文件名往往很长,但可以找到)。
// Create animator component to handle animations
let gnarkAnimator = new Animator()
gnark.addComponent(gnarkAnimator)

// Create clip
const walkClip = new AnimationState('walk')

// Add clip to animator
gnarkAnimator.addClip(walkClip)

// Activate clip
walkClip.play()

这里,我们创建了一个Animator 组件来处理实体的所有动画,以及一个 AnimationState 对象来保存当前步行动画的状态。此对象跟踪动画序列的进度,并确定每帧显示的内容。 请注意,接下来我们将此对象添加到 GLTFShape 组件,而不是添加到实体。

这里并不需要系统(system) 来处理 3D 模型的动画。 由 AnimationState 对象负责在动画进行过程中更新每个帧上的模型。

如果我们此时进行场景预览,可以看到 Gnark 如同在月球漫步。虽然感觉很酷,但这并不是我们想要的。 我们希望他走的时候能够前进!

带移动的动画

我们可以编辑存储在 3D 模型中的 walk 动画,走路时让角色网格离开固定的位置。 这样做好后,我们就完成了本教程的第 1 部分!

这对于某些用例来说是可行的,但是如果你想要任意玩家都能与动画实体交互,能做的则非常有限。例如,在第 2 部分中,我们将让 Gnark 在用户离他很近时做出反应。如果走动的动画包含了他在场景中行走时位置的变化,那么实体的 position 位置值(存储在 Transform 组件中)将始终保持不变。没有这个变化的值,我们就无法知道用户用户与他的距离。

新 SDK 中移动方式的变化

SDK 旧版本使用了 Transition 设置,由它设定了每次实体位置发生变化时实体移动的速度和方式。

SDK 5.0 版本去掉了这层抽象。 使用了依赖于游戏循环和逐帧的增量变化的更传统的方法。 移动实体时,我们使用系统 system ,在场景的每一帧上小增量更改Transform 组件上的 position 位置字段。

如果您之前使用过传统的游戏引擎,例如 Unity 或 Unreal,那么您很可能会更熟悉这种新机制。

使用 Translate 移动

移动实体的最简单方法是创建一个系统,在每个帧上调用 translate() 函数。

export class GnarkWalk {
  update() {
    let increment = Vector3.Forward().scale(0.05)
    gnark.getComponent(Transform).translate(increment)
  }
}

engine.addSystem(new GnarkWalk())

我们每帧上调用一次 update 功能,每次 Gnark 向前移动 0.05 米。 Vector3.Forward() 创建一个值为 (0, 0, 1) 的新向量。我们需要缩小这个向量,使得 Gnark 不会每秒向前移动整整一米。

如果我们现在打开场景预览,我们可以看到 Gnark 像个老板一样往前走。太棒了! 我们正朝着正确的方向前进。

让移动更流畅

想象一下,如果场景用户跟不上帧率。 运动就有可能会有跳跃感,因为并非所有帧的时间都是一样的,但每帧 Gnark 移动量是一样的。

可以对这个不均匀的时间进行补偿,使用dt(延迟时间)参数,来调整移动量。 update() 函数始终包括这个参数,其值等于处理最后一帧所用的时间。 它的值以秒为单位,因此其值始终非常小。

让我们修改一下系统代码,将 dt 考虑在内。

export class GnarkWalk {
  update(dt: number) {
    let increment = Vector3.Forward().scale(dt * 1.5)
    gnark.getComponent(Transform).translate(increment)
  }
}

由于 dt 一般是 1/30 秒,Gnark 向前移动的速度与以前大致相同,如果帧出现延迟,则移动量大小会受到 dt 值的影响。

使用线性插值

还有一个大问题,目前为止我们一直忽视了:当 Gnark 走到场景的边界时,他就会一直走到地平线上,而不会理会这个世界的限制。

修复此问题的一种简单方法是添加一个 if 语句,让其在给定地点停止。 然而,更好的方法是使用 线性插值

线性插值,简称 lerp,是游戏开发中非常流行的工具。 它允许您轻松找到位于 A 点和 B 点间某处的中间点。

lerp() 函数有三个参数:

  • 原点位置的矢量
  • 目标位置的矢量
  • 分数:从 0 到 1 的值,表示 A 点和 B 点之间的某个位置。

例如,如果原点位置为(0, 0, 0) 且目标位置为(10, 0, 10):

  • 分数为 0 时返回(0,0,0)
  • 分数为 0.3 时返回(3,0,3)
  • 分数为 1 时返回(10,0,10)

要在我们的场景中实现 lerp() 功能,我们需要存储关于 Gnark 来自哪里,要去哪里的数据,以及他到目前为止走了多远。 我们可以将这些值存储为场景脚本中的某个简单变量,如果您不打算进一步扩展场景的复杂性,那就不会有问题。

但是,如果您想保持代码清洁并且其部件可重复使用,我强烈建议您通过自定义组件处理此信息。

提示:如果您要跟踪的信息与场景中的特定实体直接相关,则可能需要将此信息存储在自定义组件中。

@Component('lerpData')
export class LerpData {
  origin: Vector3 = new Vector3(5, 0, 5)
  target: Vector3 = new Vector3(5, 0, 15)
  fraction: number = 0
}

我们还需要将此组件添加到 gnark 实体:

gnark.addComponent(new LerpData())

最后,我们需要重写我们的系统,以便它使用 lerp 移动 Gnark。 我们希望每帧场景执行以下操作:

  1. fraction 分数的值略微增加,并与之前的 dt 成比例
  2. 用起点和目标位置的固定坐标以及 fraction 的新值执行 lerp 函数
  3. 将 Gnark 的新位置设置为我们的 lerp 函数返回的值
export class GnarkWalk {
  update(dt: number) {
    let transform = gnark.getComponent(Transform)
    let lerp = gnark.getComponent(LerpData)
    if (lerp.fraction < 1) {
      lerp.fraction += dt / 6
      transform.position = Vector3.Lerp(lerp.origin, lerp.target, lerp.fraction)
    } else {
      walkClip.pause()
    }
  }
}

现在当 Gnark 到达目的地时,他会停止走动。 我们也明确地暂停了 walkClip 动画,记住走动动画与实体的实际运动是分开的。

沿着路径移动

我们真正想要的是让 Gnark 沿着一条固定的路线不停地走,在他的神庙里巡逻。 首先,让我们将希望他走过的路径存储为数组中的一组坐标。

const point1 = new Vector3(8, 0, 8)
const point2 = new Vector3(8, 0, 24)
const point3 = new Vector3(24, 0, 24)
const point4 = new Vector3(24, 0, 8)
const path: Vector3[] = [point1, point2, point3, point4]

我们将像之前一样使用 lerp 函数,但是一旦他到达 lerp 的末尾,我们不希望停止,我们希望他将目标切换到路径数组中的下一个坐标。

为了实现这一点,我们需要对存储在 LerpData 组件上的数据进行一些更改。 首先,我们将存储对 path 数组的引用。 其次,我们不存储原点和目标的坐标,而是将它们的索引存储在数组中

@Component('lerpData')
export class LerpData {
  array: Vector3[] = myPath
  origin: number = 0
  target: number = 1
  fraction: number = 0
}

最后,我们将修改我们的系统,使其循环遍历 path 数组:

export class GnarkWalk {
  update(dt: number) {
    let transform = gnark.getComponent(Transform)
    let path = gnark.getComponent(LerpData)
    path.fraction += dt / 12
    if (path.fraction < 1) {
      transform.position = Vector3.Lerp(
        path.array[path.origin],
        path.array[path.target],
        path.fraction
      )
    } else {
      path.origin = path.target
      path.target += 1
      if (path.target >= path.array.length) {
        path.target = 0
      }
      path.fraction = 0
      transform.lookAt(path.array[path.target])
    }
  }
}

update() 函数的第一部分保持不变。 需要更改的是,当 Gnark 走到某个位置时,我们移动到 path 数组中的下一个,并将 targetorigin 字段设置为新值。 如果我们到达 path 数组的末尾,我们返回起点,开始另一轮循环。

我们也使用了 Transform 组件的 lookAt() 函数让 Gnark 转向。让它在走向新的下一位置前转向。

如果你在阅读中想体验最后的场景,可以在这里 打开浏览。

后记

终于完成了! 让我们预览一下,看看 Gnark 是如何巡逻,以确保没有人进入亵渎了他的神庙。

在本教程的第二部分,我们将给予他在找到入侵者时做出反应的能力! 通过播放单独的动画,当他到达角落时,我们也会让他在转向时变得更加顺畅。

您已经可以查看代码了,在 GitHub 库中查看实现这些步骤的代码!

要查看使用新 SDK 的其他示例,请查看文档中的示例场景页面!

在虚拟世界中展示您的特色
首批拥有虚拟化身,即可先期进入 Decentraland 世界。
了解更多