为了带着小伙伴们很好的实践基于Java语言的设计模式,咱们将开发一个仿90版的红白机坦克大战小游戏。该游戏基于Java Swing组件来开发,在开发整个游戏过程中,作者会带着小伙伴一步步实现游戏中的各种功能,一步步对代码进行重构,以消除代码中的“坏味道”;同时为了更好的对游戏的功能进行扩展、对代码更好的维护并增加可读性,咱们会适当的使用设计模式。通过不断的重构和对设计模式的实践,让大伙儿真正理解设计模式的使用场景,对大家工作中编码、学习框架源码以及面试都会有一定的帮助。
最简单的Swing窗体应用程序
再回到本节的主题,咱们用基本的Java Swing组件来快速开发一个main
方法启动的包含游戏绘图板的最简单的窗体应用。
实现该窗体应用的所有代码都在一个GameMain
类中,代码如下:
package com.pf.java.tankbattle;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class GameMain extends JFrame {
public GameMain() {
// 设置窗口的标题
setTitle("坦克大战1.0");
// 设置窗口背景色
setBackground(Color.BLACK);
// 设置窗口不可拖动来改变大小
setResizable(false);
// 设置程序启动后,窗口弹出可见,同时开启后台线程来对图形化界面渲染和事件交互的监听
setVisible(true);
// 创建一个绘图板组件,用于后期游戏画面的绘制
JPanel panel = new JPanel();
// 将绘图板组件添加到窗口对象的内容绘图板中
getContentPane().add(panel);
// -------- 设置绘图板组件的外观 --------
// 设置绘图板亮灰色背景
panel.setBackground(Color.lightGray);
// 设置绘图板的宽高(像素单位)
panel.setPreferredSize(new Dimension(320, 240));
// 让整个窗口包住游戏绘图板
pack();
// 让窗口显示在屏幕正中间
setLocationRelativeTo(null);
// 给窗口添加一个监听器,监听窗口关闭时手动退出主线程
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.out.println("程序结束");
// 手动退出主线程,参数值0代表正常退出,而1代表异常情况退出
System.exit(0);
}
});
}
public static void main(String[] args) {
// 创建窗口对象,并进行窗口的初始化
new GameMain();
}
}
关于代码的说明
完整的代码后期会维护到Github上。教程中会将实现功能所涉及的关键代码都贴出来,确保大伙儿能够在阅读本套教程中,边看边练,跟上作者对游戏功能开发、代码迭代的节奏和重构的思路。为此,作者会尽量为每一行代码增加注释。游戏涉及的素材,在源码发布出来之前,作者会分享在网盘中供大伙儿下载。
以上的代码相信大伙儿都看得明白,窗体、绘图板的设置逻辑都在GameMain
类的无参构造中。运行程序后,游戏窗体默认居中弹出,窗口大小由JPanel
绘图板对象设置的宽高决定,并且不能改变大小,点击窗口关闭按钮后程序正常退出。
拆分组件类
接下来,咱们将对这一个类文件按照组件进行拆分,分成几个类文件。这也符合软件设计中的“单一职责”原则。拆分后的类文件:
游戏绘图板类MyPanel
负责游戏画面的绘制,目前仅实现了外观设置:
package com.pf.java.tankbattle;
import ...
public class MyPanel extends JPanel {
public MyPanel() {
// -------- 设置绘图板组件的外观 --------
// 设置绘图板亮灰色背景
setBackground(Color.lightGray);
// 设置绘图板的宽高(像素单位)
setPreferredSize(new Dimension(320, 240));
}
}
说明
为了减少篇幅,
import
语句后的内容都会被省略。同样,前面贴出来的代码中完全一样内容也会被省略掉,以...
代替。
窗体类MyFrame
用于窗体的外观设置、组件的添加、事件监听器的注册等等,这里有变动的地方则是,构造器接收外部传入的JPanel
对象,添加到内容面板中:
package com.pf.java.tankbattle;
import ...
public class MyFrame extends JFrame {
public MyFrame(JPanel panel) {
...
// 将绘图板组件添加到窗口对象的内容面板中
getContentPane().add(panel);
...
}
}
游戏的主类GameMain
简化为:
package com.pf.java.tankbattle;
public class GameMain {
public static void main(String[] args) {
// 创建窗体对象
new MyFrame(new MyPanel());
}
}
绘图板绘制功能
首先来认识下Java Swing窗体绘制的坐标系:
从图中可以看出,坐标点的数值是向右下方变大的,而所在的绘制组件的左上角作为坐标原点(0, 0)
,要绘制的内容,比如图形或图片,是以左上角的坐标作为起点的,比如上图中绘制的矩形,起点为左上角坐标(20, 20)
,宽度和高度都是50(像素)。
代码实现如下:
package com.pf.java.tankbattle;
import ...
public class MyPanel extends JPanel {
public MyPanel() { ... }
@Override
protected void paintComponent(Graphics g) {
// 必须要调用父类方法来完场组件的基本绘制,比如构造器中设置的背景色等等
super.paintComponent(g);
System.out.println("paintComponent...");
// 绘制一个黄色的小方格子
// 先设置画笔的颜色
g.setColor(Color.yellow);
// 填充一个矩形
g.fillRect(20, 20, 50, 50);
}
}
说明
这里重写Swing组件的
paintComponent
方法来实现自定义的绘制功能,但需要注意,必须先调用super.paintComponent(g)
完成组件基本的外观的绘制。该方法提供了一个Graphics
类型的画笔对象,我们可以通过调用其setColor(color)
方法来“沾色”并调用其相关的api以完成绘制。关于该绘制方法何时被调用,咱们打印了一行信息,运行程序会发现,在窗体第一次显示以及窗口最小化后再次出现时都会被调用,而且会被调用多次,在必要时都会进行重绘。
接下来绘制咱们的主题元素——坦克。先看下位于资源包下的素材文件:
打开tank.png
,发现这张图特别长:
这里包含了90版红白机坦克大战中所有类型的坦克以及道具等素材,咱们从中扣取第一个位置的坦克,把它绘制到绘图板中。
首先定义一个用来加载静态图片的工具类:
package com.pf.java.tankbattle;
import ...
public class ResourceMgr {
/** 代表坦克图片素材的图片对象 */
public static BufferedImage tank;
static {
// 获取加载当前类的类加载器
ClassLoader cl = ResourceMgr.class.getClassLoader();
try {
// 类文件和静态资源编译后位于相同的类路径下,因此可被加载class的类加载器直接获取到
tank = ImageIO.read(cl.getResourceAsStream("images/tank.png"));
System.out.println("游戏素材加载完毕...");
} catch (IOException e) {
e.printStackTrace();
// 如果游戏素材加载失败,则程序异常退出
System.exit(1);
}
}
}
注意
ResourceMgr
类中的静态代码块会在第一次访问其静态成员变量时被执行。因此在程序启动后应该优先加载静态资源,再开始游戏,将加载后的图片对象绘制到游戏绘图板中。package com.pf.java.tankbattle; import ... public class GameMain { public static void main(String[] args) { // 先加载静态资源,这里仅为了访问静态成员变量以执行静态代码块完成资源加载,声明的变量没有地方使用 Image imageResource = ResourceMgr.tank; // 创建窗体对象 ... } }
是时候在绘图板上绘制我们的英雄——小黄坦克了
实现的代码如下:
package com.pf.java.tankbattle;
import ...
public class MyPanel extends JPanel {
public MyPanel() {
// 设置绘图板纯黑背景,让坦克显的更耀眼些
setBackground(Color.BLACK);
...
}
@Override
protected void paintComponent(Graphics g) {
// 必须要调用父类方法来完场组件的基本绘制,比如构造器中设置的背景色等等
super.paintComponent(g);
// 先从左上角开始扣取宽高都为32像素的区域,然后绘制到绘图板坐标为(20, 20)的位置
// drawImage最后一个观察者对象可设置为null,因为不涉及到图片转换操作,也无须通知
g.drawImage(ResourceMgr.tank.getSubimage(0, 0, 32, 32), 20, 20, null);
}
}
好了,这一小节主要带着小伙伴一起快速上手Java Swing应用程序,知道怎么开发一个不涉及按钮、工具栏以及复杂布局的简单的窗体应用,并能够在JPanel组件中绘制一些内容,这为我们接下来的学习做好了铺垫。下一小节,我们将学习如何定义坦克类并用多线程来操控玩家坦克,大家加油!