游戏面板模型设计
游戏面板包括 3 个组成部分:开始游戏界面、主界面(使用鼠标控制玩家飞机)和重新开始游戏界面。这 3 个界面是通过鼠标的单击事件实现相互切换的。
在开始游戏界面的任意位置处单击,即可开始游戏。在游戏开始后,玩家飞机开始发射导弹,敌机和空投物资纷纷进入游戏面板中。在游戏进行过程中,玩家飞机每击中一架敌机,会得到 5 分的奖励,效果如图23.13所示;击中空投物资,即可同时发射两枚导弹,效果如图23.14所示。玩家飞机的生命数为 1,一旦与敌机或空投物资发生碰撞,游戏就结束。此时,游戏面板将从主界面切换到重新开始游戏界面。在重新开始游戏界面的任意位置处单击,游戏面板将从重新开始游戏界面切换到开始游戏界面。


(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();
}