史上最清晰的雷霆战机游戏开发全过程(基于java,素材和源码均齐全)

前情提要

此次咕咕为大家准备了雷霆战机游戏开发的全过程,我将整个开发都写在了文档中,有图有真相,步步到位,供大家进行细节功能实现上的参考,除此之外,游戏所需的图片与音乐素材、逻辑脑图都一 一放在了文末的彩蛋中。由于这篇文章是对此次游戏开发的大方面的总结剖析,可能在细节方面不是讲解得特别透彻,所以提前告诉了大家文末的详情文档,也可以结合开发详情文档和文章一起参考此次雷霆战机游戏的开发吼

抢先体验

我们先来看看,一个雷霆战机游戏的最终效果:

雷霆战机

观看完视频之后,相信大家对这个游戏所需要的元素已经有基本的了解了,雷霆战机游戏基本由图片素材、音乐素材和逻辑代码构成,是一个2D平面游戏;说白了,雷霆战机游戏就是由逻辑代码将图片素材和音乐素材糅合整理起来的一个东西,所以其中最重要的还是要搞清楚游戏内部的逻辑分析。

需求分析

在进行开发之前,我们需要对游戏内部所具备的基本功能要了如指掌,为此,我将总体功能分为以下4种类型:

  • 英雄战机
    1.显示英雄战机
    2.显示英雄战机的子弹
    3.英雄战机能够移动
    4.英雄战机能够连续开火,并且子弹之间需要有空隙,不能过于密集
    5.英雄战机的子弹能够击杀敌机

  • 敌方战机
    1.显示敌机
    2.显示敌机的子弹
    3.敌机能够随机移动和变换方向(PS:未知的敌人才是最可怕的,嘿嘿嘿),并且敌机是需要随机产生的,产生之后均飞向游戏界面下方直至消失
    4.敌机能够连续开火,并且子弹之间需要有空隙,不能过于密集
    5.敌机的子弹能够击杀英雄战机的子弹

  • 页面显示和音乐
    1.显示游戏的基本界面,能够自定义宽高与位置
    2.显示游戏的背景,兵且能够让背景图无限滚动,直至游戏结束
    3.游戏需要有背景音乐,开火的声音,英雄战机击杀敌机的爆炸声
    4.需要在游戏界面右上方显示英雄战机的血量值

  • 爆炸及其其余细节处理
    1.英雄战机需要具备生命值,当被敌机的子弹击中时,血量应该降低,当血量将至零时,英雄战机死亡
    2.在英雄战机的子弹接触到敌机时,需要产生爆炸效果,并且敌机消失
    3.在敌机的子弹接触到英雄战机时,需要产生爆炸效果;当英雄战机的血量降至为零时,敌机消失,并且升起游戏结束的文字,页面停止滚动,敌机不再产生
    4.游戏需要实现超级火力模式,也就是通过按住某个键,英雄战机可连续发出满屏子弹

以上就是雷霆战机游戏的基本功能需求,有了实际的功能需求,在开发过程中就已经明确了方向,接下来的事情就是将功能需求转化为具体逻辑代码的实现。

模块化处理

为了更好管理我们的代码,我们需要将游戏整体进行模块拆分的处理,每个文件负责各自的游戏功能;因此,可将整个游戏拆分成以下7个类来共同实现游戏的整体功能

  1. 主界面类:显示游戏的窗口(大小、位置、可见性)
  2. 面板类:显示游戏的内容(游戏背景、战机、敌机、子弹、爆炸)
  3. 英雄战机类:定义英雄战机相关的信息(大小、位置、移动、开火)
  4. 英雄战机子弹类:定义英雄战机的子弹的相关信息(大小、位置、移动)
  5. 敌机类:定义敌机相关的信息(大小、位置、移动、开火)
  6. 敌机子弹类:定义敌机子弹的基本信息(大小、位置、移动)
  7. 爆炸类:定义爆炸的基本信息(大小、位置、爆炸)

主界面类:RaidenGameMain

