js飞机大战( ͡° ͜ʖ ͡°)


一、游戏总体框架

飞机大战

总体框架:

	    *       index.html                              入口界面
        *       static                                  项目的素材等内容
        *          |_ src                               代码资源文件夹
        *          |   |_ mod                           模块文件夹
        *          |   |   |_ Background.js             背景模块
        *          |   |   |_ Player.js                 我方飞机
        *          |   |   |_ Boom.js                   爆炸图片
        *          |   |   |_ Bullet.js                 子弹效果
        *          |   |   |_ DialogModal.js            弹出层(死亡后重新开启游戏)
        *          |   |   |_ Enemy.js                  敌机
        *          |   |   |_ playerConfig.js           飞机配置事件
        *          |   |   |_ Score.js                  得分
        *          |   |_ lib                           封装的库
        *          |   |   |_ proto.js                  对象添加迭代器属性,实现对象解构赋值
        *          |   |_ Game.js                       游戏主函数的入口
        *          |   |_ Status.js                     数据管理中心
        *          |   |_ tool.js                       计算矩形的公有面积是否碰撞
        *          |_ images                            图片文件夹

框架图

运行方式

​ 运行index.html文件,然后按F12打开控制台,切换至移动端,刷新页面后,即可.

二、游戏总体内容

1.面向对象方案

