一.构思
首先我们应明确基本思路,飞机大战需要:
一个UI类做展示面板;
一个监听器类实现键盘控制;
一个飞行对象类作为父类,写入基本字段和方法;
四个飞行对象子类继承父类,分别是:玩家飞机、敌方飞机、玩家飞机子弹、敌方飞机子弹;
三个线程类,分别是:主线程、自动生成敌机线程、自动生成敌机子弹线程;
一个接口写入游戏基本数据。
二.游戏数据
public interface GameData {
//窗体大小
int FrameHeight=900;
int FrameWidth=700;
//敌机大小
int EnemyWidth = 80;
int EnemyHeight = 100;
//分数位置
int Fontx = 100;
int Fonty = 100;
//玩家战机初始坐标
int PlayerX = 350;
int PlayerY = 750;
//玩家战机大小
int PlayerWidth = 130;
int PlayerHeight = 150;
//玩家子弹大小
int PBW = 20;
int PBH = 30;
//敌机子弹大小
int EBW = 10;
int EBH = 20;
}
三.UI面板
public class UI extends JFrame implements GameData{
public void showUI() {
setTitle("飞机大战");//设置窗体名字
setSize(FrameWidth,FrameHeight);//设置窗体大小
setLocationRelativeTo(null);//设置窗体居中显示
setDefaultCloseOperation(3);//设置窗体后台自动关闭
setVisible(true);
}
public static void main(String[]args) {
new UI().showUI();
}
}
四.飞行对象类的创建和继承
1.父类:
一个飞行对象应有以下属性和方法:
属性:初始坐标x、y;大小width、height;速度speedx、speedy;存活状态alive。
方法:绘制自己、移动、开火、检测碰撞。
public class FlyerObject {
private int x,y;//飞行对象的坐标
private int width,height;//飞行对象的大小
private int speedx,speedy;//飞行对象的速度
private boolean alive = true;//飞行对象是否存活,布尔值默认为false
//构造方法
public FlyerObject(int x,int y,int width,int height,int speedx,int speedy) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speedx = speedx;
this.speedy = speedy;
}
//绘制飞行对象
public void draw(Graphics g) {
}
//移动
public void move() {
//先判定是否存活再移动
if(getalive(){
x += speedx;
y += speedy;
}
}
//开火
public void fire() {
}
//检测碰撞
public void collide() {
}
//相应的set和get方法
public void setalive(Boolean alive) {
this.alive = alive;
}
public boolean getalive() {
return alive;
}
public void setx(int x) {
this.x = x;
}
public int getx() {
return x;
}
public void sety(int y) {
this.y = y;
}
public int gety() {
return y;
}
public void setwidth(int width) {
this.width = width;
}
public int getwidth() {
return width;
}
public void setheight(int height) {
this.height = height;
}
public int getheight() {
return height;
}
public void setspeedx(int speedx) {
this.speedx = speedx;
}
public int getspeedx() {
return speedx;
}
public void setspeedy(int speedy) {
this.speedy = speedy;
}
public int getspeedy() {
return speedy;
}
}
2.玩家类:
玩家类除了要重写父类的绘制、移动、开火方法外,由于我们需要用键盘操控飞机移动,所以玩家类中还要写入上下左右移动的方法,因为父类中的move()方法是采用位置+速度的方式实现的,所以玩家类中的上下左右移动方法只需改变速度即可。
由于玩家飞机对象只需要一个即可,所以我们可以在玩家类中写一个静态方法,返回一个已经设置好数据的玩家飞机对象,便于外部类使用。
public class PlayerObject extends FlyerObject implements GameData{
int score = 100;//玩家分数
//构造方法
public PlayerObject(int x, int y, int width, int height, int speedx, int speedy) {
super(x, y, width, height, speedx, speedy);
}
//玩家飞机图片
static Image img = new ImageIcon("Image/飞机大战素材2.png").getImage();
//该静态方法用于外部直接获取player对象
public static PlayerObject getnewPlayer() {
return new PlayerObject(PlayerX,PlayerY,PlayerWidth,PlayerHeight,0,0);
//返回一个已经设置好数据的玩家飞机对象
}
//绘制玩家飞机
public void draw(Graphics g) {
if(getalive()){
g.drawImage(img, getx(), gety(),getwidth(),getheight(), null);
}
}
//移动
public void move() {
super.move();
if(getalive()){
//进行边界判定
if(getx()<=0) {
setx(0);
}
if(getx()+PlayerWidth>=FrameWidth) {
setx(FrameWidth-PlayerWidth);
}
if(gety()<=0) {
sety(0);
}
if(gety()+PlayerHeight>=FrameHeight) {
sety(FrameHeight-PlayerHeight);
}
}
}
//上移
public void up() {
setspeedy(-20);
}
//下移
public void down() {
setspeedy(20);
}
//左移
public void left() {
setspeedx(-20);
}
//右移
public void right() {
setspeedx(20);
}
//开火
public void fire() {
}
}
3.敌机类:
由于敌机是自动移动的,所以敌机的move方法要加上撞到边界会反弹的代码。
public class EnemyObject extends FlyerObject implements GameData{
//构造方法
public EnemyObject(int x, int y, int width, int height, int speedx, int speedy) {
super(x, y, width, height, speedx, speedy);
}
//敌机图片,注意此处只能把图片放入Image包中再填相对路径,否则后面绘制缓冲区会失败
static Image img = new ImageIcon("Image/飞机大战素材-removebg-preview.png").getImage();
//绘制
public void draw(Graphics g) {
if(getalive()){
g.drawImage(img, getx(), gety(),getwidth(),getheight(), null);
}
}
//移动
public void move() {
super.move();
if(getailve()){
if(getx()<=0||getx()+EnemyWidth>=FrameWidth) {
setspeedx(-getspeedx());//撞到边界速度取反
}
}
}
}
4.玩家子弹类:
public class PBulletObject extends FlyerObject{
public PBulletObject(int x, int y, int width, int height, int speedx, int speedy) {
super(x, y, width, height, speedx, speedy);
}
//绘制
public void draw(Graphics g) {
g.setColor(Color.BLUE);
if(getalive()){
g.fillOval(getx(), gety(), getwidth(), getheight());
}
}
//移动
public void move() {
if(getalive()){
sety(gety()-getspeedy());
}
}
}
5.敌机子弹类:
public class EBulletObject extends FlyerObject{
public EBulletObject(int x, int y, int width, int height, int speedx, int speedy) {
super(x, y, width, height, speedx, speedy);
}
//绘制
public void draw(Graphics g) {
if(getalive()){
g.setColor(Color.WHITE);
g.fillOval(getx(), gety(), getwidth(), getheight());
}
}
//移动
public void move() {
if(getalive()){
sety(gety()+getspeedy());
}
}
}
五.数组的创建和监听器的添加
该游戏一共需要三个数组,分别是:玩家子弹数组、敌机数组、敌机子弹数组,由于这三个数组需要在线程中用到,所以我们将三个数组的创建、监听器的添加、三个线程的运行都写在UI类中。
该游戏的监听器采用KeyListener,因为玩家需要操控飞机对象移动和开火,因此要往KeyListener中传入玩家飞机对象和玩家子弹数组,在监听器类中添加set()方法。
1.在UI类中:
创建数组:
Listener ls = new Listener();
ArrayList<PBulletObject> pbulletlist = new ArrayList<>();//玩家子弹数组
ArrayList<EBulletObject> ebulletlist = new ArrayList<>();//敌机子弹数组
ArrayList<EnemyObject> elist = new ArrayList<>();//敌机数组
PlayerObject player = PlayerObject.getnewPlayer();//取出玩家飞机对象,用于传入监听器类
public void showUI() {
添加监听器:
setVisible(true);
addKeyListener(ls);//添加监听器
ls.setplayer(player);//传入玩家飞机对象
ls.setlist(pbulletlist);//传入玩家子弹数组
2.在监听器类中:
因为开火的操作需要用到子弹数组,所以先往飞行对象类中的fire()方法填入参数。
//开火
public void fire(ArrayList<PBulletObject> pbulletlist) {
}
public class Listener implements KeyListener{
PlayerObject player;//玩家对象
ArrayList<PBulletObject> pbulletlist;//玩家子弹数组
public void setplayer(PlayerObject player) {
this.player = player;
}
public void setlist(ArrayList<PBulletObject> pbulletlist) {
this.pbulletlist = pbulletlist;
}
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
//对应键位添加对应操作
switch(e.getKeyCode()) {
case KeyEvent.VK_W:
player.up();
break;
case KeyEvent.VK_S:
player.down();
break;
case KeyEvent.VK_A:
player.left();
break;
case KeyEvent.VK_D:
player.right();
break;
case KeyEvent.VK_SPACE:
player.fire(pbulletlist);
}
}
public void keyReleased(KeyEvent e) {
//松手时玩家飞机停止
player.setspeedx(0);
player.setspeedy(0);
}
}
最后在玩家飞机类中把fire()方法补全
//开火
public void fire(ArrayList<PBulletObject> pbulletlist) {
if(getalive()) {
PBulletObject bullet = new PBulletObject(getx()+PlayerWidth/2,gety(),EBW,EBH,0,10);
pbulletlist.add(bullet);
}
}
六.线程的添加
1.自动生成敌机的线程:
往该线程中传入敌机数组,线程运行时,不断往数组中添加新的敌机对象,同时取随机数作为新敌机的数据。
public class EnemyThread extends Thread implements GameData{
ArrayList<EnemyObject> elist;//敌机数组
Random ran = new Random();//用于取随机数
//构造方法
public EnemyThread(ArrayList<EnemyObject> elist) {
this.elist = elist;
}
public void run() {
while(true) {
//每隔两秒生成一架敌机
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int x = ran.nextInt(FrameWidth)-EnemyWidth;//敌机初始x坐标在边框内
int y = ran.nextInt(FrameHeight/2);//敌机初始y坐标在边框上半部分
int speedx = ran.nextInt(10)-5;//敌机水平方向可以左右移动
int speedy = ran.nextInt(10)+1;//敌机竖直方向只能向下移动
//对x坐标进行设定,防止敌机撞到边界不断抽搐
if(x<0) {
x=0;
if(speedx<0) {
speedx=ran.nextInt(10);
}
}
EnemyObject enemy = new EnemyObject(x,y,EnemyWidth,EnemyHeight,speedx,speedy);
elist.add(enemy);//往敌机数组中添加敌机对象
}
}
}
2.自动生成敌机子弹的线程:
往该线程中传入敌机数组和敌机子弹数组,挨个取出敌机数组,取其坐标作为敌机子弹数组,挨个往敌机子弹数组中添加敌机子弹。
public class EBulletThread extends Thread implements GameData{
ArrayList<EnemyObject> elist;//敌机数组
ArrayList<EBulletObject> ebulletlist;//敌机子弹数组
Random ran = new Random();//随机数用于生成子弹的时间
public EBulletThread(ArrayList<EnemyObject> elist,ArrayList<EBulletObject> ebulletlist) {
this.elist = elist;
this.ebulletlist = ebulletlist;
}
public void run() {
while(true) {
try {
Thread.sleep(ran.nextInt(500)+500);//每过随机时间生成子弹
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//取出敌机
for(int i=0;i<elist.size();i++) {
EnemyObject enemy = elist.get(i);
//判断存活
if(enemy.getalive()) {
//获取坐标
int x = enemy.getx();
int y = enemy.gety();
EBulletObject bullet = new EBulletObject(x+EnemyWidth/2,y+EnemyHeight,EBW,EBH,0,10);
ebulletlist.add(bullet);//往敌机子弹数组中添加敌机子弹
}
}
}
}
}
3.主线程:
主线程中要完成一下任务:
绘制背景;
绘制玩家飞机、玩家子弹、敌机、敌机子弹;
绘制玩家分数;
检测碰撞。
(上述有关绘制的操作均在缓冲区进行,以消除闪烁)
在这之前要回飞行对象类补全检测碰撞的方法:
//检测碰撞
public boolean collide(FlyerObject flyer) {
if(flyer==null) {
return false;
}
//要双方都存活才能发生碰撞
if(getalive()&&flyer.getalive()) {
if(this.x+this.width>flyer.getx()&&this.x<flyer.getx()+flyer.getwidth()&&this.y+this.height>flyer.gety()&&this.y<flyer.gety()+flyer.getheight()) {
return true;//发生碰撞,返回true
}
}
return false;
}
主线程:
由上述可知需要往主线程中传入:玩家对象、玩家子弹数组、敌机数组、敌机子弹数组、画笔。
public class MainThread extends Thread implements GameData{
private PlayerObject player;//玩家对象
private ArrayList<PBulletObject> pbulletlist;//玩家子弹数组
private ArrayList<EnemyObject> elist;//敌机数组
private ArrayList<EBulletObject> ebulletlist;//敌机子弹数组
private Graphics g;//画笔
public MainThread(PlayerObject player,ArrayList<PBulletObject> pbulletlist,ArrayList<EnemyObject> elist,
ArrayList<EBulletObject> ebulletlist,Graphics g) {
this.player = player;
this.pbulletlist = pbulletlist;
this.elist = elist;
this.ebulletlist = ebulletlist;
this.g = g;
}
public void run() {
while(true) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//创建缓冲区
BufferedImage buff = new BufferedImage(FrameWidth,FrameHeight,BufferedImage.TYPE_INT_ARGB);//缓冲区
Graphics bg = buff.getGraphics();//缓冲区的画笔
//绘制背景
bg.drawImage(new ImageIcon("Image/apic5877.jpg").getImage()
,0,0,FrameWidth,FrameHeight,null);
//绘制玩家飞机
player.draw(bg);
player.move();
//绘制玩家子弹
for(int i=0;i<pbulletlist.size();i++) {
PBulletObject bullet = pbulletlist.get(i);
bullet.draw(bg);
bullet.move();
}
//绘制敌机
for(int i=0;i<elist.size();i++) {
EnemyObject enemy = elist.get(i);
enemy.draw(bg);
enemy.move();
}
//绘制敌机子弹
for(int i=0;i<ebulletlist.size();i++) {
EBulletObject bullet = ebulletlist.get(i);
bullet.draw(bg);
bullet.move();
}
//绘制分数面板
bg.setFont(new Font("黑体",Font.BOLD,20));//设置字体样式
bg.setColor(Color.BLACK);
bg.drawString("分数:"+player.score, Fontx, Fonty);
g.drawImage(buff, 0, 0, null);//绘制缓冲区
//检测碰撞
//玩家子弹碰敌机
for(int i=0;i<pbulletlist.size();i++) {
PBulletObject bullet = pbulletlist.get(i);//取出一颗玩家子弹,依次与敌机检测碰撞
for(int j=0;j<elist.size();j++) {
EnemyObject enemy = elist.get(j);
if(bullet.collide(enemy)) {//若发生碰撞,将两飞行物设为不存活
bullet.setalive(false);
enemy.setalive(false);
player.score++;//玩家分数增加
break;
}
}
}
//玩家碰敌机子弹
for(int i=0;i<ebulletlist.size();i++) {
EBulletObject bullet = ebulletlist.get(i);
if(player.collide(bullet)) {
bullet.setalive(false);
player.score--;
if(player.score<=0) {
player.setalive(false);
}
}
}
}
}
}
最后在UI类中创建并启动线程。
ls.setlist(pbulletlist);//传入玩家子弹数组
//主线程
MainThread thread1 = new MainThread(player,pbulletlist,elist,ebulletlist,getGraphics());
thread1.start();
//敌机线程
EnemyThread thread2 = new EnemyThread(elist);
thread2.start();
//敌机子弹线程
EBulletThread thread3 = new EBulletThread(elist,ebulletlist);
thread3.start();
}