主界面类需要实现一个window下的窗口,所以在创建主界面类的时候需要继承java的JFrame这个父类,这样才能正常的实现一个窗口。在此类我们需要在构造函数里设置窗口的宽高、位置、标题、可见性等等基本信息,然后在主函数入口实例化此类对象即可创建一个自定义窗口。
主界面类需要实现以下功能:

  • 设置窗口在显示屏中显示的位置
  • 设置窗口的大小(宽高)
  • 设置窗口的标题
  • 设置窗口关闭程序
  • 设置窗口不允许调整大小
  • 设置窗口内的十字光标
  • 设置窗口的可见性(此设置必须放在构造函数的最末尾,否则会引发一些bug)
  • 将面板类放在主界面上,通过new一个面板类对象实现
  • 为游戏窗口添加鼠标移动监听器,处理鼠标移动的操作:主要是需要实现战机跟随鼠标移动的功能
  • 为游戏窗口添加鼠标状态监听器,处理鼠标点击与释放的操作:主要是实现英雄战机可以通过鼠标的点击与释放进行开火的功能
  • 为游戏窗口添加添加鼠标监听器,监测鼠标的按下与松开状况:主要是实现通过按下S键就可以调用超级火力的功能(这里的S键是开发者自定义的按键,如果你喜欢A键,那么也可以设置为A键)

下面插入主界面类的源代码:

package com.fw.raiden;
import java.awt.Cursor;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

import javax.swing.JFrame;
public class RaidenGameMain extends JFrame {
	/**
	 * 我们游戏的主界面
	 * @author DELL
	 */
	private static final long serialVersionUID = 1L;
	//构造方法,当创建类的对象的时候,也就是new的时候自动调用
	public RaidenGameMain() {
		
		// 设置显示的位置,设置窗口的坐标: x y
		this.setLocation(550,10);
		
		// 设置窗口的大小: 宽 高
		this.setSize(800,1000);
		
		// 设置窗口关闭程序
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		// 设置窗口的标题
		this.setTitle("咕咕的压箱战机");
		
		// 设置游戏窗口不允许调整大小
		this.setResizable(false);
		
		// 设置游戏内部为十字光标
		this.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
		
		// 将Panel放在主界面中
		this.setContentPane(new RaidenGamePanel());
		
		// 为游戏窗口添加鼠标移动监听器,处理鼠标移动的操作
		this.addMouseMotionListener(new MouseMotionListener() {
			
			// 创建鼠标移动事件
			public void mouseMoved(MouseEvent e) {
				RaidenGamePanel.myHero.mouseMoved(e);
			}
			
			// 创建鼠标拖拽事件
			public void mouseDragged(MouseEvent e) {
				RaidenGamePanel.myHero.mouseMoved(e);
			}
			
		 });
		
		// 为游戏窗口添加添加鼠标监听器,监测鼠标的按下与松开状况
		// this指向本类实例对象
		this.addMouseListener(new MouseAdapter() {
			// 关于mousePressed方法的具体实现可以参考word文档
			@Override
			// 绑定鼠标按下的事件
			public void mousePressed(MouseEvent e) {
				// myHero是静态变量  可以全局访问
				// 将开火标志置为真
				Hero.fireFlag = true;
			
			}
			@Override
			// 绑定鼠标释放的事件
			public void mouseReleased(MouseEvent e) {

				// 将开火标志置为假
				Hero.fireFlag = false;
				
			}
		});
		
		// 设置键盘监听器
		this.addKeyListener(new KeyAdapter() {
			@Override
			// 绑定键盘按下事件 
			public void keyPressed(KeyEvent e) {
				// 当S键被按下时触发超级火力模式
				if(e.getKeyCode() == KeyEvent.VK_S) {
					// 调用超级火力
					RaidenGamePanel.myHero.superFire();
				}
			}
		});
		
		// 设置窗口的可见性,默认为不可见的(一定要在主界面类的构造函数的末尾再设置!!!!!!!!)
		this.setVisible(true);
	}
		
	//输入main,使用快捷键Alt + / ,选择main method
	public static void main(String[] args) {
		new RaidenGameMain();
	}
}

关于鼠标事件和键盘事件监听的设置,是使用了由java提供的方法来编写的,所以这里只需要记住这些方法即可,不必硬钻其具体代码的逻辑实现,这些方法是人家已经封装好了的,我们只需要懂得用即可,这也大大提高了我们的开发效率!

面板类:RaidenGamePanel

面板的作用是在主界面窗口的基础下再添加一层窗口,类似于PS中的图层叠加,具体的空间位置就是面板类始终在主界面窗口的上方

面板类需要将游戏的所有画面都使用java提供的paint方法画出来,其中包括背景图、英雄战机、敌机、子弹、爆炸等等,其中最重要的就是将背景图片滚动起来,那么这里我们需要用到线程的知识,在线程是无限循环调用paint方法让背景图片实现无限滚动的效果

以下是面板类需要实现的功能:

  • 加载背景图片与背景音乐文件
  • 画出背景图片
  • 游戏开始时无限循环播放背景音乐,背景图片无限滚动
  • 画出英雄战机
  • 画出英雄战机的子弹
  • 画出敌机
  • 画出敌机的子弹
  • 画出爆炸
  • 使用线程类将paint方法无限循环地被调用,使界面具备动态效果

