您的位置 首页 java

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

观前提示 : 本文涉及的小成果特别多,即使你不需要写一个和我完全相同的游戏,也可以按照需要查看某些特定功能的实现过程,说不定能够给您的程序开发带来一点小小的启发!PS:结合源代码阅读此文章更加高效。

本文章的内容可看作上一篇多线程游戏实例分享的扩展,上一篇文章介绍过的内容(如何实现一个简单的小游戏)在本文章中不再赘述,如果有疑问可以回访那篇博客( )或者私信小编。

游戏整体效果演示视频:

游戏规则简介

游戏整体框架

这里对String page使用了一个switch判断,用于判断现在玩家需要运行哪一个画面。当我们需要切换画面时(比如说从游戏界面切入到暂停界面)就可以通过使用语句更改page的值来实现界面的切换。 每一个界面都有对应的方法体

动图实现

在雷火游戏中用到了非常多的图片元素和动态图片元素,其中的绝大多数都是通过LoadingImage类加载出来的。当我们想要实现一个功能时,首先要明晰我们想要看到的具体效果,然后再制定方案。就拿这个例子来说,当我想在窗口中绘制一个动态图片时,只需要一行代码就能做到。

 bufg.drawImage(loadingImg.getMotionGraph(此处输入图片地址),0,0,null);  

我们需要实现的就是下面这一步:

在上一篇文章中讲到,我们游戏中的线程是这样运行的:

所以我们可以这样去实现想要的功能:程序最开始运行时,先将动图的每一帧加载到一个数组中,然后调用这个动图数组绘制图片时,根据目前的轮数(len)进行一个判断,判断一下现在应该要画的是动图的哪一帧图片,然后返回这个图片对象。

将动图的每一帧加载到一个数组中

 //生成图片数组对象generateGraphList
public Image[] ggList(int interval,String[] str){
/*该方法传入一个图片地址数组str,返回一个图片对象数组
其中整数interval指的是每两帧图片间隔的图片数,interval的值越大,动图运动的速度越慢
*///创建一个新的图片地址数组str,因为要留出存放间隔图片的位置
String[] strs=new String[str.length+str.length*interval];
//创建一个和strs相同长度的数组
Image[] imgs=new Image[str.length+str.length*interval];
String temp = null;
//将str数组扩充到strs数组
for(int i=0;i<str.length+str.length*interval;i++){
if(i%(interval+1)==0){
//当索引i为interval+1的整数倍时,插入str数组中对应的图片地址
strs[i]=str[i/(interval+1)];
temp=str[i/(interval+1)];
}else{
//插入间隔图片的地址(相当于复制了前面的图片)
strs[i]=temp;
}
}
for(int i=0;i<strs.length;i++){
//根据strs数组中的图片地址往图片数组imgs中加载对应的图片
ImageIcon imic=new ImageIcon(fileAddress+strs[i]);
imgs[i]=imic.getImage();
}

/*这里的imgss指的是存放imgs数组的二维数组
我们往HashMap hm_imgss中存放一对键值,这样我们使用动图数组时,
只需要根据动图数组第一张图片的地址就可以获取这个动图数组在imgss中的索引
*/hm_imgss.put(str[0], imgssCount);
imgssCount++;

return imgs;
}  

当我们需要使用动图时,调用以下方法

 //传入计数器len和图片数组键(这样程序才能知道你需要使用哪一张动图)
public Image getMotionGraph(int len,String key){
Image[]img=imgss.get(hm_imgss.get(key));
//根据计数器判断现在需要返回哪一帧图片
int count=len%img.length;
Image image=img[count];
return image;
}
当我们在创建LoadingImage对象的时候就把需要用到的图片全部加载  

当我们在创建LoadingImage对象的时候就把需要用到的图片全部加载

 public LoadingImage(){
//加载图片组(多张图片/动图)
imgss.add(ggList(10,new String[]{"图1.png","图2.png","图3.png","图4.png",……}));
}  

