用 A-Frame 建立纸板地牢

纸板地牢 是一款基于网络的地牢探险游戏,专为与 Google Cardboard 配合使用而设计,并使用 Mozilla 的虚拟现实框架 A-Frame 编写。

在本案例研究中,我将讨论我在开发 纸板地牢 过程中遇到的关键挑战,我在使用 A-Frame 方面的经验,以及我在第一次接触虚拟现实时学到的一些经验教训。

A-Frame 入门

我偶然发现了 A-Frame,它让我找到了轻松开始 VR 开发的方法。A-Frame 吸引了我,因为它与我习惯使用的网页开发概念非常自然地契合。能够通过纯标记在场景中放置实体非常强大,并且提供了非常低的入门门槛。此外,A-Frame 文档 清晰简洁 - 这对我来说非常重要,因为我是一个选择使用第三方代码/库的开发者。

老实说,我对 A-Frame 的健壮性感到惊讶。我遇到的多数障碍都与处理 VR 特定的挑战有关。

建立一个地牢

Cardboard Dungeon renderable area

纸板地牢 最初是为了快速测试 A-Frame 的一些功能而创建的。与其一开始就创建整个地牢,我的想法是拥有固定数量的房间来定义玩家周围的可渲染区域。这些房间将根据来自 JSON 文件的数据进行渲染。这将减少 DOM 中的实体数量,并允许我创建非常大的地牢,而不会对性能造成任何影响。

一个房间很简单,始终最多由四面墙、一个地板和一个天花板组成。JSON 数据定义了每个房间应该渲染哪些内容。我还选择了一个简单的网格系统来定义房间的虚拟位置 - 其中 (0,0,0) 是玩家的起点。

最初,我每次玩家触发移动时都会注入新的 A-Frame 实体。但是,在与 A-Frame 团队交谈 后,我了解到了 “visible” 组件。我决定在开始时就初始化每个渲染的空间,然后在玩家进入时切换每个房间的 “visible” 组件。

// Called once during scene initialization.
Container.prototype.init = function () {
  var entity = document.createElement('a-entity');
  entity.className = 'top';
  entity.setAttribute('mixin','wall top');
  entity.setAttribute('visible', 'false');
  entity.setAttribute('position', {
    x: this.position_multipliers.x,
    y: (4 + this.position_multipliers.y),
    z: this.position_multipliers.z
  });
  document.getElementById(this.target).appendChild(entity);
  // …
};

// Called whenever the player triggers movement.
Container.prototype.render = function () {
  // Set the `visible` component on the entities for this container.
  var container = document.getElementById(this.target);
  if (this.room) {
    setAttributeForClass(container, 'top', 'visible', (this.room.data.top ? this.room.data.top : 'false'));
    setAttributeForClass(container, 'bottom', 'visible', (this.room.data.bottom ? this.room.data.bottom : 'false'));
    setAttributeForClass(container, 'left', 'visible', (this.room.data.left ? this.room.data.left : 'false'));
    setAttributeForClass(container, 'right', 'visible', (this.room.data.right ? this.room.data.right : 'false'));
    setAttributeForClass(container, 'back', 'visible', (this.room.data.back ? this.room.data.back : 'false'));
    setAttributeForClass(container, 'front', 'visible', (this.room.data.front ? this.room.data.front : 'false'));
  }
  // …
};

function setAttributeForClass (parent, class_name, attribute, value) {
  var elements = parent.getElementsByClassName(class_name);
  for (var i = 0; i < elements.length; i++) {
    elements[i].setAttribute(attribute, value);
  }
}

一开始,我在玩家周围渲染了一个 3×3 的区域,但我将其扩展到了 3×3×3 以便进行垂直移动。我还将其扩展到南北东西方向的 2 个方块,以帮助营造距离感。

VR 教训 #1:比例

屏幕上的比例并不能很好地转换为头戴设备中的比例。在屏幕上,高度看起来可能没问题,但戴上头戴设备会极大地改变玩家对比例的感知。这在 纸板地牢 中仍然微妙地存在,尤其是在垂直移动时,墙壁看起来会比预期更高。重要的是要经常在头戴设备中测试体验。

移动

Cardboard Dungeon Traversal

地图移动是我需要解决的首要问题之一。就像 VR 中的所有事物一样,它需要大量迭代。

最初,我在地面上使用了方块 (N、E、S、W) 来触发玩家移动。这很有效,我不断迭代它以提供额外的垂直移动控制。我使这些控制具有上下文敏感性,因此只有在必要时才会显示垂直移动选项。但是,这导致玩家需要四处查看才能找到控制,并且过度依赖玩家发现控制。