下面插入面板类的源代码:

package com.fw.raiden;

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.swing.JPanel;

/**
 *游戏内容面板
 *@author DELL 
 */
@SuppressWarnings("deprecation")
public class RaidenGamePanel extends JPanel {
	private static final long serialVersionUID = 1L;
	
	// 构造方法,用于调用线程
	public RaidenGamePanel() {
		// 启动线程
		MyGameThread my = new MyGameThread();
		my.start();	
		
	}
	
	// 定义一个变量,表示的是背景图片的 y 坐标, y代表面板顶端到图片顶端的距离
	int y= -2400; // 说明面板顶端到图片顶端的距离是-2400,也就是说图片顶端在面板顶端的上面,所以面板里的内容实际上是图片中间部分的内容
		
	// 游戏結束,文字显示的起始位置
	int gameOverStrY = 1000;
	
	// 游戏結束的变量
	boolean gameFlag = false;
	
	// 定义和加载游戏背景图片
	static Image bjImg;
	
	// 通过系统的工具包类,来完成图片的加载和创建
	static Toolkit tk = Toolkit.getDefaultToolkit();
	
	
	// 加载并播放背景音乐
	static AudioClip ac;
	
	// 静态块
	static {
		// 加载音乐
		ac = Applet.newAudioClip(RaidenGamePanel.class.getClassLoader().getResource("Every Breath You Take.mid"));
		
		// 加载背景图片
			bjImg = tk.createImage(RaidenGamePanel.class.getClassLoader().getResource("bj002.jpg"));
	}
	
	// 创建战机对象
	static Hero myHero = new Hero(300,700);
	
	// 创建战机的子弹集合
	static List<HeroMissile> herMissileList = new ArrayList<>();
	
	// 创建敌机集合
	static List<Enemy> enemyList = new ArrayList<>();
	
	// 创建爆炸集合
	static List<Explode> explodeList = new ArrayList<>();

	// 创建敌机子弹集合
	static List<EnemyMissile> enemyMissileList = new ArrayList<>();
	
	@Override
	public void paint(Graphics g) {
		
		// 给敌机增加数量
		if(enemyList.isEmpty()) {
			// 创建一个随机数  代表随机产生的战机的数量
			int n = new Random().nextInt(20)+10;
			// 遍历循环 逐个将敌机加入到集合中
			for(int i =0;i<n;i++) {
				// 随机创建每架敌机的初始横纵坐标
				int x = new Random().nextInt(658);
				int y = new Random().nextInt(400)-400;
				// 随机创建敌机对象
				Enemy en = new Enemy(x,y);
				// 将敌机对象加入到集合中
				enemyList.add(en);
			}
		}
		
		// 画出背景图 
		g.drawImage(bjImg, 0, y, 800, 1200*3, this);
		
		// 画英雄战机
		myHero.paint(g);
		
		// 画出英雄战机的子弹
		// 遍历集合,逐个画出英雄战机的子弹
		for(Iterator<HeroMissile> it = herMissileList.iterator();it.hasNext();) {
			// 英雄战机子弹恒等于下一个英雄战机子弹集合中的下一个元素
			HeroMissile missile = it.next();
			// 如果英雄战机子弹的存活状态为真  那么就画出英雄战机子弹
			if(missile.live ) {
				missile.paint(g);
				// 调用击打敌机的方法
				missile.hitEnemyList(enemyList);
			}else {
			// 如果英雄战机子弹的存活状态为假  那么就从集合中移除掉该子弹
				it.remove();
			}
		}
		
		// 画出敌机
		for(Iterator<Enemy> it = enemyList.iterator();it.hasNext();) {
			// 敌机对象恒等于下一个敌机集合中的下一个元素
			Enemy enemy = it.next();
			// 如果敌机的存活状态为真  那么画出敌机
			if(enemy.lives ) {
				enemy.paint(g);
			// 如果敌机的存活状态为假  那就从集合中移除掉该敌机
			}else {
				it.remove();
			}
		}

		// 画出爆炸
		for(Iterator<Explode> it = explodeList.iterator();it.hasNext();) {
			// 爆炸对象恒等于下一个敌机集合中的下一个元素
			Explode explode = it.next();
			// 如果爆炸的存活状态为真  那么画出爆炸
			if(explode.live) {
				explode.paint(g);
			// 如果爆炸的存活状态为假  那就从集合中移除掉该爆炸
			}else {
				it.remove();
			}
		}
		
		// 画出敌机子弹
		for(Iterator<EnemyMissile> it = enemyMissileList.iterator();it.hasNext();) {
			// 敌机子弹对象恒等于下一个敌机子弹集合中的下一个元素
			EnemyMissile enemyMissile = it.next();
			// 如果敌机子弹的存活状态为真  那么画出敌机子弹
			if(enemyMissile.live) {
				enemyMissile.paint(g);
				// 调用打英雄战机的方法
				enemyMissile.hitHero(myHero);
			// 如果敌机的存活状态为假  那就从集合中移除掉该敌机
			}else {
				it.remove();
			}
		}	
			
		// 画出一段文字,显示子弹集合中的元素数量
		// 设置颜色
		g.setColor(Color.BLACK);
		// 设置字体
		g.setFont(new Font("宋体", Font.BOLD , 30));
		g.drawString("战机子弹的总数量是:"+ herMissileList.size(), 20, 50);
		// 画出一段文字,显示// 画出英雄战机的生命值
		g.drawString("英雄的生命值:"+ myHero.life, 500, 50);
		
		// 画出一段文字,显示游戏结束界面
		if(!myHero.live) {
			// 设置颜色
			g.setColor(Color.RED);
			// 设置字体
			g.setFont(new Font("宋体", Font.BOLD , 60));
			// 将文字画出
			g.drawString("GAME OVER", 250, gameOverStrY);
			// 设置初始文字位置
			gameOverStrY -= 5;
			// 当文字位置距离主界面顶端为500单位时  游戏结束的标志为真
			if(gameOverStrY <= 500) {
				gameFlag = true;
			}
		}	
}
	
