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

场景内内容创建的一个简单实验

在 9 月 16 日 Game Jam 游戏开发活动开始前,参加过六月份黑客马拉松的一些开发人员将在 Decentraland 博客中公开他们的场景设计开发秘密。本周的嘉宾是 Interweaver(虚拟世界中名为 noah)。

大家好。我是 Interweaver。主要搞互动教育网站,对在线多人虚拟世界也有着长期的兴趣。这可以追溯到童年时代,曾在《RuneScape》中砍树,在《第二人生》中以 leet scripter 通宵达旦闲逛。

2017 年底,同其它人一样遇见了 Decentraland,并且非常高兴终于能看到真正的点对点、无信任的虚拟世界的梦想,开始实现。内容创建和社区绝对是虚拟世界的核心所在,作为真正深入了解 DCL 构建工具的一种方式,我抓住机会参加了最近的创作者大赛和 SDK 黑客马拉松。

玩积木

作为社区黑客马拉松参与者,我参与过好几个有趣的项目,包括 EcoGames 小区的 教育回收游戏。在这篇博客文章中,我将讨论我是如何来设计并实现 Decentrablocks 这个项目的。我的想法非常简单:希望能够在场景内直观地创建内容,而不是在内容编辑器或使用外部的文本编辑器和建模软件(如 SDK 做的一样)。另外,我不是 3D 艺术家,因此项目完全用原始形状的物体来制作。

基于真正的黑客马拉松风格,场景在晚上 8 点到第二天早上 5 点间完成(一个工作日 - 唉</ i>)。最终结果当然有不少瑕疵,但已经实现了我的目标。这个场景可以:

  • 通过点击在一堆不同的积木中取其中的一积木
  • 带着它四处走动
  • 再次点击可以放下积木
  • 通过单击来拾取已放下的积木
  • 将积木放在地板上的符号上可以删除积木
  • 选择新积木的颜色
  • 选择积木移动模式:
    • 携带模式,积木试图与您保持固定的距离
    • 射线模式,积木沿着视线移动到最靠近实体对象处

为了方便堆积木,程序使用了半米的网格对齐,并且防止它们相互交叉,就像真正的堆积木一样😁。

总的来说,这个场景有一点《我的世界》的感觉,但这里的大多数积木都大于网格尺寸,并且直接摆放在你面前而不是放在物品栏中。 根据我的主要目标,你可以在不离开场景的情况下堆出一些有趣的东西。

自己试试吧!

Decentrablocks 的实现

好的,介绍完毕,现在让我们谈谈它是如何工作的吧!如果你想查看代码,它们都在 GitHub 上。注意:为了使数学计算更形象化,有时我只说明了 2D 情况下的设计,但 3D 下也是一样的。

自由携带功能

场景的核心机制是要能够捡起一些东西,随身携带它,然后以一定程度的精确度再次放下。现在,制作一个用于在 3D 中定位物品的良好系统可能会非常棘手。幸运的是,Decentraland 对四处走动已经提供了很大程度的空间控制,因此我决定利用它来轻松携带物品。项目的第一部分就是自由携带功能。

当您四处走动并转移视向时,只需让物体保持相对于屏幕位置不变。

一旦你理解了 世界空间本地空间 间的区别,背后的数学计算就很简单了。

世界空间 是一个全局 X,Y,Z 坐标系。在 Decentraland 场景中,+X 朝着创世纪地图上增加土地 X 坐标的方向前进,+Y 沿垂直向上方向前进,+Z 朝着地图上增加土地 Y 坐标的方向前进。 (我认为这点很令人困惑,并希望是对齐 Y 坐标并使 Z 坐标垂直,但它就是这样)。当你根据这个坐标系描述一个物体的位置时,你是在给出它在世界空间中的位置。

