练习做的白鹭引擎小型飞机射击游戏
运行 egret start server -a
并不是常见的弹幕射击类游戏懒得设计杂兵排队出场的形式,敌人只有一台机体但是会回避、主动追上玩家并射击这种行为来制造难度。
难民版Ace Combat
主舞台StageScene侦听ENTER_FRAME事件刷新飞机子弹的位置并进行各种判断
private refreshStage(e:any){
//自机操作的刷新
this._playerController.btnTrigger()
this.refreshAllPosition();
if(this._collisionCheckInterval!=3){
//每3帧进行一次碰撞检测
this._collisionCheckInterval++
return;
}
this._collisionCheckInterval=0;
//自机和敌机的碰撞
if(!this._player.isInvinsible()){...}
//自机子弹的碰撞检测
for(let b of Bullet.allArr){...}
//敌机子弹的碰撞检测
if(!this._player.isInvinsible()){...}
//拾取漂流物判断
if(this._drift && this._drift.buff){...}
}
Controller类记录按键按下的状态刷新飞机的操作
public btnTrigger(e:any=null){
if(this._directX!=0 || this._directY!=0){
this._controllee.move(this._directX, this._directY);
}
if(this._shotTriggered){
this._controllee.shot();
}
}
飞机的移动简单地使用Timer改变坐标,冲刺则是Tween,做成了有限次数且随时间回复的形式,冲刺期间全程无敌,正常做法是冲刺开始后几帧无敌这样子,这次没有做那么细。其实我蛮讨厌捉无敌帧的
this.dashGaugeTimer.addEventListener(egret.TimerEvent.TIMER, this.onDashRecover, this);
public dash(dx:number, dy:number){
if(this.dashing){
return;
}
if(this.curDashGauge == 0){
return;
}
if(!this.dashGaugeTimer.running){
this.dashGaugeTimer.start();
}
this.curDashGauge --;
this.dashing = true;
...
}
protected onDashRecover(e:any){
if(this.curDashGauge < this.maxDashGauge){
this.curDashGauge ++;
}
if(this.curDashGauge == this.maxDashGauge){
this.dashGaugeTimer.reset();
}
}
public isInvinsible():boolean{
return this.dashing || this.buffManager.invinsibleBuff!=null;
}
射击则是从子弹池中获取Bullet实例,有一个子弹方向数组的属性,可以设置多方向射击
public shot(){
if(this.shotTimer.running){
return;
}
for(let i=0; i<this.bulletDirections.length; i++){
let b = this.bulletGenerator.getOne()
b.x = this.x;
b.y = this.y - this.pHeight - 50;
let d = this.bulletDirections[i];
b.setDirection(d[0], d[1])
this.parent.addChild(b);
b.shoot()
}
this.shotTimer.start()
}
//Bullet类
public static pool:Pool<Bullet> = new Pool<Bullet>(()=>{
let b = new Bullet()
Bullet.allArr.push(b);
return b;
})
//Pool类
public getOne():T{
let ret:T = null;
for(let obj of this._arr){
if(!obj.activate){
ret = obj;
break;
}
}
if(!ret){
ret = this._func();
this._arr.push(ret);
}
ret.activate = true;
return ret;
}
拾取道具出现的Buff,做了无敌、子弹方向+2(最多5方向发射)、子弹变大(中弹判定点从1个变3个)、得到僚机这几种,其实比起怎么做,更多考虑了平衡性比如速度、持续时间这种数值问题,毕竟只要能打出弹幕extreme难度也能乱杀==并且敌机也能拾取Buff。
//僚机Buff
public startEffect(owner:egret.Sprite){
super.startEffect(owner);
this.owner = owner;
let mainP:BasePlane = owner as BasePlane
this._subA = new SubPlane(mainP, "LEFT");
this._subB = new SubPlane(mainP, "RIGHT");
}
public delBuff(e:any=null){
this._subA.dispose();
this._subB.dispose();
super.delBuff(e);
}
//无敌Buff,用Tween做了闪烁的效果
public startEffect(owner:egret.Sprite){
this.owner = owner;
this._invisibleTween = egret.Tween.get(this.owner,{loop:true}).set({ alpha: 0}).wait(100).set({ alpha: 1}).wait(100)
if(this.timer){
this.timer.start();
}
}
public delBuff(e:any=null){
this._invisibleTween.pause().set({alpha:1})
super.delBuff(e)
}
虽然可以通过子弹上限、射击间隔参数等进一步限制玩家的强度,但就不做那么严格了,仍然可以通过左右移动射出弹幕轻松击中敌机。相对地设计了3种难度:normal,elite(数值比normal高并会持续射击),extreme(数值更高,并在追及玩家时会左右移动制造弹幕)。但是ai行为设计得不太满意,造成了一些不自然的表现:
现在的做法,通过全局的ENTER_FRAME时间控制移动,导致速度值设置得较高的extreme难度敌机仿佛瞬间移动一样,之后再改成单独处理。此外速度值高于玩家飞机就可以在x轴上轻松追上玩家,如果玩家不进行射击迫使敌机在x轴上移动的话,就变得好像双方操作同步了一样,敌机总是在x轴上同步移动紧跟着玩家,看上去很怪异==
AI行为,是根据飞机、子弹的位置确定下一个行为是攻击还是躲避并确定移动点。实际效果微妙,不能说聪明但也算能作出合理的行动,偶尔回避时往另一侧子弹上撞的问题再优化。这种AI设计我个人缺乏经验和好想法。
//躲避时会获取四象限方向中子弹数最少的方向
if(bp.x < this.x && bp.y < this.y - this.pHeight){
this._bulletDistribute[0] ++;
//不考虑机位后方的子弹
return;
}else if(bp.x > this.x && bp.y < this.y - this.pHeight){
this._bulletDistribute[1] ++;
//不考虑机位后方的子弹
return;
}else if(bp.x < this.x && bp.y > this.y){
this._bulletDistribute[2] ++;
}else if(bp.x > this.x && bp.y > this.y){
this._bulletDistribute[3] ++;
}
...
//根据子弹距离决定躲多远、是否使用冲刺。
if(!sameXArea && num > 1){
//子弹距离大于1机位时不改变行为
return;
}
if(!sameXArea && num < 1){
//x轴不同但位置接近,远离0.5-1机位
this.setCurPattern(AIActType.SLIGHT_AVOID);
return;
}
if(sameXArea && num < 2 && this.curDashGauge == 0){
//不能冲刺时,x轴相同但位置较远,远离1-2机位
this.setCurPattern(AIActType.AVOID);
this._movePoint = null;
return;
}
if(sameXArea && num < 1){
//x轴相同且位置接近,冲刺远离
this.setCurPattern(AIActType.DASH_AVOID);
this._movePoint = null;
return;
}
//extreme才有的制造弹幕的行为
//x轴上穿过玩家两侧射击后正对着玩家射击
if(this._playerPosition){
if(this._playerPosition.x < this.x - this.width){
this._movePoint = new egret.Point(this._playerPosition.x - this.width, this.y);
}else if(this._playerPosition.x > this.x + this.width){
this._movePoint = new egret.Point(this._playerPosition.x + this.width, this.y);
}else{
this._curPattern = AIActType.TRACE;
this._movePoint = new egret.Point(this._playerPosition.x, this.y);
}
}
应用前后台切换的暂停/重开处理,而部分功能使用了timer,使用stop、start会导致timer重新计时,代码只是降低了timer.delay以减轻影响。
class LifecycleCallback{
private static map:{[key:string]:LcyObj}= {};
public static regist(){
egret.lifecycle.onPause = this.onPause;
egret.lifecycle.onResume = this.onResume;
}
public static addFunc(key:string, f1:()=>any, f2:()=>any){
LifecycleCallback.map[key] = new LcyObj(f1,f2);
}
public static removeFunc(key:string, f1:()=>any, f2:()=>any){
delete LifecycleCallback.map[key]
}
private static onPause(){
for(let i in LifecycleCallback.map){
LifecycleCallback.map[i].onPause();
}
}
private static onResume(){
for(let i in LifecycleCallback.map){
LifecycleCallback.map[i].onResume();
}
}
}
这个demo唯一的优点就是BGM了吧,不愧是名曲,还能剪成4段来用