本飞机大战游戏,采用面向对象的方式,通过模块化编程去编写…
同时,我们仅对外暴露出一个接口,一个Game的构造函数
此为游戏的入口文件 index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>飞机大战</title>
    <style>
        *{
            margin: 0;
        }
        html,body{
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <script type="module">
        import Game from "./static/src/Game.js";
        //初始化游戏
        //自动化做好所有事情 => 游戏初始化 (重启游戏)
        //type="module" 所有代码的作用域都是在script中,不会去全局内
        // let game = new Game(document.querySelector("body"))
        //于是用以下方法导出到全局中
        window.game = new Game(document.querySelector("body"))
    </script>
</body>
</html>

2.游戏主函数的入口Game.js

代码如下:

import "./lib/proto.js"
import status from "./Status.js";
import DialogModal from "./mod/DialogModal.js";
/*
* 游戏主函数的入口,用来初始化项目(canvas生成,事件的生成,项目尺寸数据的生成)
* */
class Game{
    constructor(container) {
        this.container = container
        //游戏的暂停状态,没有暂停的
        this.paused = false
        this.gameOver = false
        //绑定this
        this.render = this.render.bind(this)
        this.pause = this.pause.bind(this)
        this.continue = this.continue.bind(this)
        //操控dom,初始化函数,当游戏实例化时,执行constructor,并走到init()中
        this.initCanvas()
        /*
            重启游戏,包含第一次启动
        */
        this.restartGame()
    }
    /*
    * 初始化canvas标签,并设置尺寸,仅执行一次
    * */
    initCanvas(){
        this.canvas = document.createElement('canvas')
        this.canvas.style.display = 'block'
        this.canvas.width = this.container.getBoundingClientRect().width
        this.canvas.height = this.container.getBoundingClientRect().height
        this.ctx = this.canvas.getContext('2d')
        this.container.appendChild(this.canvas)
        this.restartDialog = new DialogModal(this.ctx)
        //状态初始化
        status.init(this.canvas)

        this.container.onblur = this.pause
        this.container.onfocus = this.continue

        this.size = {
            w:status.size.w,
            h:status.size.h
        }

    }

    restartGame(){

        cancelAnimationFrame(this.frame)
        //我不希望重启游戏的时候之前事件还是继续生效的
        this.removeEvent()
        //注册游戏基础事件
        this.initEvent()
        status.reset()
        this.continue()
        this.render()
    }
    //马不停蹄的去渲染,游戏的刷新页面控制器
    render(){
        this.frame =  requestAnimationFrame(this.render)
        // console.log("render")
        // requestAnimationFrame(()=>{this.render()}) //也可以采用箭头函数来实现

        if(this.paused) return
            //把笔迹擦干净
            // this.ctx.clearRect(0,0,this.size.w,this.size.h)

            this.ctx.clearRect(0,0,...this.size)
            /*首先 更新数据*/
            status.update()
            // 背景是 渲染在最底层的所以最先写
            status.render()
            // console.log(status.gameOver)
            if(status.gameOver){
                this.removeEvent()
                clearInterval(this.fireTimer)
                cancelAnimationFrame(this.frame)
                this.renderRestartRect()
                return
                // 跳转到渲染别的内容
            }
    }
    // 渲染重新开始有的界面
    renderRestartRect(){
        this.restartDialog.render()
        this.restartDialog.bindEvent()
        this.restartDialog.handle(() => {
            this.restartGame()
        })
    }
    removeEvent(){
        //将这个player之前添加的事件移除
        status.player.removeEvent(this.canvas)
    }
    /*初始化事件系统*/
    initEvent(){
        window.onresize = e => {
            status.setSize(window.innerWidth, window.innerHeight)
        }
        // 作弊按钮
        window.addEventListener("keydown", e => {
            if(e.key.toLowerCase() === "k"){
                status.enemyList.forEach( enemy => {
                    enemy.dead = true
                })
            }
        })
        status.player.initEvent(this.canvas)
    }

    //TODO 游戏暂停(事件系统会关闭)『全局』
    pause(){
        clearInterval(this.fireTimer)
        this.paused = true

    }
    //TODO 游戏继续
    continue(){
        clearInterval(this.fireTimer)
        this.fireTimer = setInterval(status.fire.bind(status),1000/8)
        this.paused = false
    }
}
export default Game

看到this.ctx.clearRect(0,0,this.size.w,this.size.h)的时候,会不会觉得有点复杂,
那么我该怎样使用一个可以去遍历的对象属性值,通过...

this.ctx.clearRect(0,0,...this.size) 

怎样变成这样呢?


3.这样一来,我们就需要这样一个文件 proto.js

/*
    拓展一些原型的方法
    可以去遍历的对象属性值,我们用在...中
    ...解构对象

    @returns {IterableIterator<*>}
*/
Object.prototype[Symbol.iterator] = function* (){
    for (let i in this){
        yield this[i]
    }
}

Function.prototype.onceBind = (function (){
    //这里的闭包只有1个,我们的bindMap只有属性名,属性值两个东西
    //针对于不同的函数,绑定不同的内容
    const bindMap = new Map()
    return function (obj){
        //需要对obj和函数进行关联 关联两个对象
        if(!bindMap.get(obj)){
            bindMap.set(obj,new Map())
        }
        //查询obj里面的map函数对应关系,每个函数  对应着 => 那个函数相同的bind返回的函数
        if(!bindMap.get(obj).get(this)){
            bindMap.get(obj).set(this,this.bind(obj))
        }
        return bindMap.get(obj).get(this)
    }
})();

/*
* 以上 onceBind:
*       用来相同函数bind相同对象的时候,返回的函数与绑定之后的函数是完全一致的
*
* 例如:
*   var obj = {};
*   var foo = function(){
*
*   }
*   var foo1 = foo.onceBind(obj)
*   var foo2 = foo.onceBind(obj)
*   foo1 == foo2
*   // true
*
*   函数 => 对象 => 唯一的绑定结果
*
* */
/* 一:
*  1.fn1.onceBind(obj1) => bindFn11  生成,存储,返回  第一次执行
*  2.再次执行 fn1.onceBind(obj1) 如果obj1已经在bindMap中,那么我就返回之前生成已存储的值
*  二:
*  3.fn2.onceBind(obj1) => 最开始并没找到,那我们就生成fn2 生成,存储,返回
*  三:
*  4:fn2.onceBind(obj2) => 最开始还是没有 我们创建 bindFn22
*  四:
*  5:fn1.onceBind(obj2) => bindFn12
* */
//一
// let bindMap = {
//     obj1:{
//         fn1:"bindFn11"
//     }
// }
// //二
// let bindMap = {
//     obj1:{
//         fn1:"bindFn11",
//         fn2:"bindFn21"
//     }
// }
// //三
// let bindMap = {
//     obj1:{
//         fn1:"bindFn11",
//         fn2:"bindFn21"
//     },
//     obj2:{
//         fn2:"bindFn22"
//     }
// }
// //四
// let bindMap = {
//     obj1:{
//         fn1:"bindFn11",
//         fn2:"bindFn21"
//     },
//     obj2:{
//         fn2:"bindFn22",
//         fn1:"bindFn12"
//     }
// }

/*
* Map是es6的方法:类似于对象,但比对象还要强大,键值对都可以是对象
* */

4.上面配置都出来了,怎么可能游戏没有背景呢 Background.js

import status from "../Status.js";
export default class Background {
    constructor(ctx) {
        this.ctx = ctx
        this.vy = 2
        //图片的位置和大小

        this.rect1 = {
            x:0,
            y:0,
            ...status.size
        }
        this.rect2 = {
            x:0,
            y:this.rect1.y - status.size.h,
            ...status.size
        }
        this.init()
    }
    /*
        在加载的时候,准备好渲染的图片以及自己的位置
    */
    init(){
        this.img = new Image()
        this.img.src = "static/images/bg.jpg"
    }
    reset(){
        this.rect1.y = 0
    }
    render(){
        // this.ctx.drawImage(this.img,0,0,status.size.w,status.size.h)
        // this.ctx.drawImage(this.img,0,0,...status.size)
        this.ctx.drawImage(this.img,...this.rect1)
        this.ctx.drawImage(this.img,...this.rect2)
    }
    update(){
        this.rect1.y += this.vy
        this.rect2.y = this.rect1.y - status.size.h
        //边界判断,瞬间归0
        if(this.rect1.y >= status.size.h){
            this.rect1.y = 0
        }
    }
}

5.背景都有啦,咱们开始造玩家吧 Player.js

import status from "../Status.js";
import playerConfig from "./playerConfig.js";
import boomImgList from "./Boom.js";
export default class Player {
    constructor(ctx) {
        this.ctx = ctx
        //飞机是不可以拖拽的
        this.draged = false

        //角色的大小位置
        this.rect = {
            x:status.size.w / 2 - 49,
            y:status.size.h - 65,
            w:98,
            h:65
        }
        //是否处于爆炸
        this.booming = false
        this.boomingCount = 0
        this.vip = 1
        this.init()
        this.level = 1
    }
    init(){
        this.playerImg = new Image()
        this.img = this.playerImg
        this.img.src = "static/images/hero.png"
    }

    reset(){
        this.dead = false
        this.booming = false
        this.boomingCount = 0
        this.img = this.playerImg
        this.rect = {
            x:status.size.w / 2 - 49,
            y:status.size.h - 65,
            w:98,
            h:65
        }
    }

    render(){
        this.ctx.drawImage(this.img,...this.rect)
    }

    update(){
        //不允许飞机飞到屏幕外
        if(this.rect.x < 0){
            this.rect.x = 0
        }
        if(this.rect.x > status.size.w - this.rect.w){
            this.rect.x = status.size.w - this.rect.w
        }
        if(this.rect.y < 0){
            this.rect.y = 0
        }
        if(this.rect.y > status.size.h - this.rect.h){
            this.rect.y = status.size.h - this.rect.h
        }
        //如果处于boom的时候
        if(this.booming && this.boomingCount < boomImgList.length){
            this.img = boomImgList[this.boomingCount++]
        }
        if(this.boomingCount === boomImgList.length){
            this.dead = true
        }
    }
    //初始化当前飞机的事件
    initEvent(dom){
        //先说几个交互任务
        //点是否在this.rect中 => 元素对象之间的交互(
        // 1.点和矩形是否重合 => 矩形和点是否有重合区域
        // 2.子弹和飞机交互 => 矩形和矩形是否有重合区域
        // 3.敌机和我方飞机的交互 => 矩形和矩形是否有重合区域
        // )
        // 函数bind方法每次都返回的是一个全信的函数,自己封装一个基于函数和绑定主题的唯一的函数bind结果
        // console.log("注册drag事件")
        playerConfig.forEach(item=>{
            item.handleList.forEach(fn =>{
                dom.addEventListener(item.type,fn.onceBind(this))
            })
        })
    }
    //移除之前注册事件
    //我们的bind每一次都返回新的函数,所以没有办法移除
    removeEvent(dom){
        playerConfig.forEach(item=>{
            item.handleList.forEach(fn =>{
                dom.removeEventListener(item.type,fn.onceBind(this))
            })
        })
    }
    kill(){
        this.booming = true
    }
}

6.来给我方飞机来个’皮肤’ playerConfig.js

import rectCollide from "../tools.js";

export default [
    {
        type: 'touchstart',
        handleList:[
            function(e){
                // console.log("touch")
                const mouseRect = {
                    x : e.changedTouches[0].clientX - 5,
                    y : e.changedTouches[0].clientY - 5,
                    w : 10,
                    h : 10
                }

                if(rectCollide(mouseRect,this.rect)){
                    this.draged = true
                }
            }
        ]
    },{
        type: 'touchmove',
        handleList:[
            function(e){
                if(!this.draged){
                    return
                }
                this.rect.x = e.changedTouches[0].clientX - this.rect.w /2
                this.rect.y = e.changedTouches[0].clientY - this.rect.h /2
            }
        ]
    },
    {
        type: 'touchend',
        handleList:[
            function(e){
                this.draged = false
            }
        ]
    }
]

7.咱也不可能打仗不带枪啊 Bullet.js

//这里是子弹 咻咻咻~~
import status from "../Status.js";

export default class Bullet{
    constructor(ctx) {
        this.ctx = ctx
        this.dead = false
        this.rect = {
            x:0,
            y:0,
            w:18,
            h:27
        }
        this.vy = -3
        this.init()
    }
    setPosition(rect){
        this.rect.x = rect.x + (rect.w - this.rect.w)/2
        this.rect.y = rect.y - this.rect.h/2
    }
    init(){
        this.img = new Image()
        this.img.src = "static/images/bullet.png"
    }
    render(){
        this.ctx.drawImage(this.img,...this.rect)
    }
    update(){
        this.vy -= 0.1
        this.rect.y += this.vy
        //死亡判断
        if(this.rect.y < - this.rect.h){
            this.kill()
        }
    }
    kill(){
        //杀死子弹
        this.dead = true
    }
}

8.敌机来喽 Enemy.js

//敌机来喽
import status from "../Status.js";
import boomImgList from "./Boom.js";

export default class Enemy{
    constructor(ctx) {
        this.ctx = ctx
        this.rect = {
            x:Math.random() * (status.size.w-60),
            y:-40,
            w:60,
            h:40
        }
        this.boomingCount = 0
        this.lives = 2
        //死了一半
        this.booming = false
        //死完了
        this.dead = false
        this.vy = Math.random() * 2 + 1
        this.init()
    }
    init(){
        this.img = new Image()
        this.img.src = "static/images/enemy.png"
    }
    render(){
        this.ctx.drawImage(this.img,...this.rect)
    }
    update(){
        //更新敌机事件
        this.rect.y += this.vy
        //飞到屏幕外面移除
        if(this.rect.y > status.size.h + this.rect.h){
            this.kill()
        }
        //如果是处于booming的状态,那就修改this.img
        if(this.booming && this.boomingCount<boomImgList.length){
            this.img = boomImgList[this.boomingCount++]
        }
        if(this.boomingCount === boomImgList.length){
            this.dead = true
        }
    }
    kill(count = 1){
        this.lives -= count;
        if(this.lives <= 0){
            this.booming = true
            //杀死敌机了 杀死了
            // this.dead = true
        }

    }

}

9.子弹爆裂图片 Boom.js

let length = 19
let boomImgList = []
for(let i = 0; i < length; i++){
    let img = new Image()
    img.src = `static/images/explosion${i+1}.png`
    boomImgList.push(img)
}
export default boomImgList

10.来看个分数吧 Score.js

import status from "../Status.js"

export default class Score {
  constructor(ctx){
    this.ctx = ctx
    this.ctx.font = "20px serif"
    this.ctx.fillStyle = "#ffffff"
    this.ctx.fontWeight = "bold"
    this.count = 0
  }
  add(){
    this.count ++
  }
  getMsg(){
    this.msg = `击杀敌机${this.count}`
    return this.msg
  }
  reset(){
    this.count = 0
  }

  render(){
    // console.log(this.getMsg(), status.size.w - 50, 0, 50)
    this.ctx.beginPath()
    this.ctx.fillText(this.getMsg(), status.size.w - 100, 20, 100);
  }
}


11.继续努力吧(死亡重新开始喽) 弹出层 DialogModal.js

import Status from "../Status.js"
import rectCollide from "../tools.js"
export default class DialogModal {
  constructor(ctx){
    this.ctx = ctx
    this.rect = {
      x: Status.size.w / 4,
      y: Status.size.h / 4,
      w: Status.size.w / 2,
      h: Status.size.h / 2,
    }
    this.init()
  }

  init(){
    this.img = new Image()
    this.img.src = "static/images/restart.png"
  }

  render(){
    this.rect = {
      x: Status.size.w / 4,
      y: Status.size.h / 4,
      w: Status.size.w / 2,
      h: Status.size.w / 2,
    }
    this.ctx.drawImage(this.img, ...this.rect)
  }
  click (e) {
    let touchRect = {
      x: e.touches[0].clientX - 5,
      y: e.touches[0].clientY - 5,
      w: 10,
      h: 10
    }
    if(rectCollide(touchRect, this.rect)){
      this.removeEvent()
      this.fn()
    }
  }
  handle(fn){
    this.fn = fn
  }


  bindEvent(){
    Status.canvas.addEventListener("touchstart", this.click.onceBind(this))
  }
  removeEvent(){
    Status.canvas.removeEventListener("touchstart", this.click.onceBind(this))
  }
}


12.数据管理中心 不管是项目的尺寸还是碰撞,都在我这里哦Status.js

/*
    数据管理中心
    项目尺寸 飞机和子弹的碰撞,点击位置和飞机的关系
    记录全局状态,可以在任何模块里去引入,然后获取全局的值
*/
import Background from "./mod/Background.js";
import Player from "./mod/Player.js";
import Bullet from "./mod/Bullet.js";
import Enemy from "./mod/Enemy.js";
import Score from "./mod/Score.js";
import rectCollide from "./tools.js";

class Status {
    constructor() {
        this.size = {
            w:0,
            h:0
        }
        this.gameOver = false
    }
    init(canvas){
        this.canvas = canvas
        this.ctx = this.canvas.getContext('2d')
        this.size.w = canvas.width
        this.size.h = canvas.height
        //初始化项目中的元素,包括背景,敌机等
        this.bg = new Background(this.ctx)
        this.player = new Player(this.ctx)
        this.score = new Score(this.ctx)
        //子弹列表
        this.bulletList = []
        //敌机列表
        this.enemyList = []
    }
    //发射子弹
    fire(){
        let bullet = null
        // 创建子弹并且渲染子弹 多个的
        switch (this.player.level) {
            case 1:
                bullet = new Bullet(this.ctx)
                bullet.setPosition(this.player.rect)
                this.bulletList.push(bullet)
                break
            case 2:
                bullet = new Bullet(this.ctx)
                let rect1 = {
                    x: this.player.rect.x - 10,
                    y: this.player.rect.y,
                    w: this.player.rect.w,
                    h: this.player.rect.h
                }
                bullet.setPosition(rect1)
                this.bulletList.push(bullet)

                bullet = new Bullet(this.ctx)
                let rect2 = {
                    x: this.player.rect.x + 10,
                    y: this.player.rect.y,
                    w: this.player.rect.w,
                    h: this.player.rect.h
                }
                bullet.setPosition(rect2)
                this.bulletList.push(bullet)
                break
        }
    }

    update(){
        this.bg.update()
        this.player.update()

        if(this.player.dead){
            this.gameOver = true
            return
        }


        this.bulletList.forEach( bullet => {
            bullet.update()
        })

        // 维护有效子弹
        this.bulletList = this.bulletList.filter( bullet => !bullet.dead)

        // 随机生成敌机
        if(Math.random() < 0.06){
            this.enemyList.push(new Enemy(this.ctx))
        }

        this.enemyList.forEach( enemy => {
            enemy.update()
        })
        // 维护敌机的生死
        this.enemyList = this.enemyList.filter( enemy => !enemy.dead)
        if(this.score.count > 20){
            this.player.level = 2
        }
        // 碰撞检测 子弹和 敌机的碰撞
        this.bulletList.forEach( bullet => {
            this.enemyList.forEach( enemy => {
                // 如果任何一架飞机和任何一发子弹有重合
                if(rectCollide(bullet.rect, enemy.rect)){
                    bullet.kill()
                    if(!enemy.booming){
                        enemy.kill(this.player.vip)
                        if(enemy.lives <= 0){
                            this.score.add()
                        }
                    }
                }
            })
        })

        // 我方飞机和敌机的碰撞

        this.enemyList.forEach( enemy => {
            if(rectCollide(enemy.rect, this.player.rect)  && !enemy.booming){
                enemy.kill(this.player.vip)
                this.player.kill()
            }
        })

    }
    render(){
        this.bg.render()
        this.player.render()
        // this.bullet.render()
        this.bulletList.forEach(bullet =>{
            bullet.render()
        })
        this.enemyList.forEach(enemy=>{
            enemy.render()
        })
        this.score.render()
    }
    reset(){
        this.gameOver = false
        this.bg.reset()
        this.player.reset()
        this.score.reset()
        this.bulletList = []
        this.enemyList = []
    }

    setSize(w, h){
        console.log("set")
        this.size.w = w
        this.size.h = h
    }

}

/*
    仅导出一次,
    也就是说,不管在哪里导入,都是相同的实例
*/
export default new Status()

13.飞机碰撞 tools.js

/*
* @param rectA :第一个矩形
* @param rectB :第二个矩形
* 计算这两个矩形 是否有重合的地方
* @return Number 表示是否重合
* */

function rectCollide(rectA,rectB){
    //计算矩形的公有面积,如果面积大于0,那么就相交,否则就不相交

    //理论应该是左上位置的值
    //两个矩形左上角x坐标中的最大值
    const xMin = Math.max(rectA.x,rectB.x)
    //两个矩形左上角y坐标中的最大值
    const yMin = Math.max(rectA.y,rectB.y)

    //理论应该是右下位置的值
    //两个矩形右下角x坐标中的最小值
    const xMax = Math.min(rectA.x + rectA.w,rectB.x + rectB.w)
    //两个矩形右下角y坐标中的最小值
    const yMax = Math.min(rectA.y + rectA.h,rectB.y + rectB.h)
    //计算宽高
    const width = xMax - xMin
    const height = yMax - yMin
    //如果有面积,就可以返回大于0的数,否则返回0
    if(width > 0 && height > 0){
        return width * height
    }else{
        return 0
    }
}
export default rectCollide

三、说在后头

在Player.js中 ,我写了个this.vip = 1,大家能想到什么嘛 (โ◑ヮ◑) ♡ (◐ヮ◐ใ),只有充钱才能变得强大. 在这里插入图片描述
当然,身为一个作者,怎么能连自己的飞机都打不完呢?不不不,这样绝对不可以.( ͡° ͜ʖ ͡°)于是偶在Game.js中,加入了作弊按钮哦,毕竟你可以用浏览器打开嘛~

		window.addEventListener("keydown", e => {
            if(e.key.toLowerCase() === "k"){
                status.enemyList.forEach( enemy => {
                    enemy.dead = true
                })
            }
        })

( ͡° ͜ʖ ͡°) ( ͡° ͜ʖ ͡°) ( ͡° ͜ʖ ͡°) ( ͡° ͜ʖ ͡°)
完整版代码呦~

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值