原 ionic+js+html5 飞行射击

js+html5写一个简单的飞行游戏引擎,游戏画面使用canvas绘图,引擎核心代码不到500行,原生js,没有依赖。

代码地址:https://github.com/hunjixin/ShootGame

游戏对象设计

飞机(包括玩家和敌人)、子弹、击中效果。具体属性见代码注释

/**
 * 基类
 */
function EObject (isShot) {
  this.Oid = -1 // id
  this.AllHp = 1 // 总HP
  this.Hp = 1 // 当前Hp
  this.icon // 图片
  this.width = 0 // 宽度
  this.height = 0 // 高度
  this.speedY = 5 // Y速度
  this.speedX = 5 // X速度
  this.position = {x: 0,y: 0} // 位置
  this.isDie = false // 是否死亡
  this.isShot = false // 是否处于发射状态
  this.shotInterVal = 500 // 发射周期
  this.enableShot = isShot // 是否发射

  var that = this
  this.interval // 发射器
  this.setShot = function (time) {
    if (! this.enableShot) return false
    this.shotInterVal = time
    clearTimeout(this.interval)
    this.interval = setInterval(function () {
      that.isShot = true
    }, time)
  }
}
/**
 * 敌军
 * @param {*是否发射} isShot 
 */
function Enemy (isShot) {
  this.enableShot = isShot
  this.type = 'common'
  EObject.call(this, isShot)
}
/**
 * 爆炸
 */
function Bullet () {
  EObject.call(this,false)
}
/**
 * 子弹
 */
function Shot () {
  this.type = 'common'
  this.Attact = 1 // 攻击力
  belong = 0
  EObject.call(this,false)
}
/**
 * 
 * @param {*玩家} isShot 
 */
function Player (isShot) {
  this.enableShot = isShot
  EObject.call(this, isShot)
}

事件设计:

玩家左右移动,飞机位置,涉及到的事件包括click,mousedown,mousemove,mouseup。当玩家点击屏幕时,直接触发的是canvas,然而需要触发的是在canvas上画出的对象,所以引擎内部需要实现一套以游戏对象为中心的事件机制。

事件包装:包装事件对象从中抽取需要的数据,封装成一个统一的内部事件对象

事件注册:按照object-action-callback的形式注册。

事件触发:玩家点击屏幕时,在外部事件中进行事件包装,再按照action-eventinfo的方式触发内部事件,内部事件管理者检索之前注册的对象,如果有效就调用注册的callback执行特定的对象操作。

这样设计主要是考虑如果直接使用dom事件,那么每个事件对每个需要触发的事件都要独立的有效性检查,代码重合和扩炸性都很差。通过这个方式可以将游戏引擎事件和dom事件隔离开,也方便了添加新的对象事件。

外部事件转内部事件:

  //移动事件
  var moveFunc = (function () {
    return function () {
      eventRelative.triggerEvent('mouseMove', pacakgeEvent(arguments[0]))
    }
  })()
 //按下事件
  var moveDownFunc = (function () {
    return function () {
      eventRelative.triggerEvent('mouseDown', pacakgeEvent(arguments[0]))
    }
  })()
  //抬起事件
  var moveUpFunc = (function () {
    return function () {
      eventRelative.triggerEvent('mouseUp', pacakgeEvent(arguments[0]))
    }
  })()
  //点击事件
  var clickFunc = (function () {
    return function () {
      eventRelative.triggerEvent('click', pacakgeClick(arguments[0]))
    }
  })()
  //事件输入
  this.EventInput = {
    mouseDown: moveDownFunc,
    mouseUp: moveUpFunc,
    click: clickFunc,
    move: moveFunc
  }

事件包装:

  //包装按键按下,抬起,移动事件
  var pacakgeEvent = function (event) {
    var evnetInfo = {
      position: {x: 0,y: 0}
    }
    if (option.isAndroid) {
      evnetInfo.position.x = event.gesture.center.pageX - player.width / 2 - event.gesture.target.offsetLeft
      evnetInfo.position.y = Util.sceneYTransform(event.gesture.center.pageY) - player.height / 2
    }else {
      evnetInfo.position.x = event.offsetX - player.width / 2
      evnetInfo.position.y = Util.sceneYTransform(event.offsetY) - player.height / 2
    }
    return evnetInfo
  }
  //包装单击事件
  var pacakgeClick = function (event) {
    var evnetInfo = {
      position: {x: 0,y: 0}
    }

    if (option.isAndroid) {
      evnetInfo.position.x = event.pageX - event.target.offsetLeft
      evnetInfo.position.y = Util.sceneYTransform(event.pageY)
    }else {
      evnetInfo.position.x = event.offsetX
      evnetInfo.position.y = Util.sceneYTransform(event.offsetY)
    }
    return evnetInfo
  }

