Egret实战开发笔记,飞行射击游戏(四)

目录

    • 简介
    • 实现效果
    • 代码及过程
      • 一 .NPC是个数不确定的对象,需要使用工厂。
      • 二.有限状态机
      • 三.NPC阵列生成
      • 四.工厂之间的相互关系
      • 五 玩家子弹与NPC碰撞,玩家子弹打中NPC**
    • 总结

简介

今天是开发飞行射击游戏第四天,NPC机制和攻击血量效果。

实现敌机阵列效果出现和玩家子弹击中敌机NPC 血量减少至0消失。

欢迎大家和我一起学习哦~ 下面是详细过程代码,还有遇到的错误以及改正方法。

实现效果

代码及过程

一 .NPC是个数不确定的对象,需要使用工厂。

所有被工厂管理的对象都要有vis属性,只要=false,就消失。
1)创建NPC类和NPCManager类

public game:MainGame;
public im:egret.Bitmap[] = [];	//2张拼起来的图片所以用数组			//这里出现错误,原来写到
public vy:number;			//速度
public vis:boolean;			//工厂生存属性
public nm:NPCManager;			//上级指针
public constructor(x:number,y:number,vy:number,nm:NPCManager) {

	super();
	this.nm = nm;
	this.x = x ;this.y = y;
	this.vy = vy;

	//这个NPC是2张图片拼起来的所以第二张翻转正好拼成一个。为什么是半个图片,节省资源
	this.im[0] = Main.createBitmapByName("npc5_png");
	this.im[0].anchorOffsetX = this.im[0].width;
	this.im[0].anchorOffsetY = this.im[0].height/2;
	this.addChild(this.im[0]);
	this.im[1] = Main.createBitmapByName("npc5_png");
	this.im[1].anchorOffsetX = this.im[1].width;
	this.im[1].anchorOffsetY = this.im[1].height/2;
	this.im[1].scaleX = -1;
	this.addChild(this.im[1]);

	this.vis = true;
}

public update(){
	this.y +=this.vy;
	if(this.y > 1000){
		this.vis = false;
	}
}

出现的错误:
public im:egret.Bitmap[] = [];
原来写的public im:egret.Bitmap[]; 没有写=[ ]; 应该加强记忆

整理:完成一个NPC,但是游戏需要多个NPC,需要使用工厂管理,工厂需要create方法生成,同时,create方法生成的需要放到仓库中,再把仓库中的对象一个一个更新update。
只要是管理个数可变的对象,所有的工厂代码都是一样的,

2)NPCManager类
把ZDManager中代码全复制到NPCManager中名字相应修改就可以
public nm:Array; //Array动态数组(容器),通过添加,添加对象,移除对象。

public game:MainGame;

public constructor(game:MainGame) {
	super();
	this.game = game;
	this.nm = new Array();
}
//每create一次就new一个
public create(x:number,y:number,v:number){
	//生成子弹
	let one = new NPC(x,y,v,this);
	//添加到世界
	this.addChild(one);
	//放到仓库数组最后
	this.nm.push(one);
	
}
//更新所有子弹,找到每一颗子弹,每颗更新
public update(){
	//整个仓库长度 ,利用循环可以循环出所有子弹
	for(let i = 0 ; i < this.nm.length ; i++){
		//找到每颗子弹
		let one = this.nm[i];
		one.update();
		//若子弹太多,仓库会满,所以子弹需要移除
		//子弹出屏,vis == false。移除
		if(one.vis == false){
			//先从场景移除
			this.removeChild(one);
			//仓库移除 
			this.nm.splice(i ,1);
			//移除一个对象,长度-1
			i--;
		}
	}
}

在Maingame中申请,构造,更新,调用create方法。就可以显示

  public nm:NPCManager;
 this.nm = new NPCManager(this);
 this.addChild(this.nm);			//注意:nm要放在所有场景除背景外的最底层。
  this.nm.update();
   this.t--;
       if(this.t <=0){
           this.nm.create(Math.random()*480, -200,Math.random()*5 + 10);
           this.t = Math.random()* 20 + 10;
      }

二.有限状态机

多个NPC写多个NPC1,2,3类,都让它继承NPC类,而工厂管理时,管理NPC就可以,不用再管理单独的个体。这就是有限状态机的思想
父类与子类的继承关系,管理者管理父类,使用子类实现个体多样性的设计模式
将NPC类改为NPC0类

