新年Java小游戏之「年兽大作战」祝您笑口常开
Posted Java架构云海
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了新年Java小游戏之「年兽大作战」祝您笑口常开相关的知识,希望对你有一定的参考价值。
这个游戏加上编写文章,上班摸鱼时间加上回家的空闲时间,大概花了三天多。
java写这玩应真的很痛苦,各种状态位,各种图片和逻辑判断,脑袋都快炸了。而且肯定没有前端的精致,效果一般,偶尔会有卡顿,各位就图一乐,随便捧捧场啊。过程大于结果。
- 源码gitee地址:http://gitee.com/wei_rong_xi…
一、玩法介绍
进入初始界面,会看到一只大年兽位于正中间,然后是一直小老虎,也就是我们的玩家,点击【空格】即可开始游戏:
敲击空格,将进入游戏。从上至下分别是:
- 年兽的血量【NIANS HP】
- 移动的年兽
- 最下方的小老虎【玩家】
玩家通过【←】【→】键移动小老虎方向,使用【S】键发射炮弹:
当击中年兽后,会有烟花出现在背景:
每击中年兽三次,年兽会扔下炸弹:
如果玩家被击中,则直接【game over】 ,通过【空格】键重新开始:
当每击中年兽10次,其血量【❤】就会减少一个,年兽会随机扔下不同种类的爆竹,当前是11种,玩家可以移动方向键获取:
当玩家成功接到炮弹后,再次击中年兽,会更换背景烟花的种类。原本我想把子弹也换了,后来是实在整不动了。我玩了半天,想截个图,半天没成功,给自己心态玩崩了,就是下面的烟火:
当把年兽击败后,会出现新年快乐的字样:
上述就是全部玩法了,其实可以有更多扩展的,java写这东西实在写的太痛苦了。
二、代码介绍
效果不太好,但是学学代码实现总是好的吧,下面我简单说说怎么实现的。
2.1 程序入口【Frame】
使用Frame作为界面的基础和入口,可以设置大小,标题,展示位置等等,最主要的再次基础上添加一个面板,是我们游戏的实现:
public static void main(String[] args)
//1.创建窗口对象
Frame frame = new Frame("年兽大作战");
// 设置窗体大小为900x800
frame.setSize(900, 800);
// 设置窗体为居中格式
frame.setLocationRelativeTo(null);
// 设置窗体不可改变
frame.setResizable(false);
// 在窗体中添加一个面板
frame.add(new GamePanel());
// 设置窗体可见
frame.setVisible(true);
// 窗口点击关闭
frame.addWindowListener(new WindowAdapter()
@Override
public void windowClosing(WindowEvent arg0)
System.exit(0);
);
复制代码
2.2 构造器【GamePanel】
第一步,定义一个空参构造,需要添加焦点事件,和键盘事件监听,定时器启动页面刷新,后面后有定时器的创建:
public GamePanel()
// 获取焦点事件
this.setFocusable(true);
// 添加键盘监听事件
this.addKeyListener(this);
// 启动定时器
timer.start();
复制代码
2.3 游戏逻辑实现【GamePanel】
在启动类当中,我们在Frame当中添加了一个GamePanel,作用是后面游戏的所有内容展现都在其中,包括页面,游戏逻辑等。
代码较为复杂,我只说关键点,全部代码在全篇开头的gitee链接,感兴趣自己获取。
class GamePanel extends JPanel implements KeyListener, ActionListener
复制代码
如上所示,GamePannel继承了JPanel,同时实现了KeyListener和ActionListener。
- JPanel
这是jdk提供的,使用java进行绘图的基础容器。面板不会向除其自身背景以外的任何内容添加颜色。但是,您可以轻松地为它们添加边框,并以其他方式自定义它们的绘画。
- KeyListener
这个接口是用来监听键盘事件的接口,提供一下几个方法:
public interface KeyListener extends EventListener
/**
* 当按键被键入时调用
*/
public void keyTyped(KeyEvent e);
/**
* 当按键下压时调用
*/
public void keyPressed(KeyEvent e);
/**
* 当按键释放时调用
public void keyReleased(KeyEvent e);
复制代码
本文中我使用了keyPressed和keyReleased。
keyPressed主要用来完成键盘操作的移动,和射击功能。每当我们有按键操作,都会被它监听到,产生相应的事件。
此处有坑: 如果将按键一个一个的在此处判断,比如 if(左键) else if(设计) 这样,那么当你同时按下这两个按键,将会导致它们都失效。
解决办法如下:
- 定义一个全局Set,用来存放每次按键的事件。
static Set<Integer> keys = new HashSet<>();
复制代码
当按键下压时添加:
/**
* description: 键盘按下未释放
*
* @param e
* @return: void
* @author: weirx
* @time: 2022/1/10 14:02
*/
@SneakyThrows
@Override
public void keyPressed(KeyEvent e)
// 添加按钮下压事件到set
InitProcessor.keys.add(e.getKeyCode());
// 遍历执行按钮事件
multiKeys();
复制代码
在执行一个遍历方法,不断地去执行业务逻辑判断:
public void multiKeys()
for (Integer key : InitProcessor.keys)
int keyCode = key;
//空格键
if (keyCode == KeyEvent.VK_SPACE)
// 方向左键
else if (keyCode == KeyEvent.VK_LEFT)
// 射击
else if (keyCode == KeyEvent.VK_S)
复制代码
然后在我们释放按键的时候,使用如下的方式将这个set的key释放掉:
/**
* description: 释放按键
* @param e
* @return: void
* @author: weirx
* @time: 2022/1/11 15:39
*/
@Override
public void keyReleased(KeyEvent e)
//按钮释放,则将该事件移除
InitProcessor.keys.remove(e.getKeyCode());
复制代码
- 动作监听器【ActionListener】
这个是整个画面能够动态呈现的引擎,我们使用定时器的方式,每到定时时间则会监听到动作事件,进行数据逻辑判断。
其接口如下:
public interface ActionListener extends EventListener
/**
* 当事件发生时
*/
public void actionPerformed(ActionEvent e);
复制代码
定时器定义:
/**
* 定时器
*/
private Timer timer = new Timer(15, this);
复制代码
接口actionPerformed部分代码展示:
/**
* description: 定时器回调位置
* @param e
* @return: void
* @author: weirx
* @time: 2022/1/11 15:38
*/
@Override
public void actionPerformed(ActionEvent e)
// 当前年兽向右移动的情况
if (InitProcessor.LEFT.equals(InitProcessor.moveDirection))
// 被击中,换方向
if (InitProcessor.hit)
InitProcessor.moveDirection = InitProcessor.RIGHT;
// 判断移动到边界
if (InitProcessor.nian_x > 30)
InitProcessor.nian_x -= InitProcessor.moveSpeed * 2;
else
InitProcessor.moveDirection = InitProcessor.RIGHT;
InitProcessor.nian_x += InitProcessor.moveSpeed * 2;
else
// 被击中,换方向
if (InitProcessor.hit)
InitProcessor.moveDirection = InitProcessor.LEFT;
// 当前年兽向左移动的情况
// 判断移动到边界
if (InitProcessor.nian_x < 640)
InitProcessor.nian_x += InitProcessor.moveSpeed * 2;
else
InitProcessor.moveDirection = InitProcessor.LEFT;
InitProcessor.nian_x -= InitProcessor.moveSpeed * 2;
//设置烟火的展示时间,定时器刷新50次,不准确,但是至少能明显感受到烟花存在
if (InitProcessor.hitShow == 50)
InitProcessor.hit = false;
InitProcessor.hitShow = 0;
// 自增展示次数
InitProcessor.hitShow++;
// 刷新页面
repaint();
timer.start();//启动计时器
复制代码
到以上为止,按键事件,和定时器事件都完成了,可以说全部的逻辑判断都在上面去实现。下面我们关注在图像是如何出现的。
- 图像展示基础【JComponent】
前面我们似乎没有看到这个组件的身影,那么它是在哪里呢?看下面的类图:
如上所示,GamePanel继承JPanel,而JPanel又继承了JComponent。JComponent有一个方法我们需要重写,这也就是我们实现图像展示的方法,其提供了绘制UI的能力,我们重写即可,部分代码如下:
/**
* description: 画页面
*
* @param g
* @return: void
* @author: weirx
* @time: 2022/1/10 13:40
*/
@Override
protected void paintComponent(Graphics g)
// 清屏效果
super.paintComponent(g);
// 游戏未开始
if (!InitProcessor.isStared)
background.paintIcon(this, g,0,0);
InitProcessor.nian.paintIcon(this, g, 250, 130);
InitProcessor.tiger.paintIcon(this, g, 220, 470);
// 绘制首页
// 设置游戏文字
g.setColor(Color.ORANGE);
g.setFont(new Font("幼圆", Font.BOLD, 50));
g.drawString("年兽大作战", 325, 550);
// 设置开始提示
g.setColor(Color.GREEN);
g.setFont(new Font("幼圆", Font.BOLD, 30));
g.drawString("按【空格】键开始游戏", 300, 620);
g.drawString("按【←】【→】键移动", 300, 660);
g.drawString("按【S】键发射炮弹", 300, 700);
else if (isGameOver)
//输出gameover
InitProcessor.gameOver.paintIcon(this, g, 10, 10);
// 设置开始提示
g.setColor(Color.GREEN);
g.setFont(new Font("幼圆", Font.BOLD, 20));
g.drawString("按【空格】再次开始游戏", 340, 600);
复制代码
关键点是使用Graphics绘制文字,背景,颜色等等内容。
图片需要使用ImageIcon类来进行绘画,我将ImageIcon初始化部分封装了,所以上面没显示,常规使用如下:
ImageIcon nian = new ImageIcon(PATH_PREFIX + "nian.png");
nian.paintIcon(this, g, 250, 130);
复制代码
2.4 游戏的血液【InitProcessor】
为什么这么说是血液呢?因为这个类是我自己实现的一个初始化类,其中的内容是串联整个游戏的关键点,像身体的血液一样。
通过写这个游戏,我发现最关键的点在于【状态】,可以说全部的页面动画展示都在于一个状态,无论是子弹的运动,年兽的运动,包括礼花图片的切换,以及各种图片的坐标等等。
所以我专门抽象了这个类,用于各种状态的初始化,部分代码如下:
/**
* @description: 初始化处理器
* @author:weirx
* @date:2022/1/11 10:15
* @version:3.0
*/
public class InitProcessor
/**
* 游戏是否开始,默认是false
*/
public static Boolean isStared = false;
/**
* 游戏是否暂停,默认是false
*/
public static Boolean isStopped = false;
/**
* 礼花横坐标
*/
public static int youWillBeKill_x = 0;
/**
* 礼花纵坐标
*/
public static int youWillBeKill_y = nian_y + 200;
/**
* 展示炮弹
*/
public static Boolean showYouWillBeKill = false;
public static Boolean isGameOver = false;
/**
* 图片路径
*/
public final static String PATH_PREFIX = "src/main/java/com/wjbgn/nianfight/pic/";
public static ImageIcon nian = new ImageIcon(PATH_PREFIX + "nian.png");
public static ImageIcon tiger = new ImageIcon(PATH_PREFIX + "tiger\\tiger2.png");
public static ImageIcon heart = new ImageIcon(PATH_PREFIX + "blood\\heart.png");
/**
* 礼花容器
*/
public static List<FireworksDO> fireworksDOS = initFireworks();
/**
* 花容器
*/
public static List<FlowersDO> flowersDOS = initFlowers();
/**
* 初始化爆竹
*/
private static List<FlowersDO> initFlowers()
List<FlowersDO> list = new ArrayList<>();
list.add(new FlowersDO(1, PATH_PREFIX + "flowers\\flower1.png"));
list.add(new FlowersDO(2, PATH_PREFIX + "fireworks\\flower2.png"));
list.add(new FlowersDO(3, PATH_PREFIX + "fireworks\\flower3.png"));
list.add(new FlowersDO(4, PATH_PREFIX + "fireworks\\flower4.png"));
list.add(new FlowersDO(5, PATH_PREFIX + "fireworks\\flower5.png"));
list.add(new FlowersDO(6, PATH_PREFIX + "fireworks\\flower6.png"));
list.add(new FlowersDO(7, PATH_PREFIX + "fireworks\\flower7.png"));
list.add(new FlowersDO(8, PATH_PREFIX + "fireworks\\flower8.png"));
list.add(new FlowersDO(9, PATH_PREFIX + "fireworks\\flower9.png"));
list.add(new FlowersDO(10, PATH_PREFIX + "fireworks\\flower10.png"));
list.add(new FlowersDO(11, PATH_PREFIX + "fireworks\\flower11.png"));
return list;
/**
* description: 初始化礼花种类
*
* @return: void
* @author: weirx
* @time: 2022/1/11 10:58
*/
public static List<FireworksDO> initFireworks()
List<FireworksDO> list = new ArrayList<>();
list.add(new FireworksDO(1, PATH_PREFIX + "fireworks\\fireworks1.png"));
list.add(new FireworksDO(2, PATH_PREFIX + "fireworks\\fireworks2.png"));
list.add(new FireworksDO(3, PATH_PREFIX + "fireworks\\fireworks3.png"));
list.add(new FireworksDO(4, PATH_PREFIX + "fireworks\\fireworks4.png"));
list.add(new FireworksDO(5, PATH_PREFIX + "fireworks\\fireworks5.png"));
list.add(new FireworksDO(6, PATH_PREFIX + "fireworks\\fireworks6.png"));
list.add(new FireworksDO(7, PATH_PREFIX + "fireworks\\fireworks7.png"));
list.add(new FireworksDO(8, PATH_PREFIX + "fireworks\\fireworks8.png"));
list.add(new FireworksDO(9, PATH_PREFIX + "fireworks\\fireworks9.png"));
list.add(new FireworksDO(10, PATH_PREFIX + "fireworks\\fireworks10.png"));
list.add(new FireworksDO(11, PATH_PREFIX + "fireworks\\fireworks11.png"));
return list;
/**
* description: 初始化方法,用于重新开始游戏
*
* @return: void
* @author: weirx
* @time: 2022/1/11 10:39
*/
public static void init()
isStared = false;
isStopped = false;
attack = false;
nian_x = 325;
nian_y = 50;
tiger_x = 325;
tiger_y = 660;
bullet_x = tiger_x + 20;
bullet_y = tiger_y - 20;
moveSpeed = 1;
moveDirection = LEFT;
hit = false;
hitCount = 0;
hitShow = 0;
nianBlood = 10;
success = false;
keys = new HashSet<>();
fireworks_x = 0;
fireworks_y = nian_y + 200;
showFireworks = false;
currentFireworks = null;
takeFireworks = false;
currentFlowers = null;
youWillBeKill = 0;
youWillBeKill_x = 0;
youWillBeKill_y = nian_y + 200;
showYouWillBeKill = false;
isGameOver = false;
复制代码
2.5 实体类【FireworksDO】【FlowersDO】
这是两个实体类,分别定义的爆竹和礼花样式,用于初始化,代码如下:
import javax.swing.*;
import static com.wjbgn.nianfight.nianshou.InitProcessor.PATH_PREFIX;
/**
* @description: 花容器
* @author:weirx
* @date:2022/1/11 11:05
* @version:3.0
*/
public class FlowersDO
private Integer id;
private String path;
private ImageIcon flower;
public ImageIcon getFlower()
return flower;
public void setFlower(ImageIcon flower)
this.flower = flower;
public Integer getId()
return id;
public void setId(Integer id)
this.id = id;
public String getPath()
return path;
public void setPath(String path)
this.path = path;
public FlowersDO(Integer id, String path)
this.id = id;
this.path = path;
this.flower = new ImageIcon(PATH_PREFIX + "flowers\\flower" + id + ".png");
复制代码
/**
* @description: 礼花实体类
* @author:weirx
* @date:2022/1/11 11:01
* @version:3.0
*/
public class FireworksDO
/**
* id
*/
private Integer id;
/**
* 图片路径
*/
private String path;
public ImageIcon getFirework()
return firework;
public void setFirework(ImageIcon firework)
this.firework = firework;
private ImageIcon firework;
public Integer getId()
return id;
public void setId(Integer id)
this.id = id;
public String getPath()
return path;
public void setPath(String path)
this.path = path;
public FireworksDO(Integer id, String path)
this.id = id;
this.path = path;
this.firework = new ImageIcon(PATH_PREFIX + "fireworks\\fireworks" + id + ".png");
复制代码
2.6 图片素材
游戏中使用了大量的图片素材,我在网上找了两个网站不错,一个是png图片网站,是免费的,还有一个是免费切图的,挺好用,都分享给大家,建议在 掘金插件 里收藏:
- 免费png图片网址(英文):http://www.cleanpng.com/
- 免费切图网址(简单版直接微信关注):http://www.uupoop.com/
我使用的素材都在项目的pic目录下:
三、总结
首先感谢掘金举行的这些活动,让我有机会尝试这样的一次编码过程。
写java好几年了,其实从没有使用 javax.swing 和 java.awt 包下面的内容开发过代码,对于现在用户体验为前提的大环境下,综合编码体验,和游戏运行体验来看,确实是不太友好,不太符合环境背景。但是也是一次不错的学习过程。
- 问题总结 目前整个游戏还是存在一些bug的,后面有时间再翻出来调试吧,此处先记录一下: 子弹有时并不是从老虎正前方射出的,与实际的坐标存在偏差:此问题出现的原因我推测来自线程间共享变量的同步问题。子弹的初始横坐标取决于小老虎当前所在的横坐标,这个坐标同步没做好。 关于按键切换、同时两个按键等情况造成卡顿的问题:前面解决两个按键同时按下的方案可能不是最优解,后面还需要优化。 怪兽扔炸弹、爆竹随着年兽移动存在偏移:炸弹和爆竹的初始横坐标,是年兽当前的横坐标,需要一个变量记录当前年兽的位置,作为炸弹和爆竹的初始横坐标。
关于游戏就说这么多了,感兴趣的去文章开篇的gitee下载源码就行了。
祝大家新年快乐,笑口常开!
以上是关于新年Java小游戏之「年兽大作战」祝您笑口常开的主要内容,如果未能解决你的问题,请参考以下文章