1. 目标
一分钟在窗口显示内容
2. 面板
JFrame类似一个窗口,但是不具备绘制功能。
我们想在空白的窗口上显示内容,比如背景图、游戏对象、UI控件等,则需要用到javax.swing中的面板类JPanel。
JFrame窗口和JPanel面板的关系就类似画板和画布的关系,我们只能在画布上绘图。
3. 面板分层
我们解了面板是绘图的容器,但游戏画面中包含许多的元素,比如背景、游戏对象、UI控件等,我们把这些元素都绘制在一个面板中是否合适呢?
答案是否定的,如果只用一个面板绘制这些元素,会带来一系列问题:
-
逻辑混乱:背景绘制、角色碰撞、按钮点击事件都会混杂在JPanel的paintComponent() 方法和各类监听器中,随着游戏复杂度提升,代码会显得越来越臃肿;
-
渲染效率低下:例如,在游戏中角色每帧移动,即使背景和UI完全不变,面板也会重新绘制所有元素,造成大量不必要的重复计算;
-
元素显示冲突:需要严格控制绘制顺序,如果绘制顺序出错,先绘制UI,再绘制角色,再绘制背景,导致UI被角色遮挡或游戏对象被背景覆盖。
为了解决以上问题,我们将游戏面板进行分层显示,至少包含三个层级面板(背景层、游戏层、UI层),这些层级面板之间的绘制和事件监听各自为营、互不干扰:
4. 代码实现
4.1 背景层面板BgPanel
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/**
* 背景面板类,继承自JPanel
* 专门用于显示游戏或应用程序的背景图片,支持背景图自适应面板大小
*/
public class BgPanel extends JPanel {
// 用于存储背景图片的缓冲图像对象
// BufferedImage支持更灵活的图像操作,适合需要频繁绘制的场景
private BufferedImage bgImage;
/**
* 构造方法:初始化背景面板
*/
public BgPanel() {
// 设置布局管理器为BorderLayout
// BorderLayout会让添加的组件自动填充整个面板,适合作为背景面板
setLayout(new BorderLayout());
// 加载背景图片
try {
// 从指定路径读取图片文件
// 这里的路径是相对路径,表示项目根目录下的assets/images/bg.png
bgImage = ImageIO.read(new File("assets/images/bg.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 重写面板的绘图方法
* 该方法会在面板需要刷新时自动调用(如窗口大小改变、调用repaint()时)
* @param g 绘图上下文对象,用于执行绘图操作
*/
@Override
protected void paintComponent(Graphics g) {
// 调用父类的paintComponent方法,确保面板正常刷新(如清除原有绘制内容)
super.paintComponent(g);
// 绘制背景图片
// bgImage:要绘制的图片对象
// 0, 0:图片在面板中的起始坐标(左上角)
// getWidth(), getHeight():图片要拉伸到的宽度和高度(即面板的大小)
// this:当前面板作为图像观察者,监听图片加载状态
if (bgImage != null) { // 确保图片加载成功后再绘制,避免空指针异常
g.drawImage(bgImage, 0, 0, getWidth(), getHeight(), this);
}
}
}
4.2 游戏层面板GamePanel
import javax.swing.*;
import java.awt.*;
/**
* 游戏面板类,继承自JPanel
* 用于绘制和管理游戏中的动态元素(如角色、道具等)
* 作为中间层面板,通常叠加在背景面板之上
*/
public class GamePanel extends JPanel {
/**
* 构造方法:初始化游戏面板
*/
public GamePanel() {
// 设置布局管理器为BorderLayout
// 便于后续添加子组件时按方位(东、南、西、北、中)布局
setLayout(new BorderLayout());
// 设置面板为透明
// 关键作用:避免遮挡下层面板(如背景面板),确保背景可见
setOpaque(false);
}
/**
* 重写绘图方法,自定义面板内容绘制
* 该方法会在面板刷新时自动调用,用于绘制游戏元素
*
* @param g 绘图上下文对象,提供绘图相关的方法
*/
@Override
protected void paintComponent(Graphics g) {
// 调用父类的paintComponent方法
// 作用:1. 清除面板原有内容,避免画面残留;2. 执行面板默认的绘制逻辑
super.paintComponent(g);
// 设置当前绘图颜色为深灰色
g.setColor(Color.DARK_GRAY);
// 在面板左上角绘制一个60x60像素的矩形(表示游戏角色对象)
// 参数说明:x=100, y=100(距离左上角坐标的相对位置);width=60, height=60(矩形宽高)
g.fillRect(100, 100, 60, 60);
}
}
4.3 UI层面板UIPanel
import javax.swing.*;
import java.awt.*;
/**
* UI面板类,继承自JPanel
* 用于展示游戏中的用户界面元素(如分数显示、控制按钮等)
* 作为顶层面板,通常叠加在游戏面板之上,提供用户交互功能
*/
public class UIPanel extends JPanel {
// 分数显示标签,用于实时展示游戏分数
private JLabel scoreLabel;
// 暂停按钮,用于控制游戏暂停/继续
private JButton pauseBtn;
/**
* 构造方法:初始化UI面板及其中的控件
*/
public UIPanel() {
// 设置布局管理器为FlowLayout,左对齐
// 参数说明:FlowLayout.LEFT(左对齐)、水平间距20px、垂直间距10px
// 适合排列按钮、标签等小型UI元素
setLayout(new FlowLayout(FlowLayout.LEFT, 20, 10));
// 设置面板为透明
// 确保不会遮挡下层的游戏面板和背景面板
setOpaque(false);
// 初始化分数标签
scoreLabel = new JLabel("Score: 0");
// 设置字体:Arial字体、粗体、20号大小
scoreLabel.setFont(new Font("Arial", Font.BOLD, 20));
// 设置文字颜色为白色(适合深色背景)
scoreLabel.setForeground(Color.WHITE);
// 将标签添加到面板中
add(scoreLabel);
// 初始化暂停按钮
pauseBtn = new JButton("Pause");
// 为按钮添加点击事件监听器
pauseBtn.addActionListener(e -> {
// 点击按钮时输出日志(实际开发中可替换为暂停游戏逻辑)
System.out.println("Pause button clicked");
});
// 将按钮添加到面板中
add(pauseBtn);
}
}
4.4 GameWindow构造调整
public GameWindow(String title, int width, int height) {
// 保存窗口标题、宽度和高度到成员变量
this.title = title;
this.width = width;
this.height = height;
// 初始化主窗口JFrame
frame = new JFrame(title);
// 设置窗口关闭操作:关闭窗口时退出程序
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 创建层级面板JLayeredPane,用于管理不同层级的面板
// 层级面板允许组件按层级叠加显示,解决元素遮挡问题
JLayeredPane layeredPane = new JLayeredPane();
// 设置层级面板的首选大小为窗口大小
layeredPane.setPreferredSize(new Dimension(width, height));
// 1. 添加背景层(最底层)
BgPanel bgPanel = new BgPanel();
// 设置背景面板大小与窗口一致,位置从左上角(0,0)开始
bgPanel.setBounds(0, 0, width, height);
// 将背景面板添加到层级面板的默认层(最低层级)
layeredPane.add(bgPanel, JLayeredPane.DEFAULT_LAYER);
// 2. 添加游戏元素层(中间层)
GamePanel gamePanel = new GamePanel();
// 设置游戏面板大小与窗口一致,覆盖在背景层之上
gamePanel.setBounds(0, 0, width, height);
// 将游戏面板添加到调色板层(层级高于默认层)
layeredPane.add(gamePanel, JLayeredPane.PALETTE_LAYER);
// 3. 添加UI层(最顶层)
UIPanel uiPanel = new UIPanel();
// 设置UI面板大小与窗口一致,覆盖在所有层之上
uiPanel.setBounds(0, 0, width, height);
// 将UI面板添加到模态层(层级高于调色板层,确保UI控件优先显示)
layeredPane.add(uiPanel, JLayeredPane.MODAL_LAYER);
// 将层级面板添加到主窗口
frame.add(layeredPane);
// 调整窗口大小以适应层级面板的首选大小
frame.pack();
// 设置窗口在屏幕中居中显示
frame.setLocationRelativeTo(null);
}
5. 代码说明
以上我们创建了三个面板类BgPanel、GamePanel、UIPanel,然后由底到顶分别置于不同的层级(辅助类为JLayeredPane,用于管理不用层级的面板)。
另外,我们移除了frame.setSize()方法,使用JLayeredPane的setPreferredSize()方法,最后调用frame.pack()方法调整窗口以适应子元素的大小。
如果你尚不清楚面板的布局,可以温习一下swing的基本知识,但不必过多深入,掌握常用组件的使用即可。
为什么不能继续使用frame.setSize()?
因为setSize方法设置的是整个窗口(包括标题栏、边框)的大小,减去这部分后,内部绘制区域将小于我们预期的800 x 600,这将导致游戏元素显示不完全。
重新运行程序,呈现效果如下:
如果你做到了这一步,那么恭喜你,你已经很🐂啦!
源码链接:GitCode - 全球开发者的开源社区,开源代码托管平台
6. 思考:如何让游戏对象移动起来?
目前,我们已经成功在窗口上显示内容了,但这些内容都是静态的,依然无趣,下一节我们让游戏对象动起来试试。