Java实现飞机大战(详细思路与过程,含源代码)

演示

飞机大战

源代码下载: https://github.com/Fattybenny/javaswingproject/tree/main/java%E9%A3%9E%E6%9C%BA%E5%A4%A7%E6%88%98.

构思

首先,要把整体的游戏框架和内容构思出来(根据预先构思游戏里存在的组件内容和游戏功能抽象出指定类)。以我的小游戏为例:

1.主界面框架类:GameFrame(extends JFrame)
           显示开始界面

2.弹出界面类:Dialog (extends JDialog)
            弹出设置界面(声音开关)、弹出游戏成功、失败界面
JDialog 窗体的功能是从一个窗体中弹出另一个窗体,就像是在使用 IE 浏览器时弹出的确定对话框JDialog 窗体与 JFrame 窗体形式基本相同设置窗体的特性时调用的方法名称都基本相同,如设置窗体大小、窗体关闭状态等

3.游戏面板类:GamePanel (extends JPanel)
           真正显示飞机大战动态游戏画面,并且还添加了按钮JButton用于控制游戏开始暂停。

4.玩家飞机类:MyPlane
           移动玩家飞机、画玩家飞机等其他与玩家飞机相关的方法

5.敌机类:EnemyPlane
           移动敌机、画敌机

6.BOSS飞机类:BossPlane
           移动BOSS飞机、画BOSS飞机

7.子弹类(也可以分三个类:玩家飞机子弹、敌机子弹、BOSS子弹)
           移动子弹、绘制子弹

8.碰撞类:Collision
           检测各种碰撞情况

9.爆炸类:Break
           绘制飞机爆炸图片

10.声音类:Sound
           控制声音的播放与暂停

11.主类:Main
            开启程序

详细分析

GameFrame

在这里插入图片描述

背景动画

动态的图像(视频)原理:视频由一张张静态的图片快速变换形成,连续的图像变化每秒超过24帧(frame)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面;看上去是平滑连续的视觉效果。

在java程序中,如果通过人手动点击一次换一次图片,那么要想实现肉眼看见的视频效果需要我们一秒钟至少点击24次,这是非常困难的。而我们希望的是通过一次点击就可以产生动画效果,让其自动每隔一段极短的时间就换一次图片。于是可以采用多线程的方法来实现。由于动画效果是在当前的GameFrame类中实现的,可以直接定义一个内部类继承Thread,当然也可以新建一个class文件定义。

获取包中的图片:

Image img;
img=Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("/Capture/飞机.mp4_"+i+".png"));
//获得资源的URL:this.getClass().getResource(String name)
//                                         单斜杠 /开头表示从根目录开始
private class setBackground2 extends Thread
{
    Image img;
    Graphics mg;
    @Override
		public void run() 
		{
		 while(true)
		 {
              for(int i=0;i<200;i++)//200张图片为一个完整动画
			  {	
                img=Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("/Capture/飞机.mp4_"+i+".png"));
                mg.drawImage(img, 0, 0, null);
                 try {
				  Thread.sleep(10);
		       	} catch (InterruptedException e) {
				// TODO Auto-generated catch block
			 	e.printStackTrace();
			      }		  
                }
          }        		
		}		
}

菜单选项

设置不同的JLabel标签添加到界面的相应位置。

一般容器都有默认布局方式,但是有时候需要精确指定各个组建的大小和位置,就需要用到空布局。
首先利用setLayout(null) 语句将容器的布局设置为null布局(空布局)
再调用组件的setBounds(int x, int y, int width,int height) 方法设置组件在容器中的大小和位置,单位均为像素。

通过JLabel组件添加一张图片:

ImageIcon background = new ImageIcon(this.getClass().getResource("/images/mainback.png"));
 back = new JLabel(background);
 back.setBounds(0,700,1800, 300);
this.getContentPane().add(back);
        //设置标签
		 label01 = new JLabel("开始游戏");
		 label01.setFont(new Font("acefont-family", Font.BOLD, 50));
		 label01.setForeground(Color.blue);//设置字体背景颜色
		 label01.setBounds(820, 740, 400, 120);//起点宽高
		 
		 label02 = new JLabel("选择飞机");
		 label02.setFont(new Font("acefont-family", Font.BOLD, 50));
		 label02.setBounds(820, 830, 400, 120);
		 
		 label03 = new JLabel(icon);
		 label03.setBounds(600, 740, 250, 120);
		
		 label04 = new JLabel(icon);
		 label04.setBounds(600, 830, 250, 120);
		 label04.setVisible(false);
		 ... ...

