游戏状态管理

游戏状态管理

1 游戏状态管理

1.1 游戏状态

如果游戏一直是游戏运行状态,显然是不合理的,正常的逻辑是:

  1. 游戏启动以后进入准备好状态
  2. 当点击鼠标以后,游戏开始运行
  3. 鼠标离开游戏范围时后,进入暂停
  4. 如果英雄机的生命被消耗干净,则游戏结束
1.2 鼠标单击事件

鼠标点击事件处理:

  1. 声明鼠标事件处理类(鼠标事件监听器)
  2. 创建鼠标事件处理类的对象,并且注册到面板上
  3. 当有鼠标事件发生时候,Java API会自动调用事件处理方法

案例:

鼠标事件处理类(鼠标事件监听器)

package demo01;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class MouseAction extends MouseAdapter{
    @Override
    public void mouseClicked(MouseEvent e){
        System.out.println("鼠标点击了");
    }
 }

重构World的action方法,创建监听器对象并且注册到面板

 public void action(){//启动方法
        Timer timer=new Timer();
        LoopTask task=new LoopTask();//定时器任务
        timer.schedule(task,1000,1000/100);//规定计划每间隔多少时间执行
     //将鼠标事件处理对象(鼠标监听器对象)挂载到当前面板上
     MouseAction action=new MouseAction();
     this.addMouseListener(action);
     
     this.addMouseMotionListener(new MouseAdapter(){
         @Override
         public void mouseMoved(MouseEvent e){
             //鼠标事件发生时候,e对象中包含着鼠标相关的数据
             //如x,y
             int x=e.getX();//获得发生鼠标事件时候,鼠标x坐标
             int y=e.getY();//获得发生鼠标事件时候,鼠标y坐标
             //System.out.println("鼠标移动ok:"+x+","+y);
             hero.move(x,y);// 把英雄机移动动xy的位置
         }
    });
}

2 READY状态

利用鼠标事件控制变量state的状态。

游戏启动以后立刻进入READY状态,READY状态时候游戏效果实现步骤:

  1. 在World 类上声明游戏的4种状态,并且定义变量存储当前状态,默认值上READY

  2. 单击鼠标时切换到运行状态,监听鼠标事件,在点击鼠标时候,如果为READY状态,就切换到RUNNING状态,修改鼠标移动事件,READY状态时候,游戏不移动

  3. 修改定时任务LoopTask,处理READY状态时候各种对象状态

    • 飞机不能出场,飞机也无需移动,子弹无需移动,不需要检测碰撞
    • 为了增强视觉效果,可以让天空移动。
  4. 绘制开始图片,修改paint方法,在READY状态时候绘制开始图片

2.1 定义四种状态

重构World类:

public class World extends JPanel{
    
	public static final int READY=0;//准备状态
	public static final int RUNNING=1;//运行
	public static final int PAUSE=2;//暂停
	public static final int GAME_OVER=3;//结束
	/*
	 *当前状态 ,初始值是READY
	 */
	private int state=READY;//把准备状态设置为state
2.2 重构鼠标事件

重构World类Action方法中的鼠标事件,RUNNING状态下才能移动英雄机,添加鼠标点击事件,点击发生时候将READY状态切换到RUNNING状态:

public void action(){//启动方法
        Timer timer=new Timer();
        LoopTask task=new LoopTask();//定时器任务
        timer.schedule(task,1000,1000/100);//规定计划每间隔多少时间执行
     //将鼠标事件处理对象(鼠标监听器对象)挂载到当前面板上
      this.addMouseListener(new MouseAdapter(){
         @Override
         public void mouseClicked(MouseEvent e){
             //鼠标点击执行的方法
             if(state==READY){//如果状态为准备状态
                 state=RUNNING;//把状态设置为运行
             }
         }
    });
     this.addMouseMotionListener(new MouseAdapter(){
         @Override
         public void mouseMoved(MouseEvent e){
             if(state==RUNNING){//如果准备状态切换为运行状态。缩进括起来以后为只有运行为RUNNING才运行
             //鼠标事件发生时候,e对象中包含着鼠标相关的数据
             //如x,y
             int x=e.getX();//获得发生鼠标事件时候,鼠标x坐标
             int y=e.getY();//获得发生鼠标事件时候,鼠标y坐标
             //System.out.println("鼠标移动ok:"+x+","+y);
             hero.move(x,y);// 把英雄机移动动xy的位置
             }
         }
    });
}
2.3 控制物体移动

重构World类,更改定时任务LoopTask类,设定只有在运行时候才能开火,创建飞机,移动物体,碰撞检查,清理无用飞机:

private class LoopTask extends TimerTask{//需要隐藏的属性和方法定义为private
		public void run() {
			index++;
			if(state==RUNNING) {
				fireAction();
				createPlane();//如果还想创建飞机直接调用方法
				objectMove();
				hitDetection();
				clean();//勿忘调用
				runAway();
			}
			sky.move();//不进行move方法。paint方法重写。进行单独处理将无法呈现连续的天空(sky)
			//调用重写绘制方法,这个方法会自动执行paint
			repaint();
		}
	}
2.4 绘制开始图片

重构World类中的paint方法在READY时候绘制开始图片,原理是在原有内容上面覆盖了一个半透明的图片:

public void paint(Graphics g) {
    sky.paint(g);
    hero.paint(g);

    for(int i=0; i<bullets.length; i++) {
        //i=0  1  2  3  4  5a
        bullets[i].paint(g);
    }
    //调用每个飞机的多态方法,实现多态的绘制
    for(int i=0; i<planes.length; i++) {
        planes[i].paint(g);
    }
    g.setColor(Color.white);//画白色
    g.drawString("SCORE:"+score,20,40);//分数在框内20,40位置显示
    g.drawString("LIFE:"+life,20,60);//血量在框内20,60位置显示
    if(state==READY) {//当前状态为准备状态
        Images.start.paintIcon(this, g, 0, 0);//飞机大战图像
    }
}
2.5清除测试数据

清除World类构造器中的测试数据,保留天空和英雄,planes和bullets数组内容清除掉,但是数组要保持0长度的数组,表示天空中没有任何物体,否则会因为planes或者bullets为null造成空指针异常:

public World(){
    //创建空数组,表示开始时候天空中没有物体
    //物体出场时候会自动扩容
    planes new FlyigObject[0];
    bullets=new Bullets[0];
    sky=new Sky();
    hero =new Hero(148,380);
}

3 RUNNING与PAUSE

运行状态RUNNING与暂停状态PAUSE处理逻辑不难,在开发调试游戏过程中始终是运行状态下调试的,而暂停状态就是让一切物体不能运动,处理方案如下:

  1. 更新定时器任务,仅在运行状态时候执行(已实现)

    • 可以创建新飞机
    • 可以射击新子弹
    • 可以移动每个物体
    • 可以检测是否发生碰撞
    • 清理无用的飞机子弹
  2. 运行时候,如果鼠标离开了当前面板区域,就进入暂停状态

  3. 暂停期间如果鼠标进入到当前面板区域就切换成运行状态

  4. 运行状态时候绘制暂停界面图片

3.1 鼠标进入,鼠标离开事件
public void action(){//启动方法
        Timer timer=new Timer();
        LoopTask task=new LoopTask();//定时器任务
        timer.schedule(task,1000,1000/100);//规定计划每间隔多少时间执行
     //将鼠标事件处理对象(鼠标监听器对象)挂载到当前面板上
      this.addMouseListener(new MouseAdapter(){
         @Override
         public void mouseClicked(MouseEvent e){
             //鼠标点击执行的方法
             if(state==READY){//如果状态为准备状态
                 state=RUNNING;//把状态设置为运行
             }
         }
          @Override
          public void mouseEntered(MouseEvent e){
              System.out.println("鼠标进入");
          }
          @Override
          public void mouseExited(MouseEvent e){
              System.out.println("鼠标离开");
          }
    });
     this.addMouseMotionListener(new MouseAdapter(){
         @Override
         public void mouseMoved(MouseEvent e){
             if(state==RUNNING){//如果准备状态切换为运行状态。缩进括起来以后为只有运行为RUNNING才运行
             //鼠标事件发生时候,e对象中包含着鼠标相关的数据
             //如x,y
             int x=e.getX();//获得发生鼠标事件时候,鼠标x坐标
             int y=e.getY();//获得发生鼠标事件时候,鼠标y坐标
             //System.out.println("鼠标移动ok:"+x+","+y);
             hero.move(x,y);// 把英雄机移动动xy的位置
             }
         }
    });
}
3.2 切换运行/暂停状态

重构World类中action方法中的鼠标事件,增加鼠标进入事件和离开事件:

public void action(){//启动方法
    Timer timer=new Timer();
    LoopTask task=new LoopTaskk();//定时器任务
    timer.schedule(task,1000,1000/100);//规定计划每间隔多少时间执行
    //将鼠标事件处理对象(鼠标监听器对象)挂载到当前面板上
    this.addMouseListener(new MouseAdapter(){
        @Override
        public void mouseClicked(MouseEvent e){
            //鼠标点击执行的方法
            if(state==READY){//如果状态为准备状态
                state=RUNNING;//把状态设置为运行
            }
        }
        @Override
        public void mouseEntered(MouseEvent e){
            System.out.println("鼠标进入");
            if(state==PAUSE){
                state=RUNNING;
            }
        }
        @Override
        public void mouseExited(MouseEvent e){
            System.out.println("鼠标离开");
            if(state==RUNNING){
                state=PAUSE;
            }
        }
    });
    this.addMouseMotionListener(new MouseAdapter(){
        @Override
        public void mouseMoved(MouseEvent e){
            if(state==RUNNING){//如果准备状态切换为运行状态。缩进括起来以后为只有运行为RUNNING才运行
                //鼠标事件发生时候,e对象中包含着鼠标相关的数据
                //如x,y
                int x=e.getX();//获得发生鼠标事件时候,鼠标x坐标
                int y=e.getY();//获得发生鼠标事件时候,鼠标y坐标
                //System.out.println("鼠标移动ok:"+x+","+y);
                hero.move(x,y);// 把英雄机移动动xy的位置
            }
        }
    });
}
3.3 显示暂停图片

在游戏界面上覆盖一个半透明的暂停图片,感知目前的游戏状态:

public void paint(Graphics g) {
    sky.paint(g);
    hero.paint(g);

    for(int i=0; i<bullets.length; i++) {
        //i=0  1  2  3  4  5a
        bullets[i].paint(g);
    }
    //调用每个飞机的多态方法,实现多态的绘制
    for(int i=0; i<planes.length; i++) {
        planes[i].paint(g);
    }
    g.setColor(Color.white);//画白色
    g.drawString("SCORE:"+score,20,40);//分数在框内20,40位置显示
    g.drawString("LIFE:"+life,20,60);//血量在框内20,60位置显示
    if(state==READY) {//当前状态为准备状态
        Images.start.paintIcon(this, g, 0, 0);//飞机大战图像
    }
    if(state==PAUSE){
        Images.pause.paintIcon(this, g, 0, 0);//暂停图像
    }
}

4 GAME_OVER

游戏结束:如果英雄的生命数量为0,不能再创建英雄了则游戏结束:

  1. 当英雄和某个飞机相撞时,飞机被撞死,英雄机也死亡
  2. 英雄在死亡状态下播放死亡动画,播放完毕后进入僵尸状态
  3. 如果英雄是僵尸状态,生命值减1,创建新英雄,如果没有生命可以减少了,则游戏结束
  4. 只有英雄活着才能射击
  5. 游戏结束状态时候点击鼠标进入READY状态
  6. 游戏结束状态时候绘制游戏结束图片
4.1 检测英雄是否发生碰撞

在World中创建runAway(逃跑)方法,用于检测英雄逃跑过程是否与飞机发生碰撞。

算法规则:

1.如果英雄是活着的时候,才能检测英雄是否与其他飞机发生了碰撞,如果碰撞发生,则英雄死掉,被撞飞机也要死掉,英雄死掉以后会播放死亡动画,动画播放完毕进入僵尸状态

如果英雄当前状态是动画播放完毕以后的僵尸状态,就创建一架新飞机,并且减少英雄机的数量

在定时循环中调用runAway方法

代码:

/*
	 * 检查英雄躲避飞机期间,是否发生了与飞机碰撞
	 */
private void runAway() {
    if(hero.isLiving()) {
        for(int i=0; i<planes.length; i++) {
            if(!planes[i].isLiving()) {
                continue;
            }
            if(hero.duang(planes[i])) {
                hero.goDead();
                planes[i].goDead();
                break;
            }
        }
    }else if(hero.isZombie()) {//如果飞机是僵尸状态
        if(life>0) {//如果生命大于0
            hero=new Hero(138,380);//那么再创建一架新飞机
            //保险:清理当前的全部飞机,避免与新英雄机相撞
            for(int i=0; i<planes.length; i++) {
                planes[i].goDead();
            }
            life--;//生命减少一条
        }else {
            state=GAME_OVER;//生命耗尽,游戏结束
        }
    }
}

在定时任务中调用这个runAway方法:

private class LoopTask extends TimerTask{//需要隐藏的属性和方法定义为private
    public void run() {
        index++;
        if(state==RUNNING){
            fireAction();
            createPlane();//如果还想创建飞机直接调用方法
            //调用重写绘制方法,这个方法会自动执行paint
            objectMove();
            hitDetection();
            runAway();
            clean();//勿忘调用
        }
        //调用重写绘制方法,这个方法会自动执行paint
        sky.move();//不进行move方法。paint方法重写。进行单独处理将无法呈现连续的天空(sky)
        repaint();
    }
}
4.2 重构射击方法

只能英雄活着的时候才能射击(自动):

private void fireAction(){
    if(! hero.isLiving()){
        return;
    }
    if(index % 15==0){
        //在定时任务中执行英雄开火方法
        Bullet[] bubu=hero.openFire();
        //子弹数组扩容
        int len=bullets.length;
        Bullet[] arr=Arrays.copyOf(bullets,len+bubu.length);
        //将子弹添加到新数组最后位置
        System.arraycopy(bubu, 0, arr, len, bubu.length);
        //替换原数组
        bullets=arr;
    }
}
4.3 鼠标事件处理

重构鼠标事件处理方法:

  1. 重构鼠标单击事件:游戏结束状态时候点击鼠标回到READY状态,此时需要清理全部变量的值,将数据恢复到开始游戏的状态
  2. 重构鼠标移动事件:只有英雄活着的时候才能移动英雄的位置:
public void action(){//启动方法
    Timer timer=new Timer();
    LoopTask task=new LoopTask();//定时器任务
    timer.schedule(task,1000,1000/100);//规定计划每间隔多少时间执行
    //将鼠标事件处理对象(鼠标监听器对象)挂载到当前面板上
    this.addMouseListener(new MouseAdapter(){
        @Override
        public void mouseClicked(MouseEvent e){
            //鼠标点击执行的方法
            if(state==READY){//如果状态为准备状态
                state=RUNNING;//把状态设置为运行
            }else if(state==GAME_OVER){
                score=0;
                life=3;
                planes=new FlyingObject[0];
                bullets=new Bullet[0];
                hero=new Hero(148,380);
                state=READY;
            }
        }
        @Override
        public void mouseEntered(MouseEvent e){
            System.out.println("鼠标进入");
            if(state==PAUSE){
                state=RUNNING;
            }
        }
        @Override
        public void mouseExited(MouseEvent e){
            System.out.println("鼠标离开");
            if(state==RUNNING){
                state=PAUSE;
            }
        }
    });
    this.addMouseMotionListener(new MouseAdapter(){
        @Override
        public void mouseMoved(MouseEvent e){
            if(state==RUNNING){//如果准备状态切换为运行状态。缩进括起来以后为只有运行为RUNNING才运行
                //鼠标事件发生时候,e对象中包含着鼠标相关的数据
                //如x,y
                int x=e.getX();//获得发生鼠标事件时候,鼠标x坐标
                int y=e.getY();//获得发生鼠标事件时候,鼠标y坐标
                //System.out.println("鼠标移动ok:"+x+","+y);
                if(hero.isLiving()){    
                    hero.move(x,y);// 把英雄机移动动xy的位置
                }
            }
        }
    });
}
4.4 绘制GAME_OVER图片

游戏结束状态时候显示“游戏结束”界面:

public void paint(Graphics g) {
    sky.paint(g);
    hero.paint(g);

    for(int i=0; i<bullets.length; i++) {
        //i=0  1  2  3  4  5a
        bullets[i].paint(g);
    }
    //调用每个飞机的多态方法,实现多态的绘制
    for(int i=0; i<planes.length; i++) {
        planes[i].paint(g);
    }
    g.setColor(Color.white);//画白色
    g.drawString("SCORE:"+score,20,40);//分数在框内20,40位置显示
    g.drawString("LIFE:"+life,20,60);//血量在框内20,60位置显示
    if(state==READY) {//当前状态为准备状态
        Images.start.paintIcon(this, g, 0, 0);//飞机大战图像
    }
    if(state==PAUSE){
        Images.pause.paintIcon(this, g, 0, 0);//暂停图像
    }
    if(state==GAME_OVER){
        Images.gameover.paintIcon(this, g, 0, 0);//结束图像
    }
}

飞机大战内存管理

