第五章 制作游戏
本章,你将利用你所学的动画及高级绘制知识编写一个简单的太空战争游戏。你可以用我给你提供的素材来完成这个游戏,游戏思路是这样的,玩家拥有一辆可以左右移动的飞船,左右移动由键盘上的方向键控制,点击空格按钮时可以开火。屏幕上方的外星人会前后移动并随机发射火力,当玩家的子弹打中外星飞船时,会触发碰撞检测消灭外星人,如果玩家碰上外星人发射的子弹也会阵亡。示意图中所有的图形目前都由简单的矩形构成,下面是示意图,我们现在开始来编写相关代码。
使用精灵图绘制太空飞船
新建一个目录,并在该目录中创建名为mygame.html
的文件,然后复制game1.html
到其中,这个文件中包含你刚刚试玩的游戏的代码。
我们要做的第一件事是升级我们的太空飞船。在这里我使用的图片来自于 Lost Garden如下:
首先我们需要改变飞船大小以适应图片。我们希望飞船的大小为46px*46px
,添加以下代码到刚刚你复制的game1.html的顶部。
var can = document.getElementById("canvas");
var c = can.getContext('2d');
//new code
player.width = 46;
player.height = 46;
接下来 我们需要把图片加载为一个对象,以便我们可以使用它。创建一个名为ship_image
的变量,然后调用loadResources()
方法在一开始就载入图像
player.width = 46;
player.height = 46;
//new code
var ship_image;
loadResources();
function loadResources() {
ship_image = new Image();
ship_image.src = "images/Hunter1.png";
}
然后我们去drawPlayer
函数的底部,我们改变最后两行的代码使得我们的飞船不再是一个矩形而是一个像模像样的飞船了。
c.fillStyle = "red";
c.fillRect(player.x,player.y, player.width, player.height);
c.drawImage(ship_image,
25,1, 23,23, //src coords
player.x, player.y, player.width, player.height //dst coords
);
我们看看效果如何,我们的原图片上其实有八架飞船,但是我们其实只是想绘制其中的一架。通过传入对应的图片坐标和绘制地点,drawImage
函数只会绘制图片中的一部分。源坐标定义了将会以那一部分图片为参考,绘制坐标规定了图片将会绘制在那里,以及绘制的大小。你一定注意到这里我把飞船的大小设置为了46*46
,这是源图片23*23
的两倍,我故意这样做,是为了营造一种像素感。保存文件,刷新浏览器,你将看到如下效果
用精灵动画绘制子弹和炸弹
接下来我们使用精灵动画的方法绘制飞船和外星舰队的子弹,首先我们还是存储这些图片我变量,代码如下:
var ship_image;
var bomb_image;
var bullet_image;
loadResources();
function loadResources() {
ship_image = new Image();
ship_image.src = "images/Hunter1.png";
bomb_image = new Image();
bomb_image.src = "images/bomb.png";
bullet_image = new Image();
bullet_image.src = "images/bullets.png";
}
上述代码将加载下面这两个图片
上面两幅图片都包含多个精灵。不过这次我们想要用到所有的精灵,每一帧都显示不同的图片以营造一种动画的效果。我们可以通过循环来营造这种动画,但是还是和以前一样,我们的每次绘制都只显示其中的一部分,只是每一帧我们都动态的改变其坐标位置。
function drawPlayerBullets(c) {
c.fillStyle = "blue";
for(i in playerBullets) {
var bullet = playerBullets[i];
var count = Math.floor(bullet.counter/4);
var xoff = (count%4)*24;
//c.fillRect(bullet.x, bullet.y, bullet.width,bullet.height);
c.drawImage(
bullet_image,
xoff+10,0+9,8,8,//src
bullet.x,bullet.y,bullet.width,bullet.height//dst
);
}
}
上述代码除了新增变量xoff,count,bullet
其余部分和之前的代码看起来很像。我们给每一发子弹都添加了一个计数器,这个数随子弹的创建而创建,从0开始,并且每一帧增加1,count
变量用以表示counter
的四分之一。如果每帧子弹的样式都发生的改变,这样太快了,我们除以4让动画慢下来。
xoff
变量由count
对4取余获得,意味着这个数会在0~3
直接循环,然后我们又乘以了24(每一幅精灵图的宽度),这样的话xoff
就会在0,24,48,72
之间波动了,这样使得我们的x坐标实现了渐变,此外我们还加上了10的作用是抵消图片左边边距。
我们用相同的方法在createEnamyBullet
和drawEnemyBullets
函数中绘制炸弹,代码如下:
function createEnemyBullet(enemy) {
return {
x:enemy.x,
y:enemy.y+enemy.height,
width:4,
height:12,
width:30,
height:30,
counter:0,
}
}
function drawEnemyBullets(c) {
for(var i in enemyBullets) {
var bullet = enemyBullets[i];
c.fillStyle = "yellow";
c.fillRect(bullet.x, bullet.y , bullet.width, bullet.height);
var xoff = (bullet.counter%9)*12 + 1;
var yoff = 1;
c.drawImage(bomb_image,
xoff,yoff,11,11,//src
bullet.x,bullet.y,bullet.width,bullet.height//dest
);
}
}
注意在上述代码中,我们设置了外星舰队炮弹的默认大小为30,这是为了使得碰撞检测大小和图片大小一致,我们也需要绘制太空船子弹的firePlayerBullet
函数中做一样的事情。
function firePlayerBullet() {
//create a new bullet
playerBullets.push({
x: player.x,
x: player.x+14,
y: player.y - 5,
width:10,
height:10,
width:20,
height:20,
counter:0,
});
}
现在,代码效果如下,怎么样很酷吧。
图形化外星飞船
下面我们来让外星飞船更加逼真一些,但是使用和上面不一样的方法。这次我们用程序来绘图,所有的图像都由代码来生成,我们希望外星飞船是一个绿色的圈圈中间包含很多白色的小点。看起来像是下面这样:
我们新建一个名为drawEnemy()
的函数来绘制这个,首先修改已有的drawEnemies()
函数匹配这个新的drawEnemy()
函数。
function drawEnemies(c) {
for(var i in enemies) {
var enemy = enemies[i];
if(enemy.state == "alive") {
c.fillStyle = "green";
drawEnemy(c,enemy,15);
}
if(enemy.state == "hit") {
c.fillStyle = "purple";
enemy.shrink--;
drawEnemy(c,enemy,enemy.shrink);
}
//this probably won't ever be called.
if(enemy.state == "dead") {
c.fillStyle = "black";
c.drawEnemy(c,enemy,15);
}
}
}
新建drawEnemy()
函数如下:
function drawEnemy(c,enemy,radius) {
if(radius <=0) radius = 1;
var theta = enemy.counter;
c.save();
c.translate(0,30);
//draw the background circle
circlePath(c, enemy.x, enemy.y, radius*2);
c.fill();
//draw the wavy dots
for(var i=0; i<10; i++) {
var xoff = Math.sin(toRadians(theta+i*36*2))*radius;
var yoff = Math.sin(toRadians(theta+i*36*1.5))*radius;
circlePath(c, enemy.x + xoff, enemy.y + yoff, 3);
c.fillStyle = "white";
c.fill();
}
c.restore();
}
function toRadians(d) {
return d * Math.PI * 2.0 / 360.0;
}
function circlePath(c, x, y, r) {
c.beginPath();
c.moveTo(x,y);
c.arc(x,y, r, 0, Math.PI*2);
}
上述代码稍微有一点点复杂,我们一步步的分析一下。drawEnemy
函数接收绘制对象c
,所绘制的敌人enemy
,旋涡的角度radius
三个参数。首先依据最初敌军的数量计算一个基础的角度,这样做的目的是使得球体的每一帧都是渐变,接下来绘制一个圆形并填充当前的颜色,在这里我们还封装了一个用以绘制圆形的函数circlePath
.
接下来,循环十次用以绘制白色的小球。球的位置由xoff
和yoff
控制,这两个变量看起来复杂,但是实际上很简单,x坐标为当前的角度乘以弧度的正弦值(sin),y值的计算也是一样的,但是为了让每一个都不一样,我们给theta
加上了一个偏移i*36*2
,y轴上的偏移也是类似的i*36*1.5
。如果偏差相同,这些小球将在同一条线上,稍微设置不同使得他们形成涡流状。这里使用这些特定的值也是我不断尝试得出的,不同的尝试会产生很多有意思的结果,你只需要选择你最喜欢的就行了,你也可以改变这些参数的大小看看效果如何。
接下来,我们给外星飞船消失添加渐变动画。我们重新drawOverlay
函数,并改变globalAlph
值和文字
function drawOverlay(c) {
if(overlay.counter == -1) return;
//fade in
var alpha = overlay.counter/50.0;
if(alpha > 1) alpha = 1;
c.globalAlpha = alpha;
c.save();
c.fillStyle = "white";
c.font = "Bold 40pt Arial";
c.fillText(overlay.title,140,200);
c.font = "14pt Arial";
c.fillText(overlay.subtitle, 190,250);
c.restore();
}
现在再看一下,实现的效果
用粒子模拟器制造爆炸效果
最后让我们利用粒子模拟器在玩家死亡的时候添加爆炸效果。首先我们把爆炸移到一个类似下面这样的单独的函数。
function drawPlayer(c) {
if(player.state == "dead") return;
if(player.state == "hit") {
c.fillStyle = "yellow";
c.fillRect(player.x,player.y, player.width, player.height);
drawPlayerExplosion(c);
return;
}
c.drawImage(ship_image,
25,1, 23,23, //src coords
player.x, player.y, player.width, player.height //dst coords
);
}
接下来,我们创建一个简单的粒子系统,前章我们已经讲过,粒子系统其实就是由一系列的粒子随着帧而改变。对于爆炸,我们希望的效果是粒子从玩家所在的位置开始,然后以随机的角度和速度向四周扩散。创建粒子的代码如下:
var particles = [];
function drawPlayerExplosion(c) {
//start
if(player.counter == 0) {
particles = []; //clear any old values
for(var i = 0; i<50; i++) {
particles.push({
x: player.x + player.width/2,
y: player.y + player.height/2,
xv: (Math.random()-0.5)*2.0*5.0, // x velocity
yv: (Math.random()-0.5)*2.0*5.0, // y velocity
age: 0,
});
}
}
我们知道Math.random()
的返回值在0~1之间,我们减去0.5,然后乘以2得到的结果就在-1~1之间。接下来我们我们以足够快的速度放大它,这里我们设置为乘以5。
接下来我们需要绘制每一个粒子。
if(player.counter > 0) {
for(var i=0; i<particles.length; i++) {
var p = particles[i];
p.x += p.xv;
p.y += p.yv;
var v = 255-p.age*3;
c.fillStyle = "rgb("+v+","+v+","+v+")";
c.fillRect(p.x,p.y,3,3);
p.age++;
}
}
};
新的粒子的位置为老的例子的位置乘以速度。同时我们也基于粒子生命计算了一个变量v
用以储存颜色,我们使用rgb颜色模式,因此我们希望例子的颜色从255开始,逐渐消失,因此我们设置颜色从白色逐渐渐变到黑色。
最终我们的游戏效果如下:
小结
本章只是简单的告诉了你使用HTML Canvas可以达到一种什么样的效果,我希望你能进一步通过改变背景色,调整游戏速度,使用新的精灵图来改进本游戏。
全套的失乐园图像可以在这里找到,这个网站包含了大量优质的关于游戏设计的文章,如果你有兴趣,我非常推荐你阅读。