游戏面板模型设计

游戏面板包括 3 个组成部分:开始游戏界面、主界面(使用鼠标控制玩家飞机)和重新开始游戏界面。这 3 个界面是通过鼠标的单击事件实现相互切换的。

在开始游戏界面的任意位置处单击,即可开始游戏。在游戏开始后,玩家飞机开始发射导弹,敌机和空投物资纷纷进入游戏面板中。在游戏进行过程中,玩家飞机每击中一架敌机,会得到 5 分的奖励,效果如图23.13所示;击中空投物资,即可同时发射两枚导弹,效果如图23.14所示。玩家飞机的生命数为 1,一旦与敌机或空投物资发生碰撞,游戏就结束。此时,游戏面板将从主界面切换到重新开始游戏界面。在重新开始游戏界面的任意位置处单击,游戏面板将从重新开始游戏界面切换到开始游戏界面。

image 2024 03 06 18 00 17 017
Figure 1. 图23.13 玩家飞机只能发射一枚导弹
image 2024 03 06 18 00 41 719
Figure 2. 图23.14 玩家飞机能够同时发射两枚导弹

(1)创建继承 JPanel 类的游戏面板类 GamePanel,在类中,使用静态变量声明 BufferedImage 类型的飞机大战游戏需要使用的图片,使用静态变量定义窗体的宽度和高度,使用静态代码块和 ImageIO 类中的 read() 方法初始化图片资源。代码如下:

import javax.imageio.ImageIO;import java.awt.image.BufferedImage;public class GamePanel extends JPanel {
    // 常量:表示窗体的宽度和高度
    public static final int WIDTH = 360;
    public static final int HEIGHT = 600;

    public static BufferedImage startImage;  // 游戏开始时的窗体背景图片
    public static BufferedImage backgroundImage;  // 窗体背景图片
    public static BufferedImage enemyImage;  // 敌机图片
    public static BufferedImage airdropImage;  // 空投物资图片
    public static BufferedImage ammoImage;  // 导弹图片
    public static BufferedImage player1Image;  // 玩家飞机图片(喷气量小)
    public static BufferedImage player2Image;  // 玩家飞机图片(喷气量大)
    public static BufferedImage gameoverImage; // 游戏结束图片

    static {       // 初始化图片资源
        try {
            startImage = ImageIO.read(GamePanel.class.getResource("start.png"));
            backgroundImage = ImageIO.read(GamePanel.class.getResource("background.png"));
            enemyImage = ImageIO.read(GamePanel.class.getResource("enemy.png"));
            airdropImage = ImageIO.read(GamePanel.class.getResource("airdrop.ong"));
            ammoImage = ImageIO.read(GamePanel.class.getResource("ammo.png"));
            player1Image = ImageIO.read(GamePanel.class.getResource("player1.png"));
            player2Image = ImageIO.read(GamePanel.class.getResource("palyer2.png"));
            gameoverImage = ImageIO.read(GamePanel.class.getResource("gameover.png"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

(2)当背景图片、玩家飞机、导弹等其他会飞的模型第一次显示在屏幕上时,系统会自动调用 paint() 方法,触发绘图代码。paint() 方法包括画背景图片、画玩家飞机、画导弹、画会飞的模型(敌机或者空投物资)、画分数和画游戏状态 6 个功能模块。paint() 方法和 6 个功能模块的代码如下:

public void paint(Graphics g) {
    g.drawImage(backgroundImage, 0,0, null); // 画背景图片
    paintPlayer(g);  // 画玩家飞机
    paintAmmo(g);  // 画导弹
    paintFlyModel(g);  // 画会飞的模型
    paintScores(g);   // 画分数
    paintGameState(g);  // 画游戏状态
}

public void paintPlayer(Graphics g) {
    g.drawImage(player.getImage(), player.getX(), player.getY(), null);
}

public void paintAmmo(Graphics g) {
    for (int i = 0; i < ammos.length; i++) {
        Ammo a = ammos[i];
        g.drawImage(a.getImage(), a.getX()-a.getWidth()/2, a.getY(), null);
    }
}

public void paintFlyModel(Graphics g) {
    for (int i = 0; i < flyModels.length; i++) {
        FlyModel f = flyModels[i];
        g.drawImage(f.getImage(), f.getX(), f.getY(), null);
    }
}

public void paintScores(Graphics g) {
    int x = 10;
    int y = 25;
    Font font = new Font(Font.SANS_SERIF, Font.BOLD, 14);
    g.setColor(Color.YELLOW);
    g.setFont(font);
    g.drawString("SCORE:" + scores, x, y);
    y += 20;
    g.drawString("LIFE:" + player.getLifeNumbers(), x, y);
}

public void paintGameState(Graphics g) {
    switch(state) {
        case START: g.drawImage(startImage, 0, 0, null); break;
        case OVER: g.drawImage(gameoverImage, 0,0, null); break;
    }
}

(3) 编写 paint() 方法的过程中:在画玩家飞机时,引入了玩家飞机对象 player;在画导弹时,引入了导弹数组,这是因为导弹是多个,需要借助数组予以存储;在画会飞的模型时,引入了会飞的模型数组,与导弹数组的作用相同,会飞的模型数组被用来存储多个敌机和空投物资;在画游戏状态时,引入了表示游戏状态的变量 state 以及表示游戏开始和游戏结束的两个常量 START 和 OVER。然而,在使用上述被引入的玩家飞机对象 player、导弹数组、会飞的模型数组和表示游戏状态的变量 state 以及表示游戏开始和游戏结束的两个常量 START 和 OVER 之前,需要先在游戏面板类 GamePanel 中予以声明或定义。代码如下:

private int state;         // 游戏的状态
// 常量:表示游戏的状态
private static final int START = 0;
private static final int RUNNING = 1;
private static final int OVER = 2;
private FlyModel[] flyModels = {};
private Ammo[] ammos = {};
private Player player = new Player();

(4) 在飞机游戏大战中,通过鼠标的单击事件,实现游戏界面的切换。具体地说,在开始游戏界面的任意位置处单击,即可开始游戏。在游戏开始后,玩家飞机与敌机或空投物资发生碰撞,游戏结束。此时,游戏面板将从主界面切换到重新开始游戏界面。在重新开始游戏界面的任意位置处单击,游戏面板将从重新开始游戏界面切换到开始游戏界面。其中,游戏开始后的动画设计过程,通过 Timer 类予以实现。此外,上述的鼠标单击事件和 Timer 类的代码实现均被编写在 load() 方法中。代码如下:

private int scores = 0;
private Timer timer;
private int interval = 1000 / 100;

public void load() {
    // 鼠标监听事件
    MouseAdapter mouseAdapter = new MouseAdapter() {
        public void mouseMoved(MouseEvent e) {
            if (state == RUNNING) {
                int x = e.getX();
                int y = e.getY();
                player.updateXY(x,y);
            }
        }
        public void mouseClicked(MouseEvent e) {
            switch(state) {
                case START:
                    state = RUNNING;break;
                case OVER:
                    flyModels = new FlyModel[0];
                    ammos = new Ammo[0];
                    player = new Player();
                    scores = 0;
                    state = START;
                    break;
            }
        }
    };
    this.addMouseListener(mouseAdapter);
    this.addMouseMotionListener(mouseAdapter);

    timer = new Timer();
    timer.schedule(new TimerTask() {
        public void run() {
            if (state == RUNNING) {
                flyModelsEnter();
                step();
                fire();
                hitFlyModel();
                delete();
                overOrNot();
            }
            repaint();
        }
    }, interval, interval);
}

(5)使用 Timer 类实现游戏开始后的动画设计的过程如下。

  • 空投物资或者敌机进入游戏面板中。每 400 秒产生一架敌机或者一个空投物资,这里需要借助 nextInt() 方法来实现。因为空投物资是随机产生的,所义通过 Random 类的对象产生随机数:当随机变量 type 的值为 0 时,产生一个空投物资对象;当随机变量 type 的值不为 0 时,产生一个敌机对象。代码如下:

    int flyModelsIndex = 0;       // 初始化会飞的模型的入场时间
    
    /**
    * 空投物资或者敌机入场
    */
    public void flyModelsEnter() {
        flyModelsIndex++;
        if (flyModelsIndex % 40 == 0) {  // 每隔400毫秒 (10*40) 生成一个会飞的模型
            FlyModel obj = nextOne(); // 随机生成一个空投物资或者敌机
            flyModels = (FlyModel[])Arrays.copyOf(flyModels, flyModels.length + 1);
            flyModels[flyModels.length - 1] = obj;
        }
    }
    
    /**
    * 随机生成一个空投物资或者敌机
    */
    public static FlyModel nextOne() {
        Random random = new Random();
        int type = random.nextInt(20);   //[0,20)
        if (type == 0) {
            return new Airdrop();   // 空投物资
        } else {
            return new Enemy();    // 敌机
        }
    }
  • 敌机、空投物资、导弹和玩家飞机开始移动。敌机和空投物资被归纳为会飞的模型类,被存储在会飞的模型数组中,而导弹则被存储在导弹数组中。对于会飞的模型、导弹和玩家飞机,当它们各自调用对应的 move() 方法时,即可实现各自移动的效果。代码如下:

    public void step() {
        for (int i=0; i < flyModels.length; i++) {
            FlyModel f = flyModels[i];
            f.move();
        }
        for (int i=0; i < ammos.length; i++) {
            Ammo b = ammos[i];
            b.move();
        }
        player.move();
    }
  • 玩家飞机发射导弹。玩家飞机每 300 毫秒发射一枚导弹。玩家飞机对象通过调用玩家飞机模型类中的 fireAmmo() 方法,以实现发射导弹。代码如下:

    int fireIndex = 0;      // 初始化玩家飞机发射导弹的时间
    
    /**
    * 玩家飞机发射导弹
    */
    public void fire() {
        fireIndex++;
        if (fireIndex % 30 == 0) {   // 每300毫秒发射一枚导弹
            Ammo[] as = player.fireAmmo();  // 玩家飞机发射导弹
            ammos = Arrays.copyOf(ammos, ammos.length + as.length);
            System.arraycopy(as, 0, ammos, ammos.length - as.length, as.length);
        }
    }
  • 导弹击中敌机或者空投物资。判断导弹击中敌机或者空投物资,当敌机或者空投物资被击中时,删除被击中敌机或者空投物资。如果敌机被击中,那么玩家飞机获得分数奖励;如果空投物资被击中,那么玩家飞机将同时发射两枚导弹。代码如下:

    public void hitFlyModel() {
        for (int i=0; i < ammos.length; i++) {
            Ammo aos = ammos[i];
            bingoOrNot(aos);
        }
    }
    
    public void bingoOrNot(Ammo ammo) {
        int index = -1;
        for (int i=0; i < flyModels.length; i++) {
            FlyModel obj = flyModels[i];
            if(obj.shootBy(ammo)) {
                index = i;
                break;
            }
        }
        if (index != -1) {
            FlyModel one = flyModels[index];
            FlyModel temp = flyModels[index];
            flyModels[index] = flyModels[flyModels.length - 1];
            flyModels[flyModels.length - 1] = temp;
            flyModels = (FlyModel[])Arrays.copyOf(flyModels, flyModels.length - 1);
            if (one instanceof Hit) {
                Hit e = (Hit)one;
                scores += e.getScores();
            } else {
                player.fireDoubleAmmos();
            }
        }
    }
  • 删除移动到游戏面板外的敌机、空投物资和导弹。敌机、空投物资和导弹都可以移动到游戏面板外。为此,“删除移动到游戏面板外的敌机、空投物资和导弹”可以被理解为“保留游戏面板内的敌机、空投物资和导弹”。代码如下:

    public void delete() {
        int index = 0;
        FlyModel[] flyingLives = new FlyModel[flyModels.length];
        for (int i=0; i < flyModels.length; i++) {
            FlyModel f = flyModels[i];
            if(!f.outOfPanel()) {
                flyingLives[index++] = f;
            }
        }
        flyModels = (FlyModel[])Arrays.copyOf(flyingLives, index);
    
        index = 0;
        Ammo[] ammoLives = new Ammo[ammos.length];
        for (int i=0; i < ammos.length; i++) {
            Ammo ao = ammos[i];
            if(!f.outOfPanel()) {
                ammoLives[index++] = ao;
            }
        }
        ammos = (Ammo[])Arrays.copyOf(ammoLives, index);
    }
  • 判断游戏是否结束。当玩家飞机与敌机或空投物资发生碰撞时,玩家飞机的生命数由 1 变为 0,并且删除发生碰撞的敌机或空投物资。此外,将游戏状态设置为表示游戏结束的 OVER。代码如下:

    public void overOrNot() {
        if (isOver()) {    // 游戏结束
            state = OVER;   // 改变状态
        }
    }
    
    public boolean isOver() {
        for(int i=0; i < flyModels.length; i++) {
            int index = -1;
            FlyModel obj = flyModels[i];
            if (player.hit(obj)) {
                palyer.ioseLifeNumbers();
                index = i;
            }
            if (index != -1) {
                FlyModel t = flyModels[index];
                flyModels[index] = flyModels[flyModels.length - 1];
                flyModels[flyModels.length - 1] = t;
                flyModels = (FlyModel[])Arrays.copyOf(flyModels, flyModels.length - 1);
            }
        }
        return player.getLifeNumbers() <= 0;
    }

(6) main() 方法被称作 Java 程序的入口,如果一个程序没有 main() 方法,那么这个程序无法被运行。在飞机大战游戏中,main() 方法包含了加载游戏面板、设置窗体的相关属性以及调用 load() 方法控制游戏界面并加载会飞的模型等内容。代码如下:

// 程序的入口
public static void main(String[] args) {
    JFrame frame = new JFrame("飞机大战");
    GamePanel gamePanel = new GamePanel();
    frame.add(gamePanel);
    frame.setSize(WIDTH,HEIGHT);
    frame.setAlwaysOnTop(true);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
    gamePanel.load();
}