游戏实战——一个冒险小游戏(持续更新)
Posted 姬如乀千泷
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏实战——一个冒险小游戏(持续更新)相关的知识,希望对你有一定的参考价值。
一步一步copy一款冒险小游戏
前言:想做一个小游戏已经很长时间了,奈何之前的自己心有余而力不足,在学习了几个月java之后,信心也逐渐增长起来,终于在一周前动身了
1.背景绘制
1.1背景图片
//用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测试背景移动
首先添加键盘监视器
在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角色移动
要达到移动的效果,需要逐一的绘制移动帧,所以添置一个标志量,每次调用跑步函数标志量加一,并且按模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角色跑步
跑步和走路类似,只是绘制的图片和移动速度不同,本来想要实现双击两次移动键就跑起来,玩过地下城与勇士的朋友们该是知道的,但是搞了好长时间都没有实现出来,因为键盘按压长按的模式是不停的调用来实现的,不然可以设置标志位,判断是否是第二次敲击,执行完之后标志位在置反就可以,所以用了另一种耳熟能详的方式来改变状态,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角色跳跃
跳跃图片:
跳跃按理说和走路是一样的,初步时也做了这样的猜想,只需要每帧暂停零点几秒就行了,也做了如下实践,空格键盘监听就不做展示了别忘了在键盘松开的监听中加入非跳跃状态判断,为此,在角色类中加入标志位
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普通攻击
图片素材
和跳跃线程如出一辙,用子线程绘制攻击帧
//添加监听 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跳跃攻击
同样是老套路
图片:
标志位: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)
图片:
自己画的帧分辨率和大小都和原图不一样,调试花费了太长时间,不过总体来说实现原理,并不难:为角色类新加一个释放技能的标志位,之后所有技能释放都用此标志位进行并发控制检测
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老成员了,不介绍了
}
}
效果如下:
6.技能特效绘制
6.1拔刀斩技能特效
拔刀斩的斩击类似于子弹,所以放在了子弹类中
在技能类休眠的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(); } }
效果如下:
以上是关于游戏实战——一个冒险小游戏(持续更新)的主要内容,如果未能解决你的问题,请参考以下文章
Pygame实战:爆肝!几千行代码实现《机甲闯关冒险游戏》,太牛了!(❤️建议收藏起来慢慢学❤️)
python 写游戏好简单啊,我用键盘可以随意控制角色了python 游戏实战 04
如何使用 Unity制作微信小游戏,微信小游戏制作方案 最新完整详细教程来袭持续更新