内部事件管理机制:

  var eventRelative = {
    click: [],        
    mouseDown: [],     
    mouseUp: [],
    mouseMove: [],
    //附加事件中 object-action-callback
    attachEvet: function (target, action, callback) {
      var eventMsg = {target: target,callback: callback}
      var funcs = this[action]
      if (!funcs) throw new Error('not support event')
      funcs.push(eventMsg)
    },
    //触发事件中 action-eventInfo
    triggerEvent: function (action, eventInfo) {
      var funcs = this[action]
      if (!funcs) throw new Error('not support event')
      for (var i = 0;i < funcs.length;i++) {
        if (Util.isEffect(funcs[i].target, action, eventInfo)) {
          funcs[i].callback(funcs[i].target, eventInfo)
        }
      }
    }
  }

内部事件注册:

  //玩家开始移动
  eventRelative.attachEvet(player, 'mouseDown', function (obj, eventInfo) {
    plainMoveState.isMouseDown = true
  })
  //玩家停止移动
  eventRelative.attachEvet(player, 'mouseUp', function (obj, eventInfo) {
    plainMoveState.isMouseDown = false
  })
  //重置事件
  eventRelative.attachEvet(scene, 'click', function (obj, eventInfo) {
    if (!isRunning && !plainMoveState.isMouseDown) {
      isRunning = true
      reset()
    }
  })
  //玩家移动中
  eventRelative.attachEvet(scene, 'mouseMove', function (obj, eventInfo) {
    if (plainMoveState.isMouseDown === true) {
      plainMoveState.position.x = eventInfo.position.x
      plainMoveState.position.y = Util.sceneYTransform(eventInfo.position.y)
    }
  })

引擎核心设计:绘图、碰撞检测、对象运动、对象清理

鉴于js单线程问题,如果将所有的逻辑写在一条线上会导致单一流程过长,很可能无法保证画面的顺畅(要保证最低的24帧,那么两次渲染之间的事件间隔不到50ms)。

为了避开这个坑,一条核心原则是将4个模块完全隔离,每个模块的依赖仅仅是特定对象的状态,每个模块产生的影响也仅仅是修改特定对象的状态。设计类似于一个状态机。如子弹发射,对象会上挂一个time,每隔一段时间将自身的发射状态修改成可发射,对象运动模块会检查每个对象的发射状态,如果是可以发射的状态就为它创建子弹对象,再把状态修改成不可发射状态,玩家飞机移动的也采用了类似的机制。

实现方法是通过js的time定时触发模块的运行,通过调整time的触发间隔来控制系统的状态变化周期。由此带来的另一个好处是可以拉长不重要的模块触发间隔来节省资源(如对象清理,这个模块需要频繁的遍历,重建数组,慢)。

时间周期驱动

  this.Start = function () {
    // 拦截作用 必要时可以扩展出去
    var before = function (callback) {
      return function () {
        if (!isRunning) return
        callback()
      }
    }
    drawTm = setInterval(before(draw), 50)
    drawTm = setInterval(before(checkCollection), 50)
    moveTm = setInterval(before(objectMove), 50)
    clearTm = setInterval(before(clearObject), 5000)
  }

绘图模块:

绘图分为两个部分,一个是顶部的hp横条,一个是下方游戏主场景。为了避免频繁的绘制canvas,使用了双内存的技术,主场景先在一个内存canvas上绘制,最后再一次性绘制到主场景位置上。

/**
  * 绘图
  */
  function drawBuffer () {
    var canvas = document.createElement('canvas')
    var tempContext = canvas.getContext('2d')
    canvas.height = option.ctxHeight
    canvas.width = option.ctxWidth

    function drawEobject (eobj, rotateValue) {
      tempContext.drawImage(eobj.icon,
        eobj.position.x , eobj.position.y,
        eobj.width, eobj.height)
    }
    // 背景
    tempContext.drawImage(option.resources.bg, 0, 0,
      option.ctxWidth,
      option.ctxHeight)
    // 子弹
    for (var index in shots) {
      var shot = shots[index]
      drawEobject(shot)
    }
    // 飞机
    drawEobject(player)
    // 敌军
    for (var index in enemies) {
      var enemy = enemies[index]
      drawEobject(enemy)
    }
    // 死亡
    for (var index in bullets) {
      var bullet = bullets[index]
      drawEobject(bullet)
    }
    // 绘制文本
    if (option.isDebug) {
      var arr = statInfo.getDebugArray()
      for (var index = 0;index < arr.length;index++) {
        tempContext.strokeText(arr[index], 10, 10 * (index + 1))
      }
    }

    // head
    context.drawImage(option.resources.head, -5, 0, option.ctxWidth + 10, headOffset)
    // hp
    for (var index = 0;index < player.Hp;index++) {
      var width = (option.resources.hp.width + 5) * index + 5
      context.drawImage(option.resources.hp, width, 0, 20, headOffset)
    }
    // scene
    context.drawImage(canvas, // 绘制
      0, 0, canvas.width, canvas.height,
      0, headOffset, option.ctxWidth, option.ctxHeight - headOffset)
  }

