微信飞机大战

您好,本篇文章主要描述如何用面向对象编程思想仿写微信飞机大战。
采用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 总结

原来做一个游戏需要做好很多工作,这是我以前作为一个玩家所无法体会到的,在此感谢给予过我游戏乐趣的前辈们。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容