祝您笑口常开

image.png
春节要到了,看惯了前端各种小游戏,确实做得很好,很精致。但是我也要为后端程序员稍微做一点贡献,做一款java版本的【年兽大作战】。
这个游戏加上编写文章,上班摸鱼时间加上回家的空闲时间,大概花了三天多。
java写这玩应真的很痛苦,各种状态位,各种图片和逻辑判断,脑袋都快炸了。而且肯定没有前端的精致,效果一般,偶尔会有卡顿,各位就图一乐,随便捧捧场啊。过程大于结果。

一、玩法介绍

进入初始界面,会看到一只大年兽位于正中间,然后是一直小老虎,也就是我们的玩家,点击【空格】即可开始游戏:
image.png
敲击空格,将进入游戏。从上至下分别是:

  • 年兽的血量【NIAN’S HP】
  • 移动的年兽
  • 最下方的小老虎【玩家】


image.png
玩家通过【←】【→】键移动小老虎方向,使用【S】键发射炮弹
image.png
当击中年兽后,会有烟花出现在背景:
image.png
每击中年兽三次,年兽会扔下炸弹:
image.png
如果玩家被击中,则直接【game over】 ,通过【空格】键重新开始:
image.png
当每击中年兽10次,其血量【❤】就会减少一个,年兽会随机扔下不同种类的爆竹,当前是11种,玩家可以移动方向键获取
image.png
当玩家成功接到炮弹后,再次击中年兽,会更换背景烟花的种类。原本我想把子弹也换了,后来是实在整不动了。我玩了半天,想截个图,半天没成功,给自己心态玩崩了,就是下面的烟火:
image.png
当把年兽击败后,会出现新年快乐的字样:
image.png
上述就是全部玩法了,其实可以有更多扩展的,java写这东西实在写的太痛苦了。

二、代码介绍

效果不太好,但是学学代码实现总是好的吧,下面我简单说说怎么实现的。

2.1 程序入口【Frame】

使用Frame作为界面的基础和入口,可以设置大小,标题,展示位置等等,最主要的再次基础上添加一个面板,是我们游戏的实现:

image.png

2.2 构造器【GamePanel】

第一步,定义一个空参构造,需要添加焦点事件,和键盘事件监听,定时器启动页面刷新,后面后有定时器的创建:
image.png

2.3 游戏逻辑实现【GamePanel】

在启动类当中,我们在Frame当中添加了一个GamePanel,作用是后面游戏的所有内容展现都在其中,包括页面,游戏逻辑等。
代码较为复杂,在这里我只说关键点

class GamePanel extends JPanel implements KeyListener, ActionListener

如上所示,GamePannel继承了JPanel,同时实现了KeyListener和ActionListener。

  • JPanel

这是jdk提供的,使用java进行绘图的基础容器。面板不会向除其自身背景以外的任何内容添加颜色。但是,您可以轻松地为它们添加边框,并以其他方式自定义它们的绘画。

  • KeyListener