如果我需要画出上面这段代码表示的动图,使用的代码是

 LoadingImage ldimg=new LoadingImage();
g.drawImage(loadingImg.getMotionGraph("图1.png"),0,0,null);  

判断碰撞的更新

在上一个版本的飞机大战中,我们的子弹经常会穿过怪物,导致没有击中。这一次博主对判断碰撞进行了十分细致的优化,大大改善了碰撞判断的准确性。

我们可以写出一个方法,当我们需要判断两个飞行物是否碰撞时,只需要将两个飞行物对象传入该方法,该方法就能返回一个是否碰撞的Boolean值。该方法对以下四种碰撞情况进行判断,当其中任何一种情况成立时,返回true,即两个飞行物已经碰撞。

 private Boolean jgat(FlyObject f1,FlyObject f2){//传入两个飞行物对象
int f1_x=0;
int f1_y=0;
f1_x=f1.location.x;
f1_y=f1.location.y;

int f2_x=0;
int f2_y=0;
f2_x=f2.location.x;
f2_y=f2.location.y;

int f1_w=hm.get(f1.imgName).x;
int f1_h=hm.get(f1.imgName).y;
int f2_w=hm.get(f2.imgName).x;
int f2_h=hm.get(f2.imgName).y;

Boolean attack=(f2_x-f1_x<f1_w&f1_x-f2_x<f2_w&f2_y-f1_y<f1_h&f1_y-f2_y<f2_h);
return attack;
}  

判断碰撞更新后的效果:

HashMap 的使用

另外,上面的代码还用到了HashMap(哈希表),HashMap的作用就是创建一个表格,这个表格可以让我们通过键(key)去找到对应的值( value )。

比如说我知道一个同学的名字,我想要根据这个名字(键key)去查找到该同学的学号(值value),这就是一个非常简单的类似HashMap的例子。

HashMap有两个最常用的方法,一个是put(),另一个是get(),put顾名思义就是往HashMap中放入一对键值,get就是通过键去获取HashMap中对应的值。

在上面这个例子中我们通过飞行物的名字imgName(key)去找到飞行物图片的宽和高(valule)。

 private HashMap<String,Vector>hm=new HashMap<>();//创建HashMap对象,尖括号中存储的key和value可以是任何类

//当我们需要往HashMap中存入数据时
hm.put("我机.png", new Vector(100,100));//这里的key是飞行物图片名称,value是飞行物图片的宽高

//当我们需要通过飞行物图片的名字去获取图片的宽高时
int f1_w=hm.get(f1.imgName).x;
int f1_h=hm.get(f1.imgName).y;  

开始界面

整个开始界面的图形元素有三类,一类是在画面中穿插而过的飞机;一类是雷火的图案,一类是画面中心的按任意键进入游戏。博主在这里将它们的实现方法分别讲述一下。

飞机飞过动画效果实现

在上一篇博客中我们讲到过如何生成敌机,这里的动画效果使用的原理也是类似的。

首先我们需要创建一个列表来存放开场动画的飞机,并且准备两张图片。

 //开场飞机
public ArrayList<FlyObject>startfos=new ArrayList<>();
  

 //生成开场飞机动画
if(flag_start%10==0){
for(int i=0;i<6;i++){
//生成开场动画飞机
Vector location=new Vector(i*400,-100);
Vector  speed =new Vector(-10,10);
FlyObject fo=new FlyObject(location,speed,10,"开场敌机-左下.png");

Vector location2=new Vector(-800+i*400,900);
Vector speed2=new Vector(10,-10);
FlyObject fo2=new FlyObject(location2,speed2,-100,"开场敌机-右上.png");
//将开场动画飞机放入准备好的startfos列表中
startfos.add(fo);
startfos.add(fo2);
}
}

//绘制开场画面飞机图片,飞机移动
for(int i=0;i<startfos.size();i++){
FlyObject fo=startfos.get(i);
bufg.drawImage(loadingImg.getGraph(fo.imgName), fo.location.x, fo.location.y, null);
fo.move();
}

