前情提要

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

抢先体验

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

雷霆战机

观看完视频之后,相信大家对这个游戏所需要的元素已经有基本的了解了,雷霆战机游戏基本由图片素材、音乐素材和逻辑代码构成,是一个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 ythis.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 methodpublic 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<>();@Overridepublic 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)) {// 如果相交  敌机子弹消亡  英雄战机的生命值减10this.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

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

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

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

  1. java 雷霆战机游戏 飞机大战 全过程教学+免费素材(附全部源代码)

    这个游戏已是我第二次编写了,之前写过一个简易版的飞机大战类似demo.这次在上一次基础上添加了许多元素,增添了可玩性. 游戏效果图如下: ps :完整源码+视频教程+论文文档 :java雷霆战机完整资 ...

  2. html考眼力游戏,史上最考眼力的猜图游戏:点亮最野足迹

    史上最考眼力的猜图游戏:点亮最野足迹 考眼力考眼力啦......你有柯南一样的观察力?有福尔摩斯一样的判断力吗?<最野假期>为你送上一款史上最考眼力的剧照猜图游戏,以下是央视少儿频道< ...

  3. 史上最清晰的函数空间讲解

    史上最清晰的函数空间讲解 1.什么是数学的空间? 数学的空间定义了研究工作的对象和遵循的规则,研究工作的对象在空间中称之为元素,遵循的规则在空间中称之为结构,结构有线性结构(加法和数乘)和拓扑结构(距 ...

  4. 单链表-史上最清晰的尾插法和头插法

    02.单链表-史上最清晰的尾插法和头插法 1.单链表 链表是一系列的存储数据元素的单元,通过指针(引用)串联起来的,因此每个单元至少有两个域,一个域用于数据元素的存储,另一个域是指向其他单元的指针. ...

  5. 标星 2.7w+ 堪称史上最全的微信小程序开发资源汇总

    [公众号回复 "1024",免费领取程序员赚钱实操经验] 2017 年 1 月,微信小程序一夜成名. 微信小程序成名后,各大厂开始效仿,相继出现了支付宝.百度.今日头条.QQ.抖音 ...

  6. 【游戏开发】《Java游戏服务器架构实战》项目在windows上部署

    [游戏开发]<Java游戏服务器架构实战>项目在windows上部署 文章目录 [游戏开发]<Java游戏服务器架构实战>项目在windows上部署 一.配置项目基础环境 二. ...

  7. 史上最强Tomcat8性能优化,网易云课堂java高级架构师

    点击"Server Status",输入用户名.密码进行登录,tomcat/tomcat 登录之后可以看到服务器状态等信息,主要包括服务器信息,JVM,ajp和http信息 AJP ...

  8. 暑期Android游戏开发——小兔子跳铃铛(附源码)

    暑期Android游戏开发--小兔子跳铃铛(附源码) 一. 背景说明 我在南京的一所高校学习软件工程.学院里每年会举行一次"创新杯"软件比赛,鼓励同学自主学习和创新.我和几个好兄弟 ...

  9. 《大富翁手机游戏开发实战--基于Cocos2d-x 3.2引擎》现已登陆各大网络销售平台发售

    <大富翁手机游戏开发实战--基于Cocos2d-x 3.2引擎>现已登陆各大网络销售平台发售! 部分网售地址: 当当:http://product.dangdang.com/2378178 ...

  10. unity塔防游戏开发之基于标点法来简易实现敌人路线移动

    unity塔防游戏开发之基于标点法来简易实现敌人路线移动 首先创建一个物体,把它设置为静态物体,给他创建一系列子物体,将这些子物体放在各个路线的转角处,这样就实现标点的目的,然后将这些路标的位置都设置 ...

最新文章

  1. mysql按升序创建索引_MySQL8新特性:降序索引详解
  2. MAC电脑快捷键整理
  3. 第一次在Linux服务器上部署项目,看完这篇轻松应对
  4. SET QUERY_GOVERNOR_COST_LIMIT
  5. echarts.js:1136 Uncaught Error: Initialize failed: invalid dom.
  6. 为什么SpringBoot的jar可以直接运行
  7. 知方可补不足~CSS中margin,padding,border-style有几种书写规范
  8. 程序员相亲竟然因为这个被拒绝了......
  9. QQ 可注销版本上线;拼多多成立技术顾问委员会;董明珠:建议偷手机判 10 年 | 极客头条...
  10. php vue seo,处理 Vue 单页面 SEO 的另一种思路
  11. 网络共享服务器 samba
  12. 如何在ppt全屏演示时仍然显示任务栏?
  13. SIFT(Scale Invariant Feature Transform) 算法小结及实验
  14. Vue之v-on之修饰符prevent(007)
  15. 【SCOI2009】粉刷匠
  16. 2021年高处作业安装拆除维护证考试题库及安装拆除维护试题解析
  17. LeetCode(力扣)_接雨水
  18. 系统安装部署系列教程(一):安装原版系统镜像
  19. 使用3DMax制作一个象棋棋子
  20. Cocos2d-x 2.0.1 学习tests示例(一)Manual Transformation

热门文章

  1. 洛杉矶湖人队的科比 - 布莱恩特,一个最大的
  2. 决赛巅峰之战落幕,第二届翼支付杯大数据建模大赛完美收官
  3. 北京车管所 与 换领驾驶证过程
  4. 数据全生命周期管理,华为FusionData一个方案搞定
  5. 空间后方交会编程c语言,单像空间后方交会(python实现)
  6. Oracle LiveLabs实验:Application Continuity Fundamentals
  7. 知识库构建前沿:自动和半自动知识提取
  8. ubuntu18.04安装cudnn出现错误:FreeImage is not set up correctly. Please ensure FreeImae is set up correctly
  9. 中国顶级CEO经典语录
  10. 自定义圆角的ImageView 还可以实现图片的圆形、椭圆形展示。