对象碰撞检测模块:

检查玩家和敌军,玩家和子弹,敌军和子弹之间的碰撞,减hp,生成爆炸效果等等。

 // 检测碰撞
  var checkCollection = function () {
    var plainRect = {
      x: player.position.x,
      y: player.position.y,
      width: player.width,
      height: player.height
    }
    for (var i = enemies.length - 1;i > -1;i--) {
      var enemy = enemies[i]
      if (enemy.isDie) continue
      var enemyRect = {
        x: enemy.position.x,
        y: enemy.position.y,
        width: enemy.width,
        height: enemy.height
      }
      // 检查子弹和飞机的碰撞
      for (var j = shots.length - 1;j > -1;j--) {
        var oneShot = shots[j]
        if (oneShot.isDie) continue

        if (player.Oid == oneShot.belong && Util.inArea({x: oneShot.position.x + oneShot.width / 2,y: oneShot.position.y}, enemyRect)) {
          enemy.Hp--
          oneShot.Hp--
          if (enemy.Hp <= 0) {
            statInfo.kill[enemy.type]++

            enemy.isDie = true
            var bullet = new Bullet()
            bullet.isDie = false
            bullet.icon = option.resources.bullet
            bullet.width = 8
            bullet.height = 8
            bullet.position.x = oneShot.position.x + oneShot.width / 2
            bullet.position.y = oneShot.position.y
            bullets.push(bullet)
            setTimeout((function (enemy, bullet) {
              return function () {
                Util.removeArr(enemies, enemy)
                Util.removeArr(bullets, bullet)
              }
            })(enemy, bullet), 500)
          }
          // 子弹生命  穿甲弹
          if (oneShot.Hp <= 0) {
            oneShot.isDie = true
            setTimeout((function (shot) {
              return function () {
                Util.removeArr(shots, shot)
              }
            })(oneShot), 500)
          }
        }
      }
      // 检查玩家和飞机的碰撞
      if (Util.isChonghe(plainRect, enemyRect)) {
        enemy.Hp--
        player.Hp--
        if (enemy.Hp <= 0) {
          enemy.isDie = true
          setTimeout(function () {
            enemies = enemies.slice(0, i).concat(enemies.slice(i + 1, enemies.length))
          }, 100)
        }
      }
    }

    // 检查玩家是否被击中
    for (var j = shots.length - 1;j > -1;j--) {
      var oneShot = shots[j]
      if (oneShot.isDie) continue

      if (player.Oid != oneShot.belong && Util.inArea({x: oneShot.position.x + oneShot.width / 2,y: oneShot.position.y}, plainRect)) {
        player.Hp--
        oneShot.Hp--
        if (oneShot.Hp <= 0) {
          oneShot.isDie = true
          setTimeout((function (shot) {
            return function () {
              Util.removeArr(shots, shot)
            }
          })(oneShot), 500)
        }
      }
    }

    if (player.Hp <= 0) {
      isRunning = false
    }
  }

对象运动模块:

控制子弹发射,位置,敌军生成,位置。

 //对象移动
  var objectMove = function () {
    // 生成新的个体
    if (player.isShot) {
      var shot = Util.createShot(player, 0)
      shots.push(shot)
      player.isShot = false
      statInfo.emitShot[shot.type]++
    }

    if (plainMoveState.isMouseDown) {
      player.position = plainMoveState.position
    }

    if (Math.random() < 0.07) // 百分之七生成敌军
    {
      var rad = Math.random() * 3 + ''
      statInfo.allEnemy++
      Util.createEnemy(parseInt(rad.charAt(0)) + 2)
    }

    if (Math.random() < 0.01) // 百分之一生成强力敌军
    {
      statInfo.allEnemy++
      Util.createEnemy(1)
    }

    for (var index in shots) {
      if (shots[index].isDie) continue
      var shot = shots[index]
      shot.position.y -= shot.speedY
    }

    for (var index in enemies) {
      if (enemies[index].isDie) continue
      var enemy = enemies[index]
      enemy.position.y += enemy.speedY
      if (enemy.isShot) {
        var shot = Util.createShot(enemy, 1)
        shots.push(shot)
        enemy.isShot = false
        statInfo.emitShot[shot.type]++
      }
    }
  }

