十一、使用 MERN 使虚拟现实游戏动态化

在本章中,我们将扩展 MERN skeleton 应用以构建 MERN VR 游戏应用,并使用它将前一章中开发的静态 React 360 游戏动态化,方法是将示例游戏数据替换为直接从 MERN 服务器获取的游戏细节。

为了使 MERN VR 游戏成为一款完整、动态的游戏应用,我们将实现以下功能:

  • MongoDB 中存储游戏细节的游戏模型模式
  • 用于游戏积垢操作的 API
  • 游戏创建、编辑、列表和删除的反应视图
  • 更新 React 360 游戏以从 API 获取数据
  • 用动态游戏数据加载虚拟现实游戏

动态梅恩虚拟现实游戏

MERN VR Game 上的注册用户将能够制作和修改自己的游戏,方法是为游戏世界提供等矩形图像和 VR 对象资源,包括放置在游戏世界中的每个对象的变换属性值。该应用的任何访问者都将能够浏览制造商添加的所有游戏,并玩任何游戏来查找和收集游戏世界中与每个游戏的线索或描述相关的 3D 对象:

The code for the complete MERN VR Game application is available on GitHub at github.com/shamahoque/mern-vrgame. You can clone this code and run the application as you go through the code explanations in the rest of this chapter. 

与创建、编辑和列出虚拟现实游戏相关的功能所需的视图将通过扩展和修改 MERN skeleton 应用中的现有 React 组件来开发。下图中的组件树显示了构成本章开发的 MERN 虚拟现实游戏前端的所有自定义 React 组件:

博弈模型

第 10 章开发基于网络的虚拟现实游戏中,游戏数据结构部分列出了每个游戏所需的细节,以实现为游戏定义的清道夫狩猎功能。我们将根据这些关于游戏的具体细节、虚拟现实对象以及游戏制作者的参考来设计游戏模式。

博弈模式

game.model.js中定义的游戏模型的 Mongoose 模式中,我们将为

  • 游戏名称
  • 世界图像 URL
  • 线索文本
  • 一个数组,包含要添加为可收集答案对象的 VR 对象的详细信息
  • 包含 VR 对象详细信息的数组,这些对象是错误的对象,无法收集
  • 指示游戏创建和更新时间的时间戳
  • 对制作游戏的用户的引用

GameSchema的定义如下。

mern-vrgame/server/models/game.model.js

const GameSchema = new mongoose.Schema({
  name: {
    type: String,
    trim: true,
    required: 'Name is required'
  },
  world: {
    type: String, trim: true,
    required: 'World image is required'
  },
  clue: {
    type: String,
    trim: true
  },
  answerObjects: [VRObjectSchema],
  wrongObjects: [VRObjectSchema],
  updated: Date,
  created: {
    type: Date,
    default: Date.now
  },
  maker: {type: mongoose.Schema.ObjectId, ref: 'User'}
})

虚拟对象模式

游戏模式中的answerObjectswrongObjects字段都是 VRObject 文档的数组,VRObject Mongoose 模式将分别定义用于存储 OBJ 文件和 MTL 文件 URL 的字段,以及每个 VR 对象的 React 360transform值、scale值和color值。

mern-vrgame/server/models/game.model.js

const VRObjectSchema = new mongoose.Schema({
  objUrl: {
    type: String, trim: true,
    required: 'ObJ file is required'
  },
  mtlUrl: {
    type: String, trim: true,
    required: 'MTL file is required'
  },
  translateX: {type: Number, default: 0},
  translateY: {type: Number, default: 0},
  translateZ: {type: Number, default: 0},
  rotateX: {type: Number, default: 0},
  rotateY: {type: Number, default: 0},
  rotateZ: {type: Number, default: 0},
  scale: {type: Number, default: 1},
  color: {type: String, default: 'white'}
}) 

当一个新的游戏文档保存到数据库中时,answerObjectswrongObjects数组将填充符合此模式定义的 VRObject 文档。

游戏模式中的数组长度验证

游戏文档中的answerObjectswrongObjects数组在保存到游戏集合中时,每个数组中必须至少包含一个 VRObject 文档。为了将最小数组长度的验证添加到游戏模式中,我们将在GameSchema中的answerObjectswrongObjects路径中添加以下自定义验证检查。

mern-vrgame/server/models/game.model.js

GameSchema.path('answerObjects').validate(function(v) {
  if (v.length == 0) {
    this.invalidate('answerObjects',
   'Must add alteast one VR object to collect')
  }
}, null) 
GameSchema.path('wrongObjects').validate(function(v) {
  if (v.length == 0) {
    this.invalidate('wrongObjects', 
    'Must add alteast one other VR object') 
  }
}, null) 

这些模式定义将满足根据 MERN VR 游戏规范开发动态 VR 游戏的所有要求

游戏 API

MERN VR 游戏的后端将公开一组 CRUD API,用于从数据库中创建、编辑、读取、列出和删除游戏,这些 API 可用于应用的前端,包括 React 360 游戏实现中的 fetch 调用。

创建 API

登录应用的用户将能够使用createAPI 在数据库中创建新游戏。

路线