  1. 方法区:在内存中分配一个静态区域,分配以后不回收,全部的类加载到方法区,加载以后长期占用空间,空间就不回收了
  2. 栈:内存中存储方法中局部变量数据的临时区域,方法定义的局部变量,以及方法参数都分配在栈中,this也是局部变量,也分配在栈中,一旦方法执行结束,方法占用的区域就会释放,释放的栈空间可以重复利用
  3. 堆:内存中用于存储对象(属性)的区域,Java的对象全部分配在堆中,分配对象时候按照类中定义实例对象分配对象存储空间,对象在new运算时候创建的,直到没有引用对象了,Java垃圾回收器就会回收对象,对象回收以后内存空间可以被重新利用。

完整飞机大战代码

重构的细节:

  1. 反转爆炸动画加载顺序,目的是反转播放顺序
  2. 重构部分if语句为switch case
  3. 爆炸动画居中绘制
  4. 加快爆炸动画播放速度
  5. 重构World类的构造器,抽取了初始化算法,与鼠标点击事件复用
1 Images 图片素材管理类
package demo1;

import javax.swing.ImageIcon;

public class Images {//管理图片
	public static ImageIcon[] airplane;
	public static ImageIcon[] bigplane;
	public static ImageIcon[] bee;
	public static ImageIcon bullet;
	public static ImageIcon[] hero;
	public static ImageIcon[] bom;
	public static ImageIcon sky;
	public static ImageIcon start;
	public static ImageIcon pause;
	public static ImageIcon gameover;
	static {//先创建数组,然后对数组初始化
		airplane=new ImageIcon[2];
		airplane[0]=new ImageIcon("images/airplane0.png");
		airplane[1]=new ImageIcon("images/airplane1.png");

		bigplane=new ImageIcon[2];
		bigplane[0]=new ImageIcon("images/bigairplane0.png");
		bigplane[1]=new ImageIcon("images/bigairplane1.png");

		bee=new ImageIcon[2];
		bee[0]=new ImageIcon("images/bee0.png");
		bee[1]=new ImageIcon("images/bee1.png");

		bullet=new ImageIcon("images/bullet.png");

		hero=new ImageIcon[2];
		hero[0]=new ImageIcon("images/hero0.png");
		hero[1]=new ImageIcon("images/hero1.png");

		bom=new ImageIcon[4];
		bom[0]=new ImageIcon("images/bom1.png");
		bom[1]=new ImageIcon("images/bom2.png");
		bom[2]=new ImageIcon("images/bom3.png");
		bom[3]=new ImageIcon("images/bom4.png");

		sky=new ImageIcon("images/background.png");

		start=new ImageIcon("images/start.png");

		pause=new ImageIcon("images/pause.png");

		gameover=new ImageIcon("images/gameover.png");
	}
	public static void main(String[] args) {
		System.out.println(airplane[0].getImageLoadStatus());//8
		System.out.println(airplane[1].getImageLoadStatus());
		System.out.println(bigplane[0].getImageLoadStatus());
		System.out.println(bigplane[1].getImageLoadStatus());
		System.out.println(bee[0].getImageLoadStatus());
		System.out.println(bee[1].getImageLoadStatus());
		System.out.println(bullet.getImageLoadStatus());
		System.out.println(hero[0].getImageLoadStatus());
		System.out.println(hero[1].getImageLoadStatus());
		System.out.println(bom[0].getImageLoadStatus());
		System.out.println(bom[1].getImageLoadStatus());
		System.out.println(bom[2].getImageLoadStatus());
		System.out.println(bom[3].getImageLoadStatus());
		System.out.println(sky.getImageLoadStatus());
		System.out.println(start.getImageLoadStatus());
		System.out.println(pause.getImageLoadStatus());
		System.out.println(gameover.getImageLoadStatus());
	}
}
2 FlyingObject 飞行物类
package demo1;

import java.awt.Graphics;

import javax.swing.ImageIcon;
/*
 * 父类中定义从子类抽取的属性和方法
 * 这种抽取方式称为“泛化”
 */
public abstract class FlyingObject {

