js+html5写一个简单的飞行游戏引擎,游戏画面使用canvas绘图,引擎核心代码不到500行,原生js,没有依赖。
代码地址:
游戏对象设计:
飞机(包括玩家和敌人)、子弹、击中效果。具体属性见代码注释
/** * 基类 */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,安卓