Skip to content

Latest commit

 

History

History
1050 lines (743 loc) · 43.8 KB

File metadata and controls

1050 lines (743 loc) · 43.8 KB

十、基于 Web 的虚拟现实游戏开发

虚拟现实VR)和增强现实AR技术的出现正在改变用户与软件交互的方式,进而改变他们周围的世界。VR 和 AR 的可能应用是数不胜数的,尽管游戏行业是早期采用者,但这些快速发展的技术有可能改变多学科和多行业的模式。

为了演示 MERN stack 与 React 360 如何轻松地将虚拟现实功能添加到任何 web 应用中,我们将在本章和下一章中讨论并开发一款基于 web 的动态虚拟现实游戏。

通过涵盖以下主题,本章将重点介绍定义 VR 游戏的功能和使用 React 360 开发游戏视图:

  • 虚拟现实游戏规范
  • 开发 3D VR 应用的关键概念
  • 开始使用 React 360
  • 定义游戏数据
  • 实现游戏视图
  • 捆绑 React 360 代码以与 MERN 骨架集成

梅恩虚拟现实游戏

MERN VR 游戏 web 应用将通过扩展 MERN 框架并使用 React 360 集成 VR 功能来开发。这将是一个动态的、基于网络的虚拟现实游戏应用,注册用户可以在其中制作自己的游戏,任何访问该应用的人都可以玩这些游戏:

游戏本身的功能将非常简单,可以将虚拟现实引入基于 MERN 的应用,而不必深入研究 React 360 的高级概念,这些概念可用于实现更复杂的虚拟现实功能。

The code to implement features of the VR game using React 360 is available on GitHub at github.com/shamahoque/MERNVR. You can clone this code and run the application as you go through the code explanations in the rest of this chapter. 

游戏特色

MERN VR 游戏中的每个游戏基本上都是一个不同的 VR 世界,用户可以与 360 度全景世界中不同位置的 3D 对象进行交互。

游戏玩法类似于寻宝游戏,为了完成每个游戏,用户必须找到并收集与每个游戏的线索或描述相关的 3D 对象。这意味着游戏世界将包含一些玩家可以收集的虚拟现实对象,以及一些无法收集的虚拟现实对象,但游戏制作者可以将其作为道具或提示放置。

本章的重点

在本章中,我们将使用 React 360 构建游戏功能,主要关注与实现前面定义的功能相关的概念。一旦游戏功能准备就绪,我们将讨论如何捆绑 React 360 代码,并准备与第 11 章中开发的 MERN 应用代码集成,使用 MERN使 VR 游戏动态化。

反应 360 度

React 360 可以使用 React 中相同的声明式和基于组件的方法构建虚拟现实体验。React 360 的底层技术利用 Three.js JavaScript 3D 引擎在任何兼容的 web 浏览器中使用 WebGL 呈现 3D 图形,还提供使用 web VR API 访问 VR 耳机的功能。

尽管 React 360 构建在 React 之上,并且应用在浏览器中运行,但 React 360 与 React Native 有很多共同之处,因此使 React 360 应用跨平台运行。这也意味着来自 React Native 的概念也适用于 React 360。涵盖所有 React 360 概念超出了本书的范围,因此我们将重点关注构建游戏并将其与 MERN stack web 应用集成所需的概念。

开始使用 React 360

React 360 提供开发人员工具,可以轻松开始开发新的 React 360 项目。React 360 文档中详细介绍了开始的步骤,因此我们将仅总结这些步骤,并指出与开发游戏相关的文件。

由于我们已经为 MERN 应用安装了节点,我们可以从安装 React 360 CLI 工具开始:

npm install -g react-360-cli

使用此 CLI 工具创建新的应用并安装所需的依赖项:

react-360 init MERNVR

这将在当前目录中名为MERNVR的文件夹中添加应用以及所有必要的文件。最后,我们可以从命令行进入此文件夹,并运行应用:

npm start

start命令将初始化本地开发服务器,默认 React 360 应用可在http://localhost:8081/index.html浏览器中查看。

为了更新 starter 应用并实现我们的游戏功能,我们将主要修改index.js文件中的代码,并对MERNVR项目文件夹中的client.js文件进行一些小的更新。

index.js中 starter 应用的默认代码应如下所示,它表示欢迎您在浏览器的 360 世界中反应 360 文本:

import React from 'react'
import { AppRegistry, StyleSheet, Text, View } from 'react-360'

