前言:在一定的java基础上就可以进行飞机大战小游戏的编写了。整个小游戏主要涉及到的基础知识为:类与对象,鼠标事件监听,线程、重绘等。
整体思路框架
设计一个初始界面,在开始后出现自己的战机和敌机且自己的战机不断地发射子弹。当子弹碰到敌机后加分,若自己碰到敌机则游戏结束。
需要的类
1.入口类 2.自己的战机类 3.敌机类 4.子弹类 5.背景类 (6.抽象类 7.数据类)
在设计类的时候可以注意到,战机、敌机和子弹都属于飞行物体,它们都拥有几个相同的属性和方法。例如XY坐标,长度、宽度、速度,移动方法,绘制方法等。因此可以建立一个抽象的飞行类作为父类,战机类、敌机类、子弹类继承父类的属性和方法,并重写其中的抽象方法,为我们的代码编写提供方便。
一、编写入口类(初始化界面)
首先,建立一个入口类(MainFrame)继承JPanel.实例化一个窗体,并设置窗体的各种属性。
public static void main(String[] args) {
// TODO Auto-generated method stub
JFrame jf = new JFrame();
MainFrame mf = new MainFrame(); //入口类名为MainFrame
jf.add(mf);
jf.setTitle("飞机大战");
jf.setDefaultCloseOperation(3);
jf.setSize(700,1000);
jf.setLocationRelativeTo(null);
jf.setVisible(true);
mf.iniUI();
}
这时候,我们已经拥有了一个界面。其中,iniUI是一个初始化的方法,其中包括了监听和线程两个内容。当然,可以将监听和线程分开来写成监听类和线程类,但因为这个小游戏中用到的内容不多便写在了一起,避免传参出现不便和错误。
二、背景类(天空类)
根据思路,在初始化了界面之后我们需要一个背景类。即在界面中绘制一张背景图,通过图像坐标的改变,实现背景不断的移动,造成战机不断前进的状态景象。可以想象,只有一张图片很难在移动后实现循环的绘制,因此采用两张图片。当一张图片移出画面后另一张接上,同时改变前一张图片的坐标。如此形成循环往复。
//读取图片
static {
try {
bgimg1 = ImageIO.read(Sky.class.getResourceAsStream("/picture/背景.jpg"));//相对位置
bgimg2 = ImageIO.read(Sky.class.getResourceAsStream("/picture/背景.jpg"));
}catch(IOException e){
e.printStackTrace();
}
}
//构造方法
public Sky(){
height = 1000;
speed =1;
y1 = 0;
y2 = -height; //从画面外开始画
}
//绘制背景图
public void paint(Graphics g) {
g.drawImage(bgimg1, 0, y1, null);
g.drawImage(bgimg2, 0, y2, null);
}
//背景图的移动
public void move() {
y1+=speed;
y2+=speed;
if(y1>=height) {
y1=-height+1;//此处的1为微小的调整,可根据实际情况修改
}else if(y2>=height) {
y2=-height+1;
}
}
在写完这个天空类之后,将其在入口类中实例化后调用它的move方法,就可以看到滚动的天空背景图片了。
三、抽象飞行体类
根据思路,总结出战机、敌机和子弹类都具有的几个共同的属性和方法为:
- X和Y的坐标
- 图片的宽度和高度
- 绘画方法(根据需要重写)
- 移动方法
- 是否碰撞方法
- 获取图片的方法(抽象方法需要重写)
- 飞行体是否死亡和设置的方法
- 飞行体是否出界的方法
注意抽象类的声明关键字为abstract
public int x;
public int y;//横纵坐标
public int width;
public int height;//宽度高度
//绘画方法
public void paint(Graphics g) {
g.drawImage(getImage(), x, y, null);
}
//判断碰撞方法
public boolean isHit(Flyobject fly) {
int x = obj.x-this.x;
int y = obj.y-this.y;
int r = 100;//判断半径
return (x*x+y*y<r*r);
//判断碰撞的方法可以自己定义 可以设置更为宽松或严格的标准
}
//是否死亡
public boolean isdead() {
return state == dead;
}
//设置为死亡状态
public void setdead() {
state = dead;
}
//获取图片
public abstract BufferedImage getImage();
//因为各自的移动方法和判断出界有较大不同 写在各自的类里了 当然也可以在这里定义
四、战机类、敌机类、子弹类
战机类、敌机类和子弹类都是飞行体这一抽象类的子类。因此继承飞行体这一父类之后可以直接使用里面定义好的属性值,重写里面的方法即可。
战机类
因为子弹的初始位置在战机的机头位置,因此将创建子弹的方法写在战机类里。实例化战机后调用该方法创建子弹。
继承飞行体抽象类。
private static BufferedImage heroimage;
static {
try {
heroimage = ImageIO.read(Hero.class.getResourceAsStream("/picture/LiPlane.png"));
} catch (IOException e) {
e.printStackTrace();
}
}//读取图片
//构造方法
public Hero() {
x = 280;
y = 800;
width = heroimage.getWidth();
height = heroimage.getHeight();
}
public void paint(Graphics g) {
g.drawImage(getImage(), x, y, null);
}
public void move(int x,int y) {
this.x = x - width/2;
this.y = y - height/2;
//此处是左右边界判断
if(this.x >= 700-width) {
this.x = 700-width;
}else if(this.x <= 0){
this.x = 0;
}
//此处是上下边界判断
if(this.y >= 1000-height) {
this.y = 1000-height;
}else if(this.y<=0){
this.y = 0;
}
}
//创建子弹方法
public Bullet creatBullet() {
Bullet b = new Bullet(this.x+this.width/2-2,this.y-2);
//调用Bullet构造方法 具体可见下文中bullet类
return b;
}
@Override
public BufferedImage getImage() {
return heroimage;
}
敌机类
在创建敌机类之前应该想到,敌机可以用数组或者用ArrayList来存储,且敌机出现的位置应该是纵坐标一致,横坐标随机。
继承飞行体抽象类。
private static BufferedImage enemyimage;
static {
try {
enemyimage = ImageIO.read(Enemy.class.getResourceAsStream("/picture/敌机1.png"));
} catch (IOException e) {
e.printStackTrace();
}
}//创建图片
private int enemyspeed; //飞行速度
//构造方法
public Enemy() {
width = enemyimage.getWidth();
height = enemyimage.getHeight();
Random rand = new Random();
x = (int)(rand.nextInt(700-width)); //随机产生横坐标
y = -height;
enemyspeed = 2; //给定飞行速度为2
}
public void move() {
y+=enemyspeed;
}
//是否出界
public boolean outofbound() {
return y > 1000;
}
@Override
public BufferedImage getImage() {
return enemyimage;
}
子弹类
子弹类的创建与敌机类是大同小异的,同样需要数组或ArrayList来存储子弹对象。
继承飞行体抽象类。
private static BufferedImage bulletImage;
private int bulletspeed; //子弹速度
//构造方法
public Bullet(int x,int y) {
x=x;
y=y;
height=bulletImage.getHeight();
width=bulletImage.getWidth();
bulletspeed = 3;
}
static {
try {
bulletImage = ImageIO.read(Bullet.class.getResourceAsStream("/picture/zd.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
public void paint(Graphics g) {
g.drawImage(getImage(), x, y, null);
}
public void move() {
this.y-=bulletspeed;
}
public boolean outofbound() {
return this.y <height;
}
public BufferedImage getImage() {
return bulletImage;
}
五、回到入口类中
准备工作已经完成,现在可以回到入口类中完成整个代码了。梳理整个过程我们应该可以得知,我们还应该解决至少以下几个疑问:
- 如何创建子弹和敌机?
- 如何绘制战机、敌机和子弹?
- 怎样调用它们的移动方法?
- 在子弹碰撞敌机后,如何将它们俩从各自的ArrayList中去除?
- 如何让它们周而复始地自己完成运动?
- 得分机制如何?
因此我们需要以下方法来解决这些问题,并通过这些方法的调用,最终完成我们的飞机大战小游戏。
———————————————————————————————————
实例化sky 和 hero(战机)
创建子弹和敌机的动态数组
定义得分 和 子弹与敌机的计数器
定义战机的状态
private Hero hero = new Hero();
private Sky sky = new Sky();
public ArrayList<Bullet> bullets = new ArrayList<Bullet>();
public ArrayList<Enemy> enemies = new ArrayList<Enemy>();
//飞机的得分
private int score = 0;
//子弹和敌机的计数器,初始化设定为0
private int bulletCount = 0;
private int enemiesCount = 0;
//定义战机的状态 开始为0运行为1死亡为2 STATE为当前状态
public static int BEGIN = 0;
public static int RUNNING = 1;
public static int OVER = 2;
public static int STATE = 0;
在这里将移动的方法都写到一个方法里,通过sky对象调用sky类里的move方法。将动态数组中的每个子弹和敌机都取出来,分别调用它们各自的移动方法。
//移动方法
public void moveAction() {
//背景移动
sky.move();
//发射子弹
for(int i=0;i<bullets.size();i++) {
bullets.get(i).move();
}
//敌机飞行
for(int i=0;i<enemies.size();i++) {
enemies.get(i).move();
}
}
通过已经实例化的对象调用各种绘画方法
public void paint(Graphics g) {
super.paint(g);
sky.paint(g);
//开始状态时绘制背景图(logoImage)
if(STATE == 0) {
g.drawImage(logoImage, 0, 0, null);
}
//绘制己方飞机
hero.paint(g);
//绘画得分
if(STATE!=0) {
paintScore(g);
}
for(int i= 0 ;i< bullets.size();i++) {
bullets.get(i).paint(g);
}
for(int i= 0;i<enemies.size();i++) {
enemies.get(i).paint(g);
}
//overImage为结束时候的画面 即如果战机状态为死亡则绘制结束画面
if(STATE==2) {
g.drawImage(overImage, 6, 400, null);
}
}
//绘制分数的方法
public void paintScore(Graphics g) {
Font font = new Font(Font.SANS_SERIF,Font.BOLD,30); //设置字体
g.setColor(Color.RED); //设置颜色
g.setFont(font);
g.drawString("SCORE:"+score, 15, 45); //绘画文字
}
创建和移除子弹的方法
因为创建和移除敌机的方法几乎完全相同,因此不再赘述
public void createBullet() {
bulletCount++;
//此处的50即为控制子弹生成的速率的参数
if(bulletCount % 50==0) {
Bullet b = hero.creatBullet();
bullets.add(b); //在动态数组中加入新的子弹
bulletCount=0;
}
}
public void removeBullet() {
for(int i=0;i<bullets.size();i++) {
if(bullets.get(i).outofbound()==true || bullets.get(i).isdead()==true) {
bullets.remove(i);
}
}
//如果子弹超过边界或者子弹已经判定为死亡状态则移除子弹
}
处理碰撞情况的方法(自己和敌机碰撞、敌机和子弹碰撞)
//子弹和敌机碰撞
public void bulletHitEnemy() {
for(int i=0;i<bullets.size();i++) {
Bullet bullet = bullets.get(i);
for(int j = 0;j<enemies.size();j++) {
Flyobject enemy = enemies.get(j);
if(bullet.isHit(enemy)) {
//设置子弹和敌机的状态为死亡
bullet.setdead();
enemy.setdead();
//加分
score += 2;
}
}
}
}
//自己和敌机碰撞
public void herohitenemy() {
for(int i=0;i<enemies.size();i++) {
Flyobject enemy = enemies.get(i);
if(hero.isHit(enemy)) {
//设置状态为死亡
enemy.setdead();
hero.setdead();
STATE = OVER;
}
}
}
最后的准备工作也都已经完成了,现在只需要增加界面的监听并在线程中调用它们即可,在文章最前提到的iniUI 方法将实现它们。
public void iniUI() {
MouseAdapter adapter = new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if(STATE==BEGIN) {
STATE = RUNNING;
}else if(STATE==OVER) {
STATE = BEGIN;
score = 0;
//重新初始化自己的飞机
hero = new Hero();
//重新初始化子弹数组
bullets = new ArrayList<Bullet>();
//重新初始化敌机数组
enemies = new ArrayList<Enemy>();
}
}
public void mouseMoved(MouseEvent e) {
if(STATE==RUNNING) {
int x1 = e.getX();
int y1 = e.getY();
hero.move(x1,y1);
}
}
};
this.addMouseListener(adapter);
this.addMouseMotionListener(adapter);
//定义游戏的定时器
//所要执行的任务 延迟时间(毫秒) 执行各后续任务的时间间隔(毫秒)
Timer timer = new Timer();
timer.schedule(new TimerTask(){
public void run() {
if(STATE == RUNNING) {
moveAction();
createBullet();
removeBullet();
createEnemy();
removeEnemy();
//子弹和敌机碰撞的方法
bulletHitEnemy();
//自己和敌机碰撞的方法
herohitenemy();
}
repaint();
}
},10,10);
}
此处有两点需要特别提示的地方。一是没有新建监听类进行监听。adapter是适配器的意思,也就是使用了MouseAdapter类了以后只需要重写自己需要的监听方法即可,不需要将所有的方法都进行重写。二是没有新建线程类,而是使用了Timer类。其实,Timer类实现了Runnable的接口,使用时相当于一个线程。schedule的具体意义在代码段里的注释中已经注明。
此时,已经完成了所有的编写。当然还有很多值得改进的地方,可以将一些常用的方法和数据都写到各自的类里,使入口类的书写更为清晰简洁。
改进思路
- 增加敌机被击中后的爆炸动画效果
- 抽象类实例化不同的敌机类,得分也设置为不同
- 设置奖励系统
- 设置生命系统和等级闯关系统,达到一定分数后增加难度
- 提供界面使玩家自选难度
如有问题,希望各位大佬指正!