专栏首页魂祭心原 ionic+js+html5 飞行射击

原 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 条评论
登录 后参与评论

相关文章

  • 原 Promise 实现

    魂祭心
  • 原 js判断旋转中的图片里的元素与背景的某

    魂祭心
  • 原 (手工)base64加密解密

    魂祭心
  • JavaScript笔记整理

    整理一篇工作中的JavaScript脚本笔记,不定时更新,笔记来自网上资料或者自己经验归纳。

    用户1208223
  • 我自己开发的工具,打印出百度贴吧某用户发表过的所有帖子

    Jerry Wang
  • 我自己开发的工具,打印出百度贴吧某用户发表过的所有帖子

    Jerry Wang
  • 关于W3Cschool定义的设计模式--常用的9种设计模式的介绍

      tip:每种设计模式,其实都是为了更高效的,更方便的解决在面对对象编程中所遇到的问题。

    不会飞的小鸟
  • 分享前端开发常用代码片段

    如果你的网页中需要使用大量初始不可见的(例如,悬停的)图像,那么可以预加载这些图像。

    程序员宝库
  • iview 下拉刷新loadTop报错解决

    蓓蕾心晴
  • 智能搜索框实现思路--源码和流程图详解

    我们在浏览各大网站的时候,包括百度、淘宝、京东、雅虎等等网站,当我们输入一个单词或者文字的时候,下面会有一行行待选项供我们选择,很多的公司在做网站的时候也会考虑...

    何处锦绣不灰堆

扫码关注云+社区

领取腾讯云代金券