您好,本篇文章主要描述如何用面向对象编程思想仿写微信飞机大战。
采用JS脚本来控制HTML5新标签canvas画布,因为只操作一个DOM,画面会更加流畅,很多基于网页开发的小游戏都是用canvas画布实现的。
0 准备工作
首先,我们要准备好各种图片,如飞机的雪碧(精灵)图、子弹图、背景图。
然后,对整个游戏流程进行构思:
①游戏开始前的图片预加载
②让背景图片动起来
③绘制英雄机,英雄机跟随鼠标移动
④英雄机能发射子弹
⑤绘制3种敌机,赋予它们不同的血量与下落速度
⑥子弹与敌机的碰撞检测
⑦英雄机与敌机的碰撞检测
⑧显示分数,与结束游戏
1 HTML+CSS
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>微信飞机大战</title>
</head>
<body>
//
<canvas id="canvas" width="320" height="528" style="border: 2px solid gray"></canvas>
//引入脚本文件
<script src=""airplane></script>
</body>
</html>
2 JS
2.1 所有图片预加载
//获取画布与画布上下文
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//分数变量 游戏是否开始变量 英雄机对象
var score = 0;
var gameStart = false;
var theHero = null;
// 创建对象来接收对应的图片对象
var bgImg = '';
var heroImg = '';
var enemy1Img = '';
var enemy2Img = '';
var enemy3Img = '';
var bullet1Img = '';
//绘制进度条构造函数
function Loading(){
this.width = 220;
this.height = 12;
this.x = 50;
this.y = 258;
}
//创建进度条原型
Loading.prototype = {
//画进度条的外框
drawStroke : function(){
ctx.beginPath();
ctx.strokeRect(this.x,this.y,this.width,this.height);
ctx.stroke();
ctx.closePath();
},
//画已加载的填充图
drawFill : function(){
ctx.beginPath();
ctx.fillStyle = 'orangered';
ctx.fillRect(this.x,this.y,this.width,this.height);
ctx.fill();
ctx.closePath();
}
}
// 预加载函数
function load(){
var loadS = new Loading();
var loadSW = loadS.width;
loadS.drawStroke();
var loadF = new Loading();
loadF.width = 0;
loadF.drawFill();
//用一个数组把图片地址保存起来
var arr = ['img/bg.png','img/hero.png',
'img/enemy1.png','img/enemy2.png',
'img/enemy3.png','img/bullet1.png'
];
var index = 0;
//利用循环,创建图片对象
for(var i = 0;i<arr.length;++i){
var img = new Image();
img.src = arr[i];
//利用正则表达式判断图片对象是否为背景图,如果是就用一个变量保存起来,其他图片也是类似
if(/bg/.test(arr[i])){
bgImg = img;
}
else if(/hero/.test(arr[i])){
heroImg = img;
}
else if(/enemy1/.test(arr[i])){
enemy1Img = img;
}
else if(/enemy2/.test(arr[i])){
enemy2Img = img;
}
else if(/enemy3/.test(arr[i])){
enemy3Img = img;
}
else if(/bullet1/.test(arr[i])){
bullet1Img = img;
}
//当本次循环的图片加载完毕,运行该函数
img.onload = function(){
//加载增量
index++;
//进度条的长度随着图片加载个数的增加而变长
loadF.width = (index/arr.length)*loadSW;
ctx.clearRect(0,0,canvas.width,canvas.height);
loadF.drawFill();
if(index >=arr.length){
//当图片全部加载完毕,绘画初始界面
theHero = new Hero(heroImg,6);
ctx.fillStyle = 'black';
ctx.font = '30px Consoles';
ctx.fillText('飞机大战',canvas.width/3.5,canvas.height/3);
ctx.fillStyle = 'orange';
ctx.fillRect(canvas.width/3.5,canvas.height-200,130,50);
ctx.fillStyle = 'black';
ctx.fillText('开始游戏',canvas.width/3.3,canvas.height-163);
}
}
}
}
load();
2.2 为开始按钮添加点击事件
因为在画布内,所以只能根据位置判断是否点击到按钮
canvas.onmousedown = function(e){
var e = e||window.event;
var x = e.clientX - canvas.offsetLeft;
var y = e.clientY - canvas.offsetTop;
// 开始游戏的框大小内 canvas.width/3.5,canvas.height-200,130,50
if(x>=canvas.width/3.5&&(x<=canvas.width/3.5+130)&&y>=canvas.height-200&&y<canvas.height-200+50){
gameStart = true;
//动画主函数
move();
//点击完就把画布点击事件置为空
canvas.onmousedown = null;
}
}
2.3 背景移动函数
利用ctx.drawImage()绘制两张背景图,同时向下移动,当第一张图片移动到底部后就改变图片位置。两张图一直轮播。
// 设置背景移动量与背景位置变量
var bgChange = 1;
var bgPos = 0;
// 背景移动函数
function bgMove(){
bgPos += bgChange;
if(bgPos==canvas.height){
bgPos = 0;
ctx.drawImage(bgImg,0,0);
}
ctx.drawImage(bgImg,0,-canvas.height+bgPos);
ctx.drawImage(bgImg,0,bgPos);
}
2.4 各种对象的构造函数
2.4.1 英雄机构造函数
//传入对象为英雄机图片
function Hero(obj,health){
this.obj = obj;
this.width = obj.width/health;
this.height = obj.height;
//英雄机血量
this.health = health;
this.boom = 0;
//英雄机初始位置
this.x = canvas.width/2 -this.width/2;
this.y = canvas.height - this.height;
}
Hero.prototype.draw = function(){
ctx.drawImage(this.obj,this.width*this.boom,0,this.width,this.height,this.x,this.y,this.width,this.height);
}
2.4.2 子弹构造函数
//创建一个数组存放子弹对象
var arrBullet = [];
function Bullet(x,y,speedY){
//子弹初始位置
this.x = theHero.x+31;
this.y = theHero.y-15;
//子弹向上运动速度
this.speedY = speedY || -20;
this.width = bullet1Img.width;
this.height = bullet1Img.height
}
Bullet.prototype = {
draw : function(){
ctx.drawImage(bullet1Img,this.x,this.y);
},
move : function(){
this.draw();
this.y += this.speedY;
},
//当子弹到达顶部,返回布尔值true
clear : function(){
if(this.y<=0){
return true;
}
else{
return false;
}
}
}
2.4.3 敌机构造函数
//创建一个数组用来存放敌机对象
var arrEn1 = [];
//敌机构造函数
function Enemy(obj,speedY,health,type){
//传入的对象为 3种不同的敌机图片
this.obj = obj;
this.width = obj.width/health;
this.height = obj.height;
//敌机出现位置设定为随机
this.x = rnd(canvas.width-this.width,0);
this.y = rnd(-canvas.height,-obj.height);
this.speedY = speedY || 1;
//敌机血量
this.health = health;
//敌机是否被打中
this.boom = 0;
//敌机类型
this.type = type;
}
Enemy.prototype = {
//绘画敌机
draw : function(){
ctx.drawImage(this.obj,this.width*this.boom,0,this.width,this.height,this.x,this.y,this.width,this.height);
},
//敌机移动
move : function(){
this.draw();
this.y += this.speedY;
},
//如果敌机超越画布底部 把清除敌机的布尔值置为true
clear : function(){
if(this.y>=canvas.height){
return true;
}
else{
return false;
}
}
}
//随机函数
function rnd(max,min){
return Math.random()*(max-min+1)+min;
}
2.5 英雄机跟随鼠标
canvas.onmousemove = function(e){
var e = e||window.event;
var ex = e.clientX - canvas.offsetLeft - 33;
var ey = e.clientY - canvas.offsetTop - 41;
theHero.x = ex;
theHero.y = ey;
}
3 主动画函数
//创造一个变量用来决定敌机出现概率
var num = 0;
function move(){
num++;
if(num==1000){
num = 0;
}
//每次开始绘画时,先清除画布
ctx.clearRect(0,0,canvas.width,canvas.height);
//调用背景移动函数
bgMove();
//绘画英雄机
theHero.draw();
// 创造子弹对象,并把该对象插入到子弹数组
if(num%5==0){
var bullet = new Bullet();
arrBullet.push(bullet);
}
// 创造小敌机对象(出现几率最大),并把该对象插入到敌机数组
//(出现几率最大,速度最快2.5,血量最低5)
if(num%45==0){
var en1 = new Enemy(enemy1Img,2.5,5,'en1');
arrEn1.push(en1);
}
// 创造中敌机对象,并把该对象插入到敌机数组
//(出现几率最小,速度一般2,血量一般7)
if(num%160==0){
var en3 = new Enemy(enemy3Img,2,7,'en2');
arrEn1.push(en3);
}
// 创造大敌机对象,并把该对象插入到敌机数组
//(出现几率最小,速度最慢1.5,血量最高10)
if(num%360==0){
var en2 = new Enemy(enemy2Img,1.5,10,'en3');
arrEn1.push(en2);
}
// 遍历子弹数组画子弹
for(var i =0;i<arrBullet.length;++i){
//如果子弹到达底部,从数组中清除该对象
//清除后减少数组长度并跳出本次循环
if(arrBullet[i].clear()){
arrBullet.splice(i,1);
i--;
continue;
}
//如没有被清除则绘画该子弹对象
arrBullet[i].move();
}
// 遍历敌机数组画敌机
for(var j = 0;j<arrEn1.length;++j){
arrEn1[j].y += arrEn1[j].speedY;
//如果玩家得分超过15000就加快敌机速度
if(score>=15000){
arrEn1[j].y += arrEn1[j].speedY*1.3;
}
//如果敌机到达画布底部,从数组中清除该对象
if(arrEn1[j].clear()){
arrEn1.splice(j,1);
j--;
continue;
}
//若没有被清除则绘画该敌机对象
arrEn1[j].draw();
}
// 英雄机与敌机碰撞检测 (矩形碰撞)
for(var i = 0;i<arrEn1.length;++i){
//获得英雄机上下左右位置
var heroL = theHero.x;
var heroT = theHero.y;
var heroR = theHero.x + theHero.width;
var heroB = theHero.y + theHero.health;
//获取当前循环中敌机上下左右位置
var enL = arrEn1[i].x;
var enT = arrEn1[i].y;
var enR = arrEn1[i].x + arrEn1[i].width;
var enB = arrEn1[i].y + arrEn1[i].height;
//矩形碰撞条件为:不碰撞的4种情况取反
if(!(enR<heroL||enB<heroT||enL>heroR||enT>heroB)){
//判断敌机是否爆炸完成,是则清除该敌机对象
if(arrEn1[i].boom==arrEn1[i].health-1){
arrEn1.splice(i,1);
i--;
continue;
}
//碰撞到时,英雄机与敌机爆炸增量都自增1
else{
theHero.boom++;
arrEn1[i].boom++;
}
}
//如果英雄机爆炸量与血量相同,则判断游戏结束
if(theHero.boom>theHero.health-1){
theHero.boom = theHero.health;
theHero.draw();
//把游戏进行布尔值置为false
gameStart = false;
//绘画所得分数
ctx.fillStyle = 'orangered';
ctx.font = '20px Consoles';
ctx.fillText('游戏结束,您获得的分数:'+score,10,canvas.height/2);
break;
}
}
//绘画英雄机血量条
ctx.fillRect(10,canvas.height-20,350*((theHero.health-1-theHero.boom)/theHero.health),10);
// 判断子弹击中敌机
for(var i = 0;i<arrBullet.length;++i){
for(var j=0;j<arrEn1.length;++j){
//两个循环分别遍历子弹数组与敌机数组
//获得每个子弹对象的位置与每个敌机的位置
var btL = arrBullet[i].x;
var btR = arrBullet[i].x + arrBullet[i].width;
var btT = arrBullet[i].y;
var btB = arrBullet[i].y + arrBullet[i].height;
var enL = arrEn1[j].x;
var enR = arrEn1[j].x + arrEn1[j].width;
var enT = arrEn1[j].y;
var enB = arrEn1[j].y + arrEn1[j].height;
//对它们逐一进行判断是否碰撞
if(!(btL>enR||btR<enL||btT>enB||btB<enT)){
//碰撞到就在子弹数组中删除该子弹对象
arrBullet.splice(i,1);
i--;
//打中一下,爆炸增量加1
arrEn1[j].boom++;
//爆炸增量等于血量时,清除该敌机对象
if(arrEn1[j].boom==arrEn1[j].health-1){
//击中对应敌机加对应分数
if(arrEn1[j].type=='en1'){
score += 300;
}
else if(arrEn1[j].type=='en2'){
score += 500;
}
else if(arrEn1[j].type=='en3'){
score += 1000;
}
//删除对应敌机对象
arrEn1.splice(j,1);
j--;
break;
}
break;
}
}
}
//在左上角绘画分数
ctx.fillStyle = 'black';
ctx.font = '20px Consoles';
ctx.fillText('分数:'+score,10,30);
ctx.fillStyle = 'orange';
//如果游戏进行布尔值为true,
//则利用window.requestAnimationFrame重复调用move主动画函数
//进行不断绘画
if(gameStart == true){
window.requestAnimationFrame(move);
}
}
4 最后效果图
5 总结
原来做一个游戏需要做好很多工作,这是我以前作为一个玩家所无法体会到的,在此感谢给予过我游戏乐趣的前辈们。