NPC类中申请

public vis:number;
public nm:NPCManager;
构造	
this.nm = nm;
this.vis = true;

父类当中update方法,完成npc的更新,但是我是父类,不是具体的NPC, 具体的npc是子类,不同的npc有不同的路线,作为父类,应该怎么动?父类不知道子类怎么动,也不需要管理子类怎么动。

在父类中update方法,实际的方法由子类写,所有的子类都需要具备update方法,但是不知怎么写,就需要把这个方法变为抽象。
抽象就是方法不写。

public abstract update();
**抽象方法:**我自己这个类,我自己不知道这个方法干什么,但是继承我的子类必须要实现的方法
父类完成,继承子类必须实现的方法。
如果类中包含抽象方法那么一定是抽象类。abstract class NPC extends egret.Sprit{}
这时,NPC0类继承NPC类,将egret.sprite改为NPC 并且super(nm); 就可以
之后将NPC0中的重复NPC的属性删除,

在NPCManager类中,create方法添加id:number;

let one = null;
		switch(id){
			case 0 :
				one = new NPC0(x,y,v,this);
			break;
		}

2.新建NPC1类,敌机飞下来转向回去
class NPC1 extends NPC{}

必须有update方法,否则NPC1会报错.因为NPC类是抽象方法,子类必须有.
需要实现敌机的飞下来转回去,需要多状态,申请t,m,vy:速度dy:停留位置。

public constructor(x:number,y:number,vy:number,dy:number,nm:NPCManager) {

	super(nm);
	this.x = x ;this.y = y;
	this.vy = vy;
	this.dy = dy;

	this.m = 0 ;
	this.t = 0;

	this.im[0] = Main.createBitmapByName("npc6_png");
	this.im[0].anchorOffsetX = this.im[0].width;
	this.im[0].anchorOffsetY = this.im[0].height/2;
	this.addChild(this.im[0]);
	this.im[1] = Main.createBitmapByName("npc6_png");
	this.im[1].anchorOffsetX = this.im[1].width;
	this.im[1].anchorOffsetY = this.im[1].height/2;
	this.im[1].scaleX = -1;
	this.addChild(this.im[1]);
}

public update(){
	switch(this.m){
		//向下飞
		case 0 :
			//向下飞
			this.y +=this.vy;
			//飞到停留位置
			if(this.y > this.dy){
				this.y = this.dy;
				this.t = 0 ;
				//进入下一状态
				this. m = 1 ;
			}
			break;
		//停留	
		case 1 :
			this.t++;
			//图层转向 
			//10次 主循环的时间从0变到180度
			if(this.x < 240) //屏幕左边逆时针转
			this.rotation = -180 * this.t/10;
			else 			//屏幕右边顺时针转
			this.rotation =  180 * this.t/10;
			if(this.t >=10){
				this.t = 0;
				this.m = 2 ;
			}
		break;
		//向上飞
		case 2 :
			this.y -=this.vy;
			//出屏,仓库清除
			if(this.y < -100){
				this.vis = false;
			}
		break;
	}
}

三.NPC阵列生成

1.NPC的生成模式 飞行射击游戏中常用的三种模式:
1)随机生成模式:一般就是随机生成,也有时根据游戏时间推进。时间越长或分数越高,生成NPC等级越高
2)时间队列生成:NPC按照固定队列,根据时间生成,到固定时间生成固定NPC,例如:30次主循环屏幕左生成3个NPC,50次主循环后屏幕右生成3个NPC。只要主循环到固定次数,就生成NPC。不管前面有没有NPC,有没有消失,都会出现。
3)阵列生成:一波一波出现,例如:左边3个NPC,若没有全打爆,则后面的不会出现。