		// 开发一个线程类,用来不断增加Y坐标的值,是一个内部类
		class MyGameThread extends Thread{
			public void run() {
				//播放背景音乐
				ac.loop();
				
				while(true){
					// 如果游戏结束标志为真,停止线程
					if(gameFlag) {
						// 停止播放音乐
						ac.stop();
						// 停止线程
						return;
					}
					
					// 滚动背景图片
					y += 3; // 如果要缩短面板顶端至图片顶端的距离,需要加正数,才能使面板顶端至图片顶端的距离逐渐缩小
					
					// 重新调用paint方法
					RaidenGamePanel.this.repaint();
					
					// 当图片顶端到达主界面顶端时 重置背景图片顶端至主界面顶端的距离  这就是每游戏运行一段时间后页面会闪一下的原因
					if(y >= 0){
						y = -2400;
					}
					try {
						// 休眠30毫秒 然后继续画出所有元素
						sleep(30); // 30毫秒   1秒=1000毫秒
					} catch (InterruptedException e) {
						// 捕获异常并打印栈堆信息
						e.printStackTrace();
					}
					
				}
			}
		}
		
	
}

此类中需要特别注意线程的run方法是需要重写的,并且在面板类的构造函数中是需要启动线程的,启动线程需要调用线程类的start方法;paint方法和repaint方法均是java内部封装的,调用即可,特别需要注意drawImage方法的六个参数,第一个参数是具体加载图片的Image变量,第二个参数是图片初始位置的横坐标,第三个参数是图片初始位置的纵坐标,第四个参数是图片的宽度,第五个参数是图片的高度,第六个参数是指向本类对象的this指针,而在其他类中为null

英雄战机类:Hero

英雄战机类中自定义的方法有两个很重要,比如开火的方法,超级火力的方法

那么以下是英雄战机类需要实现的功能:

  • 初始化英雄战机的宽高
  • 英雄战机需要有生命值
  • 英雄战机需要有存活状态
  • 加载英雄战机的图片并画出
  • 加载开火的音乐文件并在开火时播放
  • 自定义一个开火的方法,使主界面类中能够调用此方法完成子弹的连续发射
  • 自定义一个超级火力的方法,使得主界面类中能否调用此方法完成英雄战机子弹的全屏扫射
  • 自定义一个英雄战机跟着鼠标移动的方法,使得主界面类中能够调用
  • 设置一个获取英雄战机区域的方法,使得敌机子弹类中的击打英雄战机方法中能够被调用,此方法为java内置的封装方法

下面插入英雄战机类的源代码:

package com.fw.raiden;

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;

@SuppressWarnings("deprecation")
public class Hero {
	// 战机所处的位置
	int x;
	int y;
	
	// 战机的宽度和高度,是一个常量
	public static final int HERO_WIDTH = 150;
	public static final int HERO_HEIGHT =150;	
	
	// 构造方法
	public Hero(int x,int y) {
		this.x = x;
		this.y = y;
	}
	