	public static final int LIVING=1;
	public static final int DEAD=0;
	public static final int ZOMBIE=-1;//定义活着,死亡,僵尸

	protected int state=LIVING;//定义开始飞机都为活着

	protected int life=1;//定义生命值都为一条命

	protected double x;//需要留给子类的属性和方法定义为保护的protected
	protected double y;
	protected double width;
	protected double height;
	protected double step;
	protected ImageIcon image;//所有属性
	/**
	 * 当前对象动画帧,如果没有动画帧如;子弹,天空,则此属性保持null
	 */
	protected ImageIcon[] images;

	/**
	 * 爆炸动画帧,如果没有保持null
	 */
	protected ImageIcon[] bom;

	/**
	 * 动画帧播放计数器,%数组长度得到播放动画帧的位置
	 */
	protected int index=0;
	/*
	 * 无参数构造器,减少子类的编译错误
	 */
	public FlyingObject() {
	}
	public FlyingObject(double x, double y, ImageIcon image, ImageIcon[] Images, ImageIcon[] bom) {
		                                    //当前图片        全部动画帧       爆炸图片                                     
		this.x=x;
		this.y=y;
		this.image=image;
		this.images=Images;
		this.bom=bom;
		width=image.getIconWidth();
		height=image.getIconHeight();
	}
	public abstract void move();
	/*
	 * 被打击方法,没打击溢出加少名
	 * 如果打击成功,减命则返回true,否则false
	 */ 
	public boolean goDead() {
		if(state==LIVING) {//如果当前状态为活着
			life=0;//把生命值设成0
			state=DEAD;//把当前状态设为死亡的
			return true;//死亡返回true
		}else {
			return false;//没死亡返回false
		}
	}
	 public boolean hit(){
	        if(life>0){
	            life--;
	            if(life==0){
	                state=DEAD;
	            }
	            return true;
	        }
	        return false;
	    }
	/*
	 * 动画帧的播放方法
	 */
	public void nextImage() {
		switch(state) {//设置当前状态
		case LIVING://第一种情况,活着,注意符号
			
		    if(images==null) {//没有动画帧时候,不播放动画帧图片
			    return;
	 	}
		    //System.out.println(index +","+(index % images.length));//打桩
		   image=images[index++/30 % images.length];//除以30等于调慢30倍
		   break;//switch语句要写break
		case DEAD://另一种情况 死了,注意符号
			int index=i++/10;
			if(bom==null) {//如果没有这个照片,不做处理
				return;
			}
			if(index==bom.length) {//照片如果播放到头,成僵尸状态
				state=ZOMBIE;
				return;
			}
			image=bom[index];//不是僵尸状态,正常播放取照片交给当前照片
		}
	}

	public void paint(Graphics g) {
		nextImage();//换照片,换动画帧,然后再(在)绘制
        //居中绘制照片,主要是绘制爆炸效果居中
        int x1=(int)(x+(width-image.getIconWidth())/2);
        int y1=(int)(y+(height-image.getIconHeight())/2);
		image.paintIcon(null, g, (int)x, (int)y);
	}

	@Override
	public String toString() {
		String className=getClass().getName();
		return className+" [x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "]";
	}
	/*
	 * 碰撞检测方法
	 * 1,在父类型定义的duang方法可以被任何子类继承,所有子类都获得碰撞检测功能
	 * 2.将方法中的数据类型定义为FluingObject就可以处理各种多态的参数
	 */
	public boolean duang(FlyingObject bu) {
		FlyingObject p=this;
		//计算小飞机的内切圆数据
		double r1=Math.min(p.width, p.height)/2;
		double x1=p.x+p.width/2;
		double y1=p.y+p.height/2;
		//计算子弹的内切圆数据
		double r2=Math.min(bu.width, bu.height)/2;
		double x2=bu.x+bu.width/2;
		double y2=bu.y+bu.height/2;
		//利用勾股定理计算圆心距离
		double a=y2-y1;
		double b=x2-x1;
		double c=Math.sqrt(a*a + b*b);
		//如果圆心距离小于半径和就表示两个圆相交 就是发生了碰撞
		//System.out.println(c+","+(r1+r2));
		return c<r1+r2;
	}
	public boolean isLiving() {
		return state==LIVING;//是不是活的,状态等于活着
	}
	public boolean isDead() {//是不是死的,状态等于死亡
		return state==DEAD;
	}
	public boolean isZombie() {//是不是僵尸,状态等于僵尸
		return state==ZOMBIE;
	}
	
	public boolean outOfBounds() {
		return(y<-height-50)||(y>700+50);
	}
}
3 Sky天空
package demo1;

import java.awt.Graphics;

public class Sky extends FlyingObject{
	private double y0;//需要隐藏的属性和方法定义为私有的private
	
    public Sky() {
		super(0,0,Images.sky,null,null);
		step =0.8;
		y0=-height;
	}
    
	public void move() {
		y+=step;
		y0+=step;//(两个坐标都在移动)

		if(y>=height) {
			System.out.println("第一个照片返回:y="+y);
			y=-height;
		}
		if(y0>=height) {
			System.out.println("第二个照片返回:y0="+y0);
			y0=-height;
			System.out.println("y="+y+" y0="+y0+" width="+width+" height="+height);
		}
	}
	public void paint(Graphics g) {//改写父类中的方法
		image.paintIcon(null, g, (int)x, (int)y);
		image.paintIcon(null, g, (int)x, (int)y0);//对天空做修改,对两张照片进行处理
	}
}
4 Bullet子弹
package demo1;