//清理飞出画面之外的飞机
if(startfos.size()>200){for(int i=0;i<startfos.size()-200;i++){startfos.remove(0);}}  

雷火图案

在开场画面中,雷火字样图案从窗口最上端向下移动,一段时间后停止在窗口中心位置,这是我们想要实现的效果。

在这里博主定义了一个flag_start,每执行完一轮画图操作这个flag_start就会加一(和len的作用相似),当这个整数小于26时,雷火字样图案的纵坐标每一轮都增加固定的值,从而达到向下以固定速度进行移动的效果。当这个整数大于等于26时,雷火字样图案在固定点进行绘制。

 if(flag_start<26){
bufg.drawImage(loadingImg.getMotionGraph(flag_start,"开始界面_按任意键进入游戏_1.png"),0,500-flag_start*20,null);
bufg.drawImage(loadingImg.getGraph("开始界面_雷火.png"),0,-500+flag_start*20,null);
bufg.drawImage(loadingImg.getMotionGraph(flag_start,"开始界面_雷火_动图1.png"),0,-500+flag_start*20,null);
flag_start++;
}else{
bufg.drawImage(loadingImg.getMotionGraph(flag_start,"开始界面_按任意键进入游戏_1.png"),0,0,null);
bufg.drawImage(loadingImg.getGraph("开始界面_雷火_动图底图.png"),0,0,null);
bufg.drawImage(loadingImg.getMotionGraph(flag_start,"开始界面_雷火_动图1.png"),0,0,null);
flag_start++;
//绘出目前要按的键
bufg.drawImage(loadingImg.getGraph(nowPress),nowPressx,nowPressy, null);
}  

按键效果

当鼠标置于“按任意键进入游戏”字样时,字体会出现发光效果。包括在后面很多的页面中,鼠标置于按键上时都会有发光效果。这里我们定义一个String类的对象nowPress,我们可以在鼠标监听器中设置当鼠标置于对应字样图案上方时,将nowPress的值更新为这个图案的名称;当鼠标移开时我们就将nowPress定义为””。这样就可以实现只在鼠标放置于对应图案上方时,图案才会有发光效果。

 //鼠标监听器中的MouseMove函数
public void mouseMoved(MouseEvent e) {
int x=e.getX();
int y=e.getY();//判断按键
if(x>150&x<1050&y>600&y<700){
myThread.nowPress="开始界面_按任意键进入游戏_触碰.png";//一张发光的图片
myThread.nowPressx=0;
myThread.nowPressy=0;
}else{//鼠标移开时
myThread.nowPress="";
}
}  

游戏界面

技能实现

相较于上一版飞机大战,技能的使用是一个新增的功能。

雷火游戏目前可以使用的技能有3个,S键是保护罩(使用后抵御一切攻击,消耗3技能点),D键是火球(火球会缓慢向右移动,并炸毁触及的所有敌机,消耗2技能点),F键是向我机周围发射多重环形高杀伤火力(消耗1技能点)。

我们可以构造一个使用技能的方法,键盘监听器监听到按键事件时,调用此方法并传入按键代码完成技能的调用。

 //使用技能
public void useSkill(int keyCode){//键盘监听器传入按键代码
if(skillPoint>0){
switch(keyCode){//keycode为70时是F键,83是S键,68是D键
case 70:if(skill_F_wait==SKF){skill_F_wait=0;skillPoint--;skill_F+=5;} break ;
case 83:if(skill_S_wait==SKS&skillPoint>2){skill_S_wait=0;skillPoint-=3;skill_S+=300;}break;
case 68:if(skill_D_wait==SKD&skillPoint>1){
/*火球技能,我们可以将火球视作一种特殊的子弹,以下代码
便是创建火球这个子弹并放入bults列表中*/skill_D_wait=0;
skillPoint-=2;
Vector location=new Vector(mps.get(mps.size()-5).location.x,mps.get(mps.size()-5).location.y-100);
Vector speed=new Vector(5,0);
bults.add(new FlyObject(location,speed,100,"技能D-火球.png"));
}break;
}
}else if(keyCode==70|keyCode==83|keyCode==68){
//当玩家使用技能按键,但技能点不足时窗体左下角出现“技能点不足”的提示
Vector location=new Vector(30,730);
explosions.add(new FlyObject(location,30,"技能点不足.png"));
}
}  