	// 表示生死状态
	boolean live = true;
	
	// 表示英雄战机生命值
	int life = 1000;
	
	// 开火标识
	static boolean fireFlag = false;
	
	// 定义一个子弹引用,表示是上一发子弹,对象均为引用类型,类似于指针
	HeroMissile oldMissile;
	
	// 战机图片
	static Image heroImg;
	
	// 加载开火声音
	static AudioClip ac;
	
	// 工具包类
	static Toolkit tk =Toolkit.getDefaultToolkit();
	
	// 静态加载块
	static {
		ac = Applet.newAudioClip(Hero.class.getClassLoader().getResource("zzam.au"));
		heroImg = tk.createImage(Hero.class.getClassLoader().getResource("hero010.png"));
	}
	
	// 画出战机
	public void paint(Graphics g) {
		// 如果英雄战机已经死亡,那么不再画出英雄战机
		if(!live) {
			return;
		}
		// 画出英雄战机
		g.drawImage(heroImg, x, y, HERO_WIDTH, HERO_HEIGHT, null);
		
		// 如果开火标识为真,调用fire方法进行开火
		if(fireFlag) {
			fire();
		}
	}

	// 调整鼠标与英雄战机的相对坐标,由于英雄战机的图片不同,那么产生的坐标偏差也会不同
	public void mouseMoved(MouseEvent e) {
		x = e.getX() - 85;
		y = e.getY() - 130;
	}
	
	// 开火方法
	public void fire() {
		
		// 判断上一发子弹是否飞出一定距离或者为空
		if(oldMissile == null){
			// 播放开火声音
			ac.play();
			// 创建新的英雄战机子弹
			HeroMissile missile = new HeroMissile(x+17,y - 60);
			// 将新的英雄战机子弹添加至集合中
			RaidenGamePanel.herMissileList.add(missile);
			// 恒赋值为上一颗英雄战机子弹
			oldMissile = missile;
			
		}else if(Math.abs(y - oldMissile.y ) >100 || oldMissile.live == false){//解决战机与敌机重合而无法开火的bug
			// 播放开火声音
			ac.play();
			// 创建新的英雄战机子弹
			HeroMissile missile = new HeroMissile(x+17,y - 60);
			// 将新的英雄战机子弹添加至集合中
			RaidenGamePanel.herMissileList.add(missile);
			// 恒赋值为上一颗英雄战机子弹
			oldMissile = missile;	
		}
	}
	
	// 获取英雄战机区域
	public Rectangle getRect() {
		return new Rectangle(x,y,HERO_WIDTH,HERO_HEIGHT);
	}
	
	// 超级火力方法
	public void superFire(){
		// 一次性产生十倍的子弹  并且子弹的横坐标是依次增加的  这就造成了全屏扫射的奇观
		for(int i = 0;i<10;i++) {
			HeroMissile missile = new HeroMissile(130*i,y - 60);
			RaidenGamePanel.herMissileList.add(missile);
		}
	}
}

此类需要注意超级火力产生的逻辑,是因为同时产生了与普通子弹相比的十倍数量的子弹,并且让其每一颗子弹的初始化横坐标为依次增加的情况。还需要关注子弹密集问题和英雄战机与敌机机身接触无法发射子弹的bug是如何处理的,可结合文档一同理解。

英雄战机子弹类:HeroMissile

英雄战机子弹类肩负着子弹的产生和发射的大任,并且还要有能攻打敌机的能力,但是实现起来并不是那么复杂

以下是英雄战机子弹类所需的功能:

  • 初始化英雄战机子弹的宽高
  • 加载英雄战机子弹的图片并画出
  • 英雄战机子弹在发射后能够自行往上移动
  • 英雄战机子弹能够击打敌机
  • 英雄战机子弹需要有存活状态

下面插入英雄战机子弹类的源代码:

package com.fw.raiden;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.util.Iterator;
import java.util.List;


/**
 * 英雄战机子弹类
 * @author DELL
 * 
 */
public class HeroMissile {
	
	// 定义英雄战机子弹类的属性
	int x;
	int y;
	
	// 构造方法
	public HeroMissile(int x,int y) {
		this.x = x;
		this.y = y;
	}
	
	// 定义是否爆炸的标识
	static boolean bangFlag = false;
	
	// 英雄战机子弹类的宽高属性
	public static final int HERO_MISSILE_WIDTH = 117;
	public static final int HERO_MISSILE_HEIGHT = 117;
	
	// 定义英雄战机子弹图片
	static Image heroMissileImg;
	
	// 定义英雄战机子弹生死状态的变量
	boolean live = true;

