代码地址: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,安卓