案例研究:Building Racer
Racer 开发的基于网络的移动 Chrome Experiment 是由Active Theory 。 最多 5 位朋友可以连接他们的手机或平板电脑,在每个屏幕上进行比赛。 借助 Google Creative Lab 的概念、设计和原型以及 Plan8 的声音,我们在 I/O '13 上发布之前迭代了 8 周的构建。 既然游戏已经上线几周了,我们就有机会回答开发者社区关于游戏运作方式的一些问题。 以下是主要功能的细分以及我们最常被问到的问题的答案。
轨道
我们面临的一个相当明显的挑战是如何制作一款能够在各种设备上运行良好的基于网络的手机游戏。 玩家需要能够使用不同的手机和平板电脑进行比赛。 一名玩家可能拥有一台 Nexus 4 并想与他拥有 iPad 的朋友比赛。我们需要想出一种方法来确定每场比赛的通用赛道尺寸。该解决方案必须涉及使用不同尺寸的轨道,具体取决于比赛中包含的每个设备的规格。
计算轨道尺寸
当每个玩家加入时,有关他们设备的信息将发送到服务器并与其他玩家共享。 在建造轨道时,此数据用于计算轨道的高度和宽度。 我们通过找到最小屏幕的高度来计算高度,宽度是所有屏幕的总宽度。 因此在下面的示例中,轨道的宽度为 1152 像素,高度为 519 像素。
红色区域显示此示例中轨道的总宽度和高度。
this.getDimensions = function() {
var response = {};
response.width = 0;
response.height = _gamePlayers[0].scrn.h; //first screen height
response.screens = [];
for (var i = 0; i < _gamePlayers.length; i++) {
var player = _gamePlayers[i];
response.width += player.scrn.w;
if (player.scrn.h < response.height) {
//find the smallest screen height
response.height = player.scrn.h;
}
response.screens.push(player.scrn);
}
return response;
}
绘制轨道
Paper.js 是一个运行在 HTML5 Canvas 之上的开源矢量图形脚本框架。 我们发现 Paper.js 是为轨道创建矢量形状的完美工具,因此我们使用它的功能在 Canvas 元素上渲染在 Adobe Illustrator 中构建的 SVG 轨道。 为了创建轨道,TrackModel 类将 SVG 代码附加到 DOM 并收集有关要传递给 TrackPathView 的原始尺寸和定位的信息,后者将轨道绘制到画布上。
paper.install(window);
_paper = new paper.PaperScope();
_paper.setup('track_canvas');
var svg = document.getElementById('track');
var layer = new _paper.Layer();
_path = layer.importSvg(svg).firstChild.firstChild;
_path.strokeColor = '#14a8df';
_path.strokeWidth = 2;
绘制轨道后,每个设备都会根据其在设备排列顺序中的位置找到其 x 偏移量,并相应地定位轨道。
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}
然后可以使用 x 偏移来显示轨道的适当部分
CSS 动画
Paper.js 使用大量的 CPU 处理来绘制轨道车道,这个过程在不同的设备上会花费或多或少的时间。 为了处理这个问题,我们需要一个加载程序来循环,直到所有设备都完成对轨道的处理。 问题在于,由于 Paper.js 的 CPU 要求,任何基于 JavaScript 的动画都会跳帧。 输入 CSS 动画,它在单独的 UI 线程上运行,使我们能够平滑地为“BUILDING TRACK”文本的光泽设置动画。
.glow {
width: 290px;
height: 290px;
background: url(img/track-glow.png) 0 0 no-repeat;
background-size: 100%;
top: 0px;
left: -290px;
z-index: 1;
-webkit-animation: wipe 1.3s linear 0s infinite;
}
@-webkit-keyframes wipe {
0% { -webkit-transform: translate(-300px,0); }
25% { -webkit-transform: translate(-300px,0); }
75% { -webkit-transform: translate(920px,0); }
100% { -webkit-transform: translate(920px,0); }
}
CSS 精灵
CSS 对于游戏内效果也派上了用场。 功率有限的移动设备一直忙于为跑过轨道的汽车制作动画。 因此,为了更加兴奋,我们使用精灵作为在游戏中实现预渲染动画的一种方式。 在 CSS sprite 中,过渡应用基于步进的动画,该动画会更改背景位置属性,从而产生汽车爆炸
#sprite {
height: 100px;
width: 100px;
background: url(sprite.jpg) 0 0 no-repeat;
-webkit-animation: play-sprite 0.33s linear 0s steps(9) infinite;
}
@-webkit-keyframes play-sprite {
0% { background-position: 0px 0px; }
100% { background-position: -900px 0px; }
}
这种技术的问题是你只能使用单行布局的精灵表。 为了遍历多行,动画必须通过多个关键帧声明链接起来。
#sprite {
height: 100px;
width: 100px;
background: url(sprite.jpg) 0 0 no-repeat;
-webkit-animation-name: row1, row2, row3;
-webkit-animation-duration: 0.2s;
-webkit-animation-delay: 0s, 0.2s, 0.4s;
-webkit-animation-timing-function: steps(5), steps(5), steps(5);
-webkit-animation-fill-mode: forwards;
}
@-webkit-keyframes row1 {
0% { background-position: 0px 0px; }
100% { background-position: -500px 0px; }
}
@-webkit-keyframes row2 {
0% { background-position: 0px -100px; }
100% { background-position: -500px -100px; }
}
@-webkit-keyframes row3 {
0% { background-position: 0px -200px; }
100% { background-position: -500px -200px; }
}
渲染汽车
与任何赛车游戏一样,我们知道为用户提供加速和操控感非常重要。 应用不同的牵引力对于游戏平衡和乐趣因素很重要,因此一旦玩家对物理有了感觉,他们就会获得成就感并成为更好的赛车手。
我们再次调用了带有大量数学实用程序的 Paper.js。 我们使用它的一些方法来沿着路径移动汽车,同时调整汽车的位置和每一帧平滑的旋转。
var trackOffset = _path.length - (_elapsed % _path.length);
var trackPoint = _path.getPointAt(trackOffset);
var trackAngle = _path.getTangentAt(trackOffset).angle;
_velocity.length += _throttle; //apply the throttle
if (!_throttle) {
//slow down since the throttle is off
_velocity.length *= FRICTION;
}
if (_velocity.length > MAXVELOCITY) {
_velocity.length = MAXVELOCITY;
}
_velocity.angle = trackAngle;
trackOffset -= _velocity.length;
_elapsed += _velocity.length;
//find if a lap has been completed
if (trackOffset < 0) {
while (trackOffset < 0) trackOffset += _path.length;
trackPoint = _path.getPointAt(trackOffset);
console.log('LAP COMPLETE!');
}
if (_velocity.length > 0.1) {
//render the car if there is actually velocity
renderCar(trackPoint);
}
我们在优化汽车渲染时,发现了一个有趣的点。 在 iOS 上,最佳性能是通过对汽车应用 translate3d 变换实现的:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
在 Chrome for Android 上,最佳性能是通过计算矩阵值并应用矩阵变换实现的:
var rad = _rotation.rotation * (Math.PI * 2 / 360);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var a = parseFloat(cos).toFixed(8);
var b = parseFloat(sin).toFixed(8);
var c = parseFloat(-sin).toFixed(8);
var d = a;
_car.style.webkitTransform = 'matrix('+a+', '+b+', '+c+', '+d+', '+_position.x+', '+_position.y+')';
保持设备同步
开发中最重要(也是最困难)的部分是确保游戏跨设备同步。 我们认为,如果汽车偶尔因为连接速度慢而跳过几帧,用户可能会宽容,但如果您的汽车四处跳跃,同时出现在多个屏幕上,那就不会很有趣了。 解决这个问题需要大量的试验和错误,但我们最终确定了一些使它起作用的技巧。
计算延迟
同步设备的起点是了解从 Compute Engine 中继接收消息需要多长时间。 棘手的部分是每台设备上的时钟永远不会完全同步。 为了解决这个问题,我们需要找出设备和服务器之间的时间差。
为了找到设备和主服务器之间的时间偏移,我们发送一条带有当前设备时间戳的消息。 然后服务器将回复原始时间戳以及服务器的时间戳。 我们使用响应来计算实际的时间差。
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
这样做一次是不够的,因为到服务器的往返并不总是对称的,这意味着响应到达服务器可能比服务器返回它花费的时间更长。 为了解决这个问题,我们多次轮询服务器,取中值结果。 这使我们在设备和服务器之间的实际差异的 10 毫秒以内。
加速/减速
当玩家1按下或松开屏幕时,加速事件被发送到服务器。 一旦收到,服务器会添加其当前时间戳,然后将该数据传递给所有其他玩家。
当设备接收到“加速开启”或“加速关闭”事件时,我们可以使用服务器偏移量(上面计算的)来找出接收该消息所花费的时间。 这很有用,因为玩家 1 可能会在 20 毫秒内收到消息,但玩家 2 可能需要 50 毫秒才能收到它。 这将导致汽车在两个不同的地方,因为设备 1 会更快地开始加速。
我们可以花时间接收事件并将其转换为帧。 在 60fps 时,每帧为 16.67ms——因此我们可以增加汽车的速度(加速度)或摩擦力(减速度)以弥补它错过的帧。
var frames = time / 16.67;
var onScreen = this.isOnScreen() && time < 75;
for (var i = 0; i < frames; i++) {
if (onScreen) {
_velocity.length += _throttle * Math.round(frames*.215);
} else {
_this.render();
}
}
在上面的示例中,如果玩家 1 的屏幕上有汽车并且接收消息所花费的时间少于 75 毫秒,它将调整汽车的速度,加快速度以弥补差异。 如果设备不在屏幕上或消息花费的时间太长,它将运行渲染功能并实际使汽车跳到需要的地方。
保持汽车同步
即使考虑到加速延迟,汽车仍可能不同步并同时出现在多个屏幕上; 特别是在从一台设备过渡到另一台设备时。 为了防止这种情况发生,更新事件会频繁发送,以使汽车在所有屏幕上都保持在赛道上的相同位置。
逻辑很简单:每 4 帧,如果汽车在屏幕上可见,则该设备将它的值发送到其他每个设备。 如果汽车不可见,应用程序会使用收到的值更新值,然后根据获取更新事件所花费的时间将汽车向前移动。
this.getValues = function() {
_values.p = _position.clone();
_values.r = _rotation;
_values.e = _elapsed;
_values.v = _velocity.length;
_values.pos = _this.position;
return _values;
}
this.setValues = function(val, time) {
_position.x = val.p.x;
_position.y = val.p.y;
_rotation = val.r;
_elapsed = val.e;
_velocity.length = val.v;
var frames = time / 16.67;
for (var i = 0; i < frames; i++) {
_this.render();
}
}
结论
一听到 Racer 的概念,我们就知道它有可能成为一个非常特别的项目。 我们快速构建了一个原型,让我们对如何克服延迟和网络性能有了一个粗略的了解。 这是一个具有挑战性的项目,让我们在深夜和漫长的周末忙得不可开交,但当游戏开始成型时,感觉很棒。 最终,我们对最终结果非常满意。 Google Creative Lab 的概念以一种有趣的方式突破了浏览器技术的极限,作为开发人员,我们不能要求更多。
下载
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 有效的管理 Web 大型应用的内存使用
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论