问题与解决:
1.先添加的组件会覆盖影响到后添加的组件。
例如这里有三个JLabel组件,其中一个是带有背景图片的,其他两个是带有文字的,一定要最后添加带有背景图片的,否则无法将文字显示在图片上。
在这里插入图片描述
2.注意画笔绘制图片的覆盖问题:
后面经常要用到drawimage方法,要注意,画笔后画的内容会覆盖先画的内容;画笔画的内容会覆盖类似JLabel这种组件,无论组件先添加还是后添加(解决办法:在组件添加到相应的容器之后再设置坐标位置(setbounds),或者如上文所说的通过按正确顺序添加JLabel组件的方法来达到设置背景图片的功能,避免了用drawimage)。

添加(键盘)监听器

public void keyadapter()
	{
		this.requestFocusInWindow();
		this.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent e) {				
                 int key = e.getKeyCode();				
				//监听向上或向下按
				if(key == KeyEvent.VK_DOWN || key == KeyEvent.VK_UP) {
					label03.setVisible(!label03.isVisible());
					label04.setVisible(!label04.isVisible());
					
					if(label03.isVisible()) {
						label01.setForeground(Color.blue);
						label02.setForeground(Color.black);
					} else {
						label01.setForeground(Color.black);
						label02.setForeground(Color.blue);
					}
				}
				if(key == KeyEvent.VK_ENTER && label03.isVisible()) {
                 //添加游戏面板JPanel
                   add(...);
                  //并移除之前添加的JLabel组件
                    remove(label02);
					remove(label03);
					remove(label04);
					remove(back);
                 ... ...
				}
				... ...
						}
		});
	}
	

问题与解决
焦点的获取
在使用键盘监听器的时候,一定要让监听对象获取焦点,如果焦点不在监听对象上,那么键盘输入的内容就无法被监听到。
使用requestFocusInWindow()方法

注意在监听器方法中的this关键字
在添加监听器时有两种不同的方法:
1.this.addKeyListener(listener);
listener是用户定义的监听器类,这个类implements相应接口。
2.直接在当前类定义监听器;

