飞机大战项目梳理

飞机大战项目总结

项目需求:实现由纯Java完成的飞机大战小游戏

  • 游戏分为四个状态,开始、运行、暂停、结束,当第一次进入游戏时程序默认为开始状态,需要玩家点击游戏界面才能运行游戏,游戏运行过程中如果鼠标移出游戏界面区域程序自动暂停游戏,当鼠标再次移入时游戏回复运行,当英雄机生命值等于 0 时游戏结束,再次点击回到开始界面。
  • 在游戏中,英雄机随鼠标移动而移动并自动发射子弹,敌机和小蜜蜂从游戏界面最上方随机出现,击毁敌机获得分数加成,击毁小蜜蜂获得生命值或火力值奖励。
  • 英雄机默认火力值为 0 单倍火力,当火力值大于 0 时发射双倍火力,每次发射火力值减 2 ,直到变为 0 则单倍火力。
    飞机大战项目梳理_第1张图片

下面开始介绍书写代码流程

可以分为三大步:

一、首先进行类的构造,将游戏所涉及的类创建好,并构造好游戏界面。
二、在游戏界面中加入所有类,并渲染使其动起来,删除越界飞行物。
三、加入碰撞判断,统计分数和生命值判断,并加入游戏状态完善程序。

一、首先我们来实现第一步:

  • 本游戏中有四个实体类,分别为敌机类、蜜蜂类、英雄机类、子弹类;
  • 我们不难发现这四个类都是在天上飞,并且都有类似属性,那么我们可以抽取一个共同的父类叫做飞行物类Flyingobject,该父类拥有五个属性:x、y坐标、宽度、高度。
  • 继承飞行物父类后我们还需要添加各个实体类特有的属性和构造方法。
  • 在敌机类和子弹类中我们需要添加一个speed属性作为每次刷新界面它们y坐标所移动的距离。
  • 蜜蜂类除了向下移动外还会左右移动所以我们需要给蜜蜂类添加横向和纵向两个方向的speed属性,另外击毁蜜蜂类会获得随机奖励,所以我们还需要设置一个AwardType属性来确定奖励返回类型。
  • 在英雄机类中需要设置生命值和火力值属性,另外为了达到动态视觉效果还需要一个图片数组和index属性切换两张英雄机图片。
  • 为方便以后的扩展,这里我们还将定义两个接口,敌人Enemy接口,奖励Award接口,在敌人接口中定义一个返回分数的方法,在奖励接口中定义一个返回奖励类型的方法,敌机类实现敌人接口,蜜蜂类实现奖励接口。
  • 其次我们还需要一个游戏主体类ShootGame,我们将main方法定义在此类中,在这个类中我们需要利用JFrame和Jpanel画出游戏背景,所以我们让ShootGame类继承Jpanel。
  • 拥有画板之后我们需要将各个类在画板中显示出来,这时我们需要重写Jpanel中的 paint()方法,将英雄机,敌机,蜜蜂,子弹分别在游戏界面中实现。

具体代码如下:
飞行物父类 FlyingObject:

public abstract class FlyingObject {

    protected int x;
    protected int y;
    protected int width;
    protected int height;
    protected BufferedImage image;

英雄机类 Hero:

public class Hero extends FlyingObject{

    private int LIFE;
    private int DOUBLE_FIRE;

    private BufferedImage[] images;
    private int index;

    public Hero() {
        this.image = ShootGame.hero0;
        this.width = image.getWidth();
        this.height = image.getHeight();
        this.x = 150;
        this.y = 400;
        this.LIFE = 3;
        this.DOUBLE_FIRE = 0;
        images = new BufferedImage[]{ShootGame.hero0,ShootGame.hero1};
        index = 0;
    }

敌机类 Airplane:

public class Airplane extends FlyingObject implements Enemy {

    private int speed = 2;

    public Airplane() {
        this.image = ShootGame.airplane;
        this.width = image.getWidth();
        this.height = image.getHeight();
        this.y = -this.height;
        Random random = new Random();
        this.x = random.nextInt(ShootGame.WIDTH - this.width);
    }

蜜蜂类 Bee:

public class Bee extends FlyingObject implements Award {

    private int xspeed = 1;
    private int yspeed = 2;
    private int awardType;