这个接口是用来监听键盘事件的接口,提供一下几个方法:

  1. public interface KeyListener extends EventListener {
  2. /**
  3. * 当按键被键入时调用
  4. */
  5. public void keyTyped(KeyEvent e);
  6. /**
  7. * 当按键下压时调用
  8. */
  9. public void keyPressed(KeyEvent e);
  10. /**
  11. * 当按键释放时调用
  12. public void keyReleased(KeyEvent e);
  13. }

本文中我使用了keyPressed和keyReleased。
keyPressed主要用来完成键盘操作的移动,和射击功能。每当我们有按键操作,都会被它监听到,产生相应的事件。
此处有坑: 如果将按键一个一个的在此处判断,比如 if(左键) else if(设计) 这样,那么当你同时按下这两个按键,将会导致它们都失效。
解决办法如下:

  • 定义一个全局Set,用来存放每次按键的事件。
    1. static Set<Integer> keys = new HashSet<>();
    当按键下压时添加: ```java /**
    • description: 键盘按下未释放 */ @SneakyThrows @Override public void keyPressed(KeyEvent e) { // 添加按钮下压事件到set InitProcessor.keys.add(e.getKeyCode()); // 遍历执行按钮事件 multiKeys(); }
  1. 在执行一个遍历方法,不断地去执行业务逻辑判断:
  2. ```java
  3. public void multiKeys() {
  4. for (Integer key : InitProcessor.keys) {
  5. int keyCode = key;
  6. //空格键
  7. if (keyCode == KeyEvent.VK_SPACE) {
  8. }
  9. // 方向左键
  10. else if (keyCode == KeyEvent.VK_LEFT) {
  11. }
  12. // 射击
  13. else if (keyCode == KeyEvent.VK_S) {
  14. }
  15. }
  16. }

然后在我们释放按键的时候,使用如下的方式将这个set的key释放掉:

  1. /**
  2. * description: 释放按键
  3. */
  4. @Override
  5. public void keyReleased(KeyEvent e) {
  6. //按钮释放,则将该事件移除
  7. InitProcessor.keys.remove(e.getKeyCode());
  8. }
  • 动作监听器【ActionListener】
  • 这个是整个画面能够动态呈现的引擎,我们使用定时器的方式,每到定时时间则会监听到动作事件,进行数据逻辑判断。

其接口如下:

  1. public interface ActionListener extends EventListener {
  2. /**
  3. * 当事件发生时
  4. */
  5. public void actionPerformed(ActionEvent e);
  6. }

定时器定义:

  1. /**
  2. * 定时器
  3. */
  4. private Timer timer = new Timer(15, this);

接口actionPerformed部分代码展示:

  1. /**
  2. * description: 定时器回调位置
  3. * @param e
  4. * @return: void
  5. * @author: weirx
  6. * @time: 2022/1/11 15:38
  7. */
  8. @Override
  9. public void actionPerformed(ActionEvent e) {
  10. // 当前年兽向右移动的情况
  11. if (InitProcessor.LEFT.equals(InitProcessor.moveDirection)) {
  12. // 被击中,换方向
  13. if (InitProcessor.hit) {
  14. InitProcessor.moveDirection = InitProcessor.RIGHT;
  15. }
  16. // 判断移动到边界
  17. if (InitProcessor.nian_x > 30) {
  18. InitProcessor.nian_x -= InitProcessor.moveSpeed * 2;
  19. } else {
  20. InitProcessor.moveDirection = InitProcessor.RIGHT;
  21. InitProcessor.nian_x += InitProcessor.moveSpeed * 2;
  22. }
  23. } else {
  24. // 被击中,换方向
  25. if (InitProcessor.hit) {
  26. InitProcessor.moveDirection = InitProcessor.LEFT;
  27. }
  28. // 当前年兽向左移动的情况
  29. // 判断移动到边界
  30. if (InitProcessor.nian_x < 640) {
  31. InitProcessor.nian_x += InitProcessor.moveSpeed * 2;
  32. } else {
  33. InitProcessor.moveDirection = InitProcessor.LEFT;
  34. InitProcessor.nian_x -= InitProcessor.moveSpeed * 2;
  35. }
  36. }
  37. //设置烟火的展示时间,定时器刷新50次,不准确,但是至少能明显感受到烟花存在
  38. if (InitProcessor.hitShow == 50) {
  39. InitProcessor.hit = false;
  40. InitProcessor.hitShow = 0;
  41. }
  42. // 自增展示次数
  43. InitProcessor.hitShow++;
  44. // 刷新页面
  45. repaint();
  46. timer.start();//启动计时器
  47. }

到以上为止,按键事件,和定时器事件都完成了,可以说全部的逻辑判断都在上面去实现。下面我们关注在图像是如何出现的。

  • 图像展示基础【JComponent】

前面我们似乎没有看到这个组件的身影,那么它是在哪里呢?看下面的类图:
image.png
如上所示,GamePanel继承JPanel,而JPanel又继承了JComponent。JComponent有一个方法我们需要重写,这也就是我们实现图像展示的方法,其提供了绘制UI的能力,我们重写即可,部分代码如下

  1. /**
  2. * description: 画页面
  3. */
  4. @Override
  5. protected void paintComponent(Graphics g) {
  6. // 清屏效果
  7. super.paintComponent(g);
  8. // 游戏未开始
  9. if (!InitProcessor.isStared) {
  10. background.paintIcon(this, g,0,0);
  11. InitProcessor.nian.paintIcon(this, g, 250, 130);
  12. InitProcessor.tiger.paintIcon(this, g, 220, 470);
  13. // 绘制首页
  14. // 设置游戏文字
  15. g.setColor(Color.ORANGE);
  16. g.setFont(new Font("幼圆", Font.BOLD, 50));
  17. g.drawString("年兽大作战", 325, 550);
  18. // 设置开始提示
  19. g.setColor(Color.GREEN);
  20. g.setFont(new Font("幼圆", Font.BOLD, 30));
  21. g.drawString("按【空格】键开始游戏", 300, 620);
  22. g.drawString("按【←】【→】键移动", 300, 660);
  23. g.drawString("按【S】键发射炮弹", 300, 700);
  24. } else if (isGameOver) {
  25. //输出gameover
  26. InitProcessor.gameOver.paintIcon(this, g, 10, 10);
  27. // 设置开始提示
  28. g.setColor(Color.GREEN);
  29. g.setFont(new Font("幼圆", Font.BOLD, 20));
  30. g.drawString("按【空格】再次开始游戏", 340, 600);
  31. }
  32. }

关键点是使用Graphics绘制文字,背景,颜色等等内容。
图片需要使用ImageIcon类来进行绘画,我将ImageIcon初始化部分封装了,所以上面没显示,常规使用如下:

  1. ImageIcon nian = new ImageIcon(PATH_PREFIX + "nian.png");
  2. nian.paintIcon(this, g, 250, 130);

2.4 游戏的血液【InitProcessor】

为什么这么说是血液呢?因为这个类是我自己实现的一个初始化类,其中的内容是串联整个游戏的关键点,像身体的血液一样。
通过写这个游戏,我发现最关键的点在于【状态】,可以说全部的页面动画展示都在于一个状态,无论是子弹的运动,年兽的运动,包括礼花图片的切换,以及各种图片的坐标等等。
所以我专门抽象了这个类,用于各种状态的初始化,部分代码如下:

  1. /**
  2. * @description: 初始化处理器
  3. * @author:weirx
  4. * @date:2022/1/11 10:15
  5. * @version:3.0
  6. */
  7. public class InitProcessor {
  8. /**
  9. * 游戏是否开始,默认是false
  10. */
  11. public static Boolean isStared = false;
  12. /**
  13. * 游戏是否暂停,默认是false
  14. */
  15. public static Boolean isStopped = false;
  16. /**
  17. * 礼花横坐标
  18. */
  19. public static int youWillBeKill_x = 0;
  20. /**
  21. * 礼花纵坐标
  22. */
  23. public static int youWillBeKill_y = nian_y + 200;
  24. /**
  25. * 展示炮弹
  26. */
  27. public static Boolean showYouWillBeKill = false;
  28. public static Boolean isGameOver = false;
  29. /**
  30. * 图片路径
  31. */
  32. public final static String PATH_PREFIX = "src/main/java/com/wjbgn/nianfight/pic/";
  33. public static ImageIcon nian = new ImageIcon(PATH_PREFIX + "nian.png");
  34. public static ImageIcon tiger = new ImageIcon(PATH_PREFIX + "tiger\tiger2.png");
  35. public static ImageIcon heart = new ImageIcon(PATH_PREFIX + "blood\heart.png");
  36. /**
  37. * 礼花容器
  38. */
  39. public static List<FireworksDO> fireworksDOS = initFireworks();
  40. /**
  41. * 花容器
  42. */
  43. public static List<FlowersDO> flowersDOS = initFlowers();
  44. /**
  45. * 初始化爆竹
  46. */
  47. private static List<FlowersDO> initFlowers() {
  48. List<FlowersDO> list = new ArrayList<>();
  49. list.add(new FlowersDO(1, PATH_PREFIX + "flowers\flower1.png"));
  50. list.add(new FlowersDO(2, PATH_PREFIX + "fireworks\flower2.png"));
  51. list.add(new FlowersDO(3, PATH_PREFIX + "fireworks\flower3.png"));
  52. list.add(new FlowersDO(4, PATH_PREFIX + "fireworks\flower4.png"));
  53. list.add(new FlowersDO(5, PATH_PREFIX + "fireworks\flower5.png"));
  54. list.add(new FlowersDO(6, PATH_PREFIX + "fireworks\flower6.png"));
  55. list.add(new FlowersDO(7, PATH_PREFIX + "fireworks\flower7.png"));
  56. list.add(new FlowersDO(8, PATH_PREFIX + "fireworks\flower8.png"));
  57. list.add(new FlowersDO(9, PATH_PREFIX + "fireworks\flower9.png"));
  58. list.add(new FlowersDO(10, PATH_PREFIX + "fireworks\flower10.png"));
  59. list.add(new FlowersDO(11, PATH_PREFIX + "fireworks\flower11.png"));
  60. return list;
  61. }
  62. /**
  63. * description: 初始化礼花种类
  64. *
  65. * @return: void
  66. * @author: weirx
  67. * @time: 2022/1/11 10:58
  68. */
  69. public static List<FireworksDO> initFireworks() {
  70. List<FireworksDO> list = new ArrayList<>();
  71. list.add(new FireworksDO(1, PATH_PREFIX + "fireworks\fireworks1.png"));
  72. list.add(new FireworksDO(2, PATH_PREFIX + "fireworks\fireworks2.png"));
  73. list.add(new FireworksDO(3, PATH_PREFIX + "fireworks\fireworks3.png"));
  74. list.add(new FireworksDO(4, PATH_PREFIX + "fireworks\fireworks4.png"));
  75. list.add(new FireworksDO(5, PATH_PREFIX + "fireworks\fireworks5.png"));
  76. list.add(new FireworksDO(6, PATH_PREFIX + "fireworks\fireworks6.png"));
  77. list.add(new FireworksDO(7, PATH_PREFIX + "fireworks\fireworks7.png"));
  78. list.add(new FireworksDO(8, PATH_PREFIX + "fireworks\fireworks8.png"));
  79. list.add(new FireworksDO(9, PATH_PREFIX + "fireworks\fireworks9.png"));
  80. list.add(new FireworksDO(10, PATH_PREFIX + "fireworks\fireworks10.png"));
  81. list.add(new FireworksDO(11, PATH_PREFIX + "fireworks\fireworks11.png"));
  82. return list;
  83. }
  84. /**
  85. * description: 初始化方法,用于重新开始游戏
  86. *
  87. * @return: void
  88. * @author: weirx
  89. * @time: 2022/1/11 10:39
  90. */
  91. public static void init() {
  92. isStared = false;
  93. isStopped = false;
  94. attack = false;
  95. nian_x = 325;
  96. nian_y = 50;
  97. tiger_x = 325;
  98. tiger_y = 660;
  99. bullet_x = tiger_x + 20;
  100. bullet_y = tiger_y - 20;
  101. moveSpeed = 1;
  102. moveDirection = LEFT;
  103. hit = false;
  104. hitCount = 0;
  105. hitShow = 0;
  106. nianBlood = 10;
  107. success = false;
  108. keys = new HashSet<>();
  109. fireworks_x = 0;
  110. fireworks_y = nian_y + 200;
  111. showFireworks = false;
  112. currentFireworks = null;
  113. takeFireworks = false;
  114. currentFlowers = null;
  115. youWillBeKill = 0;
  116. youWillBeKill_x = 0;
  117. youWillBeKill_y = nian_y + 200;
  118. showYouWillBeKill = false;
  119. isGameOver = false;
  120. }
  121. }

2.5 实体类【FireworksDO】【FlowersDO】

这是两个实体类,分别定义的爆竹和礼花样式,用于初始化,代码如下:

  1. import javax.swing.*;
  2. import static com.wjbgn.nianfight.nianshou.InitProcessor.PATH_PREFIX;
  3. /**
  4. * @description: 花容器
  5. * @author:weirx
  6. * @date:2022/1/11 11:05
  7. * @version:3.0
  8. */
  9. public class FlowersDO {
  10. private Integer id;
  11. private String path;
  12. private ImageIcon flower;
  13. public ImageIcon getFlower() {
  14. return flower;
  15. }
  16. public void setFlower(ImageIcon flower) {
  17. this.flower = flower;
  18. }
  19. public Integer getId() {
  20. return id;
  21. }
  22. public void setId(Integer id) {
  23. this.id = id;
  24. }
  25. public String getPath() {
  26. return path;
  27. }
  28. public void setPath(String path) {
  29. this.path = path;
  30. }
  31. public FlowersDO(Integer id, String path) {
  32. this.id = id;
  33. this.path = path;
  34. this.flower = new ImageIcon(PATH_PREFIX + "flowers\flower" + id + ".png");
  35. }
  36. }
  37. /**
  38. * @description: 礼花实体类
  39. * @author:weirx
  40. * @date:2022/1/11 11:01
  41. * @version:3.0
  42. */
  43. public class FireworksDO {
  44. /**
  45. * id
  46. */
  47. private Integer id;
  48. /**
  49. * 图片路径
  50. */
  51. private String path;
  52. public ImageIcon getFirework() {
  53. return firework;
  54. }
  55. public void setFirework(ImageIcon firework) {
  56. this.firework = firework;
  57. }
  58. private ImageIcon firework;
  59. public Integer getId() {
  60. return id;
  61. }
  62. public void setId(Integer id) {
  63. this.id = id;
  64. }
  65. public String getPath() {
  66. return path;
  67. }
  68. public void setPath(String path) {
  69. this.path = path;
  70. }
  71. public FireworksDO(Integer id, String path) {
  72. this.id = id;
  73. this.path = path;
  74. this.firework = new ImageIcon(PATH_PREFIX + "fireworks\fireworks" + id + ".png");
  75. }
  76. }

2.6 图片素材

游戏中使用了大量的图片素材,我在网上找了两个网站不错,一个是png图片网站,是免费的,还有一个是免费切图的,挺好用,都分享给大家

  • 免费png图片网址(英文):www.cleanpng.com/
  • image.png
  • 免费切图网址(简单版直接微信关注):www.uupoop.com/
  • image.png
  • 我使用的素材都在项目的pic目录下:
  • image.png

    三、总结

    写java好几年了,其实从没有使用 javax.swing 和 java.awt 包下面的内容开发过代码,对于现在用户体验为前提的大环境下,综合编码体验,和游戏运行体验来看,确实是不太友好,不太符合环境背景。但是也是一次不错的学习过程。

  • 问题总结目前整个游戏还是存在一些bug的,后面有时间再翻出来调试吧,此处先记录一下:

    • 子弹有时并不是从老虎正前方射出的,与实际的坐标存在偏差:此问题出现的原因我推测来自线程间共享变量的同步问题。子弹的初始横坐标取决于小老虎当前所在的横坐标,这个坐标同步没做好。
    • 关于按键切换、同时两个按键等情况造成卡顿的问题:前面解决两个按键同时按下的方案可能不是最优解,后面还需要优化。
    • 怪兽扔炸弹、爆竹随着年兽移动存在偏移:炸弹和爆竹的初始横坐标,是年兽当前的横坐标,需要一个变量记录当前年兽的位置,作为炸弹和爆竹的初始横坐标。

关于游戏就说这么多了,
祝大家新年快乐,笑口常开!
年兽大作战项目 - 图17