export default class MERNVR extends React.Component {
  render() {
    return (
      <View style={styles.panel}>
        <View style={styles.greetingBox}>
          <Text style={styles.greeting}>
            Welcome to React 360
          </Text>
        </View>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  panel: {
    // Fill the entire surface
    width: 1000,
    height: 600,
    backgroundColor: 'rgba(255, 255, 255, 0.4)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  greetingBox: {
    padding: 20,
    backgroundColor: '#000000',
    borderColor: '#639dda',
    borderWidth: 2,
  },
  greeting: {
    fontSize: 30,
  }
})

AppRegistry.registerComponent('MERNVR', () => MERNVR)

index.js文件包含应用的内容和主代码。client.js中的代码包含将浏览器连接到index.js中 React 应用的样板文件。starter 项目文件夹中的默认client.js如下所示:

import {ReactInstance} from 'react-360-web'

function init(bundle, parent, options = {}) {
  const r360 = new ReactInstance(bundle, parent, {
    // Add custom options here
    fullScreen: true,
    ...options,
  })

  // Render your app content to the default cylinder surface
  r360.renderToSurface(
    r360.createRoot('MERNVR', { /* initial props */ }),
    r360.getDefaultSurface()
  )

  // Load the initial environment
  r360.compositor.setBackground(r360.getAssetURL('360_world.jpg'))
}

window.React360 = {init}

此代码基本上执行index.js中定义的 React 代码,基本上创建 React 360 的新实例,并通过将其附加到 DOM 来加载 React 代码

设置了默认的 React 360 项目后,在修改代码以实现游戏之前,我们将首先了解与开发 3D VR 体验相关的一些关键概念,以及这些概念如何应用于 React 360。

开发虚拟现实游戏的关键概念

在为游戏创建虚拟现实内容和 360 度互动体验之前,首先了解虚拟世界的一些关键方面以及如何使用 React 360 组件来处理这些虚拟现实概念是很重要的。

等矩形全景图像

游戏的虚拟现实世界将由全景图像组成,全景图像作为背景图像添加到 React 360 环境中。

全景图像通常是 360 度图像或投影到完全包围观众的球体上的球形全景图像。360 度全景图像的常用格式是等矩形格式。React 360 度目前支持等矩形图像的单声道和立体声格式。

To learn more about the 360 image and video support in React 360, refer to the React 360 docs at facebook.github.io/react-360/docs/setup.html.

此处显示的图像是等矩形 360 度全景图像的示例。要在 MERN VR 游戏中设置游戏的世界背景,我们将使用以下图像:

An equirectangular panoramic image consists of a single image with an aspect ratio of 2:1, where the width is twice the height. These images are created with a special 360 degree camera. An excellent source of equirectangular images is Flickr, you just need to search for the equirectangular tag.

通过在 React 360 环境中使用等矩形图像设置背景场景来创建游戏世界,将使虚拟现实体验身临其境,并将用户转移到虚拟位置。为了增强这种体验并在这个虚拟现实世界中有效地添加 3D 对象,我们需要更多地了解与 3D 空间相关的布局和坐标系。

三维位置–坐标和变换

我们需要了解虚拟现实世界空间中的位置和方向,以便将 3D 对象放置在所需位置,并使虚拟现实体验更真实。

三维坐标系

对于三维空间中的贴图,React 360 使用类似于 OpenGL®三维坐标系的基于米的三维坐标系,允许单个组件相对于其父组件中的布局进行三维变换、移动或旋转。

The 3D coordinate system used in React 360 is a right-handed system. This means the positive x-axis is to the right, the positive y-axis is up, and the positive z-axis is backwards. This provides a better mapping with common coordinate systems of the world space in assets and 3D world modeling. 

如果我们尝试可视化 3D 空间,用户将从下一幅图像中所示的X-Y-Z轴的中心开始。Z轴指向用户前方,用户向外看**-Z轴方向。Y轴上下运行,而X**轴从一侧到另一侧运行。

图像中的曲线箭头显示正旋转值的方向:

使改变

在以下两幅图像中,通过更改正在渲染 3D 对象的 React 360Entity组件的样式属性中的transform属性,3D book 对象被放置在两个不同的位置和方向。此处的变换基于 React 的变换样式,React 360 扩展为全 3D,考虑到 X-Y-Z 轴:

transform属性以键和值数组的形式添加到style属性中的组件中,格式如下:

style={{ ...
          transform: [ 
            {TRANSFORM_COMMAND: TRANSFORM_VALUE},
         ...
    ] 
... }}

与我们游戏中要放置的 3D 对象相关的变换命令和值是translate [x, y, z](以米为单位)、rotate [x, y, z](以度为单位)和scale(以确定对象在所有轴上的大小)。我们还将使用矩阵命令,该命令接受一个值作为表示平移、旋转和缩放值的 16 个数字的数组。

To learn more about the React 360 3D coordinates and transforms, take a look at the React 360 docs at facebook.github.io/react-360/docs/setup.html.

反应 360 个组件

React 360 提供了一系列组件,可直接用于创建游戏的 VR 用户界面。接下来,我们将总结用于构建游戏功能的特定组件。

核心部件

React 360 的核心组件包括 React Native 的内置组件:TextView。在游戏中,我们将使用这两个组件在游戏世界中添加内容。

看法

View组件是在 React Native 中构建用户界面的最基本组件,它直接映射到运行 React Native 的任何平台上的本机视图。在我们的情况下,它将出现在浏览器上的<div>

<View>
  <Text>Hello</Text>
</View>

View组件通常用作其他组件的容器,它可以嵌套在其他视图中,并且可以有零到多个任何类型的子组件。

我们将使用View组件来保存游戏世界视图,并将 3D 对象实体和文本添加到游戏中。

文本

Text组件是用于显示文本的 React 原生组件,我们将使用它在 3D 空间中渲染字符串,方法是将Text组件放置在View组件中:

<View>
      <Text>Welcome to the MERN VR Game</Text>
</View>

3D VR 体验组件

React 360 提供了一套自己的组件来创建虚拟现实体验。具体来说,我们将使用Entity组件添加 3D 对象,并使用VrButton组件捕获用户的点击。

实体

为了向游戏世界添加 3D 对象,我们将使用Entity组件,它允许我们在 React 360 中渲染 3D 对象:

<Entity
  source={{
           obj: {uri: "http://linktoOBJfile.obj "},
           mtl: {uri: "http://linktoMTLfile.obj "}
        }}
/>

包含特定 3D 对象信息的文件使用source属性添加到Entity组件中。“源”属性使用键值对对象将资源文件类型映射到它们的位置。React 360 支持波前 OBJ 文件格式,这是 3D 模型的常见表示形式。所以在 source 属性中,Entity组件支持以下键:

  • obj:OBJ 格式模型的位置
  • mtl:MTL 格式材料的位置(与 OBJ 配套)

objmtl属性的值指向这些文件的位置,可以是静态字符串、asset()调用、require()语句或 URI 字符串。

OBJ (or .OBJ) is a geometry definition file format first developed by Wavefront Technologies. It is a simple data format that represents 3D geometry as a list of vertices and texture vertices. OBJ coordinates have no units, but OBJ files can contain scale information in a human-readable comment line. Learn more about this format at paulbourke.net/dataformats/obj/. MTL (or .MTL) are material library files that contain one or more material definitions, each of which includes the color, texture, and reflection map of individual materials. These are applied to the surfaces and vertices of objects. Learn more about this format at paulbourke.net/dataformats/mtl/.

Entity组件还采用style属性中的transform属性值,因此可以将对象放置在 3D 世界空间中所需的位置和方向。在我们的 MERN VR 游戏应用中,制造商将为每个Entity添加指向 VR 对象文件的 URL(包括.obj.mtl对象,并指定transform属性值以指示 3D 对象在游戏世界中的位置和放置方式。

A good source of 3D objects is https://clara.io/, with multiple file formats available for download and use.

VrButton

React 360 中的VrButton组件将有助于为添加到游戏中的对象和Text按钮实现简单的按钮样式onClick行为。默认情况下,VrButton在视图中不可见,仅作为捕获事件的包装器,但其样式可以与View组件相同:

<VrButton onClick={this.clickHandler}>
        <View>
            <Text>Click me to make something happen!</Text>
        </View>
 </VrButton>

此组件是一个帮助器,用于管理用户跨不同输入设备的单击类型交互。触发点击事件的输入事件包括键盘上的空格键按下、鼠标左键点击和触摸屏幕。

React 360 API

除了前面讨论的 React 360 组件外,我们还将利用 React 360 提供的 API 实现诸如设置背景场景、播放音频、处理外部链接、添加样式、捕获用户视图的当前方向以及使用静态资源文件等功能。

环境

我们将使用EnvironmentAPI 使用setBackgroundImage方法从 React 代码更改背景场景:

Environment.setBackgroundImage( {uri: 'http://linktopanoramaimage.jpg' } )

此方法将当前背景图像与指定 URL 处的资源一起设置。当我们将 React 360 游戏代码与包含游戏应用后端的 MERN 堆栈集成时,我们可以使用用户提供的图像链接动态设置游戏世界图像。

本机模块

React 360 中的本机模块能够访问仅在主浏览器环境中可用的功能。在游戏中,我们将使用本机模块中的AudioModule来播放声音以响应用户活动,并使用允许访问浏览器中window.locationLocation模块来处理外部链接。这些模块可在index.js中访问,如下所示:

import {
    ...
  NativeModules
} from 'react-360'

const { AudioModule, Location } = NativeModules

音频模块

当用户与 3D 对象交互时,我们将根据是否可以收集对象以及游戏是否完成来播放声音。本机模块中的AudioModule允许向虚拟现实世界添加声音,如背景环境音频、一次性声音效果和空间化音频。在我们的游戏中,我们将使用环境音频和一次性音效

  • 环境音频:为了在游戏成功完成时播放一个音频 on loop 并设置情绪,我们将使用playEnvironmental方法,将音频文件路径作为source选项,将loop选项作为playback参数:
AudioModule.playEnvironmental({
    source: asset('happy-bot.mp3'),
    loop: true
})
  • 音效:要在用户点击 3D 对象时播放一个声音,我们将使用playOneShot方法,将音频文件路径作为source
AudioModule.playOneShot({
    source: asset('clog-up.mp3'),
})

传递给playEnvironmentalplayOneShot的选项中的source属性采用资源文件位置加载音频。可以是asset()语句,也可以是{uri: 'PATH'}形式的资源 URL 声明。

地方

在我们将 React 360 代码与包含游戏应用后端的 MERN 堆栈集成后,VR 游戏将从 MERN 服务器以包含特定游戏 ID 的声明路径启动。然后,一旦用户完成游戏,他们还可以选择离开 VR 空间,并转到包含其他游戏列表的 URL。为了在 React 360 代码中处理这些传入和传出的应用链接,我们将在本机模块中使用Location模块。

Location模块实质上是浏览器中只读window.location属性返回的Location对象。我们将使用Location对象中的replace方法和search属性来实现与外部链接相关的特性。

  • 处理外发链接:当我们想将用户从 VR 应用引导到另一个链接时,我们可以使用Location中的replace方法:
Location.replace(url)
  • 处理传入链接:当 React 360 应用从外部 URL 启动,注册组件挂载后,我们可以使用Location中的search属性访问 URL 并检索其查询字符串部分:
componentDidMount = () => {
   let queryString = Location.search
   let gameId = queryString.split('?id=')[1]
}

为了将此 React 360 组件与 MERN VR 游戏集成,并动态加载游戏详细信息,我们将捕获此初始 URL 以解析查询参数中的游戏 ID,然后使用它对 MERN 应用服务器进行读取 API 调用。此实现在第 11 章中详细阐述,使用 MERN使 VR 游戏动态化。

样式表

React-Native 的样式表 API 也可用于 React 360,以便在一个位置定义多个样式,而不是将样式添加到单个组件:

const styles = StyleSheet.create({
  subView: {
    width: 10,
    borderColor: '#d6d7da',
  },
  text: {
    fontSize: '1em',
    fontWeight: 'bold',
  }
})

可以根据需要将定义的样式添加到零部件:

<View style={styles.subView}>
  <Text style={styles.text}>hello</Text>
</View>

The default distance units for CSS properties, such as width and height, are in meters when mapping to 3D space in React 360, whereas the default distance units are in pixels for 2D interfaces, as in React Native.

VrHeadModel

VrHeadModel是 React 360 中的一个实用模块,可简化获取耳机当前方向的过程。由于用户在虚拟现实空间中四处移动,当所需功能要求将对象或文本放置在用户当前方向的前面或相对于用户当前方向时,有必要知道用户当前凝视的确切位置。

在 MERN VR 游戏中,我们将使用它在用户视图前向用户显示游戏完成消息,无论用户最终从初始位置转向何处。

例如,在收集最终对象时,用户可能正在向上或向下查看,并且无论用户在哪里注视,都应弹出已完成的消息。为了实现这一点,我们将使用VrHeadModel中的getHeadMatrix()以数字数组的形式检索当前头部矩阵,并将其设置为包含游戏完成消息的View样式属性中transform属性的值。

资产

React 360 中的asset()功能允许我们检索外部资源文件,如音频和图像文件。我们将把游戏的声音音频文件放在static_assets文件夹中,对于添加到游戏中的每个音频,使用asset()进行检索:

AudioModule.playOneShot({
    source: asset('collect.mp3'),
})

响应 360 个输入事件

为了使游戏界面具有交互性,我们将使用 React 360 中公开的一些输入事件处理程序。通过鼠标、键盘、触摸屏和游戏板交互,以及在 VR 耳机上点击gaze按钮,可以收集输入事件。我们将处理的具体输入事件是onEnteronExitonClick事件。

  • onEnter:只要平台光标开始与组件相交,就会触发此事件。我们将为游戏中的 VR 对象捕获此事件,以便当平台光标进入特定对象时,对象可以开始围绕 Y 轴旋转。
  • onExit:只要平台光标停止与组件相交,就会触发此事件。它与onEnter事件具有相同的属性,我们将使用它停止旋转刚刚退出的 VR 对象。
  • onClickonClick事件与VrButton组件一起使用,与VrButton有点击交互时触发。我们将使用它在 VR 对象上设置点击事件处理程序,并在游戏完成消息上设置点击事件处理程序,以将用户从 VR 应用重定向到包含游戏列表的链接。

通过本节讨论的 VR 相关概念和组件,我们已经准备好定义游戏数据细节并开始实施完整的 VR 游戏。

游戏详情

MERN VR 游戏中的每个游戏都将在通用数据结构中定义,React 360 应用在呈现单个游戏细节时也将遵循该数据结构。

游戏数据结构

游戏数据结构将保存详细信息,如游戏名称、指向游戏世界等矩形图像位置的 URL,以及包含要添加到游戏世界的每个 VR 对象详细信息的两个数组:

  • 名称:表示游戏名称的字符串
  • 世界:URL 指向等矩形图像的字符串,该图像托管在云存储、CDN 或存储在 MongoDB 上
  • answerObjects:包含玩家可以收集的 VR 对象细节的对象数组
  • 错误对象:一组对象,包含玩家无法收集的要放置在虚拟现实世界中的其他虚拟现实对象的详细信息

虚拟现实对象的详细信息

answerObjects数组将包含可收集的 3D 对象的详细信息,wrongObjects数组将包含无法收集的 3D 对象的详细信息。每个对象将包含指向三维数据资源文件和transform样式属性值的链接。

OBJ 和 MTL 链接

VR 对象的 3D 数据信息资源将添加到objUrlmtlUrl键中:

  • 对象:链接到 3D 对象的.obj文件
  • mtlUrl:链接到附带的.mtl文件

objUrlmtlUrl链接可能指向托管在云存储、CDN 或存储在 MongoDB 上的文件。对于 MERN VR 游戏,我们假设制造商会将 URL 添加到自己托管的 OBJ、MTL 和等矩形图像文件中。

翻译价值

VR 对象在 3D 空间中的位置将通过以下键中的translate值来定义:

  • translateX:对象沿 X 轴的平移值
  • 平移 Y:对象沿 Y 轴的平移值
  • 平移 Z:对象沿 Z 轴的平移值

所有转换值都是以米为单位的数字。

旋转值

3D 对象的方向将由以下键中的rotate值定义:

  • rotateX:物体绕 X 轴的旋转值,即上下转动物体
  • 旋转:物体绕 Y 轴的旋转值,该 Y 轴将使物体向左或向右旋转
  • rotateZ:物体绕 Z 轴的旋转值,使物体前后倾斜

所有旋转值均以数字或数字的字符串表示形式(以度为单位)。

标度值

scale值将定义 3D 对象的相对大小外观:

刻度:定义所有轴上均匀刻度的数值

颜色

如果 MTL 文件中未提供 3D 对象的材质纹理,则颜色值可以定义对象的默认颜色。

颜色:表示 CSS 中允许的颜色值的字符串值

有了这个能够保存游戏及其虚拟现实对象细节的游戏数据结构,我们可以用样本数据值在 React 360 中相应地实现游戏。

静态数据与动态数据

在下一章中,我们将更新 React 360 代码,以便从后端数据库动态获取游戏数据。现在,我们将在这里开始开发游戏功能,使用定义的游戏数据结构将虚拟游戏数据设置为state

样本数据

出于初始开发目的,可以将以下示例游戏数据设置为要在游戏视图中呈现的状态:

game: {
  name: 'Space Exploration',
  world: 'https://s3.amazonaws.com/mernbook/vrGame/milkyway.jpg',
  answerObjects: [
    { 
      objUrl: 'https://s3.amazonaws.com/mernbook/vrGame/planet.obj',
      mtlUrl: 'https://s3.amazonaws.com/mernbook/vrGame/planet.mtl',
      translateX: -50,
      translateY: 0,
      translateZ: 30,
      rotateX: 0,
      rotateY: 0,
      rotateZ: 0,
      scale: 7,
      color: 'white'
    }
  ],
  wrongObjects: [
    { 
      objUrl: 'https://s3.amazonaws.com/mernbook/vrGame/tardis.obj',
      mtlUrl: 'https://s3.amazonaws.com/mernbook/vrGame/tardis.mtl',
      translateX: 0,
      translateY: 0,
      translateZ: 90,
      rotateX: 0,
      rotateY: 20,
      rotateZ: 0,
      scale: 1,
      color: 'white'
    }
  ]
}

在 React 360 中构建游戏视图

我们将应用 React 360 概念,通过更新index.jsclient.js中的代码,使用游戏数据结构来实现游戏功能。对于工作版本,我们将从使用上一节中的示例游戏数据初始化的状态开始。

/MERNVR/index.js

export default class MERNVR extends React.Component {

    constructor() {
        super()
        this.state = {
                game: sampleGameData
                ...
            }
    }

...
}

更新 client.js 并装载到位置

client.js中的默认代码将index.js中声明的挂载点附加到 React 360 应用中的默认表面,该表面是用于放置 2D UI 的圆柱形层。为了在 3D 空间中使用基于 3D 米的坐标系进行布局,我们需要安装到Location而不是表面。所以更新client.jsrenderToSurface替换为renderToLocation

/MERNVR/client.js

  r360.renderToLocation(
    r360.createRoot('MERNVR', { /* initial props */ }),
    r360.getDefaultLocation()
  )

You can also customize the initial background scene by updating the code r360.compositor.setBackground(**r360.getAssetURL('360_world.jpg')**) in client.js to use your desired image.

使用样式表定义样式

index.js中,我们将使用我们自己的 CSS 规则更新使用StyleSheet.create创建的默认样式,以用于游戏中的组件。

/MERNVR/index.js

const styles = StyleSheet.create({
                 completeMessage: {
                      margin: 0.1,
                      height: 1.5,
                      backgroundColor: 'green',
                      transform: [ {translate: [0, 0, -5] } ]
                 },
                 congratsText: {
                      fontSize: 0.5,
                      textAlign: 'center',
                      marginTop: 0.2
                 },
                 collectedText: {
                      fontSize: 0.2,
                      textAlign: 'center'
                 },
                 button: {
                      margin: 0.1,
                      height: 0.5,
                      backgroundColor: 'blue',
                      transform: [ { translate: [0, 0, -5] } ]
                 },
                 buttonText: {
                      fontSize: 0.3,
                      textAlign: 'center'
                 }
              }) 

世界背景

为了设置游戏的 360 度世界背景,我们将使用componentDidMount内的EnvironmentAPI 中的setBackgroundImage方法更新当前背景场景。

/MERNVR/index.js

componentDidMount = () => {
    Environment.setBackgroundImage(
      {uri: this.state.game.world}
    )
}

这将用从云存储获取的示例游戏的世界图像替换 starter React 360 项目中的默认 360 背景。如果您正在编辑默认的 React 360 应用并使其运行,刷新浏览器上的http://localhost:8081/index.html链接应显示一个外部空间背景,可使用鼠标进行平移:

为了生成前面的屏幕截图,默认代码中的ViewText组件也使用自定义 CSS 规则进行了更新,以在屏幕上显示此 hello 文本。

添加 3D VR 对象

我们将使用Entity组件和answerObjectswrongObjects阵列中的示例对象详细信息向游戏世界添加 3D 对象。

首先,我们将把componentDidMount中的answerObjectswrongObjects数组连接起来,形成一个包含所有 VR 对象的数组。

/MERNVR/index.js

componentDidMount = () => {
  let vrObjects = this.state.game.answerObjects.concat(this.state.game.wrongObjects)
  this.setState({vrObjects: vrObjects}) 
    ...
}

然后在主视图中,我们将迭代vrObjects数组,添加包含每个对象细节的Entity组件。

/MERNVR/index.js

{this.state.vrObjects.map((vrObject, i) => {
     return (
                <Entity key={i} style={this.setModelStyles(vrObject, i)}
                  source={{
                    obj: {uri: vrObject.objUrl},
                    mtl: {uri: vrObject.mtlUrl}
                  }}
                 />
            )
    })
}

objmtl文件链接添加到source中,transform样式详细信息与setModelStyles(vrObject, index)一起应用于Entity组件的样式中。

/MERNVR/index.js

setModelStyles = (vrObject, index) => {
    return {
        display: this.state.collectedList[index] ? 'none' : 'flex',
        color: vrObject.color,
        transform: [
          {
            translateX: vrObject.translateX
          }, { 
            translateY: vrObject.translateY
          }, {
            translateZ: vrObject.translateZ
          }, {
            scale: vrObject.scale
          }, {
            rotateY: vrObject.rotateY
          }, {
            rotateX: vrObject.rotateX
          }, {
            rotateZ: vrObject.rotateZ
          }
        ]
      }
  }

display属性允许我们根据玩家是否已经收集到某个对象来显示或隐藏该对象。

translaterotate值将在整个虚拟现实世界中以所需的位置和方向渲染 3D 对象。

接下来,我们将进一步更新Entity代码,以允许用户与 3D 对象交互。

与虚拟现实对象交互

为了使 VR 游戏对象具有交互性,我们将使用 React 360 事件处理程序,例如onEnteronExitEntity以及onClickVrButton添加旋转动画和游戏行为。

轮换

我们想添加一个功能,每当玩家聚焦于 3D 对象时,该功能就开始围绕其 Y 轴旋转 3D 对象,即平台光标开始与呈现特定 3D 对象的Entity相交。

我们将更新上一节中的Entity组件,以添加onEnteronExit处理程序。

/MERNVR/index.js

<Entity 
     ... 
    onEnter={this.rotate(i)}
    onExit={this.stopRotate}
/>

该对象将在回车时开始旋转,并在平台光标退出该对象且不再处于播放器焦点时停止。

带有 requestAnimationFrame 的动画

rotate(index)stopRotate()方法中,我们将使用requestAnimationFrame在浏览器上实现平滑动画的旋转动画行为。

The window.requestAnimationFrame() method asks the browser to call a specified callback function to update an animation before the next repaint. With requestAnimationFrame, the browser optimizes the animations to make them smoother and more resource-efficient.

使用rotate方法,我们将在设定的时间间隔内以稳定的速率用requestionAnimationFrame更新给定对象的rotateY变换值。

/MERNVR/index.js

this.lastUpdate = Date.now() 
rotate = index => event => {
    const now = Date.now()
    const diff = now - this.lastUpdate
    const vrObjects = this.state.vrObjects
    vrObjects[index].rotateY = vrObjects[index].rotateY + diff / 200
    this.lastUpdate = now
    this.setState({vrObjects: vrObjects})
    this.requestID = requestAnimationFrame(this.rotate(index)) 
}

requestAnimationFramerotate方法作为一个递归回调函数,然后执行它,用新值重新绘制旋转动画的每一帧,然后更新屏幕上的动画。

requestAnimateFrame方法返回一个requestID,我们将在stopRotate中使用它来取消stopRotate方法中的动画。

/MERNVR/index.js

stopRotate = () => {
  if (this.requestID) {
    cancelAnimationFrame(this.requestID) 
    this.requestID = null 
  }
}

这将实现仅当 3D 对象处于查看器焦点时才设置其动画的功能。如下图所示,3D 魔方在聚焦时围绕其 Y 轴顺时针旋转:

Though not covered here, it is also worth exploring the React 360 Animated library, which can be used to compose different types of animations. Core components can be animated natively with this library, and it is possible to make other components animatable using createAnimatedComponent(). This library was originally implemented from React Native, and to learn more you can refer to the React Native documentation.

单击三维对象

为了注册添加到游戏中的每个 3D 对象的点击行为,我们需要用一个可以调用onClick处理程序的VrButton组件包装Entity组件。

我们将更新vrObjects数组迭代代码中添加的Entity组件,用VrButton组件包装它。点击时VrButton会调用collectItem方法,并将当前对象的详细信息传递给它。

/MERNVR/index.js

<VrButton onClick={this.collectItem(vrObject)} key={i}>
    <Entity  />
</VrButton>

点击 3D 对象时,collectItem方法需要针对游戏功能执行以下动作:

  • 检查点击的对象是answerObject还是wrongObject
  • 根据对象类型,播放关联的声音
  • 如果该对象是一个answerObject,则应将其收集并从视图中消失
    • 更新收集的对象列表
  • 检查此点击是否成功收集了answerObject的所有实例
    • 如果是,则向玩家显示游戏已完成消息,并播放游戏已完成的声音

因此,collectItem方法将具有以下结构和步骤:

collectItem = vrObject => event => {
  if (vrObject is an answerObject) {
     ... update collected list ...
     ... play sound for correct object collected ...
     if (all answer objects collected) {
         ... show game completed message in front of user ...
         ... play sound for game completed ...
     }
  } else {
     ... play sound for wrong object clicked ...
  }
}

接下来,我们将研究这些步骤的实现。

在单击时收集正确的对象

当用户单击 3D 对象时,我们需要首先检查单击的对象是否为应答对象。如果是,此收集的对象将隐藏在视图中,收集的对象列表将与总数一起更新,以跟踪用户在游戏中的进度。

为了检查点击的 VR 对象是否为answerObject,我们将使用indexOf方法在answerObjects数组中查找匹配项:

let match = this.state.game.answerObjects.indexOf(vrObject) 

如果vrObjectanswerObject,则indexOf返回匹配对象的数组索引,否则如果未找到匹配项,则返回-1

为了跟踪游戏中收集的物品,我们还将在collectedList中维护一个布尔值数组,并在collectedNum中维护到目前为止收集的物品总数:

let updateCollectedList = this.state.collectedList 
let updateCollectedNum = this.state.collectedNum + 1 
updateCollectedList[match] = true 
this.setState({collectedList: updateCollectedList, 
                collectedNum: updateCollectedNum}) 

使用collectedList数组,我们还将确定哪个Entity组件应该从视图中隐藏,因为关联的对象已被收集。相关Entitydisplay样式属性将根据collectedList数组中相应索引的布尔值进行设置,同时为Entity设置样式使用setModelStyles方法的组件,如前面的添加 3D VR 对象部分所示:

display: this.state.collectedList[index] ? 'none' : 'flex'

下图中,宝箱是answerObject可以点击采集,而花盆是wrongObject不能采集:

当点击宝箱时,随着collectedList的更新,宝箱从视图中消失,我们也使用AudioModule.playOneShot播放收藏音效:

AudioModule.playOneShot({
    source: asset('collect.mp3'),
}) 

但当点击花盆时,它被识别为错误的对象,我们播放另一种声音效果,表明它无法收集:

AudioModule.playOneShot({
     source: asset('clog-up.mp3'),
})

由于花盆被识别为错误的对象,collectedList未更新,并保留在屏幕上,如以下屏幕截图所示:

单击对象时执行所有这些步骤的collectItem方法中的完整代码如下所示。

/MERNVR/index.js

  collectItem = vrObject => event => {
    let match = this.state.game.answerObjects.indexOf(vrObject)
    if (match != -1) {
      let updateCollectedList = this.state.collectedList
      let updateCollectedNum = this.state.collectedNum + 1
      updateCollectedList[match] = true
      this.checkGameCompleteStatus(updateCollectedNum)
          AudioModule.playOneShot({
            source: asset('collect.mp3'),
          })
      this.setState({collectedList: updateCollectedList, collectedNum: updateCollectedNum})
    } else {
      AudioModule.playOneShot({
        source: asset('clog-up.mp3'),
      })
    }
  }

收集点击的对象后,我们还将检查是否收集了所有的answerObjects,并且游戏使用checkGameCompleteStatus方法完成,如下一节所述。

游戏完成状态

每次收集到一个answerObject时,我们会通过调用checkGameCompleteStatus来检查收集到的物品总数是否等于answerObjects数组中的物品总数,以确定游戏是否完成。

/MERNVR/index.js

 if (collectedTotal == this.state.game.answerObjects.length) {
    AudioModule.playEnvironmental({
       source: asset('happy-bot.mp3'),
       loop: true
    })
    this.setState({hide: 'flex', hmMatrix: VrHeadModel.getHeadMatrix()})
 }

如果游戏确实完成,我们将执行以下操作:

  • 使用AudioModule.playEnvironmental播放游戏完成后的音频
  • 使用VrHeadModel获取当前headMatrix值,以便将其设置为包含游戏完成消息的View组件的变换矩阵值
  • 将消息Viewdisplay样式属性设置为flex,以便消息呈现给查看者

包含祝贺玩家完成游戏消息的View组件将添加到父View组件中,如下所示。

/MERNVR/index.js

<View style={this.setGameCompletedStyle}>
   <View style={this.styles.completeMessage}>
      <Text style={this.styles.congratsText}>Congratulations!</Text>
      <Text style={this.styles.collectedText}>
            You have collected all items in {this.state.game.name}
      </Text>
   </View>
   <VrButton onClick={this.exitGame}>
      <View style={this.styles.button}>
          <Text style={this.styles.buttonText}>Play another game</Text>
      </View>
   </VrButton>
</View>

setGameCompletedStyle()方法的调用将使用更新的display值和transform矩阵值设置消息View的样式。

/MERNVR/index.js

setGameCompletedStyle = () => {
    return {
        position: 'absolute',
        display: this.state.hide,
        layoutOrigin: [0.5, 0.5],
        width: 6,
        transform: [{translate: [0, 0, 0]}, {matrix: this.state.hmMatrix}]
      }
}

这将使View与完成消息一起呈现在用户当前视图的中心,无论他们在 360 度虚拟现实世界中是向上看、向下看、向后看还是向前看:

View消息中的最后文本将充当按钮,因为我们将View包装在VrButton组件中,该组件在单击时调用exitGame方法。

/MERNVR/index.js

exitGame = () => {
    Location.replace('/') 
}

exitGame方法将使用Location.replace方法将用户重定向到可能包含游戏列表的外部 URL。

replace方法可以传递任何有效的 URL,一旦此 React 360 游戏代码与第 11 章中的 MERN VR 游戏应用集成,使用 MERN使 VR 游戏动态,replace('/')将用户带到应用的主页。

捆绑生产并与 MERN 集成

现在,我们已经用示例游戏数据实现了 VR 游戏的功能,我们可以将其准备好用于生产,并将其添加到我们的 MERN base 应用中,以了解如何将 VR 添加到现有的 web 应用中。

React 360 工具提供了一个脚本,可以将所有 React 360 应用代码捆绑到几个文件中,我们可以将这些文件放在 MERN web 服务器上,并作为指定路径的内容。

绑定 360 个文件

要创建捆绑文件,我们可以从 React 360 项目目录运行以下命令:

npm run bundle

这将在名为build的文件夹中生成 React 360 应用文件的编译版本。编译的捆绑文件为client.bundle.jsindex.bundle.js。除index.htmlstatic-img/文件夹外,这两个文件构成了整个 React 360 应用的生产版本:

-- static_img/

-- index.html

-- index.bundle.js

-- client.bundle.js

与 MERN 应用集成

我们需要将这三个文件和static_assets文件夹添加到我们的 MERN 应用中,然后确保捆绑文件引用在index.html中是准确的,最后在 Express app 中的指定路径加载index.html

添加生产文件

考虑到 MERN skeleton 应用中的文件夹结构,我们将把static_assets文件夹和捆绑包文件添加到dist/文件夹中,以保持我们的 MERN 代码井然有序,并使所有捆绑包位于同一位置。index.html文件将被放置在server文件夹中名为vr的新文件夹中:

-- ... 
-- client/
-- dist/
     --- static_img/
     --- ...
     --- client.bundle.js
     --- index.bundle.js
-- ...
-- server/
     --- ...
     --- vr/
          ---- index.html
-- ...

更新 index.html 中的引用

如图所示,生成的index.html文件引用了捆绑文件,希望这些文件位于同一文件夹中:

<html>
  <head>
    <title>MERNVR</title>
    <style>body { margin: 0 }</style>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  </head>
  <body>
    <!-- Attachment point for your app -->
    <div id="container"></div>
    <script src="./client.bundle.js"></script>
    <script>
      // Initialize the React 360 application
      React360.init(
        'index.bundle.js',
        document.getElementById('container'),
        {
          assetRoot: 'static_img/',
        }
      ) 
    </script>
  </body>
</html>

我们需要更新index.html以参考client.bundle.jsindex.bundle.jsstatic_assets文件夹的正确位置。

首先,更新对client.bundle.js的引用,如下所示:

<script src="/dist/client.bundle.js" type="text/javascript"></script>

然后,使用对index.bundle.js的正确引用更新React360.init,并将assetRoot设置为static_assets文件夹的正确位置:

React360.init(
 './../dist/index.bundle.js',
        document.getElementById('container'),
 { assetRoot: '/dist/static_img/' }
    ) 

当我们使用asset()在组件中设置资源时,assetRoot将告诉 React 360 在哪里查找资产文件。

现在,如果我们在 MERN 应用中设置了一条快速路线以返回响应中的index.html文件,那么在浏览器中访问路线将呈现 React 360 游戏。

尝试集成

为了测试这种集成,我们可以设置一个示例路由,如下所示:

router.route('/game/play')
   .get((req, res) => {
      res.sendFile(process.cwd()+'/server/vr/index.html') 
}) 

然后运行 MERN 服务器,在浏览器localhost:3000/game/play处打开路由。这将从我们的 MERN 应用中呈现本章中实现的 React 360 游戏。

总结

在本章中,我们使用 React 360 开发了一款基于 web 的虚拟现实游戏,可以轻松地集成到 MERN 应用中。

我们首先为游戏定义简单的虚拟现实功能,然后设置 React 360 进行开发,并研究了关键的虚拟现实概念,如 360 度虚拟现实世界中的等矩形全景图像、三维位置和坐标系。我们探索了实现游戏功能所需的 React 360 组件和 API,包括ViewTextEntityVrButton等组件,以及EnvironmentVrHeadModelNativeModulesAPI。

最后,我们更新了 starter React 360 项目中的代码,用示例游戏数据实现了游戏,然后捆绑了代码文件,并讨论了如何将这些编译后的文件添加到现有的 MERN 应用中。

在下一章中,我们将开发 MERN VR 游戏应用,包括游戏数据库和 API,这样我们就可以通过从 MongoDB 中的游戏集合获取数据,使本章中开发的游戏动态化。