    public Bee() {
        this.image = ShootGame.bee;
        this.width = image.getWidth();
        this.height = image.getHeight();
        this.y = -this.height;
        Random random = new Random();
        this.x = random.nextInt(ShootGame.WIDTH-this.width);
        this.awardType = random.nextInt(2);
    }

子弹类 Bullet:

public class Bullet extends FlyingObject{

    private int speed = 3;

    public Bullet(int x,int y) {
        this.image = ShootGame.bullet;
        this.width = image.getWidth();
        this.height = image.getHeight();
        this.x = x;
        this.y = y;
    }

敌人接口 Enemy:

public interface Enemy {
    int getScore();
}

奖励接口 Award:

public interface Award {

    int DOUBLE_FIRE = 0;
    int LIFE = 1;
    int getType();
}

主程序类 ShootGame:

public class ShootGame extends JPanel{

    public static final int WIDTH = 400;
    public static final int HEIGHT = 654;

    public static BufferedImage background;
    public static BufferedImage start;
    public static BufferedImage pause;
    public static BufferedImage bee;
    public static BufferedImage airplane;
    public static BufferedImage bullet;
    public static BufferedImage gameover;
    public static BufferedImage hero0;
    public static BufferedImage hero1;

	static {
        try {
            background = ImageIO.read(ShootGame.class.getResource("/img/background.png"));
            start = ImageIO.read(ShootGame.class.getResource("/img/start.png"));
            pause = ImageIO.read(ShootGame.class.getResource("/img/pause.png"));
            gameover = ImageIO.read(ShootGame.class.getResource("/img/gameover.png"));
            airplane = ImageIO.read(ShootGame.class.getResource("/img/airplane.png"));
            bee = ImageIO.read(ShootGame.class.getResource("/img/bee.png"));
            bullet = ImageIO.read(ShootGame.class.getResource("/img/bullet.png"));
            hero0 = ImageIO.read(ShootGame.class.getResource("/img/hero0.png"));
            hero1 = ImageIO.read(ShootGame.class.getResource("/img/hero1.png"));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
    @Override
    public void paint(Graphics g) {
        paintBackground(g);
        paintHero(g);
        paintFlyingObjects(g);
        paintBullet(g);
    }

    public void paintBackground(Graphics g) {
        g.drawImage(background, 0, 0, null);
    }

    public void paintHero(Graphics g) {
        g.drawImage(hero.image, hero.x, hero.y, null);
    }

    public void paintFlyingObjects(Graphics g) {
        for (FlyingObject obj:flyings) {
            if (obj instanceof Airplane){
                g.drawImage(obj.image, obj.x, obj.y, null);
            }
            if (obj instanceof Bee){
                g.drawImage(obj.image, obj.x, obj.y, null);
            }
        }
    }

    public void paintBullet(Graphics g) {
        for (Bullet bullet: bullets) {
            g.drawImage(bullet.image, bullet.x, bullet.y, null);
        }
    }

    public static void main(String[] args) {

        JFrame frame = new JFrame("fly");
        ShootGame game = new ShootGame();
        frame.add(game);

        frame.setSize(WIDTH,HEIGHT);
        frame.setAlwaysOnTop(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

二、在游戏界面中加入所有类,并渲染使其动起来,删除越界飞行物。

  • 在第二部分我们有三个任务:加入各个类实例,使其动起来,删除越界飞行物,下面我们一一实现。
  • 首先加入各个实例,那么我们在ShootGame类中需要有能装他们的变量;
  • 所以我们在ShootGame类中先定义几个成员变量来接收实例;
    private Hero hero = new Hero();
    private FlyingObject[] flyings = {};	//装载敌机类和蜜蜂类
    private Bullet[] bullets = {};
  • 有了容器之后我们便可以生成相应的实例对象了,敌机和蜜蜂属于敌人应该从游戏区域正上方随机出现并往下走,那我们在ShootGame中创建一个方法产生敌人和蜜蜂即可,但是子弹应该属于英雄机的行为,所以创建子弹的方法应该写在Hero类中。
  • 具体代码如下:

ShootGame类:

//生成一个敌人
    public FlyingObject nextOne(){
        Random random = new Random();
        int one = random.nextInt(20);
        if(one<2){
            return new Bee();
        }else {
            return new Airplane();
        }
    }

生成子弹的方法:

    public Bullet[] shoot(){
        if (DOUBLE_FIRE>0){
            Bullet[] bs = new Bullet[2];
            bs[0] = new Bullet(this.x+this.width/4, this.y-20);
            bs[1] = new Bullet(this.x+this.width/4*3, this.y-20);
            DOUBLE_FIRE-=2;
            return bs;
        }else {
            Bullet[] bs = new Bullet[1];
            bs[0] = new Bullet(this.x+this.width/2, this.y-20);
            return bs;
        }
    }
  • 实例和容器都有了那么我们接下来就能实现敌人子弹入场了,在这之前我们需要思考一个问题如果代码全部写在main()方法中那么会显得main()方法非常臃肿,逻辑结构也不清晰,那么为了解决这个问题,我们可以写一个action()方法,此方法中的代码就是我们原来要写在main()方法中的,然后在action()方法中再分别调用敌人入场、飞行物移动的方法使得逻辑结构更加清晰。
  • 因为敌人入场和飞行物移动属于系统多次循环要做的事情,也就是不管玩家操不操作敌机都要自动出来所以此处我们需要一个定时器在action()方法中每隔一段时间自动调用一次,把需要程序自动运行的事情放在定时器中完成。

定时器代码如下:

Timer timer = new Timer();
        int intervel = 10;
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
           		//此处为定时器需要执行的内容


           		repaint();	//此方法确保每次改变后重新绘制游戏界面
            }
        }, intervel, intervel);
  • 接下来我们可以正式开始写敌人和子弹入场的方法了,只需要在定时器的run()方法中调用即可

Hero类中的产生子弹方法:

public Bullet[] shoot(){
        if (DOUBLE_FIRE>0){
            Bullet[] bs = new Bullet[2];
            bs[0] = new Bullet(this.x+this.width/4, this.y-20);
            bs[1] = new Bullet(this.x+this.width/4*3, this.y-20);
            DOUBLE_FIRE-=2;
            return bs;
        }else {
            Bullet[] bs = new Bullet[1];
            bs[0] = new Bullet(this.x+this.width/2, this.y-20);
            return bs;
        }
    }

敌人入场具体代码:

int enemyIndex = 0;  
    //敌人入场
    public void enterAction() {
        enemyIndex++;
        if (enemyIndex%20 == 0){ //控制产生敌人频率 200毫秒一次
            FlyingObject one = nextOne();
            flyings = Arrays.copyOf(flyings, flyings.length+1);
            flyings[flyings.length-1] = one;
        }
    }

子弹入场具体代码:

    public void shootAction() {
        if (enemyIndex%20==0){
            Bullet[] bs = hero.shoot();
            this.bullets = Arrays.copyOf(this.bullets, this.bullets.length+bs.length);
            System.arraycopy(bs, 0, this.bullets, this.bullets.length-bs.length, bs.length);
            enemyIndex=0;
        }
    }
  • 然后我们需要让所有的类动起来,既然是所有类都具有的行为那么我们可以在父类定义一个抽象方法step(),然后创建一个stepAction()其中调用各个飞行物的step()方法,最后在定时器run()方法中调用stepAction()即可。因为不同子类的具体实现不同所以父类只能用抽象方法,定义好之后再分别实现,Hero类的step方法是切换图片达到动态的视觉效果,Hero的移动根据鼠标相关所以有另外的方法。

Airplane类的实现:

 @Override
    public void step() {

        this.y += speed;
    }

Bee类的实现:

@Override
    public void step() {
        this.y+=yspeed;
        if (x>ShootGame.WIDTH-this.width){
            this.x-=xspeed;
        }
        if (x<0){
            this.x+=xspeed;
        }
    }

Bullet类的实现:

@Override
    public void step() {
        this.y-=speed;
    }

Hero类的实现:

@Override
    public void step() {
        image = images[index++/10%images.length];
        /**
         *  index++
         *  int a = index/10
         *  int b = a%2
         *  image = images[b]
         */
    }

stepAction()方法:

//飞行物移动
    public void stepAction(){
        hero.step();
        for (FlyingObject flyingObject:flyings) {
            flyingObject.step();
        }
        for (Bullet bullet: bullets) {
            bullet.step();
        }
    }
  • 我们有了飞行物也让它们动起来了,现在我们需要删除那些越界的飞行物,不然ShootGame中的数组会越来越大影响性能。在这时我们需要一个方法来判断飞行物是否越界,如果越界那么我们便将他从数组中删除。既然每个飞行物都有越界的可能,那么我们可以在父类中同样声明一个抽象方法outOfBounds()返回 boolean类型的值来判断是否越界,英雄机除外。

蜜蜂类和敌机类的代码相同:

@Override
    public boolean outOfBounds() {
        return this.y > ShootGame.HEIGHT;
    }

子弹类:

@Override
    public boolean outOfBounds() {
        return this.y < 0;
    }
  • 然后我们只需要在ShootGame类中创建相应的方法并调用即可。
  • 删除的方法我是采用将原数组中所有不越界的元素全部存到一个新的数组,最后再用新数组覆盖原数组。
//越界删除
    public void outOfBoundsAction() {
        int index = 0;
        FlyingObject[] flys = new FlyingObject[flyings.length];
        for (int i = 0; i < flyings.length; i++) {
            if (!flyings[i].outOfBounds()) {
                flys[index] = flyings[i];
                index++;
            }
        }
        flyings = Arrays.copyOf(flys, index);

        index = 0;
        Bullet[] bs = new Bullet[bullets.length];
        for (Bullet bullet: bullets) {
            if (!bullet.outOfBounds()){
                bs[index] = bullet;
                index++;
            }
        }
        bullets = Arrays.copyOf(bs, index);
    }
  • 最后我们要完成英雄机的移动,因为英雄机跟随鼠标移动而移动,简单的来说就是鼠标的坐标就是英雄机的坐标,所以此处我们需要使用到监听器,监听鼠标对象实时获取鼠标坐标来修改英雄机的坐标。

代码如下:

MouseAdapter l = new MouseAdapter() {
            @Override
            public void mouseMoved(MouseEvent e) {
                    hero.x = e.getX()-hero.width/2;
                    hero.y = e.getY()-hero.height/2;
            }
        };

        this.addMouseListener(l);
        this.addMouseMotionListener(l);

至此,第二大部分就已完成,目前我们的程序能运行起来,也能看到子弹、敌机、蜜蜂、英雄机动态飞行,但是还不能击毁敌机以及还有游戏状态英雄机属性显示,这些将是我们最后一部分要完成的内容。

三、加入碰撞判断,统计分数和生命值判断,并加入游戏状态完善程序。

  • 在第三部分中我们要实现碰撞判断,包括子弹和敌机、小蜜蜂的碰撞,还有英雄机与敌机、小蜜蜂的碰撞,然后统计英雄机生命值、分数、火力值显示到游戏界面左上角,最后加入游戏状态判断即可。
  • 关于子弹和敌机、小蜜蜂的碰撞我们可以将方法写在父类中,然后在ShootGame类中创建方法调用即可,方法传一个子弹对象作为参数,至于碰撞算法因为子弹比较小我们可以直接看子弹坐标是否在敌机或者蜜蜂所占有的矩形区域即可。
  • 在ShootGame类中判断碰撞后还需要判断具体碰撞类型,如果是与敌机碰撞那么我们需要将分数加5分,如果是与蜜蜂碰撞我们还需要进一步判断奖励类型再执行操作,最后我们还需要将碰撞的子弹、敌机、蜜蜂对象在游戏界面中清除。

具体代码如下:
碰撞判定:

    public boolean isCrash(Bullet b){
        int x1 = this.x;
        int y1 = this.y;
        int x2 = this.x + this.width;
        int y2 = this.y + this.height;

        return b.x<x2 && b.x>x1 && b.y>y1 && b.y<y2;
    }

ShootGame类中的碰撞方法:

int score = 0;
    public void bang(Bullet b,int bindex) {
        int index = -1;
        for (int i = 0; i < flyings.length; i++) {
            if (flyings[i].isCrash(b)) {
                index = i; //记录碰撞的下标
                break;
            }
        }

        if (index!=-1) {
            FlyingObject one = flyings[index];
            if (one instanceof Enemy) {
                Enemy enemy = (Enemy) one;
                score+=enemy.getScore();
            }
            if(one instanceof Award){
                Award award = (Award) one;
                int type = award.getType();
                switch (type){
                    case Award.LIFE:
                        hero.setLIFE(hero.getLIFE()+1);
                        break;
                    case Award.DOUBLE_FIRE:
                        hero.setDOUBLE_FIRE(hero.getDOUBLE_FIRE()+50);
                        break;
                }
            }
            flyings[index] = flyings[flyings.length-1];
            flyings[flyings.length-1] = one;
            flyings = Arrays.copyOf(flyings, flyings.length-1);

            Bullet last_bullet = bullets[bindex];
            bullets[bindex] = bullets[bullets.length-1];
            bullets[bullets.length-1] = last_bullet;
            bullets = Arrays.copyOf(bullets,bullets.length-1);

        }
  • 在书写英雄机与敌机、蜜蜂的碰撞之前,我们需要将游戏状态引入进来,游戏分为四个状态,开始、运行、暂停、结束,第一次运行程序游戏进入开始状态需要点击游戏界才正式运行游戏,在游戏过程中当鼠标移出游戏界面区域时,游戏会暂停,当鼠标再次进入时游戏继续运行,英雄机生命值为零时游戏结束,游戏结束后我们需要对所有数据初始化,然后再次点击回到开始界面。
  • 在之前英雄机移动时我们引入了监听器,这时我们只需要在ShootGame类中定义好状态常量以及一个当前状态变量,然后在监听器中重写鼠标状态发生变化时的方法即可。

定义状态常量:

	private static final int START = 0;
    private static final int RUNNING = 1;
    private static final int PAUSE = 2;
    private static final int GAMEOVER = 3;
    private int state = START;

监听器中的代码:

			@Override
            public void mouseClicked(MouseEvent e) {
                switch (state){
                    case START:
                        state = RUNNING;
                        break;
                    case GAMEOVER:
                        flyings = new FlyingObject[0];
                        bullets = new Bullet[0];
                        score = 0;
                        hero = new Hero();
                        state = START;
                        break;
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                if (state == PAUSE) {
                    state = RUNNING;
                }
            }

            @Override
            public void mouseExited(MouseEvent e) {
                if (state == RUNNING) {
                    state = PAUSE;
                }
            }
  • 设定游戏状态后我们还需要创建相应的paintXxx()方法将各个游戏状态绘制,再到paint()方法中调用,最终绘制到游戏界面中,同时将分数、生命值、火力值也一同绘制上去。
public void paintLifeAndScore(Graphics g){
        g.setColor(new Color(0xFF0000));
        g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20));
        g.drawString("LIFE:"+hero.getLIFE(), 10, 65);
        g.drawString("SCORE:"+score, 10, 45);
        g.drawString("DOUBLE_FIRE:"+hero.getDOUBLE_FIRE(), 10, 25);
    }

    public void paintState(Graphics g){
        switch (state) {
            case START:
                g.drawImage(start, 0, 0, null);
                break;
            case PAUSE:
                g.drawImage(pause, 0, 0, null);
                break;
            case GAMEOVER:
                g.drawImage(gameover, 0, 0, null);
                break;
        }
    }
  • 现在我们便可以加入英雄机碰撞判定了,英雄机因为体积较大,所以判定方法与之前有出入,这里是根据两个物体的中心点坐标之间的距离是否小于两个物体的宽度或高度一半相加。

Hero类中的碰撞判定方法

public boolean isCrash(FlyingObject obj){

        int x1 = this.x + this.width/2;
        int y1 = this.y + this.height/2;
        int x2 = obj.x + obj.width/2;
        int y2 = obj.y + obj.height/2;
        int maxWidth = this.width/2 + obj.width/2;
        int maxHeight = this.height/2 + obj.height/2;

        return Math.abs(x1-x2)<maxWidth && Math.abs(y1-y2)<maxHeight;
    }
  • 在ShootGame类中调用碰撞判定方法,并在生命值为0后设置游戏状态为结束。
    ShootGame类中调用:
    public void checkGameOver() {
        if(isGameOver()){
            state = GAMEOVER;
        }
    }

    public boolean isGameOver() {
        for (int i = 0; i < flyings.length; i++) {
            if (hero.isCrash(flyings[i])) {
                hero.setLIFE(hero.getLIFE()-1);
                hero.setDOUBLE_FIRE(0);
                FlyingObject one = flyings[i];
                flyings[i] = flyings[flyings.length-1];
                flyings[flyings.length-1] = one;
                flyings = Arrays.copyOf(flyings, flyings.length-1);
            }
        }
        return hero.getLIFE()<=0;
    }

你可能感兴趣的:(个人总结)