在后端,我们将在game.routes.js中添加POST路由,验证当前用户是否已登录和授权,然后使用请求中传递的游戏数据创建新游戏。

mern-vrgame/server/routes/game.routes.js

router.route('/api/games/by/:userId')
    .post(authCtrl.requireSignin,authCtrl.hasAuthorization, gameCtrl.create)

为了处理:userId参数并从数据库中检索相关用户,我们将使用用户控制器中的userByID方法。我们还将在游戏路线中添加以下内容,因此用户可以在request对象中作为profile使用。

mern-vrgame/server/routes/game.routes.js

router.param('userId', userCtrl.userByID)

game.routes.js文件将与user.routes文件非常相似,要在 Express app 中加载这些新路由,我们需要在express.js中装载游戏路由,就像我们在验证和用户路由中所做的那样。

mern-vrgame/server/express.js

app.use('/', gameRoutes)

控制器

create控制器方法在'/api/games/by/:userId'接收到 POST 请求时执行,请求主体包含新的游戏数据。

mern-vrgame/server/controllers/game.controller.js

const create = (req, res, next) => {
  const game = new Game(req.body)
  game.maker= req.profile
  game.save((err, result) => {
    if(err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.status(200).json(result)
  })
}

在这个create方法中,使用游戏模式和从客户端传入请求体的数据创建一个新的游戏文档。将用户引用设置为游戏制作人后,此文档保存在Game集合中。

取来

在前端,我们将在api-game.js中增加相应的fetch方法,通过传递从登录用户采集的表单数据,向createAPI 发出POST请求。

mern-vrgame/client/game/api-game.js

const create = (params, credentials, game) => {
  return fetch('/api/games/by/'+ params.userId, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + credentials.t
      },
      body: JSON.stringify(game)
    })
    .then((response) => {
      return response.json();
    }).catch((err) => console.log(err)) 
}

列表 API

可以使用列表 API 从后端获取Game集合中所有游戏的列表。

路线

我们将向游戏路径添加一个 GET 路径,以检索数据库中存储的所有游戏。

mern-vrgame/server/routes/game.routes.js

router.route('/api/games')
    .get(gameCtrl.list)

/api/gamesGET请求将执行list控制器方法。

控制器

list控制器方法将查询数据库中的Game集合,将响应中的所有游戏返回给客户端。

mern-vrgame/server/controllers/game.controller.js