public class Bullet extends FlyingObject{
	
	public Bullet(double x, double y) {
		super(x,y,Images.bullet,null,null);
		this.step =4;
	}
	/*
	 * 重写继承与超类的move方法,作用就是修改了超类move的行为
	 * 超类是向下移动,修改为向上移动
	 */
	public void move() {
		y -=step;
	}
}
5 Hero 英雄
package demo01;

public class Hero extends FlyingObject{

	public Hero(double x, double y) {
		super(x,y,Images.hero[0],Images.hero,Images.bom);
	}
	/*
	 * 重写move方法,空方法,目的是不动
	 * 修改超类中规定的向下飞,改成不动
	 */
	public void move() {
	}
	/*
	 *将鼠标机移动到鼠标位置X,Y
	 *@param x鼠标位置x
	 *@param y鼠标位置y
	 */
	public void move(int x, int y) {
		this.x=x-width/2;//偏移,飞机随着鼠标移动
		this.y=y-height/2;//偏移,飞机随着鼠标移动
	}
	/*
	 * 开火方法
	 */
	public Bullet fire() {
		double x=this.x+width/2-5;//当前英雄机的位置加上英雄一半减5
		double y=this.y-20;//y减去高度,假设为20,利用飞机的xy算出子弹的xy
		Bullet bullet=new Bullet(x,y);
		return bullet;
	}
	private int doubleFire=0;
	public void doubleFire() {
		doubleFire=20;//表示双枪次数为20
	}
	public Bullet[] openFire(){
		if(doubleFire>0) {//双枪状态大于0,处于双枪状态
			doubleFire--;//表示次数减1
			double x=this.x+width/2-5;//当前英雄机的位置加上英雄一半减5
			double y=this.y-20;//y减去高度,假设为20,利用飞机的xy算出子弹的xy
			Bullet b1=new Bullet(x+15,y);//创建子弹+15
			Bullet b2=new Bullet(x-15,y);//创建子弹-15
			return new Bullet[] {b1,b2};//创建数组封装返回,不能省略new Bullet
		}else {
			Bullet bullet=fire();//调用fire方法
			return new Bullet[] {bullet};//回到单枪
		}
	}
}
6 Plane 飞机类
package demo1;

import java.util.Random;

import javax.swing.ImageIcon;

public abstract  class Plane extends FlyingObject{
	public Plane() {
	}
	/*
	 * 根据位置初始化对象数据
	 */
	public Plane(double x, double y, ImageIcon image, ImageIcon[] Images, ImageIcon[] bom) {
		super(x, y, image, Images, bom);
	}
	/*
	 * 利用算法实现飞机从屏幕上方出场
	 */
	public Plane(ImageIcon image, ImageIcon[] Images, ImageIcon[] bom) {
		Random random=new Random();
		this.image=image;
		width=image.getIconWidth();
		height=image.getIconHeight();
		x=random.nextInt(400-(int)width);
		y=-height;
		this.images=Images;
		this.bom=bom;
	}

	public void move() {
		y +=step;
	}
}
7 Enemy敌人接口
package demo1;
/*
 *敌人接口
 */
public interface Enemy {
	/**
	 * 获取当前敌人被打掉以后的得分
	 * @return
	 */
	int getScore();
}
8 Award 奖励接口
package demo1;

public interface Award {
	/*
	 * 奖品
	 */
	int DOUBLE_FIRE=1;
	/*
	 * 命
	 */
	int LIFE=2;
	/*
	 * 获取奖品
	 * @return//返回值是DOUBLE_FIRE 之一 
	 */
	int getAward();
}
9 Airplane 小飞机
package demo1;

public class Airplane extends Plane implements Enemy{
	/*
	 * 飞机从屏幕上方随机位置出场
	 */
	public Airplane() {//添加重载构造器
		super(Images.airplane[0],Images.airplane,Images.bom);
		step=Math.random()*4+1.5;
	}
	/*
	 * 从自定位置出场
	 */
	public Airplane(double x, double y, double step) {
		super(x,y,Images.airplane[0],Images.airplane,Images.bom);
		this.step=step;
	}
	@Override
	public int getScore() {
		return 10;
	}
}
10 Bigplane 大飞机
package demo1;

public class Bigplane extends Plane implements Enemy{
	
	public Bigplane() {
		super(Images.bigplane[0],Images.bigplane,Images.bom);
		step=Math.random()*3+0.5;
		life=5;//生命值为5
	}

	public Bigplane(double x, double y, double step) {
		super(x,y,Images.bigplane[0],Images.bigplane,Images.bom);
		this.step = step;
		life=5;//生命值为5
	}
	@Override
	public int getScore() {
		return 100;
	}
}
11 蜜蜂(奖品)
package demo1;

