游戏
飞机大战甲壳虫
我们这周主要讲解的是飞机大战甲壳虫这个游戏,通过上、下、左、右或者W、A、S、D 等键控制自身飞机的方向,通过SPACE空格发射子弹击毁敌对目标,每击毁一个敌对目标得一分。为了让大家清楚这个游戏的制作过程,所以写了这个总结。大家和我一起来看看吧!!
HTML页面制作
通过css设置了页面背景、字体颜色、canvas画布的位置,隐藏了game-over游戏结束 的 ID。
创建一个Canvas对象
相信大家都知道,可以通过JS创建画布,也可以直接在HTML页面上创建画布,然后再通过document.getELementById()来获取,我们这里用JS创建画布:
//创建画布
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
//设置画布的宽高
canvas.width = 512;
canvas.height = 480;
document.body.appendChild(canvas);
载入图片
加载图片首先我们要创建对象
var resouceCache = {}; //存放所有的图片对象,键是路径,值是img对象
var readyCallback = [];//存放需要回调的函数
判断传过来的参数是否是数组
function load(urlOrArray){
//如果是数组,直接循环,如果只是个url直接执行_load方法
if(urlOrArray instanceof Array){
urlOrArray.forEach(function(url,index,arr){
_load(url);
})
}else{
_load(urlOrArray);
}
}
判断对象里面的所有值是不是都是对象,如果都是则返回true
function isReady(){
var flag = true;
for(var i in resouceCache){
if(!resouceCache[i]){
flag = false;
}
}
return flag;
}
为了让大家能更好的理解,请看下面的例子:
var imgs = ['images/1.jpg','images/2.jpg','images/3.jpg','images/4.jpg'];
var imgCache = {}; //存放所有的图片对象
function load(img){
//判断img是否是数组,如果是数组,直接循环,如果不是则直接执行_load方法
if(img instanceof Array){
img.forEach(function(url){
_load(url);
})
}else{
_load(img)
}
}
function _load(url){
var img = new Image();
img.onload = function(){
imgCache[url] = img;
if(isReady()){
alert('图片全部加载完成');
}
}
imgCache[url] = false;
img.src = url;
}
function isReady(){
var flag = true;
for(var i in imgCache){
if(!imgCache[i]){
flag = false;
}
}
return flag;
}
load(imgs);
看了这段代码相信大家都清楚了,上诉代码和我们在游戏中所写的代码差不多,很好理解,图片全部加载完成。
那么,接下来我们继续讲游戏中剩下的代码吧;
判断传递的参数是否是url
function _load(url){
//如果传的参数是url我们就直接返回url
if(resouceCache[url]){
return resouceCache[url];
}else{
//就new一个Image对象
var img = new Image();
img.onload = function(){
resouceCache[url] = img;
if(isReady()){
//forEach:循环数组
readyCallback.forEach(function(func){
func();
});
}
};
resouceCache[url] = false;
img.src = url;
}
}
将函数加入到回调数组中
function onReady(func){
readyCallback.push(func);
}
构造函数
this指向函数本身(如果实在敌机中使用那就是指向敌机,在自身飞机中使用就是指向自身飞机....)
function Sprite(url, pos, size, speed, frames, dir, once){
this.url = url; //图片地址
this.pos = pos; //对象在图片上的偏移量 []
this.size = size;//对象的大小【】
this.speed = speed;//速度
this.frames = frames;//动画执行的数组【】
this.dir = dir || 'horizontal';//horizontal 平行 dir 方向
this.once = once || false;
this._index = 0;
this.done = false;
}
载入背景图片
//原型共享
Sprite.prototype = {
constructor:Sprite,
update:function(dt){
this._index += this.speed * dt;
}
};
window.Sprite = Sprite;
命名空间:方便外面使用,我们会用到两张图片(背景、飞机甲壳虫)
window.resources = {
load: load,
get: get,
onReady: onReady
}
//加载所有图像
resources.load([
'img/sprites.png',
'img/terrain.png'
]);
下面为大家举一个简单的命名空间的例子,希望能帮助大家理解命名空间
(function(){
var name = "命名空间示例";
var arr = [];
function load(){
_load();
}
function _load(){
alert(name)
}
//命名空间
window.zz = {
load:load
}
})();
zz.load();
设置一个背景图片变量(初始化变量)
//背景图片
var terrainPattern;
游戏的初始化
function init(){
terrainPattern = ctx.createPattern(resources.get('img/terrain.png'),'repeat');
main();
}
在画布canvas上加载背景图片
function render(){
//加载背景图片
ctx.fillStyle = terrainPattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
回调执行init方法
resources.onReady(init);
效果显示如下:
自身飞机图片的加载
Sprite.prototype = {
constructor:Sprite,
update:function(dt){
this._index += this.speed * dt;
},
render:function(ctx){
var frame;
//计算本身飞机旋转
if(this.speed > 0){
var max = this.frames.length;
var idx = Math.floor(this._index);
frame = this.frames[idx % max];
if(this.once && idx >= max){
this.done = true;
return;
}
}else{
frame = 0;
}
var x = this.pos[0];//x轴
var y = this.pos[1];//y轴
if(this.dir == 'vertical'){
}else{
x += frame * this.size[0];
}
ctx.drawImage(resources.get(this.url),
x, y,
this.size[0], this.size[1],
0, 0,
this.size[0], this.size[1]);
}
};
自身飞机出现的位置
var player = {//自身飞机
pos: [0,0],//出现的坐标
sprite:new Sprite('img/sprites.png',[0,0],[39,39],16,[0,1])
};
判断自身飞机是否超出边界
if(player.pos[0]>canvas.width){
player.pos[0]=canvas.width-39;
}
//左
if(player.pos[0]<0){
player.pos[0]=0;
}
//上
if(player.pos[1]<0){
player.pos[1]=0;
}
//下
if(player.pos[1]>canvas.height){
player.pos[1] = canvas.height-39;
}
自身飞机通过SPACE空格键发射子弹
设置子弹变量
var bullets = []; //子弹
var lastFire = Date.now(); //第二次按下的子弹?
子弹的方向、位置、图片,具体代码如下:
if(input.isDown('SPACE') && (Date.now() - lastFire) > 100){
bullets.push({
dir: 'right',
pos: [player.pos[0] + player.sprite.size[0]/2,player.pos[1] + player.sprite.size[1]/2],
sprite: new Sprite('img/sprites.png',[0,39],[18,8])
});
bullets.push({
dir: 'top',
pos: [player.pos[0] + player.sprite.size[0]/2,player.pos[1] + player.sprite.size[1]/2],
sprite: new Sprite('img/sprites.png', [0, 50], [9,5])
});
bullets.push({
dir: 'bottom',
pos: [player.pos[0] + player.sprite.size[0]/2,player.pos[1] + player.sprite.size[1]/2],
sprite: new Sprite('img/sprites.png', [0, 60], [9, 5])
});
lastFire = Date.now();
}
子弹发射,具体代码如下:
var i = 0;
//子弹发射的方向
for(i=0;i<bullets.length;i++){
if(bullets[i].dir == 'right'){
bullets[i].pos[0] += bulletSpeed * dt;
}else if(bullets[i].dir == 'top'){
bullets[i].pos[1] -= bulletSpeed * dt;
}else if(bullets[i].dir == 'bottom'){
bullets[i].pos[1] += bulletSpeed * dt;
}
bullets[i].sprite.update(dt);
判断子弹是否超出边界
if(bullets[i].pos[0] + bullets[i].sprite.size[0] > canvas.width){
bullets.splice(i,1);
i --;
}
}
自身飞机的移动
上面我们已经讲了,飞机通过通过上、下、左、右或者W、A、S、D 等键控制自身飞机的方向
首先我们要获得按键的值
var pressKey = {};
function setKey(e,flag){
var code = e.keyCode;
var key;
switch (code){
case 32:
key = 'SPACE';
break;
case 37:
key = 'LEFT';
break;
case 38:
key = 'UP';
break;
case 39:
key = 'RIGHT';
break;
case 40:
key = 'DOWN';
break;
default:
key = String.fromCharCode(code);
}
pressKey[key] = flag;
}
键盘点击的动作
document.addEventListener('keydown',function(e){
setKey(e,true);
},false);
document.addEventListener('keyup',function(e){
setKey(e,false);
},false);
window.addEventListener('blur',function(e){
pressKey = {};
},false);
上面我们已经知道,这样写不方便我们调用,所以我们要写一个命名空间
window.input = {
isDown: function(key){
return pressKey[key.toUpperCase()];
}
}
上诉中我们获得了上、下、左、右键的值,那么接下来我们就该让飞机通过上、下、左、右或者W、A、S、D动起来了
if(input.isDown('A') || input.isDown('LEFT')){
player.pos[0] -= playerSpeed * dt;
}
if(input.isDown('W') || input.isDown('UP')){
player.pos[1] -= playerSpeed * dt;
}
if(input.isDown('D') || input.isDown('RIGHT')){
player.pos[0] += playerSpeed * dt;
}
if(input.isDown('S') || input.isDown('DOWN')){
player.pos[1] += playerSpeed * dt;
}
敌机图片的加载
敌机出现的位置、数量.....
var singleCount = 0;
var doubleCount = 0;
设置敌机
var enemies = []; //敌机
敌机出现的位置
singleCount ++;
if(singleCount % 2 == 0){
doubleCount ++;
if(doubleCount % 5 == 0){
//敌机
enemies.push({
pos: [canvas.width,Math.random() * (canvas.height - 39)],
sprite:new Sprite('img/sprites.png',[0, 78], [79, 39], 6, [0,1,2,3,2,1])
});
doubleCount = 0;
}
}
出现敌机
for(i=0;i<enemies.length;i++){
enemies[i].pos[0] -= enemySpeed * dt;
enemies[i].sprite.update(dt);
判断敌机是否超出边界
if(enemies[i].pos[0] + enemies[i].sprite.size[0] < 0){
enemies.splice(i,1);
i --;
}
}
碰撞图片的加载
for(var i=0;i<enemies.length;i++){
//敌机的位置大小
var pos = enemies[i].pos;
var size = enemies[i].sprite.size;
for(var j=0;j<bullets.length;j++){
//子弹的位置大小
var pos2 = bullets[j].pos;
var size2 = bullets[j].sprite.size;
//子弹、敌机碰撞
if(boxCollides(pos, size, pos2, size2)){
//敌机减一个
enemies.splice(i,1);
i --;
//分数
scroses += 1;
scrose.innerHTML = scroses;
//敌机爆炸时的图片
explosions.push({
pos: [pos[0], pos[1]],
sprite: new Sprite('img/sprites.png',
[0, 117],
[39, 39],
16,
[0,1,2,3,4,5,6,7,8,9,10,11,12],
null,
true)
});
//子弹减一个
bullets.splice(j,1);
j --;
}
}
}
爆炸图片
for(i=0;i<explosions.length;i++){
explosions[i].sprite.update(dt);
判断爆炸是否超出边界
if(explosions[i].done){
explosions.splice(i,1);
i --;
}
}
}
检测碰撞效果
自身飞机和敌机碰撞在一起,游戏结束
首先我们要知道自身飞机和敌机的位置、大小,那么我们可以这样写:
function collides(x, y, r, b, x2, y2, r2, b2){
return !(r < x2 || r2 < x || b < y2 || b2 < y);
}
写好了方法之后我们就开始调用:
function boxCollides(pos, size, pos2, size2){
return collides(
//pos[0]:x,pos[1]:y
// pos[0] + size[0]:x+width
//pos[1] + size[1]:y+height
pos[0], pos[1],
pos[0] + size[0], pos[1] + size[1],
pos2[0], pos2[1],
pos2[0] + size2[0], pos2[1] + size2[1]);
}
最后判断一下,碰撞结束游戏
if(boxCollides(pos, size, player.pos, player.sprite.size)){
gameOver();
}
游戏主循环
//游戏主循环
var lastTime = Date.now();
function main(){
var now = Date.now();
//时间差
var dt = (now - lastTime) / 1000;
lastTime = now;
//回调函数
requestAnimFrame(main);
}
重新开始游戏
var isGameOver = false;
代码如下:
function reset() {
document.getElementById('game-over').style.display = 'none';
document.getElementById('game-over-overlay').style.display = 'none';
isGameOver = false;
gameTime = 0;
scroses = 0;
scrose.innerHTML = scroses;
enemies = [];
bullets = [];
player.pos = [50, canvas.height / 2];
}