const list = (req, res) => {
  Game.find({}).populate('maker', '_id name')
 .sort('-created').exec((err, games) => {
    if(err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.json(games)

  })
}

取来

在前端,为了使用这个列表 API 获取游戏,我们将在api-game.js中设置一个fetch方法。

mern-vrgame/client/game/api-game.js

const list = () => {
  return fetch('/api/games', {
    method: 'GET',
  }).then(response => {
    return response.json() 
  }).catch((err) => console.log(err)) 
}

按制造商 API 列出

该应用还将允许我们获取特定用户使用 list by maker API 制作的游戏。

路线

在游戏路线中,我们将添加一个GET路线来检索特定用户制作的游戏。

mern-vrgame/server/routes/game.routes.js

router.route('/api/games/by/:userId')
    .get(gameCtrl.listByMaker)

对该路由的GET请求将在游戏控制器中执行listByMaker方法。

控制器

listByMaker控制器方法将查询数据库中的游戏集合,以获得匹配的游戏。

mern-vrgame/server/controllers/game.controller.js

const listByMaker = (req, res) => {
  Game.find({maker: req.profile._id}, (err, games) => {
    if(err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.json(games)
  }).populate('maker', '_id name')
}

在对游戏集合的查询中,我们找到了maker字段与req.profile中指定的用户匹配的所有游戏。

取来

在前端,为了通过 maker API 为特定用户获取此列表中的游戏,我们将在api-game.js中添加fetch方法。

mern-vrgame/client/game/api-game.js

const listByMaker = (params) => {
  return fetch('/api/games/by/'+params.userId, {
    method: 'GET',
    headers: {
      'Accept': 'application/json'
    }
  }).then((response) => {
    return response.json() 
  }).catch((err) => {
    console.log(err) 
  }) 
}

读取 API

个人游戏数据将使用'/api/game/:gameId'上的readAPI 从数据库中检索。

路线

在后端,我们将添加一个GET路由,该路由使用 ID 查询Game集合,并在响应中返回游戏。

mern-vrgame/server/routes/game.routes.js

router.route('/api/game/:gameId')
    .get(gameCtrl.read)

首先处理路由 URL 中的:gameId参数,从数据库中检索单个游戏。因此,我们还将在游戏路线中添加以下内容:

router.param('gameId', gameCtrl.gameByID)

控制器

读取 API 请求中的:gameId参数将调用gameByID控制器方法,类似于userByID控制器方法。它将从数据库中检索游戏,并将其附加到将在next方法中使用的request对象。

mern-vrgame/server/controllers/game.controller.js

const gameByID = (req, res, next, id) => {
  Game.findById(id).populate('maker', '_id name').exec((err, game) => {
    if (err || !game)
      return res.status('400').json({
        error: "Game not found"
      })
    req.game = game
    next()
  })
}

next方法,在本例中是read控制器方法,只是在响应中向客户端返回这个game对象。

mern-vrgame/server/controllers/game.controller.js

const read = (req, res) => {
  return res.json(req.game)
}

取来

在前端代码中,我们将添加一个fetch方法,利用此读取 API 根据单个游戏的 ID 检索其详细信息。

mern-vrgame/client/game/api-game.js

const read = (params, credentials) => {
  return fetch('/api/game/' + params.gameId, {
    method: 'GET'
  }).then((response) => {
    return response.json() 
  }).catch((err) => console.log(err)) 
}

readAPI 将用于获取游戏详细信息的 React 视图和 React 360 游戏视图,后者将呈现游戏界面。

编辑 API

已登录的授权用户以及特定游戏的制造商将能够使用editAPI 编辑该游戏的详细信息。

路线

在后端,我们将添加一个PUT路由,允许授权用户编辑他们的游戏之一。

mern-vrgame/server/routes/game.routes.js

router.route('/api/games/:gameId')
    .put(authCtrl.requireSignin, gameCtrl.isMaker, gameCtrl.update)

'/api/games/:gameId'的 PUT 请求将首先执行gameByID控制器方法,以检索特定游戏的详细信息。还将调用requireSigninauth controller 方法以确保当前用户已登录。然后isMaker控制器方法将在最终运行游戏update控制器方法修改数据库中的游戏之前,确定当前用户是否是该特定游戏的制作者。

控制器

isMaker控制器方法确保登录用户实际上是正在编辑的游戏的制作者。

mern-vrgame/server/controllers/game.controller.js

const isMaker = (req, res, next) => {
  let isMaker = req.game && req.auth && req.game.maker._id == req.auth._id
  if(!isMaker){
    return res.status('403').json({
      error: "User is not authorized"
    })
  }
  next()
}

游戏控制器中的update方法将现有游戏细节和请求主体中接收到的表单数据合并更改,并将更新后的游戏保存到数据库中的游戏集合中。

mern-vrgame/server/controllers/game.controller.js

const update = (req, res) => {
  let game = req.game
  game = _.extend(game, req.body)
  game.updated = Date.now()
  game.save((err) => {
    if(err) {
      return res.status(400).send({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.json(game)
  })
}

取来

在视图中使用fetch方法调用editAPI,该方法获取表单数据并将其与请求一起发送到后端以及用户凭据。

mern-vrgame/client/game/api-game.js

const update = (params, credentials, game) => {
  return fetch('/api/games/' + params.gameId, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    },
    body: JSON.stringify(game)
  }).then((response) => {
    return response.json() 
  }).catch((err) => {
    console.log(err) 
  }) 
}

删除 API

经过身份验证和授权的用户将能够删除他们使用delete游戏 API 在应用上制作的任何游戏。

路线

在后端,我们将添加一条DELETE路线,允许授权制造商删除他们自己的一款游戏。

mern-vrgame/server/routes/game.routes.js

router.route('/api/games/:gameId')
    .delete(authCtrl.requireSignin, gameCtrl.isMaker, gameCtrl.remove)

'api/games/:gameId'接收到删除请求后,服务器上控制器方法的执行流程类似于编辑 API,最后调用的是remove控制器方法,而不是update

控制器

remove控制器方法在'/api/games/:gameId'收到删除请求且已验证当前用户是给定游戏的原始制作者时,从数据库中删除指定游戏。

mern-vrgame/server/controllers/game.controller.js

const remove = (req, res) => {
  let game = req.game
  game.remove((err, deletedGame) => {
    if(err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.json(deletedGame)
  })
}

取来

我们将在api-game.js中添加相应的remove方法,对删除 API 发出delete取数请求。

mern-vrgame/client/game/api-game.js

const remove = (params, credentials) => {
  return fetch('/api/games/' + params.gameId, {
    method: 'DELETE',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    }
  }).then((response) => {
    return response.json() 
  }).catch((err) => {
    console.log(err) 
  }) 
}

有了这些游戏 API,我们可以为应用构建 React 视图,还可以更新 React 360 游戏视图代码以获取和呈现动态游戏细节。

创建和编辑游戏

在 MERN VR Game 上注册的用户将能够制作新游戏并在应用中修改这些游戏。我们将添加 React 组件,允许用户修改每个游戏的游戏细节和 VR 对象细节。

制作新游戏

当用户登录到应用中时,他们将在菜单上看到一个 MAKE GAME 链接,该链接将引导他们到NewGame组件,该组件包含创建新游戏的表单。

更新菜单

我们将更新导航菜单以添加“制作游戏”按钮,如以下屏幕截图所示:

Menu组件中,我们将把Link添加到NewGame组件的路由中,就在 MY PROFILE 链接之前,在仅当用户通过身份验证时才呈现的部分中。

mern-vrgame/client/core/Menu.js

<Link to="/game/new">
   <Button style={isActive(history, "/game/new")}>
       <AddBoxIcon color="secondary"/> Make Game
   </Button>
</Link>

新游戏组件

NewGame组件使用GameForm组件呈现用户将填写的表单元素,以创建新游戏:

GameForm包含所有表单字段,它采用用户提交表单时应执行的onSubmit方法,作为NewGame组件的道具,以及任何服务器返回的错误消息。

mern-vrgame/client/game/NewGame.js

<GameForm onSubmit={this.clickSubmit} errorMsg={this.state.error}/>

clickSubmit方法使用api-game.js中的创建fetch方法向createAPI 发出 POST 请求,请求中包含游戏表单数据和用户详细信息。

mern-vrgame/client/game/NewGame.js

  clickSubmit = game => event => {
    const jwt = auth.isAuthenticated() 
    create({
      userId: jwt.user._id
    }, {
      t: jwt.token
    }, game).then((data) => {
      if (data.error) {
        this.setState({error: data.error}) 
      } else {
        this.setState({error: '', redirect: true}) 
      }
    }) 
  }

我们将在MainRouter中添加一个PrivateRoute,这样NewGame组件将以/game/new路径加载到浏览器中。

mern-vrgame/client/MainRouter.js

<PrivateRoute path="/game/new" component={NewGame}/>

编辑游戏

用户将能够使用EditGame组件编辑他们制作的游戏,该组件将呈现预先填充了现有游戏详细信息的游戏表单字段。

编辑游戏组件

就像在NewGame组件中一样,EditGame组件也会使用GameForm组件来渲染表单元素,但这次字段会显示游戏字段的当前值,用户可以更新这些值:

EditGame组件的情况下,GameForm将以给定游戏的 ID 作为道具,以便除onSubmit方法和服务器生成的错误消息(如果有)外,还可以获取游戏详细信息。

mern-vrgame/client/game/EditGame.js

<GameForm gameId={this.match.params.gameId} onSubmit={this.clickSubmit} errorMsg={this.state.error}/>

编辑表单的clickSubmit方法将使用api-game.js中的update获取方法向编辑 API 发出 PUT 请求,请求中包含表单数据和用户详细信息。

mern-vrgame/client/game/EditGame.js

clickSubmit = game => event => {
    const jwt = auth.isAuthenticated() 
    update({
      gameId: this.match.params.gameId
    }, {
      t: jwt.token
    }, game).then((data) => {
      if (data.error) {
        this.setState({error: data.error}) 
      } else {
        this.setState({error: '', redirect: true}) 
      }
    }) 
  }

EditGame组件将以MainRouter中的PrivateRoute中声明的/game/edit/:gameId路径加载到浏览器中。

mern-vrgame/client/MainRouter.js

<PrivateRoute path="/game/edit/:gameId" component={EditGame}/>

游戏形式组件

NewGameEditGame组件中使用的GameForm组件包含允许用户输入单个游戏的游戏详细信息和 VR 对象详细信息的元素。它可以从空白游戏对象开始,也可以在componentDidMount中加载现有游戏。

mern-vrgame/client/game/GameForm.js

state = {
    game: {name: '', clue:'', world:'', answerObjects:[], wrongObjects:[]},
    redirect: false,
    readError: ''
  }

如果GameForm组件从父组件(如EditGame组件)接收到gameId道具,则它将使用读取 API 检索游戏的详细信息,并将其设置为要在表单视图中呈现的状态。

mern-vrgame/client/game/GameForm.js

componentDidMount = () => {
    if(this.props.gameId){
      read({gameId: this.props.gameId}).then((data) => {
        if (data.error) {
          this.setState({readError: data.error}) 
        } else {
          this.setState({game: data}) 
        }
      }) 
    }
}

GameForm组件中的表单视图基本上有两个部分,一部分以简单的游戏细节(如名称、世界图像链接和线索文本)作为输入,另一部分允许用户向应答对象数组或错误对象数组添加可变数量的 VR 对象。

输入简单的游戏细节

简单的游戏细节部分主要是使用 Material UITextField组件添加的文本输入,并将更改处理方法传递给onChange

表格标题

表单标题将为New GameEdit Game,具体取决于现有游戏 ID 是否作为道具传递给GameForm

mern-vrgame/client/game/GameForm.js

<Typography type="headline" component="h2">
    {this.props.gameId? 'Edit': 'New'} Game
</Typography>

游戏世界形象

我们将在最顶部的img元素中呈现背景图像 URL,以向用户显示他们作为游戏世界图像 URL 添加的图像。

mern-vrgame/client/game/GameForm.js

<img src={this.state.game.world}/>
<TextField id="world" label="Game World Equirectangular Image (URL)" 
value={this.state.game.world} onChange={this.handleChange('world')}/>

游戏名称

游戏名称将添加到默认类型为text的单个TextField中。

mern-vrgame/client/game/GameForm.js

<TextField id="name" label="Name" value={this.state.game.name} onChange={this.handleChange('name')}/>

线索文本

线索文本将添加到多行TextField组件中。

mern-vrgame/client/game/GameForm.js

<TextField id="multiline-flexible" label="Clue Text" multiline rows="2" value={this.state.game.clue} onChange={this.handleChange('clue')}/>

处理输入

所有输入更改将通过handleChange方法处理,该方法将使用用户输入更新处于状态的游戏值。

mern-vrgame/client/game/GameForm.js

handleChange = name => event => {
    const newGame = this.state.game 
    newGame[name] = event.target.value 
    this.setState({game: newGame}) 
}

修改 VR 对象的数组

为了允许用户修改他们希望添加到 VR 游戏中的answerObjectswrongObjects数组,GameForm将迭代每个数组,并为每个对象呈现一个VRObjectForm组件。这样,就可以从GameForm组件中添加、删除和修改 VR 对象:

迭代并呈现对象详细信息表单

使用 Material UIExpansionPanel组件,我们将添加前面看到的表单界面,为给定游戏中的每种类型的 VR 对象数组创建一个可修改的 VR 对象细节数组。

ExpansionPanelDetails组件内部,我们将迭代answerObjects数组或wrongObjects数组,为每个 VR 对象渲染一个VRObjectForm组件。

mern-vrgame/client/game/GameForm.js

<ExpansionPanel>
   <ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>}>
      <Typography>VR Objects to collect</Typography>
   </ExpansionPanelSummary>
   <ExpansionPanelDetails>{
      this.state.game.answerObjects.map((item, i) => {
 return <div key={i}>
                  <VRObjectForm index={i} type={'answerObjects'}
 vrObject={item}
 handleUpdate={this.handleObjectChange} 
 removeObject={this.removeObject}/>
               </div> })}
      <Button color="primary" variant="raised" onClick={this.addObject('answerObjects')}>
          <AddBoxIcon color="secondary"/> Add Object
      </Button>
   </ExpansionPanelDetails>
</ExpansionPanel>

每个VRObjectForm将以vrObject本身、数组中当前的index、对象数组的类型以及通过更改细节或删除VRObjectForm组件中的对象来修改数组细节时更新GameForm中状态的两种方法作为道具。

向数组中添加新对象

添加对象按钮将允许用户添加新的VRObjectForm组件,以获取新 VR 对象的详细信息。

mern-vrgame/client/game/GameForm.js

addObject = name => event => {
    const newGame = this.state.game 
    newGame[name].push({}) 
    this.setState({game: newGame}) 
} 

这基本上只需向正在迭代的数组中添加一个空对象,并使用 name 值中指定的数组类型调用addObject方法。

从数组中删除对象

每个VRObjectForm组件也可以被删除,以从给定数组中移除对象。GameForm会将removeObject方法作为道具传递给VRObjectForm组件,这样当用户点击delete特定VRObjectForm时,该数组可以在状态下更新。

mern-vrgame/client/game/GameForm.js

removeObject = (type, index) => event => {
    const newGame = this.state.game 
    newGame[type].splice(index, 1) 
    this.setState({game: newGame}) 
}

通过从名称中指定数组类型的数组中按给定的index进行切片,将对象从数组中移除。

处理对象细节更改

当用户更改任何VRObjectForm字段中的输入值时,VR 对象详细信息将在GameForm组件状态下更新。要注册此更新,GameFormhandleObjectChange方法传递给VRObjectForm组件。

mern-vrgame/client/game/GameForm.js

handleObjectChange = (index, type, name, val) => {
    var newGame = this.state.game 
    newGame[type][index][name] = val 
    this.setState({game: newGame}) 
}

handleObjectChange方法用给定的type更新数组中index处的特定对象的字段值,从而反映在GameForm状态下存储的游戏对象中。

VRObjectForm 组件

VRObjectForm组件将呈现输入字段,以修改单个 VR 对象的详细信息,并将其添加到GameForm组件中游戏的answerObjectswrongObjects数组中:

可以从空白 VR 对象开始,也可以在componentDidMount中加载现有 VR 对象的详细信息。

mern-vrgame/client/game/VRObjectForm.js

state = {
      objUrl: '', mtlUrl: '',
      translateX: 0, translateY: 0, translateZ: 0, 
      rotateX: 0, rotateY: 0, rotateZ: 0,
      scale: 1, color:'white'
} 

componentDidMount中,状态将设置为GameForm组件作为道具传递的vrObject的详细信息。

mern-vrgame/client/game/VRObjectForm.js

componentDidMount = () => {
    if(this.props.vrObject && 
    Object.keys(this.props.vrObject).length != 0){
        const vrObject = this.props.vrObject 
        this.setState({
          objUrl: vrObject.objUrl,
          mtlUrl: vrObject.mtlUrl,
          translateX: Number(vrObject.translateX),
          translateY: Number(vrObject.translateY),
          translateZ: Number(vrObject.translateZ),
          rotateX: Number(vrObject.rotateX),
          rotateY: Number(vrObject.rotateY),
          rotateZ: Number(vrObject.rotateZ),
          scale: Number(vrObject.scale),
          color:vrObject.color
        }) 
    }
}

修改这些值的输入字段将使用物料 UITextField组件添加。

三维对象文件输入

将使用TextField组件为每个 VR 对象添加 OBJ 和 MTL 文件链接,作为文本输入。

mern-vrgame/client/game/VRObjectForm.js

<TextField
    id="obj"
    label=".obj url"
    value={this.state.objUrl}
    onChange={this.handleChange('objUrl')}
/><br/>
<TextField
    id="mtl"
    label=".mtl url"
    value={this.state.mtlUrl}
    onChange={this.handleChange('mtlUrl')}
/>

翻译值输入

VR 对象在 X、Y 和 Z 轴上的平移值将输入到number类型的TextField组件中。

mern-vrgame/client/game/VRObjectForm.js

<TextField
    value={this.state.translateX}
    label="TranslateX"
    onChange={this.handleChange('translateX')}
    type="number"
/>
<TextField
    value={this.state.translateY}
    label="TranslateY"
    onChange={this.handleChange( 'translateY')}
    type="number"
/>
<TextField
    value={this.state.translateZ}
    label="TranslateZ"
    onChange={this.handleChange('translateZ')}
    type="number"
/>

旋转值输入

围绕 X、Y 和 Z 轴的 VR 对象的rotate值将输入到number类型的TextField组件中。

mern-vrgame/client/game/VRObjectForm.js

<TextField
    value={this.state.rotateX}
    label="RotateX"
    onChange={this.handleChange('rotateX')}
    type="number"
/>
<TextField
    value={this.state.rotateY}
    label="RotateY"
    onChange={this.handleChange('rotateY')}
    type="number"
/>
<TextField
    value={this.state.rotateZ}
    label="RotateZ"
    onChange={this.handleChange('rotateZ')}
    type="number"
/>

刻度值输入

VR 对象的scale值将输入number类型的TextField组件中。

mern-vrgame/client/game/VRObjectForm.js

<TextField
    value={this.state.scale}
    label="Scale"
    onChange={this.handleChange('scale')}
    type="number"
/>

对象颜色输入

VR 对象的颜色值将输入到text类型的TextField组件中:

mern-vrgame/client/game/VRObjectForm.js

<TextField
    value={this.state.color}
    label="Color"
    onChange={this.handleChange('color')}
/>

删除对象按钮

VRObjectForm将包含一个Delete按钮,该按钮将执行GameForm道具表单中接收到的removeObject方法:

mern-vrgame/client/game/VRObjectForm.js

<Button onClick={this.props.removeObject(this.props.type, this.props.index)}>
     <Icon>cancel</Icon> Delete
</Button>

removeObject方法将获取对象数组类型和数组索引位置的值,以在GameForm状态下从相关 VR 对象数组中移除给定对象。

处理输入更改

当输入字段中的任何 VR 对象详细信息发生更改时,handleChange方法将更新VRObjectForm组件的状态,并使用GameForm作为道具传递的handleUpdate方法将GameForm状态中的 VR 对象更新为对象详细信息的更改值。

mern-vrgame/client/game/VRObjectForm.js

handleChange = name => event => {
    this.setState({[name]: event.target.value}) 
    this.props.handleUpdate(this.props.index, 
                            this.props.type, 
                            name, 
                            event.target.value) 
}

有了这个实现,创建和编辑游戏表单就就位了,并为不同大小的数组提供了 VR 对象输入表单。任何注册用户都可以使用这些表单在 MERN VR 游戏应用上添加和编辑游戏。

游戏列表视图

MERN VR 游戏的访问者将从主页上的列表和个人用户配置文件中访问应用上的游戏。主页将列出应用上的所有游戏,特定制造商的游戏将列在其用户配置文件页面上。列表视图将迭代使用listAPI 获取的游戏数据,并在GameDetail组件中呈现每个游戏的详细信息。

所有游戏

当组件挂载时,Home组件将使用列表 API 获取游戏集合中所有游戏的列表。

mern-vrgame/client/core/Home.js

componentDidMount = () => {
    list().then((data) => {
      if (data.error) {
        console.log(data.error) 
      } else {
        this.setState({games: data}) 
      }
    })
}

从服务器检索到的游戏列表将被设置为状态,并迭代以呈现列表中每个游戏的GameDetail组件。

mern-vrgame/client/core/Home.js

{this.state.games.map((game, i) => {
     return <GameDetail key={i} game={game} updateGames={this.updateGames}/>
})}

GameDetail组件将被传递游戏细节和updateGames方法。

mern-vrgame/client/core/Home.js

updateGames = (game) => {
    const updatedGames = this.state.games 
    const index = updatedGames.indexOf(game) 
    updatedGames.splice(index, 1) 
    this.setState({games: updatedGames}) 
}

当用户从使用游戏制作者的editdelete选项呈现的GameDetail组件中删除游戏时,updateGames方法将更新Home组件中的列表:

制造商的游戏

用户Profile组件将通过 maker API 获取给定用户制作的游戏列表。在检索到用户详细信息后,我们将更新Profile组件中的init方法来调用listByMaker获取方法。

mern-vrgame/client/user/Profile.js

  init = (userId) => {
    const jwt = auth.isAuthenticated() 
    read({
      userId: userId
    }, {t: jwt.token}).then((data) => {
      if (data.error) {
        this.setState({redirectToSignin: true}) 
      } else {
        this.setState({user: data}) 
 listByMaker({userId: data._id}).then((data) => {
 if (data.error) {
 console.log(data.error) 
 } else {
 this.setState({games: data}) 
 }
 })
      }
    }) 
  }

类似于在Home组件中呈现游戏列表的方式,我们将在Profile组件中将从服务器检索到的游戏列表设置为状态,并在视图中对其进行迭代以呈现GameDetail组件,该组件将被传递单个游戏细节和updateGames方法。

mern-vrgame/client/user/Profile.js

{this.state.games.map((game, i) => {
    return <GameDetail key={i} game={game} updateGames={this.updateGames}/>
})}

这将为特定用户制作的每个游戏呈现一个GameDetail组件:

游戏细节组件

GameDetail组件将游戏对象作为道具,渲染游戏细节,以及链接到 VR 游戏视图的“玩游戏”按钮。如果当前用户是游戏的制作者,它还会显示editdelete按钮:

游戏详情

游戏的详细信息,如名称、世界图像、线索文本和制造商名称,将呈现给用户游戏的概览。

mern-vrgame/client/game/GameDetail.js

<Typography type="headline" component="h2">
     {this.props.game.name}
</Typography>
<CardMedia image={this.props.game.world} 
           title={this.props.game.name}/>
<Typography type="subheading" component="h4">
     <em>by</em>
     {this.props.game.maker.name}
</Typography>
<CardContent>
     <Typography type="body1" component="p">
          {this.props.game.clue}
     </Typography>
</CardContent>

玩游戏按钮

GameDetail组件中的Play Game按钮将只是一个Link组件,指向打开index.html生成的 React 360 的路由(服务器上该路由的实现在玩 VR 游戏部分讨论)。

mern-vrgame/client/game/GameDetail.js

<Link to={"/game/play?id=" + this.props.game._id} target='_self'>
   <Button variant="raised" color="secondary">
      Play Game
   </Button>
</Link>

到游戏视图的路由将游戏 ID 作为一个query参数。我们在Link上设置了target='_self',所以 React 路由跳过转换到下一个状态,并让浏览器处理此链接。这将允许浏览器直接在此路由上发出请求,并呈现服务器响应此请求发送的index.html文件。

编辑和删除按钮

只有当前登录的用户也是正在呈现的游戏的制作者时,GameDetail组件才会显示editdelete选项。

mern-vrgame/client/game/GameDetail.js

{auth.isAuthenticated().user 
    && auth.isAuthenticated().user._id == this.props.game.maker._id && 
    (<div>
       <Link to={"/game/edit/" + this.props.game._id}>
          <Button variant="raised" color="primary" 
         className={classes.editbutton}>
              Edit
          </Button>
       </Link>
       <DeleteGame game={this.props.game} 
       removeGame={this.props.updateGames}/>
    </div>)}

如果登录用户的用户 ID 与游戏中的 maker ID 匹配,则视图中会显示链接到编辑表单视图的edit按钮和DeleteGame组件。

删除游戏

登录用户可以通过点击GameDetail组件中制造商可见的delete按钮删除他们制作的特定游戏。GameDetail组件使用DeleteGame组件添加此delete选项。

删除游戏组件

为每个游戏添加到GameDetail组件的DeleteGame组件将游戏细节和removeGame方法作为GameDetail的道具,更新GameDetail所属的父组件。

mern-vrgame/client/game/GameDetail.js

<DeleteGame game={this.props.game} removeGame={this.props.updateGames}/>

这个DeleteGame组件基本上是一个按钮,点击后会打开一个确认对话框,询问用户是否确定要删除游戏:

该对话框使用 Material UI 中的Dialog组件实现。

mern-vrgame/client/game/DeleteGame.js

<Button variant="raised" onClick={this.clickButton}>
   Delete
</Button>
<Dialog open={this.state.open} onClose={this.handleRequestClose}>
   <DialogTitle>{"Delete "+this.props.game.name}</DialogTitle>
   <DialogContent>
      <DialogContentText>
         Confirm to delete your game {this.props.game.name}.
      </DialogContentText>
   </DialogContent>
   <DialogActions>
      <Button onClick={this.handleRequestClose} color="primary">
         Cancel
      </Button>
      <Button onClick={this.deleteGame} color="secondary" 
      autoFocus="autoFocus">
         Confirm
      </Button>
   </DialogActions>
</Dialog>

成功删除后,对话框关闭,并通过调用作为道具传入的removeGame方法更新包含GameDetail组件的父组件。

mern-vrgame/client/game/DeleteGame.js

deleteGame = () => {
    const jwt = auth.isAuthenticated() 
    remove({
      gameId: this.props.game._id
    }, {t: jwt.token}).then((data) => {
      if (data.error) {
        console.log(data.error) 
      } else {
        this.props.removeGame(this.props.game) 
        this.setState({open: false}) 
      }
    }) 
  }

deleteGame处理程序方法中调用的removeGame方法更新父级的状态,可以是Home组件或用户Profile组件,因此删除的游戏不再显示在视图中。

玩虚拟现实游戏

MERN VR 游戏的用户将能够在应用中打开和玩任何游戏。为了实现这一点,我们将在服务器上设置一条路由,在响应以下路径的 GET 请求时呈现由 React 360 生成的index.html

/game/play?id=<game ID>

该路径将游戏 ID 值作为一个query参数,该参数在 React 360 代码中用于通过读取 API 获取游戏详细信息。

渲染虚拟现实游戏视图的 API

打开 React 360index.html页面的 GET 请求将在game.routes.js中声明,如下所示。

mern-vrgame/server/routes/game.routes.js

router.route('/game/play')
  .get(gameCtrl.playGame)

这将执行playGame控制器方法返回index.html页面,以响应传入请求。

mern-vrgame/server/controllers/game.controller.js

const playGame = (req, res) => {
  res.sendFile(process.cwd()+'/server/vr/index.html')
}

playGame控制器方法将/server/vr/文件夹中的index.html发送给请求客户端。

在浏览器中,这将呈现 React 360 游戏代码,该代码将使用 read API 从数据库中获取游戏详细信息,并呈现游戏世界以及用户可以交互的 VR 对象。

更新 React 360 中的游戏代码

在 MERN 应用中设置好游戏后端后,我们可以更新第 10 章开发基于 Web 的虚拟现实游戏中开发的 React 360 项目代码,使其直接从数据库中的游戏集合中渲染游戏。

我们将在打开 React 360 应用的链接中使用游戏 ID,从 React 360 代码中通过读取 API 获取游戏细节,然后将数据设置为状态,以便游戏加载从数据库检索到的细节,而不是我们在第 10 章开发基于 Web 的 VR 游戏时使用的静态样本数据

代码更新后,我们可以再次将其捆绑,并将编译后的文件放入 MERN 应用中。

从链接获取游戏 ID

在 React 360 项目文件夹的index.js文件中,更新componentDidMount方法,从传入 URL 检索游戏 ID,并对读取的游戏 API 进行获取调用。

/MERNVR/index.js

componentDidMount = () => {
    let gameId = Location.search.split('?id=')[1]
    read({
          gameId: gameId
      }).then((data) => {
        if (data.error) {
          this.setState({error: data.error});
        } else {
          this.setState({
            vrObjects: data.answerObjects.concat(data.wrongObjects),
            game: data
          });
          Environment.setBackgroundImage(
            {uri: data.world}
          )
        }
    })
}

Location.search允许我们访问加载index.html的传入 URL 中的查询字符串。检索到的查询字符串为split,用于从 URL 中附加的id查询参数中获取游戏 ID 值。我们需要这个游戏 ID 值来通过读取 API 从服务器获取游戏详细信息,并将其设置为游戏状态和vrObjects值。

使用读取 API 获取游戏数据

在 React 360 项目文件夹中,我们将添加一个api-game.js文件,该文件将包含一个读取fetch方法,该方法使用提供的游戏 ID 在服务器上调用读取游戏 API。

/MERNVR/api-game.js

const read = (params) => {
  return fetch('/api/game/' + params.gameId, {
    method: 'GET'
  }).then((response) => {
    return response.json() 
  }).catch((err) => console.log(err)) 
}
export {
  read
} 

此获取方法用于 React 360 entry 组件的componentDidMount中,以获取游戏详细信息。

This updated React 360 code is available in the branch named 'dynamic-game' on the GitHub repository at: github.com/shamahoque/MERNVR.

捆绑和集成更新的代码

通过更新 React 360 代码以从服务器动态获取和呈现游戏细节,我们可以使用提供的捆绑脚本捆绑这些代码,并将新编译的文件放在 MERN VR 游戏项目目录的dist文件夹中。

要从命令行绑定 React 360 代码,请转到 React 360MERNVR项目文件夹并运行:

npm run bundle

这将在build/文件夹中生成具有更新代码的client.bundle.jsindex.bundle.js捆绑文件。这些文件以及index.htmlstatic_assets文件夹需要添加到 MERN VR 游戏应用代码中,如第 10 章开发基于 Web 的 VR 游戏所述,以集成最新的 VR 游戏代码。

完成此集成后,如果我们运行 MERN VR 游戏应用,并单击任何游戏上的 Play Game 链接,它将打开游戏视图,其中包含在 VR 场景中渲染的特定游戏的详细信息,并允许与游戏中指定的 VR 对象交互。

总结

在本章中,我们将 MERN stack 技术的功能与 React 360 集成,以开发一个用于 web 的动态 VR 游戏应用。

我们扩展了 MERN skeleton 应用,以构建一个存储 VR 游戏详细信息的工作后端。并允许我们调用 API 来处理这些细节。我们添加了 React 视图,允许用户修改游戏和浏览游戏,并选择在服务器直接渲染的指定路径上启动和玩 VR 游戏。

最后,我们更新了 React 360 项目代码,通过从传入 URL 检索查询参数,并使用 fetch 通过游戏 API 检索数据,在 MERN 应用和 VR 游戏视图之间传递数据。

React 360 代码与 MERN stack 应用的集成产生了一个功能全面、动态的基于 web 的 VR 游戏应用,展示了如何使用和扩展 MERN stack 技术,以创造独特的用户体验

在下一章中,我们将回顾本书中构建的 MERN 应用,不仅讨论遵循的最佳实践,还将讨论改进和进一步开发的范围。