本地空间,相反的是相对于某些变换(位置,旋转和比例的组合)的坐标系。它仍然具有 X,Y,Z 坐标,可以通过给出这三个数字来描述对象在本地空间中的位置,就像在世界空间中一样。但实际值会有所不同。通常谈论针对一个特定对象的本地空间;例如,用户空间是给定用户的本地空间。用户的镜头在原点 <0,0,0> 的空间,+X 方向是沿着用户的视线,+Y 方向是用户屏幕上的向上方向,距离与世界空间相同(比例为 1)。

有了这个理解,自由携带就很容易定义。如果 ObjPosWorld 是您要在世界空间中携带的对象的位置(粗体表示向量),而 ObjPosUser 是它在用户空间中的位置,在携带物体时你只需要保持 ObjPosUser 不变!这样当用户在移动时,对象在用户的屏幕上不会移动。请注意,如果用户四处走动,ObjPosWorld 将不会是常量。我们的目标是计算 ObjPosWorld,以便我们在 System 的每帧上更新它的位置,从而保持 ObjPosUser 不变。这涉及在世界空间和用户空间(在两个方向)之间进行转换。

首次拾取对象时,我们知道它的 ObjPosWorld,并希望用它取得 ObjPosUser。计算很简单。我们将用户的位置(在世界空间中)给到 UserPos,并将 UserRot 一个四元数 设置为镜头的旋转。四元数听起来有点可怕 - 您可以简单地将它们视为一组四个数字,用来表示 3D 坐标中的所有可能的旋转。

如果你用一个向量乘以一个四元数,结果本质上是一个新的向量,即旧的向量通过四元数所代表的旋转后的值。你也可以取一个四元数的逆或共轭,得到一个新的四元数,它表示旧四元数的反向旋转。我们只需要知道这些信息就可以从世界空间中的一个位置(ObjPosWorld)转换到用户空间中的一个位置(ObjPosUser):

ObjPosUser = (ObjPosWorld - UserPos) * conjugate(UserRot)

基本上,是从对象的位置减去用户的位置,然后从对象的位置反转用户的旋转。得到对象的本地用户空间坐标。

作为一个程序员,当有人写出一大堆数学算法,然后让读者写出代码做为练习时,我总是会感到沮丧,所以我不会这样。 以下就是 Decentraland SDK 使用 TypeScript 的算法实现,实体是你所携带的对象:

// Run this once, when you first pick up the object.
let objPosWorld = entity.getComponent(Transform).position

const objPosUser = objPosWorld
  .subtract(Camera.instance.position)              // Subtract UserPos
  .rotate(Camera.instance.rotation.conjugate())    // Unrotate by UserRot

现在我们有了常量 ObjPosUser,我们只需要使用它,以及最新的 UserPos 和 UserRot 值,在每一帧中重新计算 ObjPosWorld 以将所携带的对象移动到那里。请记住,当用户四处移动时,UserPos 和 UserRot 也会发生变化。这个和上面的方程是一样的,但是用 ObjPosWorld 代替 ObjPosUser 来求解:

ObjPosWorld = (ObjPosUser * UserRot) + UserPos

或,使用代码:

// Run this once per frame, in order to move the object as the player moves.
let objPosWorld = objPosUser
  .clone()                           // (使用 Clone 防止 .rotate 更改 objPosUser 的值)
  .rotate(Camera.instance.rotation)  // Rotate by UserRot
  .add(Camera.instance.position)     // Add UserPos

entity.getComponent(Transform).position = objPosWorld

此外,作为一个视觉思考者,每次有人写了一堆文字说明,然后让读者自己画图练习时,我总会感到沮丧,为避免这种情况。以下是情况说明图(这里用户恰好直视对象):

好了,这已经几乎涵盖所有自由携带功能。我已经在我的 GitHub 上提供了一个简单的,无依赖的组件和系统对实现,还用了一个小例子 game.ts 显示其用途。

对齐

此场景的设计是将积木的位置的调整以半米为增量。使得对齐积木更容易。我最初的实现是,在实际更新对象的位置之前,用每个组件的 objPosWorld 变量首先乘以 2, 调用 Math.round(),然后再除以 2,舍入到最近 0.5 米而不是 1 米:

