-By Daniel X. Moore,来自Pixel Relations, STRd6。
2010年2月1日
本文适用的浏览器为 Google Chrome/Mozilla Firefox/Apple Safari/Opera。

介绍

你想用HTML5的新特性Canvas来制作一个游戏么?那相信这篇教程会给你带来一定的帮助。
不过丑话说前面,你得有至少中级的Javascript应用能力才行。

你可以先看看这个游戏成品,或者是直接来进入文章并查看游戏的源码

创建canvas区域

为了让内容显示在屏幕上,我们需要一个canvas区域。由于本文重点是游戏制作,那么在这里我们使用jQuery来创建这一区域。

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

游戏更新

为了让游戏画面更流畅,我们需要将更新画面的频率设置的快一些。快多少呢?只要比人的眼睛和大脑的反应速度稍快就行啦!

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

那么就这样,我们就可以更新画面或是绘制空白方法了。不过千万要记得定时调用这个叫做setIntercal()的函数。

function update() { ... }
function draw() { ... }

Hello World

现在我们有了一个游戏循环更新的函数,那么接下来就可以在屏幕上绘制东西了。首先从最简单的绘制文字开始。

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Hello World!", 50, 50);
}

注意:确保在修改后你的程序还能正常运行。只有分段检查,在出问题之后才更容易确定出问题的地方——查几行代码总比查全部工程要强吧?

如果你想让静态的字体更酷一些的话(比如动起来),那就使用我们已经设定好的游戏循环更新函数吧。通过它,我们可以让文字移动时候更容易一些。

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Hello World!", textX, textY);
}

现在来让文字回旋起来吧。如果按照上面的代码,那么文字应该已经动起来了。不过在它移动后留在屏幕上的痕迹并没有被清除。先停一下,思考思考为啥会这样?其实答案很简单,因为我们在绘制新的图案之前并没有清除屏幕。所以在绘屏之前让我们先执行清屏操作。

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Hello World!", textX, textY);
}

嗯,这样我们就让一段文字在屏幕上移动了,于是这样的话,在制作游戏的道路上我们也就成功了一半了……也就是说,除了增加控制、提高游戏体验,美化游戏界面……呃,或许还有6/7的路我们就可以创造一个真正的游戏了呢。别紧张,接下来的部分,也有教程陪着你。

创建游戏控制器

游戏玩家是用于操控游戏信息,并负责绘制诸如图形之类数据的一个对象。那么接下来我们就创建一个游戏控制器,并用简单的字符串变量来储存全部信息。

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

为方便起见,让我们用一个简单的上色矩形来代表当前的游戏玩家。当我们绘制游戏时,我们将canvas清空,并绘制游戏玩家对象。

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

键盘控制

使用jQuery Hotkeys

jQuery Hotkeys Plugin让浏览器响应键盘按键变得更容易了。通过定义的keyCodecharCode,来将事件如下一样绑定

$(document).bind("keydown", "left", function() { ... });

这个函数的一大长处是不需要知道每个键的ASCII码,我们需要做的就是命令它“当玩家按某个键的时候,去做什么事儿”,jQuery Hotkeys就可以很好的完成我们想要的事儿。

游戏对象移动

JavaScript接管键盘事件的方法是通过事件驱动,所以这也就意味着我们没法检测按下的是哪个键。所以,那让我们使用自己的方法吧。

你可能会问,为什么我们不自己建立一个事件驱动么?好吧,因为键盘是通过系统的IO组件来反映操作的,而并不通过我们的游戏更新循环,所以我们并不能及时获得按键信息。那么为了用户体验的一致性,我们需要在游戏更新循环中加入自己的按键检测。

好消息是,我已经引用了一个16行的JS文件——key_status.js来让检测按键成为了可能。你只要来在looping函数中检查keydown.left或是对应的其他键的事件即可。

现在我们能检测按键了,那就用下面这个update函数来移动游戏对象。

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

那接下来,让它旋转起来吧。

你可能已经注意到了,那块东西(好吧,游戏对象)会被移出可见范围之外,那让我们锁定它的范围。还有,就是移动速度有点慢,那就加速,呵呵。

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

增加更多的输入同样简单。那让我们发射炮弹吧!

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) 好吧,至少增加按键绑定这事很简单。。。就是这样~
};

增加更多的游戏元素

子弹

让我们增加一些子弹吧。首先,需要一个数组来储存它们的信息。

var playerBullets = [];

然后我们需要一个函数来创建这些子弹实例。

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

当玩家按下发射键时,我们需要马上创建一个子弹实例,并放置到之前储存信息的数组中以便显示。

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

那么现在我们需要将上面的代码加入到游戏循环更新的函数中了。为了防止集合中已经攻击过敌人或是飞出窗体的子弹(我们称之为“无效子弹”)仍旧滞留在数组中,我们来把整个一系列子弹过滤到只剩活动的子弹。这样的话我们移除无效子弹的方法也简单多了。

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

最后一步是绘制子弹。

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

敌人

下面让我们来像增加子弹一样增加一些敌人吧。

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

读取并绘制图片

图片移动显然比色块移动看上去帅。在canvas中读取并绘制图片是很痛苦的,为了避免那些,让我们使用一个简单的公用模块。

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

碰撞检测

像机器人比赛?其实是,我们将这些元素绘制完后,它们相撞却不会产生事件。为了让他们能在合适的时候爆炸,那让我们增加一些判断吧。

让我们使用一个简单的矩形碰撞检测算法:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

请注意,我们有两个需要检测的东西。
·1、玩家的子弹>=敌方飞船
·2、玩家>=敌方飞船

完成后,让我们做一个钩子来挂载到update函数中,以便游戏更新循环可以更新我们需要的碰撞检测。

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

完成了检测,让我们加点爆炸效果吧。这个函数标记了被碰撞的对象,同时移除并播放爆炸效果。

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // 可以增加一点爆炸的图片
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // 增加爆炸的图片并结束游戏。
};

音效

声音也是个增加用户体验的不错方法。虽然说在HTML5中直接嵌入声音和图片还是挺那啥的,不过多亏我们有一个额外的js——sound.js,插入声音也变得非常简单了。

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

需要你注意的是,引入声音可能会带来一系列稳定性问题,诸如选项卡崩溃或是声音卡顿之类的。所以要有心理准备。

结束了

还是那句话,这是这个游戏成品,而且这是整套程序的zip源码包

看完教程,我希望你已经明白了用JS和html5做游戏的最基础的一些东西了。经过一些适当的抽象编程,我们可以对api更灵活地掌握,同时也学会了一些灵活应对未来问题的方法。

参考

HTML5 Canvas Cheat Sheet
HTML5 Game Engines

2 Thoughts on “简易HTML5游戏制作指南

  1. 博主 您好 你的这个翻译真的不错
    不知道有没有建立群什麽的 大家交流。。

    新手学习开发html5 希望能做一些新东西 不只是游戏。。。

    邮件交流哈。

Post Navigation