F技能

F技能向我机周围发射环状火力,想要实现这一点,我们只需要建立一个for循环,在循环中, 每生成一次子弹 就让子弹运动速度方向 转换一个角度

 //F技能
if(len%5==0&mps.size()>5&skill_F>0){
skill_F--;
int direction=60;//子弹发射的方向数量,数量越大发射的子弹越多
int speedx=20;//子弹速度
for(int i=0;i<direction;i++){
Vector location=new Vector(( double )mps.get(mps.size()-5).location.x,(double)mps.get(mps.size()-5).location.y);
Vector speed=new Vector(speedx*Math.cos(Math. PI *i*2/direction),speedx*Math.sin(Math.PI*i*2/direction));
bults.add(new FlyObject(location,speed,3,"火球.png"));
}
}  

S技能

S技能是保护罩,保护罩的作用是躲避敌人的一切攻击。要做到这一点,我们可以创建一个整数skill_S(初始值定为0),当我们使用S技能时skill_S增加一个定值。然后在判断敌机和敌机子弹是否与我机碰撞之前,先判断一下skill_S的值是否大于0,如果是,则跳过碰撞的判断;如果为否,则正常进行碰撞判断。每经过一轮画图,skill_S的值就减1。

此外,在S技能即将失效的时候,屏幕右下角会有一个闪烁警告图片显示。想要实现这个效果,我们可以每一轮对skill_S的值进行判断,当它等于某一个值的时候(不是小于等于而是等于,因为我们只需要放置一次图片),就在右下角放置一张动图。

技能冷却(绘制部分图片)

技能冷却的三种状态:

我们需要用到三张图片,首先一张透明度为40%的图片打底,绘制在最底层。然后让一张透明度为100%的图片部分叠加其上,并 随着时间不断填充整个图幅 。当技能冷却完成时,绘制一张边缘发光的图片。

这里我们遇到的一个问题是,如何显示一张图片的部分图像?博主在网络上寻找了很多裁剪图片的方法,但大多数要么很复杂,要么需要使用到第三方库,这里博主有一个非常简单易行的实现方法:

首先我们创建一个比图片尺寸要小的BufferedImage对象,然后将这个图片绘制在这个BufferedImage上,但是纵坐标应该为负值,最终要使图片的底部和BufferedImage的底部对齐。这样显示在窗体上的就是BufferedImage和图片重叠的这一部分了。我们只需要按照一定规律对BufferedImage的尺寸和图片的纵坐标进行改变,就可以实现技能加载的效果。

 //绘制透明图片
bufg.drawImage(loadingImg.getGraph("技能F2.png"),1100,800,null);
//F技能
if(skill_F_wait<SKF){
//skill_F是等待的“时间”,SKF是技能完成加载需要的“时间”
skill_F_wait++;
//下面的代码实现的就是绘制部分图片
int high_f=70*skill_F_wait/SKF+1;
BufferedImage temp_bufimg=new BufferedImage(70,high_f,2);
Graphics temp_g=temp_bufimg.getGraphics();
temp_g.drawImage(loadingImg.getGraph("技能F1.png"),0,high_f-70,null);

bufg.drawImage(temp_bufimg,1100,870-high_f,null);
}else{
//技能加载完成
skill_F_wait=SKF;
//画一张发光的图
bufg.drawImage(loadingImg.getGraph("技能F3.png"),1100,800,null);
}  

奖励机