	// 工具类
	static Toolkit tk = Toolkit.getDefaultToolkit();
	
	// 静态加载块
	static {
		// 010战机
		heroMissileImg = tk.createImage(RaidenGamePanel.class.getClassLoader().getResource("zidan0011.png"));
	}
	
	// 画出英雄战机子弹
	public void paint(Graphics g) {
		// 画出背景图
		g.drawImage(heroMissileImg, x, y, HERO_MISSILE_WIDTH, HERO_MISSILE_HEIGHT, null);
		
		// 英雄战机子弹移动
		move();
	}
	
	// 让英雄战机子弹移动
	public void move() {
		y -= 40;	
		// 英雄战机子弹飞出边界	
		if(y <= -20) {
			// 英雄战机子弹的生死状态置为假
			live = false;
		}
	}
	
	
	// 获取英雄战机子弹区域
	public Rectangle getRect() {
		return new Rectangle(x,y,HERO_MISSILE_WIDTH,HERO_MISSILE_HEIGHT);
	}
	
	// 打一个敌机的方法
	@SuppressWarnings("deprecation")
	public boolean hitEnemy(Enemy enemy) {
		// 获取英雄战机子弹的区域
		Rectangle missileRect = this.getRect();
		// 获取敌机的区域
		Rectangle enemyRect = enemy.getRect();
		
		// 判断两个区域是否相交
		if(missileRect.intersects(enemyRect)) {
			
			// 打中了 ,敌人死 ,英雄战机子弹死
			this.live =false;
			enemy.lives =false;
			
			// 爆炸声
			Explode.ac.play();
			
			// 产生爆炸
			Explode exp = new Explode(x,y);
			RaidenGamePanel.explodeList.add(exp);
			
			// 打中敌机 返回真
			return true;
			
		}
		// 没打中敌机  返回假
		return false;
	}
	
	// 打一群敌机的方法
	public void hitEnemyList(List<Enemy> enemyList) {
		
		// 将敌机集合的元素一个一个拿出来打
		for(Iterator<Enemy> it = enemyList.iterator();it.hasNext();) {
			// 创建敌机对象
			Enemy  enemy = it.next();
			// 将打一个敌机对象的结果返回
			boolean b =hitEnemy(enemy);
			// 如果打中集合中的某个敌机,那么后面的敌机就不能再打了,因为子弹已经死了
			if(b) {
				return;
			}
		}
	}		
}

本类中需要特别注意英雄战机子弹击打敌机的具体实现思路,首先先定义一个击打一个敌机的方法,是使用了英雄战机子弹是否与敌机区域相交的原理,如果相交,那么便是打中了,反之则是没有打中,打中之后敌机死亡,随之而来的就是爆炸的产生;那么有了攻打一个敌机的方法,自然而然地就能写出打一群敌机的 方法,这里需要特别注意,一个英雄战机子弹只能击打一个敌机,所以需要及时返回,不然就会出现一刻英雄战机子弹击穿同一直线上的所有敌机的情况。

敌机类:Enemy

敌机类的特点是“随机性”,对的,从敌机的产生至死亡这段时间,它的所有的动作都必须是随机的,让英雄战机无法预测其行踪,这样才不会显得有迹可循,游戏难度才不会显得相对简单

以下是敌机类需要实现的功能:

  • 初始化敌机的宽高
  • 加载敌机图片并且画出
  • 敌机需要有存活状态
  • 敌机需要“随机性”地移动
  • 敌机可以开火发射子弹
  • 需要设置一个获取敌机区域的方法,使得英雄战机子弹类中可以调用

下面插入敌机类的源代码:

package com.fw.raiden;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.util.Random;

public class Enemy {
	
	// 定义敌机的初始位置
	int x;
	int y;
	
	// 构造方法
	public Enemy(int x, int y) {
		this.x = x;
		this.y = y;
	}
	
	// 表示敌机生死状态
	boolean lives = true;
	
	// 定义敌机的宽高
	public static final int HERO_ENEMY_WIDTH = 100;
	public static final int HERO_ENEMY_HEIGHT = 100;
	
	// 定义一个表示方向的值
	int dir = 0;
	
	// 定义每次改变方向后飞行的距离
	int n = new Random().nextInt(20)+10;

	// 定义加载敌机图片的数组变量
	static Image[] img = new Image[3];
	
	// 工具包类
	static Toolkit tk =Toolkit.getDefaultToolkit();
	