VR 教训 #2:压力

将常见的交互放在玩家视线之外会造成不舒服的体验。为了触发移动而不得不注视地面意味着不断地将头部上下倾斜。将这种交互放在靠近玩家自然休息视线的位置可以创造更舒适的体验。

因此,我的最终解决方案是使用瞬移机制。玩家只需注视任何蓝色的球体即可移动到该位置,无论房间是在较低层还是较高层。我选择将其限制在玩家周围的一个地牢方块内,以保持探索的感觉。

function move (dom_element) {
  // Fetch the current and target room ids.
  var current_room_key_array = containers.center.room_id.split(',');
  var container_key = dom_element.parentElement.getAttribute('id');
  var target_room_key_array = containers[container_key].room_id.split(',');

  // Calculate the offsets.
  var offset_x = parseInt(target_room_key_array[0], 10) - parseInt(current_room_key_array[0], 10);
  var offset_y = parseInt(target_room_key_array[1], 10) - parseInt(current_room_key_array[1], 10);
  var offset_z = parseInt(target_room_key_array[2], 10) - parseInt(current_room_key_array[2], 10);

  // Apply to each room.
  Object.keys(containers).forEach(function (key) {
    var container = containers[key];
    var room_key_array = container.room_id.split(',');
    room_key_array[0] = parseInt(room_key_array[0], 10) + offset_x;
    room_key_array[1] = parseInt(room_key_array[1], 10) + offset_y;
    room_key_array[2] = parseInt(room_key_array[2], 10) + offset_z;
    var new_room_key = room_key_array.join(',');

    if (map[new_room_key]) {
      container.room = new Room(map[new_room_key].data);
      container.room_id = new_room_key;

      // Remove any existing item data.
      container.removeItems();

      // Add item if it exists in the new room data.
      if (map[new_room_key].item) {
        container.addItem(map[new_room_key].item);
      }

      container.render();
    } else {
      container.room = null;
      container.room_id = new_room_key;

      // Remove any existing item data.
      container.removeItems();
      container.render();
    }
  });
}

库存和互动

Cardboard Dungeon Inventory

库存和互动花费了最大的精力和迭代次数才能创造出有用的东西。我尝试了许多疯狂的想法,比如将玩家缩小到他们脚下,或者将他们传送到一个单独的库存房间。

虽然有趣,但这些原型突出了 VR 中便利性的问题。概念作为初始体验可能很有趣,但陌生的机制最终可能会变得不方便,最终令人恼火。

VR 教训 #3:自动移动

控制玩家会创造糟糕的体验。以 纸板地牢 为例,前面提到的缩小机制有一个动画,它缩放相机并将相机移动到玩家脚下。这很快就会产生恶心感,因为玩家无法控制动画;这是一个不自然的动作。

最终,我决定使用对玩家来说最便捷的互动方式。这只是一个位于玩家脚下的物品网格。收集地牢中的物品会将它们放置在网格中,从中可以轻松地选择物品。有时,最简单的解决方案提供最佳体验。

结论

我非常享受使用 A-Frame 创建我的游戏。它是一个强大的框架,我认为它是一个出色的快速原型工具,而且本身也是一个有用的生产工具。

我担心基于网络的 VR 会在性能方面出现问题,但我很高兴地发现情况并非如此。纹理大小是最大的性能杀手,因为它们会导致卡顿并对 延迟 产生显著影响。

A-Frame 的优点在于可以创建自己的组件来增强现有的实体和组件。我还没有机会对此概念进行太多实验,但这显然是改进 纸板地牢 体验的下一步。

最后,A-Frame 团队和社区很棒。他们的 Slack 群组 非常活跃,团队成员反应非常快。

我希望这能让你对我在构建 纸板地牢 时遇到的挑战有所了解。虚拟现实是一个新兴领域,因此答案很少,还有很多经验教训需要学习。这是一个激动人心的探索空间,A-Frame 等框架正在帮助让 VR 对想要探索这个新领域的网页开发者更加容易获得。

你可以 在这里玩 纸板地牢 (推荐使用 Google Cardboard),并且 完整的源代码可以在 GitHub 上找到

感谢阅读。

关于 Christopher Waite

Christopher Waite 是英国伦敦和萨福克郡一家网页开发公司的技术总监。他在业余时间制作游戏。

更多 Christopher Waite 的文章…