介绍 The Pond
The Pond 是一款多平台 HTML5 游戏 (源代码),探索极简主义的设计和分辨率无关的游戏玩法。The Pond 不是关于获得高分,也不是关于购买武器升级。它是关于放松和探索一个美丽的世界。
它可以在所有这些平台/所有这些商店中获得
在制作 The Pond 的过程中,我遇到了许多性能障碍,我将在本文中详细探讨(尤其是在优化代码库以适应移动设备时)。
工具
在我开始之前,我想提一下两个使编写 The Pond 代码既高效又非常令人愉快的工具:Light Table 和 CocoonJS。
Light Table 是一款 IDE(仍处于 alpha 阶段),它为实时 JavaScript 代码注入提供了集成开发环境。这意味着可以在编辑器中编辑的 JavaScript 可以预览,而无需重新加载页面。如果我们看一下游戏中的鱼的形状,我们会注意到它由 贝塞尔曲线 组成。与其尝试找到用于创建贝塞尔曲线的编辑器,不如简单地估计一个基本形状并实时修改变量,直到我对它的外观和感觉感到满意为止。
CocoonJS 另一方面提供了一个优化过的画布兼容层,以提高移动设备的性能。它不仅优化,还提供了一个界面,用于将我们的应用程序导出到许多设备(Android、iOS、Amazon(Android)、Pokki 和 Chrome 网上应用店)。
物理
The Pond 在外部可能看起来很简单,但在内部却充满了性能优化和响应功能。当我们调整游戏大小,它会更新并重新优化自身以渲染更少的物体并生成更少的鱼,如果还不够,帧速率会平滑下降以控制物理。这是由于使用了 **固定间隔物理时间步长**。 Gameprogrammingpatterns.com 提供了关于如何执行此操作以及为什么重要的良好解释,但老实说,代码最能说明问题
var MS_PER_UPDATE = 18; // Time between physics calculations
var lag = 0.0; // accumulate lag over frames
var previousTime = 0.0; // used for calculating the time delta
// main game loop
function draw(time) {
requestAnimFrame(draw); // immidiately queue another frame
lag += time - previousTime; // add time delta
previousTime = time;
var MAX_CYCLES = 18; // prevent infinite looping/hanging on slow machines
// physics calculations
while(lag >= MS_PER_UPDATE && MAX_CYCLES) {
// user input, movement, and animation calculations
physics();
lag -= MS_PER_UPDATE;
MAX_CYCLES--;
}
// if we exhausted our cycles, the client must be lagging
if(MAX_CYCLES === 0) {
// adaptive quality
lowerQuality();
}
// if 5 frames behind after update, jump
// this prevents an infinite input lag from ocurring
if(lag/MS_PER_UPDATE > 75) {
lag = 0.0;
}
// draw to canvas
paint();
}
这里需要注意的是,物理计算不是基于时间增量,而是以固定的 18 毫秒间隔计算。这一点很重要,因为它意味着任何客户端延迟都不会反映在物理计算中,而较慢的机器只会降低帧速率。
动态质量
我们注意到的下一个优化是 lowerQuality()
函数,它自适应地降低了游戏的渲染质量。它的工作方式是简单地调整绘图画布的大小(它仍然是全屏,只是被拉伸了),这反过来会导致减少的生成和碰撞。
function resizeWindow() {
// quality is a global variable, updated by lowerQuality()
$canv.width = window.innerWidth * quality/10
$canv.height = window.innerHeight * quality/10
ctx = $canv.getContext('2d')
ctx.lineJoin = 'round'
// resize HUD elements, and reduce spawning
if(GAME.state === 'playing') {
GAME.spawner.resize($canv.width, $canv.height)
GAME.levelBar.resize($canv.width, $canv.height)
GAME.levelBalls.resize($canv.width, $canv.height)
} else {
if(ASSETS.loaded) drawMenu()
}
}
生成
现在,我们一直在谈论减少生成以提高性能,所以让我解释一下它是如何发生的。生成算法通过基于窗口大小创建虚拟网格来工作。当玩家从一个网格区域移动到另一个网格区域时,相邻区域将填充敌人
Spawner.prototype.spawn = function(zone) {
// spawn 1-3 fish per 500sqpx, maybe larger maybe smaller than player
// 0.5 chance that it will be bigger/smaller
var mult = this.width*this.height/(500*500)
for(var i=0, l=(Math.floor(Math.random()*3) + 1) * mult; i < l; i++) {
// spawn coordinates random within a zone
var x = zone[0]+Math.floor(this.width*Math.random()) - this.width/2
var y = zone[1]+Math.floor(this.height*Math.random()) - this.height/2
var size = Math.random() > 0.5 ? this.player.size + Math.floor(Math.random() * 10) : this.player.size - Math.floor(Math.random() * 10)
// spawn a new fish
var fish = new Fish(true, x, y, size, Math.random()*Math.PI*2-Math.PI, Math.random()*Math.PI)
this.fishes.push(fish)
}
return zone
}
最后一块拼图是在敌人移动足够远时将其移除
// if far enough away from player, remove
if(distance(fish, player) > Math.max($canv.width, $canv.height) * 2) {
fish.dead = true
}
碰撞
下一个性能优化在于碰撞代码。碰撞不规则形状的物体可能非常困难且资源密集。一个选项是进行基于颜色的碰撞(扫描重叠颜色),但这太慢了。另一个选项可能是用数学方法计算贝塞尔曲线碰撞,但这不仅会占用大量 CPU 资源,而且编码也很困难。我最终选择了使用圆圈的近似方法。基本上,我在每条鱼内计算圆圈的位置并检测鱼之间的圆圈碰撞。布尔圆圈碰撞非常高效,因为它只需要测量物体之间的距离。最后看起来像这样(调试模式)
Fish.prototype.collide = function (fish) {
// the fish has been killed and is being removed or it is far away
if (this.dying || fish.dying || distance(this, fish) > this.size * 5 + fish.size*5) {
return false
}
// there are 6 circles that make up the collision box of each fish
var c1, c2
for (var i=-1, l = this.circles.length; ++i < l;) {
c1 = this.circles[i]
for (var j=-1, n = fish.circles.length; ++j < n;) {
c2 = fish.circles[j]
// check if they touch
if(distance(c1, c2) <= c2.r + c1.r) {
return true
}
}
}
return false
}
我们还通过仅检查可见(或几乎可见)的鱼来避免不必要的碰撞计算。
if(Math.abs(fish2.x - player.x) < $canv.width && Math.abs(fish2.y - player.y) < $canv.height) {
// check
}
绘图
在处理完物理之后,是时候优化绘图操作了。许多游戏使用精灵图进行动画 (Senshi 为例),这可以高度优化。不幸的是,我们的鱼是动态生成的,因此我们必须找到其他方法来优化绘图。首先,让我们使用 Chrome 的 JavaScript 分析器来识别瓶颈
我们在这里看到的是 stroke
使用了大量的资源。说实话,fill
也曾经在那里。这是因为在绘制鱼时,两者都被频繁调用。游戏看起来有点像这样
移除 fill
后,我看到了巨大的性能提升,而且游戏看起来好多了。drawImage
函数之所以也出现在这里,是因为我利用了 离屏画布 渲染。每条鱼都在自己的离屏画布上绘制,然后渲染到更大的可见画布上。这也是我能够通过读取像素数据轻松地将鱼炸成粒子
Fish.prototype.toParticles = function(target) {
var particles = []
// read canvas pixel data
var pixels = this.ctx.getImageData(0,0,this.canv.width, this.canv.height).data
for(var i = 0; i < pixels.length; i += 36 * Math.ceil(this.size/20) * (isMobile ? 6 : 1)) {
var r = pixels[i]
var g = pixels[i + 1]
var b = pixels[i + 2]
// black pixel - no data
if(!r && !g && !b){
continue
}
// Math to calculate position
var x = i/4 % this.canv.width - (this.canv.width/2 + this.size)
var y = Math.floor(i/4 / this.canv.width) - (this.canv.height/2)
var relativePos = rot(x, y, this.dir)
x=this.x + relativePos[0]
y=this.y + relativePos[1]
var col = new Color(r, g, b)
var dir = directionTowards({x: x, y: y}, this)
particles.push(new Particle(x, y, col, target, Math.PI*Math.random()*2 - Math.PI, this.size/20))
}
return particles
}
结束
最终,性能优化取得了成效,使游戏感觉更加精致,即使在低端移动设备上也能流畅运行。
如果您喜欢这篇文章,我经常在我的博客上发布我的开发项目,地址是 http://zolmeister.com。
The Pond 等待探索...
关于 Zoli Kahan
我喜欢编码和解决有趣的问题。我经常在我的博客 http://zolmeister.com 上发布我的技术项目,并尝试在我的 GitHub 上分享所有源代码,地址是 https://github.com/Zolmeister。
关于 Robert Nyman [名誉编辑]
Mozilla Hacks 的技术布道者和编辑。发表关于 HTML5、JavaScript 和开放 Web 的演讲和博客文章。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年起一直在从事 Web 前端开发工作——在瑞典和纽约市。他还经常在 http://robertnyman.com 上发表博客文章,喜欢旅行和结识新朋友。
8 条评论