2017 年 10 月
第 32 卷,第 10 期
此文章由机器翻译
游戏开发 - Web 游戏开发之多人游戏网络物理
通过Gary Weiss |自 2017 年 10 月
Web 游戏具有 shoddy 信誉。编写良好的游戏是可用在高端的游戏控制台上以及为装有现代图形处理器的计算机。即使移动游戏具有讲了很多以来规范"蛇。" 但 Web 游戏,如罗德 Dangerfield 获得没有尊重。现代浏览器中的高级功能所述和最新也可能会向浏览器将质量游戏体验。在这方面的最重要技术是 WebGL,这将向硬件加速图形提供直接访问。
在本文中,我将调查 Web 从特定的角度来看的游戏: 简单联网物理游戏的实现。本练习调查哪些技术和游戏开发工具均可用于的任务和压力测试网络性能、 计算效率和呈现的现代浏览器。这篇文章可被视为小调查属于多更大的主题:可以专业人员的游戏 studio 会将高端游戏真正写在浏览器?
游戏的源代码位于自由bit.ly/2toc4h4,且游戏位于可播放联机 sprocketleague.lance.gg。
游戏
我将模型的游戏涉及在体育场中解决驱动的汽车。在可以退回到目标体育场中没有大球。这一概念是由"火箭联赛,"常用 e 体育标题由 Psyonix 激发。因此,从下列对象汇集游戏: (包括目标) 的领域,汽车,一个球,作为你可以查看在图 1。播放器可以移动 car 对象向前或按相反的顺序,并打开轮。
图 1 汽车在浏览器中播放足球
游戏的体系结构
游戏实现使用权威服务器模型,因此每个客户端将转发到中央服务器的用户输入。客户端不会等待网络在应用的输入本地; 之前相反,客户端将执行什么通常称为客户端预测。此方法允许客户端继续运行的游戏逻辑和物理游戏。Catch,当然,是,当来自权威服务器会传播到的位置和速度的更新,每个客户端必须进行自己增量更正,以便游戏保留平滑的视觉体验。
呈现中呈现的主要目标访问 GPU。有几种工具,可以通过简化 WebGL 的接口来填充此槽。Babylon.js 和 Three.js 已用于很多高级 Web 游戏。为此游戏中,我将使用 A-Frame,相对较新的库,您可以生成三维场景使用 HTML 元素。A-Frame 定义新的 HTML 元素的可用作构建基块。此外,A-Frame 很好地建模使用实体组件系统设计模式的对象。A-Frame 的另一个有趣的优点是,它已设计为虚拟现实 (VR),因此可以轻松地进行游戏到 VR 体验。
物理Cannon.js 是一个相对较能够轻松使用物理引擎。它提供包含冲突检测的刚性正文机制。它已从头开始编写在 JavaScript 中,这意味着库是比 transpiled 引擎,如 ammo.js 小得多。
对象序列化和网络广播对象的位置和速度的服务器,游戏对象需要序列化。为此目的使用 Lance 游戏服务器 (Lance.gg) 开源多玩家引擎。将序列化派生 Lance GameObject 基类的游戏对象,并将其转发到客户端。高级游戏引擎为提高性能,使用 UDP 套接字,而 Lance 仍使用 TCP 套接字。这是因为 WebRTC (而不是对等对等) 的客户端服务器通信的复杂性。这是其中 Web 游戏显著滞后后面其他平台的一个区域。但是,在我的经验,生成游戏提供顺畅的体验,甚至是在网络延迟的 150ms,这是典型洲美国或欧洲中。
客户端预测在这里,太,我依赖 Lance 用于执行的位置和速度的推断。Lance 实现服务器广播的客户端的集合,计算每个位置和方向,必需的更正,并将更正以增量方式应用通过呈现循环的进度。位置更正应用计算向量差异,并将这种差异划分为增量。方向更正的内容与此相反,应用计算的四元数的差异。更正四元数qΔ服务器四元数的qs和一个客户端的四元数qc给定qΔ = qs o qc-1。此四元数可以表示为有关单个轴的旋转,然后划分为有关该轴的增量。
游戏体系结构,然后,包含的逻辑服务器、 客户端逻辑和共享的逻辑中所示图 2。客户端逻辑必须处理涉及用户,包括输入和呈现工作的集合的所有方面。服务器逻辑是通过比较而言,更简单,但它需要执行一些权威的步骤,如接受连接,创建游戏的对象,并决定谁评分。
图 2 多玩家游戏的体系结构
共享的逻辑是最有趣的实现部分,而在一个单独的部分中讨论。它包括主游戏逻辑,必须运行在权威服务器上,但还必须运行在每个客户端上之间 server 广播随着游戏。让我们开始多玩家游戏的具体细节: 通信层。
客户端服务器通信
权威服务器的多玩家游戏需要通信代码。此代码具有许多职责。 最初,它必须管理的 TCP/IP 端口上侦听传入数据的服务器的自带向上。接下来,没有握手过程时客户端连接到服务器。此握手注册新的客户端在服务器上,并建立专用的套接字客户端。身份验证或婚介进程,也可能需要某些游戏。新的客户端,连接和断开连接的现有客户端,会影响游戏。需要使其可以为新的客户端创建新的汽车将玩家分配给团队获悉游戏。
游戏执行,同时,服务器将发送给所有客户端定期广播。广播描述游戏所需的当前状态。状态包括位置、 速度、 方向 (四元数) 和角速度的每个游戏的对象。此外,游戏对象也可能具有其他非物理属性,如强度、 盾牌 power、 拼写超时等。在这里,服务器需要很高,并尽可能少的数据为是绝对必需发送。例如,不应将广播尚未移动或更改的对象。另一方面,如果刚连接新玩家,玩家需要所有游戏的对象以启动游戏的状态的完整说明。
最后,客户端服务器通信将需要发出并捕获的带事件,用于描述游戏不适合作为正常游戏进度的一部分的事件。例如,服务器可能需要传递给一个特定的玩家他已达到一个特殊的进步。此机制还可用来向所有玩家报告一条消息。
以下是其实现。在服务器端中,我将设置 express 的 node.js 服务器和 socket.io 服务了。中显示的代码图 3配置 HTTP 服务器在端口 3000,初始化 server 引擎和游戏引擎类,并将所有 HTTP 请求都路由到 dist 子目录放置所有 HTML 和资产。这意味着 node.js 服务器有两个角色。在第一个角色中,它作为服务 HTML 内容,包括 CSS 文件、 图像、 音频和 GLTF 格式中的三维模型的 HTML 服务器。在第二个角色中,HTTP 服务器充当游戏接受的服务器,socket.io 的传入连接。请注意,最佳做法是缓存实现良好性能的内容交付网络 (CDN) 后面的所有静态资产。此配置不在中的示例中显示图 3,但使用 CDN 大大提高了游戏提出性能,同时减少不必要的负载中的游戏的服务器。最后,在最后一行代码中,启动游戏。
图 3 服务器入口点
const express = require('express');
const socketIO = require('socket.io');
// Constants
const PORT = process.env.PORT || 3000;
const INDEX = path.join(__dirname, './dist/index.html');
// Network servers
const server = express();
const requestHandler = server.listen(PORT, () => console.log(`Listening on ${PORT}`));
const io = socketIO(requestHandler);
// Get game classes
const SLServerEngine = require('./src/server/SLServerEngine.js');
const SLGameEngine = require('./src/common/SLGameEngine.js');
// Create instances
const gameEngine = new SLGameEngine({ traceLevel: 0 });
const serverEngine =
new SLServerEngine(io, gameEngine, { debug: {}, updateRate: 6, timeoutInterval: 20 });
// HTTP routes
server.get('/', (req, res) => { res.sendFile(INDEX); });
server.use('/', express.static(path.join(__dirname, './dist/')));
// Start the game
serverEngine.start();
服务器已启动,但尚未处理新的连接。ServerEngine 基类声明处理程序方法的调用 onPlayerConnected 和相应 onPlayerDisconnected 处理程序的新连接。这是其中的权威服务器子类方法可以指示要创建新车或删除现有的汽车的游戏引擎的位置。中的代码段图 4从基类演示了如何使用 socket.io 以确保已连接新播放器时,将调用这些处理程序。
图 4 连接处理程序实现中使用 socket.io
class ServerEngine {
// The server engine constructor registers a listener on new connections
constructor(io, gameEngine, options) {
this.connectedPlayers = {};
io.on('connection', this.onPlayerConnected.bind(this));
}
// Handle new player connection
onPlayerConnected(socket) {
let that = this;
// Save player socket and state
this.connectedPlayers[socket.id] = {
socket: socket,
state: 'new'
};
let playerId = socket.playerId = ++this.gameEngine.world.playerCount;
// Save player join time, and emit a `playerJoined` event
socket.joinTime = (new Date()).getTime();
this.resetIdleTimeout(socket);
let playerEvent = { id: socket.id, playerId,
joinTime: socket.joinTime, disconnectTime: 0 };
this.gameEngine.emit('playerJoined', playerEvent);
socket.emit('playerJoined', playerEvent);
// Ensure a handler is called when the player disconnects
socket.on('disconnect', function() {
playerEvent.disconnectTime = (new Date()).getTime();
that.onPlayerDisconnected(socket.id, playerId);
that.gameEngine.emit('playerDisconnected', playerEvent);
});
}
// Every server step starts here
step() {
// Run the game engine step
this.gameEngine.step();
// Broadcast game state to all players
if (this.gameEngine.world.stepCount % this.options.updateRate === 0) {
for (let socketId of Object.keys(this.connectedPlayers)) {
this.connectedPlayers[socketId].socket.emit(
'worldUpdate', this.serializeUpdate());
}
}
}
}
当播放器连接时,此代码将两次发出 playerJoined 事件。游戏引擎事件在控制器上,其中的游戏代码的其他部分可能已注册到此特定事件的侦听器发出第一个事件。通过玩家的套接字发送第二个事件。在这种情况下,事件可以捕获在套接字执行,并作为客户端中,为服务器已允许了此玩家加入游戏的确认。
请注意 server 引擎 step 方法-在这里,游戏状态是广播到所有连接的播放器。此广播仅发生在固定的时间间隔。在游戏中,我选择配置的日程安排,以便每秒,执行 60 步骤中,每个第六个步骤中,会发送广播或 10 次每秒。
现在,因为它们适用于游戏具体而言,我可以子类化,请在实现方法。中所示图 5,此代码通过创建新车并将该汽车联接到蓝色团队或红色团队处理新的连接。播放器断开连接时,删除该玩家的汽车。
图 5 服务器连接处理
// Game-specific logic for player connections
onPlayerConnected(socket) {
super.onPlayerConnected(socket);
let makePlayerCar = (team) => {
this.gameEngine.makeCar(socket.playerId, team);
};
// Handle client restart requests
socket.on('requestRestart', makePlayerCar);
socket.on('keepAlive', () => { this.resetIdleTimeout(socket); });
}
// Game-specific logic for player dis-connections
onPlayerDisconnected(socketId, playerId) {
super.onPlayerDisconnected(socketId, playerId);
this.gameEngine.removeCar(playerId);
}
通信也可能发生带扩展事件必须有时发送到一个或多个客户端以异步方式的含义或数据必须发送的不是游戏对象状态的一部分。下面的示例演示如何在服务器端上使用 socket.io:
// ServerEngine: send the event monsterAttack! with data
// monsterData = {...} to all players
this.io.sockets.emit('monsterAttack!', monsterData);
// ServerEngine: send events to specific connected players
for (let socketId of Object.keys(this.connectedPlayers)) {
let player = this.connectedPlayers[socketId];
let playerId = player.socket.playerId;
let message = `hello player ${playerId}`;
player.socket.emit('secret', message);
}
在客户端侦听事件很简单:
this.socket.on('monsterAttack!', (e) => {
console.log(`run for your lives! ${e}`);
});
游戏逻辑
游戏的基本逻辑涉及到的游戏的状态,用户输入的应用程序中所示图 6。例如,按上汽油应该应用正向定向强制上一辆汽车。按刹车应将向后定向强制应用于一辆汽车,可能会将反向。启用控制轮适用角速度,依次类推。
图 6 GameEngine Step 方法
// A single Game Engine Step
step() {
super.step();
// Car physics
this.world.forEachObject((id, o) => {
if (o.class === Car) {
o.adjustCarMovement();
}
});
// Check if we have a goal
if (this.ball && this.arena) {
// Check for ball in Goal 1
if (this.arena.isObjInGoal1(this.ball)) {
this.ball.showExplosion();
this.resetBall();
this.metaData.teams.red.score++;
}
// Check for ball in Goal 2
if (this.arena.isObjInGoal2(this.ball)) {
this.ball.showExplosion();
this.resetBall();
this.metaData.teams.blue.score++;
}
}
}
随着时间的推移与物理引擎中,对调用实现这些操作。有趣的是,将正确的物理强制应用于一辆汽车提供较差的游戏体验。如果您应用正确的物理强制,就像它们在现实生活中的工作,处理和控件的汽车"看起来并不正确"玩家手中。产生的游戏操作速度太慢时。否则要愉快,我需要从非常低的速度加快 car 对象时应用人工 boost 游戏,游戏操作时只需太慢。
最后,游戏逻辑必须检查球是否传递目标文章中,并更新分数。
客户端预测
每个多玩家游戏具有不同的要求客户端预测,需要相应地配置。游戏的对象类型也可以具有特定配置。下面的代码演示游戏所需的配置的一小部分。用户输入的三个步骤或 50 毫秒,以更好地匹配的输入将应用于服务器的时间延迟。这可通过将 delayInputCount 设置为 3。syncOptions.sync 设置为"推测"若要启用客户端预测;localObjBending 设置为 0.6,,该值指示本地控制的对象位置应更正了 60%中,然后在下一步的服务器广播估计到达;remoteObjBending 设置更高版本,%到 80%,因为这些对象将更可能显著分离。最后,我设置 bendingIncrements 为 6,,该值指示,必须在一次,不会应用每个更正但而超过 6 增量为呈现循环:
const options = {
delayInputCount: 3,
syncOptions: {
sync: 'extrapolate',
localObjBending: 0.6,
remoteObjBending: 0.8,
bendingIncrements: 6
}
};
球具有其自己速度弯曲的参数 (未显示) 设置为零。这是字段的因为,当一个团队评分球获取 teleported 回中心,并将弯曲的生成速度的任何尝试会导致意外的视觉效果。
因此我介绍它尽可能通用伪代码中的,客户端预测逻辑本身是太大,无法在这里,包括图 7。它是秘方,这样可以多玩家游戏。第一步扫描最后一次服务器广播,并检查是否已创建任何新对象。它们正在同步到服务器状态之前,现有对象会记住其当前状态。第二步是 reenactment 阶段中,重新执行广播中所述的步骤以来已发生的所有步骤的游戏逻辑。输入需要重新应用,并且在 reenactment 模式下执行游戏引擎步骤。在第三个步骤中,每个对象记录所需的更正,并且将恢复为记忆状态。最后,在最后一个步骤中,可以删除标记为析构的对象。
图 7 简化客户端预测的伪代码
applySync(lastSync):
// Step 1: sync to all server objects in the broadcast
for each obj in lastSync.objects
localObj = getLocalObj(obj.id)
if localObj
localObj.rememberState()
localObj.syncTo(obj)
else
addLocalObj(obj)
// Step 2: re-enactment using latest state information
for step = lastSync.serverStep; step < clientStep; step++
processInputsForStep(step)
gameEngine.step(reenactment=true)
// Step 3: record bending and revert
for each obj in world.obects
obj.bendToCurrentState()
obj.revertToRememberedState()
// Step 4: remove destroyed objects
for each obj in world.obects
objSyncEvents = lastSync.getEvents(obj.id)
for each event in objSyncEvents
if event.name == ‘objectDestroy’
gameEngine.removeObjectFromWorld(obj.id)
在详细信息细节决定成败
没有在该游戏中多于第一次看到显示更多工作。我尚未粗略介绍了对跟踪 car 对象输入中移动设备和桌面 (移动设备中由倾斜设备控制汽车移动) 和主题类似婚介之间的差异和调试移动相机。所有这些主题不在本文的讨论范围。但是,务必提及其中一些需要 Web 游戏重要道路冲击。例如,可以运行 Web 游戏,在桌面上或平板电脑或移动设备上,因此有时 GPU 是可用,有时不是,时的低网络带宽增加的可能性越小。
还有另一个难题是从现有的标准生成兼容三维模型所需的工作流。Web 平台的游戏开发工具集不为成熟作为它们在其他平台上的对应项。最后,Web 游戏本地存储中没有其他平台提供的相同理解。因此必须格外小心 frugally 加载资产。此区域中的令人印象深刻成就是联机游戏"城市 Galaxy"(bit.ly/2uf3iWB)。
这些问题向在 Web 平台上的游戏开发人员提供新的挑战。但是,有一些明显的优势,到必须提到的 Web 平台。在浏览器上的调试环境是非常高级。Web 编程的动态特性导致了非常快原型制作和修补程序的持续交付。Web 开发人员社区是大得多。
可能 Web 游戏最明显的性能优势是在启动的方法。Web 游戏可以访问通过只需单击 Web 链接,而且不需要在开始之前预先下载客户端设备上的游戏资产的整个集。通过仅当在需要时,玩游戏体验可以开始在秒内,而不是分钟,但如有可能,脱机支持可以还利用来提高后续玩游戏,请下载资产。
例如,登录到 NBA 网站的访问者可能会发现自己针对从世界各地的播放器播放篮球比赛,在浏览器中右键。
游戏开发的 web 技术
在过去的几年中发生了重大改进在浏览器功能。主前进量是 WebGL,已将数据库设置直接访问为在浏览器中的 GPU 功能的开发。WebGL 是一个非常低级的接口,因此游戏开发人员通常依赖于如 Babylon.js、 three.js 和 A-Frame 生成复杂的情形的库中的更高级别的界面。
其他最新的 Web 技术包括 WebAudio,提供高级音频功能;媒体源扩展 (MSE),从而优化视频的访问,并且允许实时视频操作;和 WebVR,这是相对较新,并且提供包括 VR 耳机和 VR 控制器的虚拟现实 (VR) 硬件接口。使用与 WebAudio 一起 WebVR,可在 Web 驱动 VR 场景中实现 ambisonic 3D 音频体验。
即使 JavaScript 语言已更新有用方法与最新的 ECMA 262 第 6 版。JavaScript 生态系统是宽,为开发人员提供许多框架。事实上,有如此多的框架和充足有时会导致混淆的方法。
在网络前面,与 HTTP 协议一起 Websocket 和 WebRTC 是适用于应用程序的两个标准。Websocket 是简单的双向数据流,而 WebRTC 提供用于对等通信的更复杂的机制。对于多玩家游戏开发,Lance.gg 是一个开放源代码库,它处理的网络和游戏的对象的同步。
另一个有趣的技术是 WebAssembly,最近已达到跨浏览器共识,并且具有可能以通过运行传递极大的性能优势编译在浏览器中的二进制格式。
你可以轻松地找到有关所有这些技术的详细信息,在 Web 上找到。
总结
构建多玩家游戏 Web 平台是不同的。Web 自然地定向到广泛的设备和这存在于游戏开发一项重大挑战。在浏览器中的简单 UDP 网络缺少影响多玩家响应时间。但是,在 Web 技术使它成为可能的最新技术进步在相对较短时间内编写多玩家物理演示。库和在本练习中使用工作流正在快速进度,并已为当前采用。与其他资源和时间,则可以开发在浏览器的质量游戏体验。
我想要感谢讨论在文章中,Opher Vishnia 游戏合著者 Lance 和编程,设计和音乐组合处于多领域创建者与游戏开发,所涉及的共同创始人。
Gary Weiss是一种软件架构师和 Lance,开发开放源代码项目 Lance.gg 的组的共同创始人。与他联系。 garyweiss74@gmail.com
衷心感谢以下 Microsoft 技术专家对本文的审阅: Raanan Weber