	// 静态块加载敌机图片
	static {
		img[0] = tk.createImage(Enemy.class.getClassLoader().getResource("dijileft.gif"));
		img[1] = tk.createImage(Enemy.class.getClassLoader().getResource("diji.gif"));
		img[2] = tk.createImage(Enemy.class.getClassLoader().getResource("dijiright.gif"));
	}

	// 画出敌机
	public void paint(Graphics g) {
		// 当n<=0时  敌机需要改变方向和向此方向移动的距离
		if(n<=0) {
			// 每次画敌机的时候,随机改变敌机的方向,方向改变之后,飞行一段距离之后再改变方向
			dir =  new Random().nextInt(3);
			n = new Random().nextInt(20)+10;
		}
		// 在n>0之前  敌机都顺着原先的方向移动
		n--;
		
		// 画出敌机
		g.drawImage(img[dir], x, y, HERO_ENEMY_WIDTH, HERO_ENEMY_HEIGHT, null);
		
		// 敌机移动
		move();	
		
	}
	
	// 移动方法
	public void move() {
		// 所有敌机均向下运动
		y += 5;
		// 当敌机为左敌机时  需要向左移动
		if(dir==0) {
			x -=5;
		}
		// 当敌机为右敌机时  需要向右移动
		if(dir==2) {
			x +=5;
		}
		// 如果敌机超过主界面的下端  那么敌机的存活状态置为假
		if(y>1000) {
			lives=false;
		}
		
		// 每次移动时开火  
		int i = new Random().nextInt(1000);
		// 随机性的开火
		if(i > 990) {
			fire();
		}
	}
	
	// 获取敌机区域
	public Rectangle getRect() {
		return new Rectangle(x,y,HERO_ENEMY_WIDTH,HERO_ENEMY_HEIGHT);
	}
	
	// 敌机开火
	public void fire() {
		// 产生一个子弹
		EnemyMissile enemyMissile = new EnemyMissile(x + 40,y + 60);
		// 添加至子弹集合
		RaidenGamePanel.enemyMissileList.add(enemyMissile);
	}	
}

本类中需要特别注意敌机的移动需要具备随机性,所以我们大量使用了产生随机数的方法,来对敌机的移动方向和距离均作了随机性的处理

敌机子弹类:EnemyMissile

敌机子弹类最终要的任务无非就是击打英雄战机,但是不同的是,英雄战机子弹可以一击必杀敌机,但是敌机子弹需要多次击中英雄战机才能够击杀英雄战机,所以在代码设计上的逻辑式基本一致的,只是细节的不同。

以下是敌机子弹类需要实现的功能:

  • 初始化敌机子弹的宽高
  • 加载敌机子弹的图片并画出
  • 敌机子弹需要有存活状态
  • 敌机子弹在被敌机发射出去后需要向下移动
  • 需要自定义攻打英雄战机的方法
  • 需要设置获取敌机子弹区域的方法,使得在攻打英雄战机的方法中能够被调用

下面插入敌机子弹类的源代码:

package com.fw.raiden;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;

public class EnemyMissile {
	
	// 表示敌机子弹生死状态
	boolean live = true;
	
	// 敌机子弹坐标
	int x;
	int y;
	
	// 敌机子弹宽高
	int w = 20;
	int h = 30;
	
	// 构造方法
	public EnemyMissile(int x,int y) {
		this.x = x;
		this.y = y;
	}
	
	
	
	// 定义加载敌机子弹的变量
	static Image img ;
	
	// 工具包类
	static Toolkit tk =Toolkit.getDefaultToolkit();
	
	// 静态块加载敌机子弹图片
	static {
		img = tk.createImage(Enemy.class.getClassLoader().getResource("dijizidan.gif"));
	}	
	
	// paint方法,画敌机子弹
	public void paint(Graphics g) {
		g.drawImage(img, x, y, w, h, null);
		// 敌机子弹移动
		move();
		
	}	
	
	// 移动方法
	public void move() {
		
		// 敌机子弹向下移动
		y += 15;
		
		// 飞出界面下边界,消亡
		if(y >= 1030 ) {
			live = false;
		}
	}
	
	// 获取敌机子弹区域
	public Rectangle getRect() {
		return new Rectangle(x,y,w,h);
	}
	