public class Bee extends Plane implements Award{
	private int direction;//给蜜蜂一个方向
	public Bee() {
		super(Images.bee[0],Images.bee,Images.bom);
		step=Math.random()*5+2;
		direction=Math.random()>0.5?1:-1;//用随机数初始化,大于0.5的时候1,否则为负1
	} 
	public Bee(double x, double y, double step) {
		super(x,y,Images.bee[0],Images.bee,Images.bom);
		this.step=step;
		direction=Math.random()>0.5?1:-1;
	}
	/*
	 * 重写父类型move方法修改为斜向飞行
	 */
	public void move() {
		//调用父类型方法,复用父类型定义的算法

		super.move();//向下飞行y++
		x+=direction;
		if(x<0) {//如果蜜蜂碰到了左边缘
			direction=1;//往反方向飞,向右飞
		}else if(x+width>400) {//x+蜜蜂的宽度大于屏幕的宽度
			direction=-1;//从右向左飞
		}
	}
	@Override
	public int getAward() {
		return Math.random()>0.5?DOUBLE_FIRE:LIFE;
	}
}
12 World 世界
package demo1;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class World extends JPanel{

	public static final int READY=0;//准备状态
	public static final int RUNNING=1;//运行
	public static final int PAUSE=2;//暂停
	public static final int GAME_OVER=3;//结束
	/*
	 *当前状态 
	 */
	private int state=READY;//把准备状态设置为state

	private FlyingObject[] planes;//所有的可以被打掉的战机
	private Bullet[] bullets;
	private Sky sky;
	private Hero hero;
    private int index=0;//计数器
    
	private int life=3;//英雄生命值3条命
    private int score=0;
	/*
	 * 利用构造器初始化世界中每个物体
	 */
    //public void World(){
      //  init();
    //}
  
    //private void init(){}
	public World(){
		life=3;
        score=0;
		planes=new FlyingObject[0];//创建数组对象!
		bullets =new Bullet[0];
		sky=new Sky();
		hero=new Hero(138,380);

	}

	public void paint(Graphics g) {
		sky.paint(g);
		hero.paint(g);

		for(int i=0; i<bullets.length; i++) {
			//i=0  1  2  3  4  5
			bullets[i].paint(g);
		}
		//调用每个飞机的多态方法,实现多态的绘制
		for(int i=0; i<planes.length; i++) {
			planes[i].paint(g);
		}
		g.setColor(Color.white);//画白色
		g.drawString("SCORE:"+score,20,40);//分数在框内20,40位置显示
		g.drawString("LIFE:"+life,20,60);//血量在框内20,60位置显示
		if(state==READY) {//当前状态为准备状态
			Images.start.paintIcon(this, g, 0, 0);//飞机大战图像
		}
		if(state==PAUSE) {
			Images.pause.paintIcon(this, g, 0, 0);//暂停图像
		}
		if(state==GAME_OVER) {
			Images.gameover.paintIcon(this, g, 0, 0);//结束图像
		}
	}

	public static void main(String[] args) {
		JFrame frame=new JFrame();
		World world=new World();
		frame.add(world);
		frame.setSize(400,700);
		frame.setLocationRelativeTo(null);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);
		//调用action方法启动定时器
		world.action();
	}
	//private int index=0;//计数器
	//index=0 1 2 3 4 5 6 7 8 9 ....
	//              *       *
	//            index%4=0  缩小飞机出场的频率
	public void createPlane() {//创建一架飞机。进入到场地中
		if(index % 16==0) {
			Random random=new Random();
			int n=random.nextInt(10);//0-9
			Plane plane;//Java不允许一个变量里面重复定义//也可以更改为FlyingObject
			switch(n) {
			case 8:
			case 7:
				plane=new Bigplane();
				break;
			case 9:
				plane=new Bee();
				break;
			default:
				plane=new Airplane();
			}
			
			planes=Arrays.copyOf(planes, planes.length+1);//数组容量扩展+1
			//将新飞机添加到新数组到最后位置
			planes[planes.length-1]=plane;
		}
	}
	/*
	 * 添加内部类,实现定时计划任务
	 * 为何使用内部类实现定时任务
	 * 1,隐藏定时任务到world类中
	 * 2,可以访问外部类中的数据,飞机,子弹等
	 */
	private class LoopTask extends TimerTask{//需要隐藏的属性和方法定义为private
		public void run() {
			index++;
			if(state==RUNNING) {
				fireAction();
				createPlane();//如果还想创建飞机直接调用方法
				objectMove();
				hitDetection();
				clean();//勿忘调用
				runAway();
			}
			sky.move();//不进行move方法。paint方法重写。进行单独处理将无法呈现连续的天空(sky)
			//调用重写绘制方法,这个方法会自动执行paint
			repaint();
		}
	}
	private void objectMove() {//把内部类移动至当前的外部类
		for(int i=0; i<planes.length; i++) {//执行飞机移动方法;是多态的移动方法,每个飞机都不同
			if(planes[i].isLiving()) {//如果活着就动,死了便不动了
				planes[i].move();//利用for循环简化各项飞行代码
			}
		}
		for(int i=0; i<bullets.length; i++) {
			if(bullets[i].isLiving()) {//如果活着就动,死了便不动了
				bullets[i].move();
			}
		}
	}
	public void action() {//启动方法
		Timer timer=new Timer();
		LoopTask task=new LoopTask();//定时器任务
		timer.schedule(task, 100,1000/100);//规定计划每间隔多少时间执行

		this .addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				//鼠标点击执行的方法
				if(state==READY) {//如果状态为准备状态
					state=RUNNING;//把状态设置为运行
				}else if(state==GAME_OVER) {
					score=0;
					life=3;
					planes=new FlyingObject[0];
					bullets=new Bullet[0];
					hero=new Hero(138,380);
					state=READY;
				}	
			}
			@Override
			public void mouseEntered(MouseEvent e) {//鼠标进入
				System.out.println("鼠标进入");
				if(state==PAUSE) {//如果当前为暂停状态
					state=RUNNING;//那么改为运行
				}
			}
			@Override
			public void mouseExited(MouseEvent e) {//鼠标退出
				System.out.println("鼠标离开了");
				if(state==RUNNING) {//如果当前为运行状态
					state=PAUSE;//那么改为暂停
				}
			}
		});
		this.addMouseMotionListener(new MouseAdapter() {
			@Override
			public void mouseMoved(MouseEvent e) {
				if(state==RUNNING) {//如果准备状态切换为运行状态。缩进括起来以后为只有运行为RUNNING才运行
					//鼠标事件发生时候,e对象中包含鼠标相关的数据
					//如;x.y等
					int x=e.getX();//获取发生鼠标事件时候,鼠标x坐标
					int y=e.getY();//获取发生鼠标事件时候。鼠标y坐标
					//System.out.println("鼠标移动:"+x+","+y);
					if(hero.isLiving()) {	
						hero.move(x,y);// 把英雄机移动动xy的位置
					}  
				}
			}
		});
	}
	//private int score=0;
	/**
	 * 分数统计方法
	 * @param obj是被统计的对象
	 */
	public void scores(FlyingObject obj) {//对飞行物进行统计
		if(obj.isDead()) {//检测一下飞机是不是死飞机		
			if(obj instanceof Enemy) {//检测一下obj是不是敌机
				Enemy enemy=(Enemy)obj;//如果是就进行转换
				score +=enemy.getScore();//对分数进行统计
				System.out.println("score:"+score);//控制台输出一下分数
			}
			/*
			 * 处理奖励规则
			 */
			if(obj instanceof Award) {//如果被打掉的飞机是奖品类型
				Award award=(Award)obj;//转换为奖品类型
				int type=award.getAward();//拿到奖品
				if(type==Award.DOUBLE_FIRE) {//如果奖品为双枪
					hero.doubleFire();//把英雄设为双枪
				}else if(type==Award.LIFE){//如果奖品为生命
					life++;//生命加1
				}
			}
		}
	}

	public void hitDetection() {
		//拿到每个子弹
		for(int i=0; i<bullets.length; i++) {
			if(!bullets[i].isLiving()) {
				continue;
			}
			for(int j=0; j<planes.length; j++) {
				if(!planes[j].isLiving()) {
					continue;
				}
				if(planes[j].duang(bullets[i])) {
					//System.out.println(planes[j]+"撞到:"+bullets[i]);
					bullets[i].goDead();//子弹去死
					planes[j].hit();//飞机被打一下
					scores(planes[j]);//飞机打掉了检测一下
				}
			}
		}
	}

	private void fireAction() {
		if(!hero.isLiving()) {//如果不是活着状态不执行下一步
			return;
		}
		if(index % 15==0) {
			//在定时任务中执行英雄开火方法
			//Bullet bullet=hero.fire();
			Bullet[]bubu=hero.openFire();
			int len=bullets.length;
			//子弹数组扩容
			//bullets=Arrays.copyOf(bullets, bullets.length+1);
			Bullet[]arr=Arrays.copyOf(bullets, len+bubu.length);
			//将子弹添加到新数组到最后位置
			//bullets[bullets.length-1]=bullet;
			System.arraycopy(bubu,0,arr,len,bubu.length);
			bullets=arr;
		}
	}
	public void clean() {
		FlyingObject[] living=new FlyingObject[planes.length];
		int index=0;
		for(int i=0; i<planes.length; i++) {
			if(planes[i].isZombie()||planes[i].outOfBounds()) {//死亡并且解决内存泄漏走下一步
				continue;
			}
			living[index++]=planes[i];
		}
		planes=Arrays.copyOf(living, index);

		Bullet[]arr=new Bullet[bullets.length];
		index=0;
		for(int i=0; i<bullets.length; i++) {
			if(bullets[i].isDead()||bullets[i].outOfBounds()) {//死亡并且解决内存泄漏走下一步
				continue;
			}
			arr[index++]=bullets[i];
		}
		bullets=Arrays.copyOf(arr,index);
		//System.out.println(planes.length+","+bullets.length);
	}
	public void testClean() {//clean方法
		System.out.println(planes.length);
		planes[0].state=FlyingObject.ZOMBIE;
		planes[3].state=FlyingObject.ZOMBIE;
		clean();//调用方法
		System.out.println(planes.length);
	}
	/*
	 * 检查英雄躲避飞机期间,是否发生了与飞机碰撞
	 */
	private void runAway() {
		if(hero.isLiving()) {
			for(int i=0; i<planes.length; i++) {
				if(!planes[i].isLiving()) {
					continue;
				}
				if(hero.duang(planes[i])) {
					hero.goDead();
					planes[i].goDead();
					break;
				}
			}
		}else if(hero.isZombie()) {//如果飞机是僵尸状态
			if(life>0) {//如果生命大于0
				hero=new Hero(138,380);//那么再创建一架新飞机
				for(int i=0; i<planes.length; i++) {
					planes[i].goDead();
				}
				life--;//生命减少一条
			}else {
				state=GAME_OVER;//生命耗尽,游戏结束
			}
		}
	}
}
posted @ 2021-03-06 23:08  指尖上的未来  阅读(403)  评论(0)    收藏  举报