奖励机是每隔一段时间有概率刷出的,奖励机被击中后会随机掉落道具,被击毁后会掉落特殊道具。

我们需要实现以下三点:

  1. 奖励机被击中后掉落道具;
  2. 道具的随机运动路径;
  3. 击毁后随机掉落道具。

随机掉落道具

怎么设置随机掉落道具?我们可以获取一个大小在0-79的随机数,当这个随机数等于1时掉落某个道具,那么这个道具出现的概率就为1/80;如果我们想让某个道具掉落概率增加,就可以减小随机数的大小范围,或者给该道具多设置几个数字(比如说当随机数为0、1、2、3时掉落该道具,则其出现的概率就为原来的4倍)。

 private void dropProp(FlyObject fo){
if(fo.imgName.equals("奖励机_1.png")){
Random ran=new Random();
//获取一个0-79的随机数
int pt=ran.nextInt(80);
String propType=null;
switch(pt){
case 1:propType="火力蓝_奖励.png";break;
case 2:propType="技能点_奖励.png";break;
case 3:propType="技能点_奖励.png";break;
case 4:propType="生命值_奖励.png";break;
default:propType="金币.png";
}
//为掉落的道具设置随机速度
int spd_x=ran.nextInt(20)-10;
int spd_y=ran.nextInt(20)-10;
Vector speed=new Vector(spd_x,spd_y);
Vector location=new Vector(fo.location.x,fo.location.y);
props.add(new FlyObject(location,speed,320,propType));
}
}  

击中及击毁后掉落道具