objPosWorld.set(
  Math.round(objPosWorld.x * 2) / 2,
  Math.round(objPosWorld.y * 2) / 2,
  Math.round(objPosWorld.z * 2) / 2
)

现在,用户就可以携带积木,它们的位置是分散的,便于放置!但是积木仍然可以相互移动 - 这是一个非常不切实际的情况。

重叠(或网格)检测

接下来的挑战是检测位置是否已被占用,以防止在那里移动积木。实现这一目标的一种自然方法是使用一个大的多维数组,我将其称为网格,其中每个元素代表单个地块场景中的每 0.5 米的单元格(因此是一个 32x32x32 数组。)然后,您只需:

  • 在移动积木之前,检查积木将占用的网格的每个元素
  • 如果它们都是空的,移动积木,并在网格中标记占用
  • 如果用户移开积木,再次将其标记为未占用
  • 如果存在占用的情况,我们需要以某种方式解决重叠问题(见下文)

这种机制的一个很好的副作用是,当我们在读取数组前检查数组索引以确保它们在边界内(0 到 31),没在边界内的情况下我们也可以防止重叠,并避免积木放到地下或土地的边界外。

解决重叠问题

那么当用户在携带积木时试图移动它,你会怎么做呢,提示重叠?简单地不移动积木是一种选择,但对用户不友好。一个更好的解决方案是将积木移动到用户视线上的不同的未占用位置。意味着该积木似乎按预期移动(直接在用户前面),但也不再与另一个积木或场景边缘重叠。

使用 Bresenham 算法进行栅格化

为了实现这一点,我需要某种方法来确定积木在用户视线内的所有可能位置。在 google 上搜索了几分钟后,我找到了Bresenham 3D 算法,它将 3D 空间中的两个整数点之间的线段光栅化;也就是说,它将线段转换为沿着该线段的一组坐标。肯定有其他我可以使用的算法,但是这个算法很好地完成了我的目的。您可以查看我的 TypeScript 实现

使用光栅化

通过沿着用户视线的一组坐标,我们现在可以解决重叠。如果你还记得,有两种不同的积木移动模式可供考虑,携带模式和射线模式。

在携带模式下,当你四处走动时,积木会试图与你保持固定的距离。当检测到重叠时,只需遍历坐标集,从最远坐标(携带位置)并向内移动使用网格检查每个点是否重叠。您遇到的第一个可用位置就是您移动积木的位置。这样可以确保积木尽可能接近携带位置。

在射线模式下,积木会尽可能地不碰任何东西(另一个积木或场景边缘),然后停在那里。因此,只需遍历坐标集,再次使用网格检查重叠,但这次从最近的坐标(用户的位置)向外移动。您遇到的第一个不可用点之前的最后一个可用点是移动积木的位置。这确保了积木沿着用户的视线位于第一个实体对象的正前方。

结论

差不多就是这样。我们可以四处走动并且随身携带积木,有对齐功能以便于放置,而且它们不能相互交叉。对于一个 9 小时的项目来说,我对结果非常满意。它确实告诉我,虽然 Decentraland SDK 仍然还处于发展的早期阶段,但您已经可以使用它完成大量工作。

我很期待即将推出的许多新功能!特别是,如果全局点击事件可用,Decentrablocks 可以减少错误并且更容易编写,而不是总是需要单击实际对象,而且此功能计划在SDK v6.3 或更早版本中实现。

展望未来,我期待成为 Decentraland 场景建设社区的积极成员。另外还有一个 Decentraland Game Jam 游戏开发活动将在 9 月举行,通过 SDK 从零开始,经过上次的黑客松经验之后,我已经可以确信跟一些真正熟练的创造者合作会是一个绝佳机会!

我正计划参加,如果你对在去中心化的在线世界内容构建感兴趣,我希望你能加入我。

虚拟世界见!

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