The Pond – 构建多平台 HTML5 游戏

The Pond

介绍 The Pond

The Pond 是一款多平台 HTML5 游戏 (源代码),探索极简主义的设计和分辨率无关的游戏玩法。The Pond 不是关于获得高分,也不是关于购买武器升级。它是关于放松和探索一个美丽的世界。

它可以在所有这些平台/所有这些商店中获得

  • Firefox Marketplace
  • Google Play
  • Chrome Web Store
  • Amazon App store
  • Clay.io
  • Pokki

在制作 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()
  }
}

生成

现在,我们一直在谈论减少生成以提高性能,所以让我解释一下它是如何发生的。生成算法通过基于窗口大小创建虚拟网格来工作。当玩家从一个网格区域移动到另一个网格区域时,相邻区域将填充敌人

Grid Spawner

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 资源,而且编码也很困难。我最终选择了使用圆圈的近似方法。基本上,我在每条鱼内计算圆圈的位置并检测鱼之间的圆圈碰撞。布尔圆圈碰撞非常高效,因为它只需要测量物体之间的距离。最后看起来像这样(调试模式)

debug mode

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 分析器来识别瓶颈

The Pond CPU profile

我们在这里看到的是 stroke 使用了大量的资源。说实话,fill 也曾经在那里。这是因为在绘制鱼时,两者都被频繁调用。游戏看起来有点像这样

The Pond - Old rendering

移除 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。

更多由 Zoli Kahan 撰写...的文章

关于 Robert Nyman [名誉编辑]

Mozilla Hacks 的技术布道者和编辑。发表关于 HTML5、JavaScript 和开放 Web 的演讲和博客文章。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年起一直在从事 Web 前端开发工作——在瑞典和纽约市。他还经常在 http://robertnyman.com 上发表博客文章,喜欢旅行和结识新朋友。

更多由 Robert Nyman [名誉编辑] 撰写...的文章


8 条评论

  1. Nathan Campos

    很棒的游戏,我喜欢它。

    出于某种原因,Firefox 应用商店表示它在我的区域(巴西)不可用。

    2013 年 11 月 21 日 下午 12:45

    1. Zoli Kahan

      谢谢,我很高兴你喜欢它。

      不幸的是(我刚刚了解到),巴西有内容限制,要求获得 DEJUS 评级才能在应用商店中提供 (https://mdn.org.cn/en-US/Apps/Publishing/Marketplace_review_criteria#Content)。

      感谢您提请我注意此事,我将看看我能做些什么。

      2013 年 11 月 21 日 下午 4:19

      1. Felix E. Klee

        我记录了我是如何让我的游戏 ROTOGAMEsq 获得 DEJUS 评级的

        http://gamedev.stackexchange.com/questions/44466/process-of-getting-dejus-rating-brazil

        如果您有任何问题,请告诉我。

        2013 年 12 月 12 日 上午 12:57

        1. Robert Nyman [编辑]

          感谢分享!

          2013 年 12 月 12 日 上午 2:40

    2. Felix E. Klee

      奇怪的是,这款游戏在“德国”地区也无法使用。如果我点击文章中的应用商店链接,我会收到以下消息:“哦,不!请求的应用程序在您的地区不可用。如果您希望查看此应用程序的区域版本,您可能希望联系开发者。”如果我在应用商店中搜索“pond”,我会收到以下消息:“未找到结果”。

      幸运的是,我仍然在“我的应用”中拥有这款游戏,因此我可以在我的 Keon 上安装它(例如,重新初始化设备后)。但是,当我尝试评分时,我会收到以下消息:“哦,不!发生错误。”

      2013 年 12 月 12 日 下午 2:58

      1. Zoli Kahan

        感谢您告诉我。我已经向应用支持团队发送了一封电子邮件,要求澄清情况(我没有找到有关此问题的文档)。希望这能尽快得到解决。

        2013 年 12 月 16 日 上午 7:28

  2. Felipe Nascimento de Moura

    干得好!
    非常有创意、简单有趣!

    2013 年 12 月 12 日 上午 11:35

    1. Robert Nyman [编辑]

      是的,我认为它也是一款很棒的游戏。:-)

      2013年12月12日 下午12:23

本文评论已关闭。