在判断碰撞的方法中,加入以下代码,如果确定被子弹击中的飞行物为奖励机,则让奖励机扣除相应血量并调用dropProp函数随机掉落道具;如果奖励机血量小于0,说明奖励机被击毁,此时调用dropProp_bomb函数(和dropProp只有掉落道具的概率不同)。

 if(enemy.imgName.equals("奖励机_1.png")){
enemy.HP-=bullet.HP;
if(len%3==1){
dropProp(enemy);
}
if(enemys.get(i).HP<=0){
 explosion (enemy);
for(int k=0;k<4;k++){
dropProp_bomb(enemy);
}
moveAway(enemy);
}  

Boss模式

Boss技能

Boss一共有三个技能,在Boss使用技能前先获取一个随机数,这个随机数将决定Boss使用哪一个技能。

 Random ran=new Random();
if(len_boss%600==0){
bossSkill=ran.nextInt(3);//获取随机数
bossSkillTime=450;
}  

Boss技能1:

boss技能1的原理与F技能的原理相似(发射一个子弹,转换一次角度),但boss技能1只向 正前方固定角度范围 发射子弹,且有一段 固定长度的空缺

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

 //boss技能1
if(len_boss%20==0&bossSkill==0&bossSkillTime>0){
int direction=100;//代表Boss发射一圈子弹的方向数
int shootAngel=20;//代表direction中有多长一段是发射子弹的
int speedx=10;//子弹速度
int startPoint=ran.nextInt(15);//空缺开始的位置(随机)
for(int i=direction/2-shootAngel/2;i<direction/2+shootAngel/2;i++){
int flag_skip=i-direction/2+shootAngel/2;
//跳过空缺的一段子弹
if(!(flag_skip<startPoint+5&flag_skip>startPoint)){
Vector location=new Vector((double)enemys.get(0).location.x+200,(double)enemys.get(0).location.y+150);
Vector speed=new Vector((speedx*Math.cos(Math.PI*i*2/direction)),(speedx*Math.sin(Math.PI*i*2/direction)));
enemys.add(new FlyObject(location,speed,3,"boss_子弹.png"));
}
}
}
  

Boss技能2:

Boss2技能的实现很巧妙,大家仔细对比技能1和技能2的代码就会发现,它们其实长得差不多!最大不同之处在于,技能1是子弹发射时 跳过某一段子弹 (造成某一段子弹空缺),而技能2正好就是 只发射某一段子弹 。相当于同一段代码实现了两个技能两种玩法哈哈哈!

 //boss技能2
if(len_boss%2==0&bossSkill==1&bossSkillTime>0){
if(len_boss%10==0){startPoint=ran.nextInt(35);}//每隔一段时间就切换一次发射子弹的角度段
int direction=100;
int shootAngel=40;
int speedx=20;
for(int i=direction/2-shootAngel/2;i<direction/2+shootAngel/2;i++){
int flag_skip=i-direction/2+shootAngel/2;
if(flag_skip<startPoint+5&flag_skip>startPoint){
Vector location=new Vector((double)enemys.get(0).location.x+200,(double)enemys.get(0).location.y+150);
Vector speed=new Vector((speedx*Math.cos(Math.PI*i*2/direction)),(speedx*Math.sin(Math.PI*i*2/direction)));
enemys.add(new FlyObject(location,speed,3,"boss_子弹.png"));
}
}
}
  

Boss技能3:

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

Boss技能3实现的是交替使用两波数量和速度均有差别的子弹发射方式,仔细观察可以发现这两波子弹是 没有对齐的 ,这样的玩法就是我们的飞机可以在这两波子弹交错的空隙中穿插而过。

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸


flag_switch的作用就是控制两种子弹发射方式交替进行。

 //boss技能3
if(len_boss%30==0&bossSkill==2&bossSkillTime>0){
flag_switch++;
if(flag_switch%2==0){
int direction=30;
int shootAngel=30;
int speedx=8;
for(int i=direction/2-shootAngel/2;i<direction/2+shootAngel/2;i++){
Vector location=new Vector((double)enemys.get(0).location.x+200,(double)enemys.get(0).location.y+150);
Vector speed=new Vector((speedx*Math.cos(Math.PI*i*2/direction)),(speedx*Math.sin(Math.PI*i*2/direction)));
enemys.add(new FlyObject(location,speed,3,"boss_子弹.png"));
}
}else if(flag_switch%2==1){
int direction=30;
int shootAngel=30;
int speedx=7;
for(int i=direction/2-shootAngel/2;i<direction/2+shootAngel/2;i++){
Vector location=new Vector((double)enemys.get(0).location.x+200,(double)enemys.get(0).location.y+150);
Vector speed=new Vector((speedx*Math.cos(Math.PI*i*2/direction+Math.PI/direction)),(speedx*Math.sin(Math.PI*i*2/direction+Math.PI/direction)));
enemys.add(new FlyObject(location,speed,3,"boss_子弹.png"));
}
}

}
  

功能分区

Boss模式的功能分区玩法是博主自创的,规则是当我们的飞机停留在某一个区域时会有对应的效果加成。飞机停于回血区时会缓慢回血(并附有加血特效的显示);停于战斗区时发射的子弹火力会增大;在道具区停留一段时间,飞机的技能点会增加;停于攻击区时会对Boss有雷电伤害加成;停于躲避区时飞机会自动获得保护罩,不受任何攻击。

首先我们需要编写一个方法,来判断我们的飞机目前 处于什么功能区域。功能区域划分的主要依据是与Boss中心的距离(除躲避区外其余功能区都是以Boss中心为圆心的圆环或圆)。当我们的飞机进入对应的区域时,变量areaName和areaName2就更改为对应的区域名称,方便进行下一步操作。

 public void judgeFunctionArea(int x, int y) {
int distance=(int)(Math.sqrt(Math.pow(x-1000,2)+Math.pow(y-450,2)));//距离Boss中心的距离

//进入boss模式后开启判断
if(len_boss>0){
if(x>900&x<1200&!(distance<300)){
//因为躲避区是矩形区域,所以判断方式有所不同
areaName="躲避区-紫.png";
areaName2="躲避区.png";
//System.out.println("目前处于躲避区-紫");
}else{
if(distance<300){
areaName="攻击区-红.png";
areaName2="进击区.png";
//System.out.println("目前处于攻击区-红");
}else if(distance<600){
areaName="道具区-黄.png";
areaName2="技能区.png";
//System.out.println("目前处于道具区-黄");
}else if(distance<900){
areaName="战斗区-蓝.png";
areaName2="战斗区.png";
//System.out.println("目前处于战斗区-蓝");
}else if(distance<1200){
areaName="回血区-绿.png";
areaName2="回血区.png";
//System.out.println("目前处于回血区-绿");
}
}
}
  

功能分区效果实现

 //绘制功能区
if(!areaName.equals("")){
bufg.drawImage(loadingImg.getGraph(areaName),
1000-hm.get(areaName).x/2,450-hm.get(areaName).y/2,null);
bufg.drawImage(loadingImg.getGraph(areaName2),260,70,null);
switch(areaName){
case"回血区-绿.png":if(len_boss%5==0){newLife++;explosion(mps.get(mps.size()-5),0);}break;
case"躲避区-紫.png":skill_S++;break;//处于躲避区就相当于使用了技能S
case"道具区-黄.png":yellowAreaTime++;break;
case"攻击区-红.png":
//Boss模式下enemys的第一个元素为Boss
enemys.get(0).HP-=5;
//加入闪电特效
if(len_boss%20==0){explosion(enemys.get(0),1);}
if(len_boss%10==0){explosion(enemys.get(0),2);explosion(enemys.get(0),2);}
break;
case"战斗区-蓝.png":superPropTime=true;break;
//我机发射子弹时会对superPropTime进行一个判断,当其为true时发射特殊子弹
}
}

if(yellowAreaTime>100){
//计算在道具区的停留时间,yellowAreaTime每达到100就增加一个技能点
yellowAreaTime-=100;
skillPoint++;
}
  

回血区

战斗区

攻击区

躲避区

背景音乐(带调节音量效果)

背景音乐和爆炸音效的实现我使用了两个不同的类,分别是BackgroundMusic和PlayMusic。其中BackgroundMusic类继承了Runnable接口(也就是说播放背景音乐的时候也需要创建一个线程),这个类目前博主已经实现了暂停/播放、重新播放、音量调节、停止播放的功能,并且将这个类做成了一个单独的demo

背景音乐Demo(只能播放wav音频文件)

该Demo大概的实现原理是将wav音频文件的波形转化为一段一段的数组,然后逐段播放。处理文件的部分参考了CSDN某个博主的博客,因为当时没有收藏,所以没能找到参考的是哪一篇博客。如果这位博主看到了这里,希望能够联系我标记出处!

RESTART:重新播放

on_off:暂停/播放

STOP:停止播放

“+”“-”:调节音量

播放音乐

播放音乐的类(BackgroundMusic)继承了Runnable接口,所以想要播放音乐,应该先创建线程对象,然后启动线程实现音乐播放。我们可以把这一步放到BackgroundMusic的构造方法中,这样我们每次创建BackgroundMusic对象时就可以自动创建线程并播放音乐了。

 public BackgroundMusic(String fileAddress){
this.fileAddress=fileAddress;
Thread micthc=new Thread(this);
micthc.start();
on_off();
}  

如果我们想在某一个地方播放音乐,只需要:

 //游戏页面中播放的音乐
level_1_Music=new BackgroundMusic(fileAddress+"雷火BackgroundMusic.wav");
//括号中为音乐文件路径名称  

暂停界面调节音量

 private void page_pause(Graphics bufg) {
//暂停界面背景
bufg.drawImage(loadingImg.getGraph("背景_1.png"),0,0, null);
//绘制按键
bufg.drawImage(loadingImg.getGraph("暂停中.png"),0,100, null);
bufg.drawImage(loadingImg.getGraph("音量关.png"),950,200, null); 
bufg.drawImage(loadingImg.getGraph("+.png"),950,270, null); 
bufg.drawImage(loadingImg.getGraph("-.png"),950,600, null); 
bufg.drawImage(loadingImg.getGraph("空格或点击此处继续游戏.png"),0,100, null);
bufg.drawImage(loadingImg.getGraph("重新开始.png"),0,50, null);

//判断现在在放哪一个背景音乐
BackgroundMusic  bgm =new BackgroundMusic();
if(len_boss==0){
bgm=level_1_Music;
}else{
bgm=level_1_boss_Music;
}

//调节音量
if(bgm.value>5){bgm.value=5;}else if(bgm.value<1){bgm.value=0.5;}
//背景音乐暂停/继续
if(bgm.flag_pause){bufg.drawImage(loadingImg.getGraph("音量开.png"),950,200, null);}
//绘制音量大小
for(int i=0;i<bgm.value;i++){
bufg.drawImage(loadingImg.getGraph("音量.png"),950,540-i*50, null);
}

//绘出目前要按的键
bufg.drawImage(loadingImg.getGraph(nowPress),nowPressx,nowPressy, null); 
}
  

经验总结

经常使用的代码块可以编写成方法

将经常使用的代码块编写成方法,可以减少代码的重复,使代码更加简洁。比如在雷火这个小游戏中,判断碰撞(jgat函数)就经常被使用到,所以可以单独写成一个方法。

功能模块化处理

将功能模块化处理,可以方便程序的理解以及排查bug和增添功能。在雷火游戏中,每个页面(开始、暂停、游戏等)都是独立实现的,生成飞行物、绘制飞行物、判断碰撞、刷新分数等功能都独立编写了方法。特别是程序的体量比较大的时候,模块化处理能让我们的程序更加容易调试。

Debug模式的使用

debug模式主要作用是在用户指定的地方让代码停下来,然后逐步执行并发现问题。

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

打断点

双击编辑窗口靠左的区域(行数的左边),会出现一个蓝色的点。当我们以Debug模式启动程序时,运行到这一步会自动停下。

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

逐步执行

进入Debug模式后:

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

消化他人的代码

有时候我们想实现一个新的效果,比如旋转图片、播放音乐,我们可能会直接上百度或者CSDN搜索实现代码。如果我们只是把代码简单的插入到我们的程序中,那我们可能只是一个“调包侠”哈哈哈。如果你打算使用一段别人的代码,不妨仔细读一下,理解它的实现原理,理解作者的奇思妙想,寻找更多我们能运用到其他地方的知识。

最后

这个小游戏取名叫雷火,有我的一点情怀在里面。小时候还在按键机年代,我爸手机上有一个游戏就叫雷火,但是现在已经很难找到了,现在做了一个这样的游戏也算是实现了我小时候的梦想。那时候按键机游戏很多都是java开发的(开始画面会有经典的java的咖啡图标出现)当时还不知道这个图标的含义,现在,我正在用这种语言尝试着学会开发程序,这是一种很奇妙的感觉。

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸


游戏中使用到的绝大多数图片素材都是我用手绘板和PS软件绘制的,算是我的一个业余小爱好哈哈哈,所以大家不需要担心版权问题,可以放心使用。

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸


我的其他一些绘画作品

Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

最后的最后,感谢你能够看到这里!这篇文章花费了我非常大的心血,我已经记不清开发这个小游戏的那两个星期有多少次不自觉地熬到凌晨三四点,夜深人静,灵感却不断涌现,现在回想起来真是相当的充实。所以,我非常也希望能够帮助读到这篇文章的你,如果有小伙伴因为看了我的文章颇有收获,我也非常非常欢迎你来和我交流!

游戏的完整代码,解压后可以直接运行的jar包(可以直接试玩)可以私信小编【游戏】获取


原文链接:

文章来源:智云一二三科技

文章标题:Java多线程游戏实例分享-手把手教你做个超炫酷的星际争霸

文章地址:https://www.zhihuclub.com/182111.shtml

关于作者: 智云科技

热门文章

网站地图