1)随机生成

 this.nm.create( Math.floor(Math.random()*2),
        //    Math.random()*480, -200,Math.random()*5 + 10 , 300);
        //    this.t = Math.random()* 20 + 10;
        // }

2)时间队列过程:
将Maingame中update方法的create调用放在NPCManager当中,新建一个zl()方法,专用于create方法。并申请一个public t:number;//生成计时 并 this.t = 0 ;
在upda方法中调用this.zl();

	//NPC阵列
	public zl(){
		this.t++;
		switch(this.t){
			//左侧3
			case 10 :
				this.create( 0 , 100 , -100 , 15 , 0);
				this.create( 0 , 100 , -200 , 15 , 0);
				this.create( 0 , 100 , -300 , 15 , 0);
			break;
			//右侧3
			case 20 :
				this.create( 0 , 300 , -100 , 15 , 0);
				this.create( 0 , 300 , -200 , 15 , 0);
				this.create( 0 , 300 , -300 , 15 , 0);
			break;
			//中间3个
			case 30 :
				this.create( 1 , 200 , -100 , 15 , 300);
				this.create( 1 , 300 , -250 , 15 , 300);
				this.create( 1 , 100 , -250 , 15 , 300);
			break;
		}
}

根据生成NPC的不同也就是Id的不同,0号只需要位置,1号需要停留坐标。因为在使用同一个方法,0号也得有dy,但是没作用,这种情况下怎么办?
若是JAVA或者C 可以用重载。但白鹭当中不存在,也就是同名方法只能写一个,不可能通过参数的不同实现不同的方法,在白鹭当中的解决方法是用缺省参数。
在create方法中id,x,y,v,是必填的,但dy可能会没有,当参数可有可无时,就做成缺省参数。
例:public create( id:number , x:number , y:number , v?:number , dy?:number ){}
在需要缺省参数的后面加上?

当方法当中出现缺省参数,那么缺省参数之后的所有参数都必须是缺省。

若不这样写,在传参数就会出现问题:create( 0 , 100 ,100,) id0 x 100 y 100 v和dy都没有值为空
若create (0, 100 ,100 ,20); 20会给v,dy是缺省。 缺省都不填那没关系,若填了会按顺序赋值,后面少几个就少几个缺省值。

缺省参数会按照顺序赋值。

2)阵列模式:只有第一波NPC全部打爆之后,第二波才会出现。跟时间没关系
生成的波数用cID; 申请 public cID:number; //当前生成的波数
那么怎样知道第一波NPC打完呢?
用仓库长度判断。

public zl(){
		//仓库长度<=0 说明仓库是空的。
		if(this.nm.length <= 0){
			this.t++;
			//每波NPC间隔10个主循环
			if(this.t > 10)
			{	
				switch(this.cID){
					// 时间队列模式:根据时间生成  左侧3  case10,20,30
					case 0 :
						this.create( 0 , 100 , -100 , 15 );
						this.create( 0 , 100 , -200 , 15 );
						this.create( 0 , 100 , -300 , 15 );
					break;
					//右侧3
					case 1 :
						this.create( 0 , 300 , -100 , 15 );
						this.create( 0 , 300 , -200 , 15 );
						this.create( 0 , 300 , -300 , 15 );
					break;
					//中间3
					case 2 :
						this.create( 1 , 200 , -100 , 15 , 300);
						this.create( 1 , 300 , -250 , 15 , 300);
						this.create( 1 , 100 , -250 , 15 , 300);
					break;
				}
				this.t = 0;
				this.cID = 0;
			}
		}

四.工厂之间的相互关系

1)NPC子弹体系:NPC发射子弹
2)玩家打爆NPC

1)NPC和玩家子弹不一样,方向不同,碰撞对象不同。玩家子弹检测NPC,NPC碰撞检测玩家。
NPC子弹速度慢,子弹大。 因为飞行射击游戏的核心是 玩家躲子弹。
创建NZD类 ,复制ZD类到NZD类。

