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

欢迎回来! 在本课程的第 1 部分中,我们创建了一个场景,一个名为 Gnark 的角色沿着固定路径在一个古老的寺庙中转圈巡逻。现在,我们要增加两个功能,以使场景变得更为有趣。 首先,我们播放一个转弯动画使他能流畅地转向。 另外,当用户与它的距离太近时,我们让它作出响应。

如果您是从第 1 部分开始学习的,则可以继续往下学习。 您也可以下载第 1 部分的完整代码。 或者,您可以在 Github 上查看场景最终版本的代码而无需自己运行任何代码。

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

添加额外的动画

我们在第 1 部分中提到过,gnark.gltf 文件嵌入了几个动画。 在第 1 部分中,我们只使用了 walk 动画,但是在这一部分中,我们还将利用模型中保存的 turnRightraiseDead 动画。在我们使用动画前,我们先创建 AnimationState 对象。

const turnRClip = new AnimationState('turnRight')
turnRClip.looping = false
gnarkAnimator.addClip(turnRClip)
const raiseDeadClip = new AnimationState('raiseDead')
gnarkAnimator.addClip(raiseDeadClip)

在创建 animationState 对象时,我们可以配置对象的属性,包括 loopingspeedweight

默认情况下,所有 clip 对象都会循环播放动画,但是对于 turnRight 动画我们配置为 { loop: false } ,以便在激活时只播放一次。

创建一个 Timer 组件

turnRight 动画播放时,我们不希望 Gnark 再往前走。所以我们将创建一个新组件,在发生这种情况时,用来跟踪时间的流逝。

为了使这个组件更通用,并且可以在其他项目中轻松重用,我们不会将时间写死。 相反的,每次创建组件时,我们都能够设置 timeLeft 字段的值。 为此,我们为组件提供一个接受数字参数的 constructor() 函数。

@Component("timeOut")
export class TimeOut {
    timeLeft: number
    constructor( time: number){
         this.timeLeft = time
    }
}

此组件与之前组件的使用方式略有不同。 gnark 实体不会始终带有此组件的实例。 相反,我们会在需要时(每次 Gnark 转弯时)创建一个新实例,并且当完成时我们将删除该组件。

我们还将创建一个组件组,来列出具有 TimeOut 组件的每个实体。 随着 TimeOut 组件的添加和删除,gnark 实体将出现或不出现在该组中。

export const paused = engine.getComponentGroup(TimeOut)

创建一个计时器系统

我们现在将为我们的场景添加一个专门的 system 来处理 TimeOut 组件。

export class WaitSystem {
  update(dt: number) {
    for (let ent of paused.entities){
      let time = ent.ent.getComponentOrNull(TimeOut)
      if (time){
        if (time.timeLeft > 0) {
          time.timeLeft -= dt
        } else {
          ent.removeComponent(TimeOut)
        }
      }
    }
  }
}

engine.addSystem(new WaitSystem())

这个 system 非常简单且自恰; 它只是从 TimeOut 组件中减去自上一帧以来过去的时间 (dt) 。如果剩余时间用完,则会删除组件。

系统使用我们创建的组件组来遍历具有 TimeOut 组件的每个实体。 其实我们并不需要遍历这个列表,因为我们知道gnark 实体是我们场景中唯一与该系统相关的实体。 但是,能编写以后可以在其他项目中轻松重用的代码也很不错。

关于处理多个系统

我们可以简单地将计时器的代码添加到场景的 GnarkWalk 系统中,这样就可以了。但是,随着项目规模的扩大,将独立的操作行为放在明确划分的单独系统中通常是有用的。 如果你很好地设计你的场景,你可以让多个简单的独立系统最后产生复杂的行为,这些系统独立地作用于相同的实体。

这是让行为分离一个示例:如一个用来决定实体下一步位置的 AI 系统,和另一个使所有实体下降的 gravity 重力系统。 如果人工智能系统让一个实体走下悬崖,重力系统就可以独立完成这个功能,而无需 AI 系统的配合。 同样地,重力系统不需要知道角色下跌前移动的方向或它要去的地方,它只是让实体下落。

修改 walk system

我们现在需要修改 GnarkWalk 系统,以便它包含我们刚刚创建的旋转动画和 TimeOut 组件。 我们从本教程的第1部分中获取了代码并进行了一些更改,现在看起来像这样:

export class GnarkWalk {
  update(dt: number) {
    if (!gnark.hasComponent(TimeOut)){
      let transform = gnark.getComponent(Transform)
      let path = gnark.getComponent(LerpData)
      walkClip.playing = true
      if (path.fraction < 1) {
        path.fraction += dt/12
        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])
        walkClip.pause()
        turnRClip.play()
        gnark.addComponent(new TimeOut(TURN_TIME))
      }
    }
  }
}

您会注意到这些代码的大部分与我们在第 1 部分中编写的内容没有什么不同。如果你忘记了,请参阅第1部分 ,我们只是修改了以下这些内容:

  • 整个 update() 函数包含在一个条件中,它检查 gnark 实体是否添加了 TimeOut 组件。 如果组件存在,则系统不执行任何操作。
  • 我们将 walk.playing 设置为 true。 这在 TimeOut 组件到期后立即使用,Gnark 需要再次开始行走。它对动画已经播放的帧没有影响。
  • 当 Gnark 走完一段路后,我们暂停 walk 动画并播放 turnRight 动画。 请记住,turnRight 动画是非循环的,所以它只播放一次。 我们还在 gnark 实体中添加了一个 TimeOut 组件。 添加此组件后,Gnark 将停止移动,直到 WaitSystem 将其删除。

注意:我们使用LookAt 让 Gnark 转向下一个目标位置。 这是在我们开始播放 turnRight 动画的同一帧中执行的。 这是因为turnRight 动画恰好在转向然后面向前方时开始。其他模型有可能会是开始面向前方然后转向的动画,在种情况下,需要在动画结束时再更改模型的 rotation。

先进行场景预览! 应该能看到 Gnark 在他路径的每个角落都执行了 turnRight 动画!

获取用户位置

如果我们想要获得 Gnark 和用户之间的距离,我们首先需要知道用户在哪里。 为此,我们需要一个相机对象实例。

const camera = Camera.instance

创建对象后,只需调用 camera.position 即可获取用户的当前位置。

计算距离

正如我们在学校里所学到的那样(同时想知道这些知识是否有用),你可以通过一个简单的三角公式得到两点之间的距离:√a² + b²

这个三角公式只适用于 2D 空间,但它适用于我们的场景,因为我们可以假设这两个点都在地面上。

我们可以通过不计算平方根来节省处理能力。 这种优化是游戏开发中相当标准的做法。 找到一个平方根是一项要求很高的计算,我们可以轻松地省去它。 例如,我们可以简单地检查a² + b²的结果是否小于16,而不是检查整个公式的结果是否小于4。

function distance(pos1: Vector3, pos2: Vector3): number {
 const a = pos1.x - pos2.x
 const b = pos1.z - pos2.z
 return a * a + b * b
}

距离响应

让我们为场景添加另一个专门系统,以便 Gnark 能够在用户离他太近时做出响应。

export class BattleCry {
  update() {
    let transform = gnark.getComponent(Transform)
    let path = gnark.getComponent(LerpData)
    let dist = distance(transform.position, camera.position)
    if ( dist < 16) {
      raiseDeadClip.playing = true
      walkClip.playing = false
      turnRClip.playing = false
      transform.lookAt(camera.position)
    }
    else if (raiseDeadClip.playing){
      raiseDeadClip.pause()
      transform.lookAt(path.array[path.target])
    }
  }
}

engine.addSystem(new BattleCry())

在这个系统中,我们使用 camera 对象来获取用户的位置,然后使用我们创建的 distance() 函数来知晓 Gnark 和用户之间的距离。

如果这个距离小于 4 米(记住我们的距离公式不计算平方根,那么我们用 16 来代替 4 的平方),Gnark 播放 raiseDead 动画。这时,还会暂停所有其他动画,然后转向面对用户。

如果距离变得大于 4 米并且正在播放 raiseDead 动画,则停止动画并转回到他路径中的下一个目标。

我们需要做的最后一件事。 我们需要回到 GnarkWalk 系统并在 update() 函数的开头添加另一个条件。 在向前移动Gnark 之前,我们需要确保 raiseDead 动画没有播放。 添加完成后,在动画播放时他会保持静止,当停止播放时才会恢复行走。

if (!gnark.has(TimeOut) && !raiseDeadClip.playing ){
(...)

后记

终于完成了! 让我们再次运行预览。 这一次,勇敢地走到 Gnark 前,看看他是如何反应的!

如果想要体验最后的场景,可以点击这里。 要查看使用新 SDK 的其他示例,请查看文档中的场景示例页面!

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