this.addKeyListener(new KeyAdapter() {
//KeyAdapter系统提供的抽象类,他承接了KeyListener接口
//public abstract class KeyAdapter implements KeyListener
                     @Override
                     public void keyPressed(KeyEvent e) {              
                     //this.add()这里的this指的不是当前文件类,
                     //如果想调用当前文件类的方法,直接写方法名就行了,
                     //因为这里的this指的是KeyAdapter的对象
                     ...}
                     @Override
			         public void keyReleased(KeyEvent e) {
			         ...}
			         ...
			         }

GamePanel

按钮区和分数区都分别添加相应的JButton按钮,并添加ActionListener监听器即可
在这里插入图片描述
在这里插入图片描述

动态游戏显示区(双缓冲)

还是和GameFrame中的背景动画类似,都采用多线程方法。

private class MapPanel extends Canvas implements Runnable

在这里插入图片描述
问题与解决:
1.Canvas使用
Canvas是AWT组件,JPanel是Swing组件,Swing组件是以AWT组件为基础的,从理论上来说,Canvas要比JPanel更轻量些.如果canvas能满足需求,就用canvas.Canvas 组件表示屏幕上一个空白矩形区域,应用程序可以在该区域内绘图,或者可以从该区域捕获用户的输入事件。
不能直接使用该类,需要继承Canvas并重写其paint方法.
repaint paint update 三个方法的调用顺序:
repaint->update->paint
paint源码:

public void paint(Graphics g) {
    g.clearRect(0, 0, width, height);//清除界面
}

update源码:

public void update(Graphics g) {
    g.clearRect(0, 0, width, height);//清除界面
    paint(g);
}

由此看来我们可以选择性的重写update或者paint方法来满足程序绘画需要,如果不需要清除界面就去除 g.clearRect(0, 0, width, height)方法,在本例中就需要执行这一步,因为每次重绘图片如果都执行一遍清除界面操作就会出现闪烁现象。(如果是用户自己定义的绘制方法,不需要用到paint方法进行重绘,则不用重写这些方法,需要用到双缓冲技术,如下所示)

除此以外解决图片闪烁现象,还要用到双缓冲技术:
先在内存中预先分配一定大小的图片缓冲区,在将所有绘图方法绘制到缓冲区之后,再最后将图片缓冲区的内容绘制出来;

//创建图片缓冲区
BufferedImage iBuffer=new BufferedImage(1600, 1000, BufferedImage.TYPE_INT_RGB);
//在缓冲区内部绘图
Graphics gBuffer = iBuffer.getGraphics();//获得缓冲区画笔
gBuffer.drawImage(bg2, 0, bg2_y, 1600, 1000, this);
gBuffer.drawImage(planePic[planeID], myPlane_x, myPlane_y, PLANE_SIZE, PLANE_SIZE, null);
... ...
//缓冲区内部内容绘制完成,将缓冲区整体绘制
Graphics canvasg=this.getGraphics();
canvasg.drawImage(iBuffer, 0, 0, null);//把缓冲图像载入屏

Plane(myplane,enemyplane,bossplane)

这三个不同的飞机类内容基本相同:

class Plane
{  
 初始化飞机的图片、坐标等
 如果是一组图片则用Image[]数组存储
 {
 planeimg=Toolkit.getDefaultToolkit().getImage(getClass().getResource());
 }
 
 绘制飞机方法:
 {
 先判断飞机是否还存活
 存活:
 g.drawImage(planeimg,x, y, null); 
 死亡:
 调用爆炸类里的绘制爆炸图片方法(下文所示)
 }
 
 移动飞机方法:
 {
 修改飞机图片的x,y坐标
 }
}

Break

class Break
{
初始化爆炸图片
{
plane_b = new Image[6];
for(int i = 0; i < plane_b.length; i++) {
	plane_b[i] = Toolkit.getDefaultToolkit().getImage(getClass()
					.getResource("/images/bomb_enemy_" + i + ".png"));
		}
...
}

绘制爆炸图片
{
g.drawImage(plane_b[i/5], x, y, EnemyPlane.ENEMY_SIZE, EnemyPlane.ENEMY_SIZE, null);
g.drawImage(plane_b[i/5], x, y, MyPlane.PLANE_SIZE, MyPlane.PLANE_SIZE, null);
...
}
}

Collision

两张图片(飞机和飞机,飞机和子弹)是否相碰,需要判断的是矩形图片是否有重叠部分。
以飞机与飞机相碰为例:
按照x,y坐标的大小不同,总共有2*2四种情况:

在这里插入图片描述

//玩家飞机与敌机碰撞
	void plane_enemy(MyPlane m, EnemyPlane e) {
		if(m.getX_Y().getX() >= e.getX_Y().getX()-MyPlane.PLANE_SIZE
				&& m.getX_Y().getX() <= e.getX_Y().getX()+EnemyPlane.ENEMY_SIZE
				&& m.getX_Y().getY() >= e.getX_Y().getY()-MyPlane.PLANE_SIZE
				&& m.getX_Y().getY() <= e.getX_Y().getY()+EnemyPlane.ENEMY_SIZE) {
			e.stayed = false;
			if(GamePanel.live <= 50) {
				m.stayed = false;				
				GamePanel.live = 0;
			} else
				GamePanel.live -= 50;
		}
	}

Bullet

class Bullet
{
  初始化子弹图片
  {
    bullet = Toolkit.getDefaultToolkit().getImage(getClass().getResource());
  }
  绘制子弹
  {
   g.drawImage(bullet, bullet_x, bullet_y, BULLET_WIDTH,BULLET_HEIGHT, null);
   }

移动子弹
... 
}

存储子弹:

private ArrayList<Bullet> mybulletarray;//玩家飞机子弹数组
private ArrayList<Bullet> enemybulletarray;//敌机子弹数组
private ArrayList<Bullet[]> bossbulletarray;//boss子弹数组

Dialog

弹出对话框和JFrame类似,往里面添加各种组件和监听器即可。

public class Dialog extends JDialog
{
public Dialog(JFrame j, int i) {
		super(j, true);
		setLayout(null);
		setResizable(false);
		if(i == 1)
			showFail(j);//显示挑战失败
		else if(i == 2)
			showSuccess(j);//显示挑战成功对话
		else
			showSetting(j);//显示设置对话
		
		setVisible(true);
		
	}
	...
//游戏失败
private void showFail(JFrame j) {
		setTitle("提示");		
		setBounds(800, 400, 500, 300);
		jl01 = new JLabel("挑战失败");
		jl01.setFont(new Font("acefont-family", Font.BOLD, 50));
		jl01.setForeground(Color.blue);
		jl01.setBounds(65, 40, 400, 50);
		add(jl01);
		
		jl02 = new JLabel("分数" + GamePanel.sum);
		jl02.setFont(new Font("acefont-family", Font.BOLD, 30));
		jl02.setForeground(Color.RED);
		jl02.setBounds(65, 120, 400, 50);
		add(jl02);
	}
	
	....
}

Sound

public class Sound {		
	private Clip clip;
	static boolean[] b = new boolean[]{true, true, true, true};//控制声音播放
	                                 //按键音	
	//打开声音文件的方法。
	public Sound(String path){
		AudioInputStream audio;
		try {
			URL url = this.getClass().getResource(path);
			audio = AudioSystem.getAudioInputStream(url);
			clip = AudioSystem.getClip();
			clip.open(audio);
		}catch (Exception e) {
			e.printStackTrace();
		}
		
	}
		/**
		 * 停止播放
		 */
		void stop() {
			clip.stop();//暂停音频播放
		}
		
		/**
		 * 开始播放
		 */
		void start() {
			clip.start();//播放音频
		}
		
		/**
		 * 回放背景音乐设置
		 */
		void loop() {
			clip.loop(20);//回放
		}
}

想要在某个界面或时刻实现播放声音,直接new一个对象,并传入文件地址就行。
如下Main方法所示:

Main方法

public class Main {
	static GameFrame f1;
	static Sound sound;
	
      public static void main(String[] args) {
		f1=new GameFrame();
		f1.setDefaultCloseOperation(3);
		f1.setVisible(true);
		f1.setResizable(false);
		if(sound==null)
		{
			sound=new Sound("/sounds/mainback.wav");
			sound.start();
			sound.loop();
		}
	}
}

总结:

除了上文中所解决的问题,还有一点就是,对于多线程到底该什么时候去创建?或者说多线程该设置在什么位置?(以下是我自己的理解)

直观的感受就是多线程是在自动持续执行时引入的,对于飞机大战来说,动画界面一旦开始执行(在满足条件的情况下),会一直执行直到满足结束条件就退出,很明显,背景、玩家飞机、敌机、boss飞机等等这些图片都是在自动不断刷新,刚开始会想到让这些自动执行的类都实现多线程(extends Thread),要完成刷新的同步,只需要统一好阻塞时间就行了,但是这样实现会发现代码开销比较大,且不便于统一管理。所以可以将这些类统一用一个Thread类实现同步的控制(只需要在这个Thread类中调用各个类中的方法即可)。
在本例中就是用GamePanel(可以直接让GamePanel extends Thread 或者创建一个内部Thread类)来控制背景、飞机等各种图片的绘制和图片移动。

  • 25
    点赞
  • 202
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
基于Java飞机大战游戏主要需要我方飞机和敌方飞机,还有子弹,特殊nPC,开始背景,结束背景,以及背景音乐。我方飞机可以随意移动,敌方飞机无规律出现。游戏玩家通过鼠标移动控制飞机移动,我方飞机在游戏开始时就一直能发射子弹,当我方子弹碰到敌方飞机时,敌方飞机消失。当玩家飞机碰到敌方子弹时,生命值减一,直到玩家飞机生命值为一时,游戏结束。基本模块包括:游戏状态控制功能模块、游戏难度的调整模块、游戏界面绘画功能模块、玩家游戏控制功能模块等。本系统结构如下: (1)游戏状态控制功能模块:游戏的状态控制包括运行及结束游戏,首先进入游戏的开始页面,在游戏进行时,玩家必须躲避或者击落敌方飞机,玩家飞机的生命值降低为0的时候游 戏就会结束,弹出游戏结束界面 (2)游戏难度的调整模块:玩家玩的时间越长游戏的难度越高,敌方飞机的数量越多、敌方飞机出现的频率越高,玩家保证飞机的存活的难度越高,操作难度相应也高。 (3)游戏界面绘画功能模块:左上角会显示玩家飞机当前生命值,游戏时间,当前分数,其他地方用来输出玩家对我方飞机的控制与敌方飞机的动作显示 (4)玩家游戏控制功能模块: 玩家可以通过控制鼠标或者键盘来控制方飞机(Hero airplane)的移动。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GuochaoHN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值