public constructor(id:number,x:number, y:number,//子弹生成位置
					   v:number, n:number,//速度与角度							
	 					game:MainGame) {
		super();
		this.game = game;
		this.id = id;

		switch(this.id){
			case 0 :
				this.im = Main.createBitmapByName("nzd1_png");
				this.addChild(this.im);
			break;
			case 1:
				this.im = Main.createBitmapByName("nzd16_png");
				this.addChild(this.im);
			break;
}

NZDManager类:
public zm:Array<ZD>;		//Array动态数组(容器),通过添加,添加对象,移除对象。
	public game:MainGame;

	public constructor(game:MainGame) {
		super();
		this.game = game;
		this.zm = new Array();
	}
	//每create一次就new一个
	public create(id:number,x:number,y:number,v:number,n:number,game:MainGame){
		//生成子弹
		let one = new ZD(id,x,y,v,n,game);
		//添加到世界
		this.addChild(one);
		//放到仓库数组最后
		this.zm.push(one);
		
	}
	//更新所有子弹,找到每一颗子弹,每颗更新
	public update(){
		//整个仓库长度 ,利用循环可以循环出所有子弹
		for(let i = 0 ; i < this.zm.length ; i++){
			//找到每颗子弹
			let one = this.zm[i];
			one.update();
			//若子弹太多,仓库会满,所以子弹需要移除
			//子弹出屏,vis == false。移除
			if(one.vis == false){
				//先从场景移除
				this.removeChild(one);
				//仓库移除 
				this.zm.splice(i ,1);
				//移除一个对象,长度-1
				i--;
			}
		}
	}

在Maingame中申请,构造,更新
构造时//因为是躲NPC子弹,所以放在玩家上面,可以很容易被看到。
this.addChild(this.nzm);
this.addChild(this.player);

NPC子弹发射次数
放在状态里面,子弹会发射很多,每次主循环执行一次
如果放在状态改变里,子弹会发射一组

case 1 :
			//间隔性发射子弹
				if(this.t % 3 == 0){
					this.nm.game.nzm.create(0,this.x ,this.y , 15,180,this.nm.game);
					this.nm.game.nzm.create(0,this.x ,this.y , 15,160,this.nm.game);
					this.nm.game.nzm.create(0,this.x ,this.y , 15,200,this.nm.game);
				}
				this.t++;

五 玩家子弹与NPC碰撞,玩家子弹打中NPC**

需要检测玩家子弹和NPC碰撞
玩家子弹ZD,和NPC0
一个提供碰撞检测isHit(),一个update方法调用isHit(),检测碰撞
NPC提供isHit(),子弹x,y检测isHit

在ZDManager类中,存放着所有玩家打出去的子弹,update方法中,for循环整个仓库中的子弹,同时把每颗子弹one执行更新,one就是遍历仓库的每一颗子弹,然后每颗子弹更新,再拿每颗子弹的坐标和所有 NPC碰撞检测是否发生碰撞。

在调用isHit方法时,父类需要有isHit方法,依然是抽象方法,所有的子类都继承这个方法。
在NPC父类中,添加isHit()方法

在NPC类添加 public abstract idHit(x:number,y:number):boolean;
父类中若没有这个方法,子类怎么写也不能用,所有的NPC对外以父类使用 。
在管理者当中,想使用的方法,属性,必须属于父类,只能看到父类方法。
父类有了isHit方法,子类也必须有,因为父类要用。
所有子类实现此方法
在ZDManager类的update方法的one.update()添加以下碰撞代码

//j是NPC game是上一级,nm:所有NPC(工厂) ,nm:仓库数组Array.length 仓库长度
		for(let j = 0 ; j < this.game.nm.nm.length; j ++){
			let npc = this.game.nm.nm[j];
			if(npc.isHit(one.x,one.y) == true){
				//NPC消失
				npc.vis = false;
				//子弹消失
				one.vis = false;
				//子弹和NPC碰撞结束,
				break;	
			}
		}

在NPC0,1,中添加isHit()方法
父类申请hp属性
子类this.hp = 10 ;赋值,不用再申请

npc.hp -= one.gj;
//NPC消失
if(npc.hp <= 0 ){
	npc.vis = false;
}

如果是种类索引,状态机实现多样化,只需要多写一个属性就可以。
如果是以有限状态机实现,父类子类时,如果想增加属性或方法,就得在父类中增加属性或方法,同时,所有子类都得增加属性或方法。

至此,实现初步的玩家攻击NPC直至消失。 NPC hp=3攻击3次消失。完成上面图片显示的效果。

总结

今天,完成了飞行射击 NPC机制阵列效果出现和玩家击中NPC消失的效果,
名头还会增加特效和子弹弹幕效果。当中出现了许多错误,自己也在总结和完善。

因为gif图片压缩的原因,可能有一些卡顿失真,但实际效果不会卡顿,请原谅。

至此,第四天的开发笔记已经完成,学习需要坚持,坚持到最后一定会有结果,每天写下笔记来记录自己的学习内容, 以后有需要也可以查看,大家可以一起学习。

想要我一起学习的可以关注我的公众号 知言不尽 找到我,交流学习,获取图片素材和源代码。

你可能感兴趣的:(Egret实战开发笔记)