	// 打英雄战机的方法
	public void hitHero(Hero hero) {
		// 获取敌机子弹的区域
		Rectangle enemyMissileRect = this.getRect();
		// 获取英雄战机的区域
		Rectangle heroRect = hero.getRect();
		// 判断敌机子弹区域和英雄战机的区域是否相交
		if(enemyMissileRect.intersects(heroRect)) {
			// 如果相交  敌机子弹消亡  英雄战机的生命值减10
			this.live = false;
			int heroLife = hero.life - 10;
			// 当英雄战机的生命值小于且等于零时 将英雄战机的存活状态只为假
			if(heroLife<=0) {
				hero.live = false;
				// 如果英雄战机的生命值出现负数的情况  将生命值重置为零
				heroLife = 0;
			}
			// 更新生命值
			hero.life = heroLife;
			// 产生爆炸
			Explode exp = new Explode(x,y);
			// 将爆炸添加至集合中
			RaidenGamePanel.explodeList.add(exp);
		}
	}

}

此类中需要注意一些小bug,比如英雄战机的生命值不能出现负数的情况,所以我们需要对生命值进行判断,当生命值出现负数时,需要将其重置为零。还有注意敌机子弹飞出边界时,需要进行敌机子弹消亡的处理,不然会占用不必要的内存。

爆炸类:Explode

爆炸类的主要任务,就是在双方子弹击中对方时,来一顿炫酷的爆炸特效,那么在处理爆炸时就需要巧妙地处理了。

以下是爆炸类需要实现的功能:

  • 初始化爆炸的宽高
  • 加载爆炸的图片并且画出

下面插入爆炸类的源代码:

package com.fw.raiden;

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;

@SuppressWarnings("deprecation")
public class Explode {
	// 位置
	int x;
	int y;
	
	// 表示爆炸的生死状态
	boolean live = true;
	
	// 大小
	int w = 60;
	int h = 60;
	
	// 构造方法
	public Explode(int x,int y){
		ac.play();
		this.x=x;
		
		this.y=y;
	}
	
	// 数组下标
	int index = 0;
	
	// 每张图片画n次
	int n = 3;
	
	// 爆炸图片
	// 定义加载敌机图片的数组变量
	static Image[] img = new Image[11];
	
	// 加载爆炸声音
	static AudioClip ac;
	
	
	// 工具包类
	static Toolkit tk =Toolkit.getDefaultToolkit();
	
	// 静态块加载敌机图片
	static {
		ac = Applet.newAudioClip(Hero.class.getClassLoader().getResource("missle.au"));
		img[0] = tk.createImage(Enemy.class.getClassLoader().getResource("b0.gif"));
		img[1] = tk.createImage(Enemy.class.getClassLoader().getResource("b1.gif"));
		img[2] = tk.createImage(Enemy.class.getClassLoader().getResource("b2.gif"));
		img[3] = tk.createImage(Enemy.class.getClassLoader().getResource("b3.gif"));
		img[4] = tk.createImage(Enemy.class.getClassLoader().getResource("b1.gif"));
		img[5] = tk.createImage(Enemy.class.getClassLoader().getResource("b2.gif"));
		img[6] = tk.createImage(Enemy.class.getClassLoader().getResource("b3.gif"));
		img[7] = tk.createImage(Enemy.class.getClassLoader().getResource("b4.gif"));
		img[8] = tk.createImage(Enemy.class.getClassLoader().getResource("b5.gif"));
		img[9] = tk.createImage(Enemy.class.getClassLoader().getResource("b6.gif"));
		img[10] = tk.createImage(Enemy.class.getClassLoader().getResource("b7.gif"));
	}
	
	
	// 画爆炸
	public void paint(Graphics g) {
		// 如果爆炸状态为死,那么不再画出爆炸
		if(index == img.length) {
			live = false;
			return;
		}
		
		g.drawImage(img[index], x, y, w, h, null);
		
		// 让每一个图片都画3次
		if(n<=0) {
			index++;
			n = 3;
		}
		n--;
	}	
	
}

本类的终点就是如何将爆炸画得好看,并且连贯。我们使用了将图片画多次的方法,让图片存留的时间久一点,因为我们知道前面的线程类中是每休眠30毫秒就会重画一次页面,很显然30毫秒是不够的我们看一张图片的,所以我们需要画多次,得以让肉眼观察得到

彩蛋

作为一个热爱交流技术的开发者,咕咕将此次雷霆游戏开发项目的所有资料均放在网盘中啦,欢迎大家参考呀。(其中包括开发全过程详细文档、逻辑脑图、图片与音乐素材)

百度网盘链接: https://pan.baidu.com/s/19gYg58vMh33RXlwSuA0tWw

提取码: hvve

为了让大家看清楚我的文件的层级关系,我挂了个图给大家参考一下(主要是图片和音乐放的位置要准确)

在这里插入图片描述

如果百度网盘链接没法获取的话,可以下方留言哦,咕咕会第一时间收到回复滴,手码不易,点个赞呗,笔芯❥(^_-)

  • 24
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值