对象清理模块:

清理一些飞出边界的子弹,敌军。

  //对象清理
  var clearObject = function (that) {
    // 删除越界的对象  
    for (var i = shots.length - 1;i > -1;i--) {
      var oneShot = shots[i]
      if (!Util.inArea(oneShot.position, {x: -10,y: -10,width: option.ctxWidth + 10,height: option.ctxHeight + 10})) {
        Util.removeArr(shots, oneShot)
      }
    }

    for (var i = enemies.length - 1;i > -1;i--) {
      var enemy = enemies[i]
      if (enemy.isDie) {
        Util.removeArr(enemies, enemy)
        continue
      }
      if (!Util.inArea(enemy.position, {x: -100,y: -100,width: option.ctxWidth + 100,height: option.ctxHeight + 100})) {
        Util.removeArr(enemies, enemy)
      }
    }
  }

效果:

使用

      var en = new Engine()
      en.Create({
        id: 'myCanvas',
       // isAndroid: true,
        resources: {
          shot: shot,
          bullet: bullet,
          bg: bg,
          hp: hp,
          eshot: eshot,
          plainImg: plain,
          head: head,
          enes: [ene1, ene2, ene3, ene4]
        },
        attachEvent: $scope
      })
      en.Start()

测试环境ionic,安卓

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

HTML Agility Pack 搭配 ScrapySharp,彻底解除Html解析的痛苦

自从 Web 应用程序自 1993 年 W3C 设立以来就开始发展,而且 HTML 也历经了数个版本的演化(1.0 – 2.0 – 3.0 – 3.2 – 4....

22210

视觉搜索和Neo4j的最后一公里

“ 最后一公里 ”是电信行业使用的一个术语,指系统为实际使用该系统的客户提供链接。就图形数据库而言,它指的是终端用户可以从图中提取有价值的信息和洞察力。我们...

1873
来自专栏北京马哥教育

独爱 Vim 的Linux老司机理由竟然是这个!!

Vim是一个类似于Vi的著名的功能强大、高度可定制的文本编辑器,在Vi的基础上改进和增加了很多特性。VIM是自由软件。 Vim普遍被推崇为类Vi编辑器中最好的一...

3457
来自专栏企鹅号快讯

通过for循环嵌套语法绘制一个漂亮的蜂形图案

利用for循环嵌套画出一个蜂形图案。 代码如下: import turtle #导入小海龟 turtle.bgcolor('blue') #设置背景颜色 ...

2177
来自专栏landv

开启Golang编程第一章

Go is an open source programming language that makes it easy to build simple,rel...

741
来自专栏Golang语言社区

组件-实体-系统 (ECS \CES)游戏编程模型

一般来说,我们实现游戏实体都是采用面向对象的方法进行编程。每一个实体都是一个对象,并且需要一个基于类的实例化系统,允许实体通过多态 来扩展。但是,这样的方法,往...

1722
来自专栏申龙斌的程序人生

解密310 BTC(2)

价值1400万的比特币猜谜游戏刚火了几天,大奖就被一位高手全部取走,310 BTC的破解过程现在还没有公开,但已经有黑客公布了第二关的解法视频,过程相当复杂,我...

1781
来自专栏java一日一条

关于 Unicode 每个程序员应该知道的 5 件事

上周末,曝出了山寨WhatsApp Android应用程序的新闻,看似由相同的开发者提供作为了官方应用程序。欺诈分子通过在开发者名字中包含unicode非输出空...

892
来自专栏企鹅号快讯

据说看了这篇文章的小伙伴,都找到前端工作了,不信试试看

# 前端工作面试问题 本文包含了一些用于考查候选者的前端面试问题。不建议对单个候选者问及每个问题 (那需要好几个小时)。只要从列表里挑选一些,就能帮助你考查候选...

2747
来自专栏蓝天

代码规范:换行对齐问题

对一于单行代码过长时,采取换行,这个没有什么可争议的,主要焦点在新的一行从哪开始,通常有两派,一派就是如上述两段代码所示,另一派则采用如下规范:

772

扫码关注云+社区