一步一步copy一款冒险小游戏
前言:想做一个小游戏已经很长时间了,奈何之前的自己心有余而力不足,在学习了几个月java之后,信心也逐渐增长起来,终于在一周前动身了
1.背景绘制
1.1背景图片2021.4.9
//用JLabel绘制背景(绘制主界面)
public class GamePanel {
MyFrame myFrame;
public JLabel bg_label;//背景
public int bg_x=0;//背景的x坐标
public static void main(String[] args) {
new GamePanel().init();
}
public void init(){
myFrame = new MyFrame();
}
class MyFrame extends JFrame{
public MyFrame() {
this.setBounds(400,200,1200,800);//窗口位置和大小
this.setResizable(false);//设置窗口不可调整大小
this.setLayout(null);//设置Frame布局为绝对布局
this.setTitle("地下城の大冒险");
bg_label = new JLabel(DateCenter.background);//将图片嵌入label
bg_label.setBounds(0,0,2200,800);//设置背景大小和位置
this.setCursor(Toolkit.getDefaultToolkit().createCustomCursor(DateCenter.mouse, new Point(16, 16), "mycursor"));//鼠标样式改变
this.add(bg_label);
this.setVisible(true);
}
}
}
这里背景的大小比主窗口大一些,是想要随角色走动背景随之移动的效果
1.2测试背景移动2021.4.10
首先添加键盘监视器
在MyFrame() 构造函数中加入监听
MyListener listener = new MyListener(bg_label,bg_x); listener.windowsListen(this); this.addKeyListener(listener);
class MyListener implements ActionListener, KeyListener {
JLabel bg_label;//背景
public int bg_x;//背景的x坐标
public MyListener(JLabel bg_label,int bg_x) {
this.bg_label = bg_label;
this.bg_x = bg_x;
}
public void windowsListen(JFrame a){//顺便添加了窗口关闭检测
a.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
//键盘按压监听
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();//获取键盘参数
switch (keyCode) {
case KeyEvent.VK_RIGHT: {//当检测到右方向键按压
bg_x-=10;
bg_label.setBounds(bg_x, 0, 2200, 800);
}break;
case KeyEvent.VK_LEFT: {
bg_x+=10;
bg_label.setBounds(bg_x, 0, 2200, 800);
}break;
}
}
@Override
public void keyReleased(KeyEvent e) {
}
}
效果如下
2.角色绘制
2.1角色移动2021.4.14
要达到移动的效果,需要逐一的绘制移动帧,所以添置一个标志量,每次调用跑步函数标志量加一,并且按模11绘制,向左向右分开绘制,角色依旧用一个JLabel标签代替,用ImageIcon添入角色帧
//重置了listener里的键盘监听
case KeyEvent.VK_RIGHT: {
ct_label.rmove(ct_y);
}
break;
case KeyEvent.VK_UP: {
if (ct_y>=350) {//到界面边界,实际上限制了上下移动的距离,限制在背景的马路上
ct_y -= 10;
}
ct_label.juage_stright = true;
if(ct_label.juage_lr)
ct_label.lmove(ct_y);
else ct_label.rmove(ct_y);
}
break;
case KeyEvent.VK_DOWN: {
if (ct_y<=520) {//到界面边界
ct_y += 10;
}
ct_label.juage_stright = true;
if(ct_label.juage_lr)
ct_label.lmove(ct_y);
else ct_label.rmove(ct_y);
}
break;
case KeyEvent.VK_LEFT: {
ct_label.lmove(ct_y);
}
break;
import homework.Mysuper.GamePanel.*;//这里需要提前把Myframe类导进来
class MyCharacter extends JLabel {
MyCharacter ct_label = this;
JLabel bg_label;
MyFrame myFrame;
public int pressNum = 0;//按压计数器
boolean juage_lr;//判断当前向左还是向右,左true
boolean juage_stright = false;//判断当前是否属于上下走状态
public volatile int ct_x;//角色的x坐标
public volatile int ct_y;//角色的x坐标
public int bg_x;//背景的x坐标
public MyCharacter(int a,int b,int c,MyFrame e,JLabel f) {
ct_x = a;
ct_y = b;
bg_x = c;
myFrame = e;
this.setIcon(DateCenter.rstand);//角色初始化
this.setBounds(ct_x, ct_y, 150, 200);//角色初始化
this.bg_label = f;
}
public void lmove(int y_move) {//向左移动
pressNum++;//计算器加一
juage_lr = true;//方向设置为左
if(juage_stright){//上下直走状态
ct_y = y_move;
this.setIcon(juage(pressNum % 11, 1));
this.setBounds(ct_x,y_move, 150, 200);
}
else if (bg_x < 0 && ct_x < 400) {//判断是否到界面边界
bg_x += 10;
bg_label.setBounds(bg_x, 0, 2200, 800);
this.setIcon(juage(pressNum % 11, 1));
}
else {//走路状态下
ct_x-=5;
this.setIcon(juage(pressNum % 11, 1));
this.setBounds(ct_x,ct_y, 150, 200);
}
juage_stright=false;
myFrame.repaint();//刷新窗体
}
public void rmove(int y_move) {//向右移动
pressNum++;
juage_lr = false;
if(juage_stright){//上下直走状态
ct_y = y_move;
this.setIcon(juage(pressNum % 11, 0));
this.setBounds(ct_x,y_move, 150, 200);
}
else if (bg_x > -1010 && ct_x > 800) {//判断是否到界面边界
bg_x -= 10;
bg_label.setBounds(bg_x, 0, 2200, 800);
this.setIcon(juage(pressNum % 11, 0));
}else {//走路状态下
ct_x+=5;
this.setIcon(juage(pressNum % 11, 0));
this.setBounds(ct_x,y_move, 150, 200);
}
juage_stright=false;
myFrame.repaint();
}
public ImageIcon juage(int i, int t) {
if (t == 1) {//绘制向左走路
switch (i) {
case 0:
return DateCenter.lmove0;
case 1:
return DateCenter.lmove1;
case 2:
return DateCenter.lmove2;
case 3:
return DateCenter.lmove3;
case 4:
return DateCenter.lmove4;
case 5:
return DateCenter.lmove5;
case 6:
return DateCenter.lmove6;
case 7:
return DateCenter.lmove7;
case 8:
return DateCenter.lmove8;
case 9:
return DateCenter.lmove9;
case 10:
return DateCenter.lmove10;
default:
return null;
}
} else if (t == 0) {//绘制向右走路
switch (i) {
case 0:
return DateCenter.rmove0;
case 1:
return DateCenter.rmove1;
case 2:
return DateCenter.rmove2;
case 3:
return DateCenter.rmove3;
case 4:
return DateCenter.rmove4;
case 5:
return DateCenter.rmove5;
case 6:
return DateCenter.rmove6;
case 7:
return DateCenter.rmove7;
case 8:
return DateCenter.rmove8;
case 9:
return DateCenter.rmove9;
case 10:
return DateCenter.rmove10;
default:
return null;
}
}else return null;
}
}
效果如下:
可以看出,走路调用太快,看起来像是飞一样,我在网上搜索了解决办法,找到了一个办法,每次执行前后获取时间戳,判断时间戳差的长度,等大于50时在执行
//public long lastPress=0; if(System.currentTimeMillis()-lastPress>50) { ...//任务代码 lastPress=System.currentTimeMillis(); }
除此之外,加入了在键盘释放后,让角色恢复站立状态,计数恢复0
@Override public void keyReleased(KeyEvent e) { if(ct_label.juage_lr) ct_label.setIcon(DateCenter.lstand); else ct_label.setIcon(DateCenter.rstand); ct_label.pressNum=0; }
新效果如下:
目前看来,走路是没有问题的,但实际上还有一个小点,就是不能同时向纵坐标——横坐标移动,也就是不能斜着走,这里维护了一组键盘队列,每次按压时,先存入队列,然后遍历队列,执行switch,就可以并发的执行多个键位按压表现。(这点非常重要,后面许多功能实现都是以该改变为基础)
//Set<Integer> array = new HashSet<>(); array.add(e.getKeyCode());//维护一组键值 if(System.currentTimeMillis()-lastPress>50&&array.size()>0) {//新加入了一个判断条件 for (Integer integer : array) { switch... } }
并且需要在键盘松开之后,移除掉
array.remove(e.getKeyCode());
2.1角色跑步2021.4.16
跑步和走路类似,只是绘制的图片和移动速度不同,本来想要实现双击两次移动键就跑起来,玩过地下城与勇士的朋友们该是知道的,但是搞了好长时间都没有实现出来,因为键盘按压长按的模式是不停的调用来实现的,不然可以设置标志位,判断是否是第二次敲击,执行完之后标志位在置反就可以,所以用了另一种耳熟能详的方式来改变状态,shift键,按一次shift键跑步状态就置反,为角色类设置了一个标志位
//boolean juage_run = false;//判断当前是否属于跑步状态 case KeyEvent.VK_SHIFT:{//跑步 ct_label.juage_run=!ct_label.juage_run; }break;
在move函数里增加对juage_run状态的判断
public void lmove(int y_move) {//向左移动 pressNum++;//计算器加一 juage_lr = true;//方向设置为左 if (juage_stright) {//上下直走状态 ct_y = y_move; if(juage_run) { this.setIcon(juage(pressNum % 2, 7)); this.setBounds(ct_x,y_move, 150, 200); } else { this.setIcon(juage(pressNum % 11, 1)); this.setBounds(ct_x,y_move, 150, 200); } } else if (bg_x < 0 && ct_x < 400) {//判断是否到界面边界 bg_x += 10; bg_label.setBounds(bg_x, 0, 2200, 800); if(juage_run) this.setIcon(juage(pressNum % 2, 7)); else this.setIcon(juage(pressNum % 11, 1)); } else if(juage_run) {//跑步状态下 ct_x-=30; this.setIcon(juage(pressNum % 2, 7)); this.setBounds(ct_x,ct_y, 150, 200); }else {//走路状态下 ct_x -= 5; this.setIcon(juage(pressNum % 11, 1)); this.setBounds(ct_x, ct_y, 150, 200); } juage_stright = false; myFrame.repaint();//刷新窗体 }
juage函数也需要设置,对应的返回ImageIcon判断,过于繁冗,不再陈列出来
效果如下:
2.3角色跳跃2021.4.25
跳跃图片:
跳跃按理说和走路是一样的,初步时也做了这样的猜想,只需要每帧暂停零点几秒就行了,也做了如下实践,空格键盘监听就不做展示了别忘了在键盘松开的监听中加入非跳跃状态判断,为此,在角色类中加入标志位
boolean juage_jump = false;//判断当前是否属于跳跃状态
public void ljump() {//向左跳跃
juage_jump=true;
for (int i = 1; i < 8; i++) {
if(i==1){ct_y-=50;ct_label.setBounds(ct_x,ct_y,100,250);}
else if(i<=3){ct_y-=50;ct_label.setBounds(ct_x,ct_y,100,200);}
else if(i==4){ct_y-=50;ct_label.setBounds(ct_x,ct_y,150,200);}
else if(i==5){ct_y+=50;ct_label.setBounds(ct_x,ct_y,150,200);}
else if(i==6){ct_y+=50;ct_label.setBounds(ct_x,ct_y,150,250);}
else {ct_y+=100;ct_label.setBounds(ct_x,ct_y,150,200);}
ct_label.setIcon(juage(i-1,2));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
juage_jump=false;
}
实际效果就不演示了,角色根本动不了,并且由于主线程休眠的原因,在这期间也不能走动,等于角色在傻站着,顺着线程这条思路,我想能不能新建一条跳跃的线程去绘制跳跃的帧,简化成了如下
public void ljump() {//向左跳跃 juage_jump = true; new MyCharacter.Myjump(2).start(); }
class Myjump extends Thread{//跳跃线程
int i;
public Myjump(int i) {
this.i = i;
}
@Override
public void run() {
try {
for (int i = 1; i < 8; i++) {//循环绘制跳跃帧
if(i==1){ct_y-=50;ct_label.setBounds(ct_x,ct_y,100,250);}
else if(i==2){ct_y-=50;ct_label.setBounds(ct_x,ct_y,100,200);}
else if(i==3){ct_y-=50;ct_label.setBounds(ct_x,ct_y,100,200);}
else if(i==4){ct_y-=50;ct_label.setBounds(ct_x,ct_y,150,200);}
else if(i==5){ct_y+=50;ct_label.setBounds(ct_x,ct_y,150,200);}
else if(i==6){ct_y+=50;ct_label.setBounds(ct_x,ct_y,150,250);}
else {ct_y+=100;ct_label.setBounds(ct_x,ct_y,150,200);}
ct_label.setIcon(juage(i-1,this.i));
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ct_label.juage_lr)
ct_label.setIcon(DateCenter.lstand);
else ct_label.setIcon(DateCenter.rstand);//跳跃结束,恢复站立
juage_jump=false;//设置标志位
}
}
实际效果称心如意:
关于跳跃的一些细节处理,上面的效果图可以看到,在跳跃过程中,移动是有效地,并且会绘制到底部,我且称之为串台,这当然是我所不希望的,所以在move函数里加了jump状态判断,当当前处于跳跃状态时,不重新绘制icon,只移动角色x坐标,并且由于存在线程间通信的问题,我将角色的坐标添加了volatile关键字,保证修改的可见性
还有一个现象就是会出现这样的情况
在监听里,又加了一个状态判断,如果当前正在跳跃,则不继续执行jump函数,这也是为什么把对跳跃结束状态改变放到子线程中,因为如果放到主线程中,他会在调用子线程后立即执行后面的代码,将跳跃状态改为false
3.角色基本攻击绘制
3.1普通攻击2021.4.28
图片素材
和跳跃线程如出一辙,用子线程绘制攻击帧
//添加监听 case KeyEvent.VK_X:{//普通攻击 if(ct_label.juage_lr) ct_label.lattack(); else ct_label.rattack(); }break;
为了防止“串台”,增加一个攻击状态的标志位(移动、跳跃、鼠标松开恢复站位时都需要判断)
//攻击函数 public void rattack() {//向左攻击 if(!juage_attack&&!juage_jump) { juage_attack = true; new homework.supermario.MyCharacter.Myattack(5).start(); } }
class Myattack extends Thread{//攻击线程
int i;
public Myattack(int i) {
this.i = i;
}
@Override
public void run() {
try {
for (int i = 1; i < 7; i++) {
ct_label.setIcon(juage(i-1,this.i));
if(i==1) {
Thread.sleep(500);
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ct_label.juage_lr)
ct_label.setIcon(DateCenter.lstand);
else ct_label.setIcon(DateCenter.rstand);
juage_attack=false;
}
}
效果如下:
第一个帧睡眠时间比较长,所以在这个帧加了点特效:图片:
还是老方法,子线程绘制,但是需要在Gamepanel新加入入一个新标签,将其命名为ll,并通过角色类构造函数传过来:
class Mypoised extends Thread{//绘制蓄力特效
@Override
public void run() {
try {
for (int i = 1; i < 7; i++) {
if(ct_label.juage_lr)
ll.setBounds(ct_x-45,ct_y+55,50,50);
else ll.setBounds(ct_x+90,ct_y+55,50,50);
ll.setIcon(juage(i-1,6));
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
效果如下:
3.2跳跃攻击2021.5.5
同样是老套路
图片:
标志位:boolean juage_jumpx = false;//判断当前是否需要跳跃斩击
x键监听修改
if(ct_label.juage_jump)//如果当前正在跳跃 ct_label.juage_jumpx =true;//标志位为true else { attack_voice.play(); if(ct_label.juage_lr) ct_label.lattack(); else ct_label.rattack(); }
在jump线程每次循环绘制帧前加入一个条件判断
if(ct_label.juage_jumpx){//如果需要斩击 if(ct_label.juage_lr) new Myjumpx(9).start(); else new Myjumpx(10).start(); Thread.sleep(500); }
class Myjumpx extends Thread{//跳跃斩击线程
int i;
public Myjumpx(int i) {
this.i = i;
}
@Override
public void run() {
try {
for (int i = 1; i < 6; i++) {
ct_label.setSize(300,300);
ct_label.setIcon(juage(i-1,this.i));
if(i==1) {
Thread.sleep(200);
}
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
juage_jumpx=false;
}
}
效果:
4.基础攻击特效绘制
图片:
新建一个子弹类Mybullet,值得注意的点有:
1.子弹在出屏幕之后要立即被销毁掉,不然会一直消耗资源,碰到怪物暂时不论
2.光波是斜着飞行的,所以在遇到地面的时候会爆炸,所以需要提前记录角色的起跳y坐标,在到达该坐标时绘制爆炸
3.add时将label加到frame顶层
package homework.supermario;
import javax.swing.*;
import homework.supermario.GamePanel.*;
class Mybullet extends Thread{
MyFrame myFrame;//主窗口的引用
MyCharacter ct_label;//角色类的引用
public int bullet_x,bullet_y;//子弹(光波)的坐标
public boolean dirction;//角色的方向
public boolean flag;//判断是子弹还是光波,true为光波
public int jump_y;//起跳的初始y坐标
public Mybullet(int bullet_x, int bullet_y,MyFrame myFrame,MyCharacter myCharacter,boolean flag,int jump_y) {
this.bullet_x = bullet_x;
this.bullet_y = bullet_y;
this.myFrame = myFrame;
this.ct_label = myCharacter;
dirction = ct_label.juage_lr;
this.flag = flag;
this.jump_y = jump_y;
}
@Override
public void run() {
JLabel bullet = new JLabel();
myFrame.add(bullet,0);//顶层放置
if(flag){//绘制光波
try {
for (int i = 0; bullet_y<jump_y ;i++) {
bullet.setBounds(this.bullet_x,this.bullet_y,100,100);
if(dirction){
if(i<3) {bullet.setIcon(juage(i,1));}
else bullet.setIcon(juage(3,1));
bullet_x-=30;
bullet_y+=15;//光波斜着移动
}
else {
if(i<3) bullet.setIcon(juage(i,2));
else bullet.setIcon(juage(3,2));
bullet_x+=30;
bullet_y+=15;
}
Thread.sleep(50);
}
for (int i = 0; i < 3; i++) {//光波遇到地面爆炸
bullet.setIcon(juage(i,3));
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
myFrame.repaint();
}else {//绘制子弹
for (int i = 0; bullet_x >-100&&bullet_x<1200 ;i++) {
bullet.setBounds(this.bullet_x,this.bullet_y,50,50);
bullet.setIcon(juage(i%6,0));
if(dirction)
bullet_x-=30;
else bullet_x+=30;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
myFrame.repaint();
}
}
myFrame.remove(bullet);//移除子弹(光波)
}
public ImageIcon juage(int k,int l){
//根据对应的k返回对应的帧图片,根据l判断绘制子弹还是光波
大量的没营养代码
}
}
效果如下:
命令行运行jar方法:文件路径和cmd路径一致:java -jar 文件名字.jar
也可以解压缩查看源码
给大家一个当前版本的分享,后缀改成.jar就可以在命令行运行了,点我下载
5.技能绘制
老朽买了平板,可以自己画技能帧了,以上所有的帧素材都是网上扣的,但是也就如此,不能有更多的动作,所以卡了一星期
5.1拔刀斩(W)2021.5.12
图片:
自己画的帧分辨率和大小都和原图不一样,调试花费了太长时间,不过总体来说实现原理,并不难:为角色类新加一个释放技能的标志位,之后所有技能释放都用此标志位进行并发控制检测
boolean juage_skill = false;//判断当前是否正在释放技能
一个新的技能类:
import javax.swing.*;
public class MySkill {
MyCharacter ct_label;//当前角色
public MySkill(MyCharacter ct_label) {
this.ct_label = ct_label;
}
public void broach() {
if(ct_label.juage_lr)
new Mybroach(1).start();
else new Mybroach(2).start();
}
class Mybroach extends Thread {//拔刀斩线程
int i;//一些判断标志位,判断如:方向..
public Mybroach(int i) {
this.i = i;
}
@Override
public void run() {
int skill_x=0,skill_y=0;//由于分辨率等的差异,维护一组自己的坐标
if(ct_label.juage_lr)
skill_x=ct_label.ct_x-80;
else skill_x=ct_label.ct_x-50;
skill_y =ct_label.ct_y-70;
try {
for (int i = 1; i < 8; i++) {
ct_label.setIcon(juage(i, this.i));
ct_label.setBounds(skill_x,skill_y,300,300);
if (i == 7) {
Thread.sleep(600);//暂停较长时间,为绘制特效做准备
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ct_label.juage_lr)
ct_label.setIcon(DateCenter.lstand);
else ct_label.setIcon(DateCenter.rstand);//恢复站位
ct_label.juage_skill = false;//标志位改变
ct_label.setBounds(ct_label.ct_x,ct_label.ct_y,150,200);//恢复原坐标及大小
}
}
public ImageIcon juage(int i, int t) {
...//juage老成员了,不介绍了
}
}
效果如下:
5.2鬼影闪(E)2021.5.13
今天白天没课,就抓紧绘制了鬼影闪的图像帧
由于拔刀斩的铺垫,这里就不再过多赘述
public void gFlash() {//鬼影闪 if(ct_label.juage_lr) new MygFlash(3).start(); else new MygFlash(4).start(); }
class MygFlash extends Thread {//鬼影闪线程
int i;
public MygFlash(int i) {
this.i = i;
}
@Override
public void run() {
int skill_x=0,skill_y=0;//由于分辨率等的差异,维护一组自己的坐标
if(ct_label.juage_lr)
skill_x=ct_label.ct_x-40;
else skill_x=ct_label.ct_x-80;
skill_y =ct_label.ct_y-40;
try {
for (int i = 1; i < 7; i++) {
ct_label.setIcon(juage(i, this.i));
ct_label.setBounds(skill_x,skill_y,300,300);
if (i == 1) { Thread.sleep(800);}
if(i==6){//第六帧时绘制特效
Thread.sleep(1000);
}
if(i==3){ skill_x+=200*direction; }//第四帧绘制虚影+移动
if(i==4){skill_x+=200*direction; }//第五帧移动
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ct_label.juage_lr)
ct_label.setIcon(DateCenter.lstand);
else ct_label.setIcon(DateCenter.rstand);
ct_label.juage_skill = false;//标志位改变
ct_label.ct_x+=400*direction;
ct_label.setBounds(ct_label.ct_x,ct_label.ct_y,150,200);//恢复原坐标及大小
}
}
看看效果:
5.3圣灵斩击(S)2021.5.14
老套路:
监听
case KeyEvent.VK_S:{//圣灵斩击 if(!ct_label.juage_jump&&!ct_label.juage_attack&&!ct_label.juage_skill) { godX_voice.play(); ct_label.juage_skill = true; new MySkill(ct_label).godX(); } }break;
函数:
public void godX() {//圣灵斩击 if(ct_label.juage_lr) new MygodX(6).start(); else new MygodX(5).start(); }
线程:
class MygodX extends Thread {//圣灵斩击线程 int i; public MygodX(int i) { this.i = i; } @Override public void run() { int skill_x=0,skill_y=0;//由于分辨率等的差异,维护一组自己的坐标 if(ct_label.juage_lr) skill_x=ct_label.ct_x-145; else skill_x=ct_label.ct_x-45; skill_y =ct_label.ct_y-75; try { for (int i = 1; i < 10; i++) { ct_label.setIcon(juage(i, this.i)); ct_label.setBounds(skill_x,skill_y,300,300); if(i==6){ //绘制特效 Thread.sleep(800); } Thread.sleep(50); } } catch (InterruptedException e) { e.printStackTrace(); } if (ct_label.juage_lr) ct_label.setIcon(DateCenter.lstand); else ct_label.setIcon(DateCenter.rstand);//恢复站立 ct_label.juage_skill = false;//标志位改变 ct_label.setBounds(ct_label.ct_x,ct_label.ct_y,150,200);//恢复原坐标及大小 } }
效果如下:
6.技能特效绘制
6.1拔刀斩技能特效2021.5.12
拔刀斩的斩击类似于子弹,所以放在了子弹类中
在技能类休眠的600ms中加入特效绘制:if(ct_label.juage_lr) new Mybullet(skill_x-80,skill_y-80, ct_label.myFrame,ct_label,2,0).start(); else new Mybullet(skill_x+30,skill_y-80, ct_label.myFrame,ct_label,2,0).start();
当子弹类的flag参数为3时调用
else {//绘制白牙特效 for (int i = 0; i<11;i++) { bullet.setLocation(this.bullet_x,this.bullet_y); bullet.setSize(500,500); if(dirction) { bullet_x -= 60; bullet.setIcon(juage(i,4)); } else { bullet_x+=60; bullet.setIcon(juage(i,5)); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } myFrame.repaint(); } }
效果如下:
6.2鬼影闪技能特效2021.5.13
这个特效取自爱给网,自己稍微加工了一下
为了更加体现面向对象,增加程序可读性,将之前的普通攻击子弹蓄力效果和该技能特效统一设置了一个新的特效类,有两个接口,分别是角色类实例和主屏幕类实例:
import javax.swing.*;
public class MySpecial {
public JLabel ll = new JLabel();//特效标签
MyCharacter ct_label;//当前角色
MyFrame myFrame;//主屏幕
public MySpecial(MyCharacter ct_label) {
this.ct_label = ct_label;
ct_label.myFrame.add(ll,0);
myFrame = ct_label.myFrame;
}
public void poised() {//绘制蓄力特效
new Mypoised(0).start();
}
public void seam(){//鬼影闪裂缝特效
new MySeam(1).start();
}
class Mypoised extends Thread {//绘制蓄力特效
int i;
public Mypoised(int i) {this.i = i;}
@Override
public void run() {
try {
for (int i = 1; i < 7; i++) {
if (ct_label.juage_lr)
ll.setBounds(ct_label.ct_x - 45, ct_label.ct_y + 55, 50, 50);
else ll.setBounds(ct_label.ct_x + 90, ct_label.ct_y + 55, 50, 50);
ll.setIcon(juage(i - 1, this.i));
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ct_label.juage_lr)
new Mybullet(ct_label.ct_x - 45, ct_label.ct_y + 30, ct_label, 1).start();
else new Mybullet(ct_label.ct_x + 90, ct_label.ct_y + 30, ct_label, 1).start();
myFrame.remove(ll);
myFrame.repaint();
}
}
class MySeam extends Thread {//绘制裂缝特效
int i;
public MySeam(int i) {this.i = i;}
@Override
public void run() {
try {
for (int i = 1; i < 7; i++) {
if (ct_label.juage_lr)
ll.setBounds(ct_label.ct_x-400, ct_label.ct_y+20,700,150);
else ll.setBounds(ct_label.ct_x-250, ct_label.ct_y+20,700,150);
ll.setIcon(juage(i - 1,this.i));
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
myFrame.remove(ll);
myFrame.repaint();
}
}
public ImageIcon juage(int i, int t) {
...
}
}
看看技能效果
6.3圣灵斩击特效绘制
调用:new MySpecial(ct_label).crack();函数:
public void crack(){ ct_label.myFrame.add(ll,1);//将特效放在角色下一层 new MyCrack(2).start(); }
线程:
class MyCrack extends Thread {//绘制圣灵斩击地缝特效 int i; public MyCrack(int i) { this.i = i; } @Override public void run() { try { for (int i = 1; i < 7; i++) { if (ct_label.juage_lr) ll.setBounds(ct_label.ct_x-600, ct_label.ct_y+80,700,150); else ll.setBounds(ct_label.ct_x, ct_label.ct_y+80,700,150); ll.setIcon(juage(i - 1,this.i)); if(i==3) Thread.sleep(400); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } myFrame.remove(ll); myFrame.repaint(); } }
效果:
7.初始界面绘制和音效
7.1初始界面
没有游戏一进去就在主界面的,所以有了开个初始界面的想法,界面ui也是买了平板以来一直在画的,素材是用了bilibili漫画的一个原型改的,画脸实属弱项 :-<,效果是这样的:
绘制非常简单,新建一个Main类,把Label绘制上去就行:
import sun.applet.AppletAudioClip;
import javax.swing.*;
import java.applet.AudioClip;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class Main implements ActionListener {//开始主类
Frame frame = new Frame();//开始屏幕
JLabel label = new JLabel(DateCenter.beginPanel);//背景标签
MyButton button = new MyButton();//开始游戏按钮
AudioClip background_voice = new AppletAudioClip(DateCenter.background_voiceUrl);
public static void main(String[] args) {
new Main().init();
}
public void init(){
background_voice.loop();
frame.setBounds(600,200,800,530);
frame.setTitle("Lin的大冒险1.1.1");
frame.setLayout(null);
label.setBounds(0,30,800,530);
frame.add(label);
frame.addWindowListener(new WindowAdapter() {//适配器模式检测窗口监听
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
button.addActionListener(this);
frame.add(button,0);
frame.setResizable(false);
frame.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
new Thread(()->{
int k = 150;
for (int i = 0; i < 5; i++) {//开始游戏按钮动态上移消失
button.setLocation(300,k);
k-=50;
try {
Thread.sleep(200);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
frame.repaint();
}
frame.setVisible(false);
new MyFrame();
}).start();
}
}
class MyButton extends JButton{
public MyButton() {//设置一个图标按钮
setActionCommand("开始游戏");
setBounds(300,150,200,80);
setIcon(DateCenter.beginGame);//绘制图标
setOpaque(false);//设定透明效果
setContentAreaFilled(false); //去掉背景点击效果
setFocusPainted(false); // 去掉聚焦线
this.setCursor(Toolkit.getDefaultToolkit().createCustomCursor(DateCenter.mouse, new Point(16, 16), "mycursor"));//设置鼠标样式
setBorder(null);// 去掉边框
}
}
效果是这样的(录屏自动屏蔽了鼠标,看起来好像是自动播放,实际是点击开始游戏后才做反应):
7.2 音效绘制
一个好的游戏缺少不了优秀的音效资源,所以就在爱给网搜索了一些音效资源,作为一个资深的二次元,最后还是选择了雏子这套音效:
普通攻击:音效的插入非常简单, java.applet.AudioClip该类提供了音乐资源的播放,但是只支持wav格式的,推荐大家下载格式工厂去转格式,比较的好用
官方的是快速下载器,很多电脑可能会报病毒,这里有一个下载地址[点我下载](格式工厂_官方电脑版_51下载 (51xiazai.cn))
使用的时候还是先把源文件导入项目里,然后在加载,之前所有的静态资源我都是用一个数据中心类,DaterCenter去加载的,里面将所有的属性设置为静态公有就可以了
列举一个音效的使用方法:
public static URL attack_voiceUrl = DateCenter.class.getResource("/sound/attack_voice.wav");//加载静态资源
import sun.applet.AppletAudioClip; ... AudioClip attack_voice = new AppletAudioClip(DateCenter.attack_voiceUrl);//包装 attack_voice.play();//播放一次 attack_voice.loop();//循环播放 attack_voice.stop();//停止播放
如果想让声音更有层次感(背景音乐更小一些),需要从音乐源头入手,在java里是很难修改的,这里推荐大家使用WaveCN,一个交互非常傻瓜式的音频制作软件,[点我下载](WaveCN下载_WaveCN正式版官方下载[音频编辑]-华军软件园 (onlinedown.net)),调整音量的地方在这里
效果就不在这里展示了,整个流程也是非常简单,还有一点就是城镇站场音效,就是那种你站在那什么操作也不做,过一会角色会自己说话或者做一些动作,需要用到Timer类,位于Swing包里,之前导过,所以不用新导包
Timer timer = new Timer(time,ActionListener);//每time ms执行一次ActionListener
time是以ms为单位的,我这里设置了6s
ActionListener是ActionListener的一个实现类,需要重写actionPerformed方法,里面可以定义需要执行的任务代码
实质上他是一个守护线程,所以需要调用start方法开启它
//定时播放人物声音的类
class CharacterVoice implements ActionListener{
Timer timer = new Timer(6000,this);//每6秒播放一次
AudioClip stand_voice1 = new AppletAudioClip(DateCenter.stand_voice1Url);
AudioClip stand_voice2 = new AppletAudioClip(DateCenter.stand_voice2Url);
AudioClip stand_voice3 = new AppletAudioClip(DateCenter.stand_voice3Url);
AudioClip stand_voice4 = new AppletAudioClip(DateCenter.stand_voice4Url);
AudioClip stand_voice5 = new AppletAudioClip(DateCenter.stand_voice5Url);
@Override
public void actionPerformed(ActionEvent e) {
if(!juage_attack&&!juage_jump&&!juage_skill){//如果当前没有执行操作
int i = (int)(Math.random()*5);
switch (i){
case 0:{stand_voice1.play();break;}
case 1:{stand_voice2.play();break;}
case 2:{stand_voice3.play();break;}
case 3:{stand_voice4.play();break;}
case 4:{stand_voice5.play();break;}
}
}
}
public CharacterVoice(){
timer.start();
}
}
8.城镇npc
8.1背景容器类2021.5.16
城镇光秃秃的总不太好,npc的存在是必要的的(如果后期有任务交互的话,是这样想的)
为此,将背景独立出来一个容器,因为npc要随着背景一起移动,想着如果都是单个组件,会比较麻烦,所以加了一个继承JPanel的城镇类MyTown,将角色、npc还有背景label放在这个容器中,移动的时候对容器进行移动就能达到效果(这一更换意味着代码中很多地方都要修改,尤其是角色类里的背景移动):
public class MyTown extends JPanel { MyCharacter ct_label;//当前角色 MyFrame myFrame;//当前主屏幕 public int bg_x = 0; public MyTown(MyFrame myFrame) { this.myFrame = myFrame; init(); } public void init() { this.setLayout(null); JLabel label = new JLabel(DateCenter.background); label.setBounds(0, 0, 2200, 800); this.setBounds(0, 0, 2200, 900); this.add(label); myFrame.add(this, 1); } }
8.2 npc2021.5.16
图片:
有了背景容器做铺垫,npc的绘制就只需要定点固定就行了,在MyTown类里的一个内部类:class Npc extends JLabel { public int npc_x, npc_y;//npc的坐标 public boolean isIn = false;//一个判断角色是否在npc范围内的标志位 public int flag=0;//计数器 public Npc(int x,int y,ImageIcon c) { npc_x =x; npc_y =y; this.setBounds(npc_x, npc_y, 200, 200); this.setIcon(c); } }
npc动作和打招呼(这里用了两次jdk8新特性的Lamda表达式)
public void protection(Npc npc1 , Npc npc2) { new Timer(10000, (e) -> {//每10snpc做一次动作 new Thread(() -> { for (int i = 0; i < 4; i++) {//npc2的动作 npc2.setIcon(juage(i, 0)); try { Thread.sleep(200); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } npc2.setIcon(DateCenter.npc2_stand); for (int i = 0; i < 8; i++) {//npc1的动作 npc1.setIcon(juage(i, 1)); try { Thread.sleep(200); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } npc1.setIcon(DateCenter.npc1_stand); }).start(); }).start(); new Timer(100, (e) -> {//每0.1s检测一次角色是否在npc范围内 if (Math.abs(ct_label.ct_x - npc1.npc_x) < 100 && Math.abs(ct_label.ct_y - npc1.npc_y) < 100) { if (!npc1.isIn){//每次播放不同声音 npc1.flag++; if(npc1.flag%2==0) npc1_voice.play();//打招呼声音播放 else npc1_voice1.play(); } npc1.isIn = true; } else npc1.isIn = false; //标志位的设置避免了角色如果一直待在npc范围内,npc会频繁的一直打招呼的情况 if (Math.abs(ct_label.ct_x - npc2.npc_x) < 100 && Math.abs(ct_label.ct_y - npc2.npc_y) < 100) { if (!npc2.isIn){//每次播放不同声音 npc2.flag++; if(npc2.flag%2==0) npc2_voice.play(); else npc2_voice1.play(); } npc2.isIn = true; } else npc2.isIn = false; }).start(); }
效果(声音不便做演示,只演示动作)
老规矩,给一个当前版本的分享(状态栏存在一点小bug,会有覆盖现象,今天跟这bug战斗很长时间了,不知道为什么State已经放在Frame顶层了还是会覆盖,明天再说吧)
由于博客园一次只能上传10mb以内的文件,而游戏大小已经到了17mb,所以存在了我的网盘,还有第一版也存在了里面,提取码是2580,Jar文件大概率会被自动拦截,我将文件后缀修改成了rar,大家记得改回来在运行:
点我下载
9.伪造加载界面2021.5.19
回来了,家人们,这几天一直在为了实习面试学Mybatis,没有maven基础,学框架多少还是有点费力,但好在简历没通过:-(,所以又来整整活
一直觉得直接的跳转多多少少有些突然,为了更有逼格一些,我决定加一个加载界面(我是从未来穿越的,有加载界面确实有逼格不少)
由于自动加载绘帧的特殊性(跟跳跃、子弹等一样),该类在new时可以选择在线程里new,从Main类(开始游戏界面)中调用已经解决了该问题,因为Main类的开始游戏动态上移本就是子线程绘制,直接在该线程里调用即可
import javax.swing.*;
import java.awt.*;
public class MyLoad extends JFrame {
public int flag;//判断是加载城镇还是地图
JLabel label1 = new JLabel(DateCenter.load1);//加载条
JLabel label2 = new JLabel();//提示语
public MyLoad(int flag){
this.setBounds(400,100,1200,900);
this.setResizable(false);
this.setLayout(null);
this.setTitle("Linの大冒险");
this.setCursor(Toolkit.getDefaultToolkit().createCustomCursor(DateCenter.mouse, new Point(16, 16), "mycursor"));
JLabel label = new JLabel(DateCenter.load);
label.setBounds(0,-50,1200,900);
label1.setBounds(0,825,1200,30);
label2.setBounds(100,780,1200,50);
label2.setFont(new Font("仿宋",Font.BOLD,25));
this.add(label2);
this.add(label1);
this.add(label);
this.setVisible(true);
this.flag=flag;
init();
}
public void init(){
for (int i = 0; i < 6; i++) {
try {
label1.setIcon(juage(i));
Thread.sleep(600);
if(i==4)Thread.sleep(1000);//在一个固定帧停顿1s
if(i%2==0)label2.setText(randomString());//每两次帧刷新生成一次提示语
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.dispose();//加载完后销毁该窗口
if(flag==0) new MyFrame();//加载城镇
else new MyMap(1);//加载地下城
}
public String randomString(){//随机返回一个字符串
int i=(int)(Math.random()*5);
switch (i){
case 0:return "城镇的npc都特别热情,靠近他们试试看...";
case 1:return "跳跃的时候按x会有意想不到的效果...";
case 2:return "想要打怪的话往地图右边走走看...";
case 3:return "冒险世界里规定技能不能连续释放...";
case 4:return "正在打包行李...";
default:return null;
}
}
public ImageIcon juage(int t){
...
}
}
效果如下:
10.地图选择界面和地图加载
本来是想尽快解决状态栏问题的,但今天替导员去开会,实在闲的没事做,就把地图选择和地下城地图部分做了,好家伙,我直接在党员大会上画画
10.1地图选择界面(初版)2020.5.20
素材:
条件是当角色出右边界后加载,并销毁城镇MyFrame类,还有个Timer计时器去记录当前有无操作,并显示倒计时,倒计时10s结束后,返回城镇(new MyFrame)但是要注意,角色的站位必须是刚才进地下城之前的站位,所以我对MyFrame类重载了一个带有角色x、y坐标和背景容器x坐标的构造函数public MyFrame(int x,int y,int z) { ... }
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class MyChoose extends JFrame{
MyChoose myChoose =this;
int num=10;//倒计时显示
JLabel jLabel;
Timer timer;//倒计时线程
public MyChoose(){
this.setBounds(400,100,1200,900);
this.setResizable(false);
this.setLayout(null);
this.setTitle("Linの大冒险");
JLabel label = new JLabel(DateCenter.choose);
//显示一个地图(按钮形式),MyButton是在Main类时自己定义的图标按钮
MyButton myButton = new MyButton(600,225,300,200,DateCenter.map);
JLabel label2 = new JLabel("至暗森林深处");
label.setBounds(0,-50,1200,900);
myButton.setCursor(Toolkit.getDefaultToolkit().createCustomCursor(DateCenter.mouse, new Point(16, 16), "mycursor"));
//设置按钮监听
myButton.addActionListener((e)->{
//销毁窗口、关闭timer
myChoose.dispose();
timer.stop();
new Thread(()->{
new MyLoad(1);//绘制加载界面
}).start();
});
//一些界面设置
label2.setBounds(600,420,200,100);
label2.setFont(new Font("仿宋",Font.BOLD,30));
jLabel = new JLabel();//倒计时显示
jLabel.setBounds(100,20,300,300);
jLabel.setFont(new Font("仿宋",Font.BOLD,100));//设置超大号字体
this.add(jLabel,0);
this.add(label2);
this.add(myButton);
this.add(label);
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
timer = new Timer(1000,(e)->{
forTimer();
});
timer.start();
this.setVisible(true);
}
public void forTimer(){//为了在timer之外结束它所设置的方法,由于之前在npc处的守护线程也是用的lamda表达式,所以无法在内部关闭timer线程,后来也是用了提升作用域+函数这种方法
if(num==-1) {//倒计时结束
new MyFrame(1900,500,-950);
this.dispose();//销毁该窗口
timer.stop();//窗口销毁之后必须手动关闭timer,不然会一直占用资源
}
else jLabel.setText(""+num);//显示倒计时
num--;
}
}
看看效果:
10s无操作自动退出:(请手动勿略状态栏bug)
除了状态栏外,还有个小问题,角色出来后的站位方向,我悄咪咪做个修改,就不展示了
10.2地图加载2020.5.21
先上图片:
地图的加载是非常简单的,但麻烦的是角色的绘制,肯定是想当然的复用当前的角色类,但是角色类里有好多不兼容的类型,比如需要背景的移动,现在多少有点明白代码规范的重要性了,不规范的面向对象也只能是表面,所以开始想的是,在MyMap里在搞一套像MyFrame和MyTown一样关系的组合,命名为MyMap和MyBackground,由于类型兼容问题,将其分别封装成JFrame和JPanel传参,并在角色类里强制类型转换,虽然看起来好像没什么大问题,编译也通过了,但是会报出运行时错误,最后还是用了老办法,重载了角色类的构造方法,并新添加了属性和一个标志位:boolean juage_where=false;//判断当前在城镇还是地下城,false为在地下城
public MyCharacter(MyMap e,JLabel f) { juage_where=true; myMap = e; this.setIcon(DateCenter.rstand); this.setBounds(ct_x, ct_y, 150, 200); this.bg_label =f; myMap.add(this,0); }
然后所有跟背景移动和背景容器添加(特效、子弹等)有关的修改都必须加上对标志位的判断,在没有更好的办法之前,只能这样凑合了,幸好目前的技能和特效不是很多,修改起来没有想象中那么麻烦:
import javax.swing.*;
import java.awt.*;
public class MyMap extends JFrame{
public int flag;//记录当前是第几张地图
MyMap myMap=this;
public MyCharacter ct_label;//角色
public JLabel jLabel;//背景图片
public MyMap(int flag) {
init();
this.flag=flag;
jLabel=new JLabel(juage(flag));
jLabel.setBounds(0,0,1200,750);
ct_label = new MyCharacter(myMap,jLabel);
MyListener listener = new MyListener(ct_label);
listener.windowsListen(this);
ct_label.addKeyListener(listener);
this.addKeyListener(listener);
this.setCursor(Toolkit.getDefaultToolkit().createCustomCursor(DateCenter.mouse, new Point(16, 16), "mycursor"));
this.add(jLabel);
this.setVisible(true);
}
public void init(){
this.setBounds(400,100,1200,900);
this.setResizable(false);
this.setLayout(null);
this.setTitle("Linの大冒险");
myMap =this;
new MyState(this);
}
public ImageIcon juage(int i){
...
}
}
先看看当前效果:
感觉还不错,传送门的事就交给明天了,5.20不想太苦逼
好了,来继续说说传送门的事2021.5.21
目前还没加怪物,所以传送门默认开启(如果加了怪物之后就要判定当前地图内是否还有怪物),传送门需要不停自动绘制动态效果,所以必定需要一个线程,也是用提升作用域的方式:class MyDoor extends JLabel{ Timer timer; MyDoor myDoor =this; int num; public MyDoor(){ this.setBounds(1100,200,100,400); myMap.add(this,1); timer = new Timer(200,(e)->{ myDoor.setIcon(juage(num++%7,1)); if(ct_label.ct_x>1100){//如果角色进入传送门 if(flag<3) new MyMap(flag+1);//进入下一张图 else new MyFrame(1900,500,-950);//返回城镇 myMap.dispose(); timer.stop(); } });timer.start(); } }
看看效果:
可以看到,状态栏消失不见的小bug我也是偷偷修复了,修复起来不是很难,我将它一同放置到了panel中,就不会出现覆盖现象,当然只是针对城镇中的状态栏,地下城中的状态栏本就在同一个Frame中,一同放到panel中只需要每次移动panel的时候,重新定位一下(状态栏的x坐标应是panelx坐标的相反数,所以每次向相反方向移动就可以)if (bg_panel.bg_x < 0 && ct_label.ct_x+bg_panel.bg_x < 400) {//判断是否到界面边界 bg_panel.bg_x += 25; bg_panel.move();//移动背景 myState.move(-25);//移动状态栏 ...
//MyState中的move函数 public void move(int x){ ms_x+=x; setBounds(ms_x,720,1200,130); }
下期专门搞这个状态栏,显示血量、加一些药水、技能显示和计时等