通过这个游戏能学到什么?
GUI 应用程序开发
事件处理
数据结构和算法
计时器的应用
图形界面设计与美化
简单介绍
1. 游戏窗口和界面设计
首先,我们创建一个继承自JFrame的游戏窗口类PictureFrame
。这个类负责显示游戏界面和处理游戏逻辑。在界面设计中,我们使用了JPanel来布置拼图块和按钮,同时使用了JLabel来显示游戏标题和背景图像。
2. 游戏数据和逻辑
游戏的拼图状态由一个二维数组dataS
表示,其中0代表空格。我们定义了一个与之相对应的胜利状态数组winDatas
,用于判断游戏是否胜利。游戏的核心逻辑包括移动拼图块、检查游戏状态是否胜利以及打乱拼图等功能。
3. 用户交互和事件处理
通过给按钮和窗口添加事件监听器来处理用户交互。用户可以通过按钮点击或键盘按键来移动拼图块。此外,还实现了一些其他功能,如显示提示信息、重置游戏等。
4. 计时器和界面更新
为了增加游戏的趣味性,我们添加了一个计时器来记录游戏时间。每秒钟计时器会更新一次,并在窗口标题栏显示当前游戏所用时间。同时,每次拼图块移动后,界面会重新绘制以反映最新的游戏状态。
关键代码
1.计时器
private Timer timer; // 计时器对象
private int seconds; // 计时器秒数
/**
* 启动计时器,并更新计时器的秒数
*/
private void startTime() {
// 创建计时器,每隔一秒触发一次 actionPerformed 方法
timer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
seconds++; // 每次触发计时器,秒数加一
setTitle("动漫拼图 - 所用时间: " + seconds + "秒"); // 更新窗口标题,显示当前游戏进行的时间
}
});
seconds = 0; // 初始化秒数为0
timer.start(); // 启动计时器
}
首先声明了一个 Timer
对象 timer
和一个整型变量 seconds
来记录秒数。然后,定义了一个 startTime
方法用于启动计时器。在 startTime
方法中:
-
创建了一个
Timer
对象timer
,指定了计时器的触发间隔为1000毫秒(即1秒),并传入一个ActionListener
匿名类作为参数。这个匿名类实现了ActionListener
接口,重写了actionPerformed
方法,在每次计时器触发时执行相应的操作。 -
在
actionPerformed
方法中,每次计时器触发时,秒数seconds
自增1,并调用setTitle
方法更新窗口标题,显示当前游戏进行的时间。 -
最后,初始化秒数为0,并调用
timer.start()
启动计时器。
2.二元数组
/**
* 随机打乱拼图的顺序
*/
private void randomArry() {
// 创建一个 Random 对象,用于生成随机数
Random r = new Random();
// 使用双重循环遍历二维数组
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS.length; j++) {
// 生成两个随机数,分别表示要交换的另一个元素的行索引和列索引
int x = r.nextInt(dataS.length);
int y = r.nextInt(dataS[x].length);
// 交换当前位置和随机位置的值
int tmp = dataS[i][j];
dataS[i][j] = dataS[x][y];
dataS[x][y] = tmp;
}
}
// 使用标签在循环中寻找空格(值为0)的位置
wc:
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS.length; j++) {
if (dataS[i][j] == 0) {
x = i; // 记录空格的行索引
y = j; // 记录空格的列索引
break wc; // 跳出循环
}
}
}
}
-
创建了一个
Random
对象r
,用于生成随机数。 -
使用两层嵌套的
for
循环遍历二维数组dataS
,在每个位置上进行数值交换。在内部循环中,首先生成两个随机数x
和y
,分别代表要交换的另一个元素的行索引和列索引。 -
交换数组中当前位置和随机位置的值。通过临时变量
tmp
,将当前位置的值暂存起来,然后将当前位置的值更新为随机位置的值,最后将随机位置的值更新为暂存的值,完成交换操作。 -
使用标签
wc
在嵌套循环中实现了一个带标签的break
语句,用于在找到空格(值为0)时跳出循环,以获取空格的位置
3. 随机数
/**
* 随机打乱拼图的顺序
*/
private void randomArry() {
// 创建一个 Random 对象,用于生成随机数
Random r = new Random();
// 使用双重循环遍历二维数组
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS.length; j++) {
// 生成两个随机数,分别表示要交换的另一个元素的行索引和列索引
int x = r.nextInt(dataS.length);
int y = r.nextInt(dataS[x].length);
// 交换当前位置和随机位置的值
int tmp = dataS[i][j];
dataS[i][j] = dataS[x][y];
dataS[x][y] = tmp;
}
}
// 使用标签在循环中寻找空格(值为0)的位置
wc:
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS.length; j++) {
if (dataS[i][j] == 0) {
x = i; // 记录空格的行索引
y = j; // 记录空格的列索引
break wc; // 跳出循环
}
}
}
}
- 创建
Random
对象r
:通过Random
类的构造函数创建一个随机数生成器对象。 - 使用双重循环遍历二维数组
dataS
:外层循环控制行,内层循环控制列,以访问二维数组的每个元素。 - 在内部循环中,使用
nextInt(int n)
方法生成两个随机数x
和y
:nextInt(int n)
方法返回一个在[0, n)
范围内的随机整数。 - 使用生成的随机数
x
和y
,交换当前位置(i, j)
和随机位置(x, y)
上的元素值:这样就实现了数组元素的随机打乱。 - 最后,在循环中寻找空格(值为0)的位置,并记录其行索引和列索引,以便后续游戏操作。
4.图片交换逻辑
/**
* 向上移动空格
*/
private void moveUp() {
// 如果空格在最顶部,则不执行移动操作
if (x == 0) {
return;
}
// 如果空格不在最顶部,则执行向上移动操作
if (x > 0) {
// 交换空格和上方相邻方块的值
dataS[x][y] = dataS[x - 1][y];
dataS[x - 1][y] = 0;
// 更新空格位置
x--;
}
// 检查游戏是否成功
if (isSuccess()) {
success();
}
// 重新绘制游戏界面
rePaintView();
}
/**
* 向下移动空格
*/
private void moveDown() {
// 如果空格在最底部,则不执行移动操作
if (x == 3) {
return;
}
// 如果空格不在最底部,则执行向下移动操作
if (x < 3) {
// 交换空格和下方相邻方块的值
dataS[x][y] = dataS[x + 1][y];
dataS[x + 1][y] = 0;
// 更新空格位置
x++;
}
// 检查游戏是否成功
if (isSuccess()) {
success();
}
// 重新绘制游戏界面
rePaintView();
}
/**
* 向左移动空格
*/
private void moveLeft() {
// 如果空格在最左侧,则不执行移动操作
if (y == 0) {
return;
}
// 如果空格不在最左侧,则执行向左移动操作
if (y > 0) {
// 交换空格和左侧相邻方块的值
dataS[x][y] = dataS[x][y - 1];
dataS[x][y - 1] = 0;
// 更新空格位置
y--;
}
// 检查游戏是否成功
if (isSuccess()) {
success();
}
// 重新绘制游戏界面
rePaintView();
}
/**
* 向右移动空格
*/
private void moveRight() {
// 如果空格在最右侧,则不执行移动操作
if (y == 3) {
return;
}
// 如果空格不在最右侧,则执行向右移动操作
if (y < 3) {
// 交换空格和右侧相邻方块的值
dataS[x][y] = dataS[x][y + 1];
dataS[x][y + 1] = 0;
// 更新空格位置
y++;
}
// 检查游戏是否成功
if (isSuccess()) {
success();
}
// 重新绘制游戏界面
rePaintView();
}
-
moveUp() 方法: 该方法用于将空格向上移动。首先检查空格是否在最顶部,如果不在顶部,则将空格与上方相邻方块的值进行交换,然后更新空格的位置。如果移动后游戏状态符合胜利条件,则调用
success()
方法将游戏状态设置为胜利状态。最后,调用rePaintView()
方法重新绘制游戏界面。 -
moveDown() 方法: 该方法用于将空格向下移动。首先检查空格是否在最底部,如果不在底部,则将空格与下方相邻方块的值进行交换,然后更新空格的位置。如果移动后游戏状态符合胜利条件,则调用
success()
方法将游戏状态设置为胜利状态。最后,调用rePaintView()
方法重新绘制游戏界面。 -
moveLeft() 方法: 该方法用于将空格向左移动。首先检查空格是否在最左侧,如果不在左侧,则将空格与左侧相邻方块的值进行交换,然后更新空格的位置。如果移动后游戏状态符合胜利条件,则调用
success()
方法将游戏状态设置为胜利状态。最后,调用rePaintView()
方法重新绘制游戏界面。 -
moveRight() 方法: 该方法用于将空格向右移动。首先检查空格是否在最右侧,如果不在右侧,则将空格与右侧相邻方块的值进行交换,然后更新空格的位置。如果移动后游戏状态符合胜利条件,则调用
success()
方法将游戏状态设置为胜利状态。最后,调用rePaintView()
方法重新绘制游戏界面。
完整代码
(1)主文件:PictureFrame
package com.game1;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyAdapter;
import java.util.Random;
/**
* 游戏窗口类,用于显示游戏界面和处理游戏逻辑
*/
public class PictureFrame extends JFrame {
// 游戏数据数组,用于存储拼图的状态
private int[][] dataS = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 用于比较的胜利状态数组
private int[][] winDatas = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// 空格所在的行和列
private int x;
private int y;
// 按钮和面板组件
private JButton top;
private JButton bottom;
private JButton left;
private JButton right;
private JButton help;
private JButton clear;
private JPanel jp;
// 计时器和秒数
private Timer timer;
private int seconds;
// 时间标签
private JLabel timeLabel;
/**
* 构造函数,初始化游戏界面和相关组件
*/
public PictureFrame() {
initFrame();
randomArry();
paintView();
addButtonEvent();
addKeyboardListener();
// 启动计时器
startTime();
this.setVisible(true);
this.setFocusable(true);
}
/**
* 游戏成功时的操作,将游戏状态设置为胜利状态
*/
private void success() {
dataS = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
// 禁用移动按钮
top.setEnabled(false);
right.setEnabled(false);
bottom.setEnabled(false);
left.setEnabled(false);
}
/**
* 检查当前游戏状态是否为胜利状态
*
* @return 是否胜利
*/
public boolean isSuccess() {
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS[i].length; j++) {
if (dataS[i][j] != winDatas[i][j]) {
return false;
}
}
}
return true;
}
/**
* 重新绘制游戏界面
*/
private void rePaintView() {
jp.removeAll();
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS[i].length; j++) {
JLabel imageLabel = new JLabel(new ImageIcon("day09\\images\\" + dataS[i][j] + ".png"));
imageLabel.setBounds(j * 90, i * 90, 90, 90);
jp.add(imageLabel);
jp.repaint();
}
}
}
/**
* 给按钮添加事件监听器
*/
private void addButtonEvent() {
top.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveUp();
requestFocusInWindow();
}
});
bottom.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveDown();
requestFocusInWindow();
}
});
left.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveLeft();
requestFocusInWindow();
}
});
right.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveRight();
requestFocusInWindow();
}
});
help.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(PictureFrame.this, "联系:Dlop", "提示", JOptionPane.INFORMATION_MESSAGE);
}
});
clear.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
randomArry();
rePaintView();
}
});
}
/**
* 添加键盘事件监听器
*/
private void addKeyboardListener() {
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
switch (keyCode) {
case KeyEvent.VK_UP:
moveUp();
break;
case KeyEvent.VK_DOWN:
moveDown();
break;
case KeyEvent.VK_LEFT:
moveLeft();
break;
case KeyEvent.VK_RIGHT:
moveRight();
break;
}
}
});
}
/**
* 向上移动空格
*/
private void moveUp() {
if (x == 0) {
return;
}
if (x > 0) {
dataS[x][y] = dataS[x - 1][y];
dataS[x - 1][y] = 0;
x--;
}
if (isSuccess()) {
success();
}
rePaintView();
}
/**
* 向下移动空格
*/
private void moveDown() {
if (x == 3) {
return;
}
if (x < 3) {
dataS[x][y] = dataS[x + 1][y];
dataS[x + 1][y] = 0;
x++;
}
if (isSuccess()) {
success();
}
rePaintView();
}
/**
* 向左移动空格
*/
private void moveLeft() {
if (y == 0) {
return;
}
if (y > 0) {
dataS[x][y] = dataS[x][y - 1];
dataS[x][y - 1] = 0;
y--;
}
if (isSuccess()) {
success();
}
rePaintView();
}
/**
* 向右移动空格
*/
private void moveRight() {
if (y == 3) {
return;
}
if (y < 3) {
dataS[x][y] = dataS[x][y + 1];
dataS[x][y + 1] = 0;
y++;
}
if (isSuccess()) {
success();
}
rePaintView();
}
/**
* 随机打乱拼图
*/
private void randomArry() {
Random r = new Random();
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS.length; j++) {
int x = r.nextInt(dataS.length);
int y = r.nextInt(dataS[x].length);
int tmp = dataS[i][j];
dataS[i][j] = dataS[x][y];
dataS[x][y] = tmp;
}
}
wc:
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS.length; j++) {
if (dataS[i][j] == 0) {
x = i;
y = j;
break wc;
}
}
}
}
/**
* 绘制游戏界面
*/
private void paintView() {
JLabel title = new JLabel(new ImageIcon("day09\\images\\title.png"));
title.setBounds(354, 27, 232, 57);
this.add(title);
jp = new JPanel();
jp.setBounds(150, 114, 360, 360);
jp.setLayout(null);
for (int i = 0; i < dataS.length; i++) {
for (int j = 0; j < dataS[i].length; j++) {
JLabel imageLabel = new JLabel(new ImageIcon("day09\\images\\" + dataS[i][j] + ".png"));
imageLabel.setBounds(90 * j, 90 * i, 90, 90);
jp.add(imageLabel);
}
}
this.add(jp);
JLabel canZhao = new JLabel(new ImageIcon("day09\\images\\canzhaotu.png"));
canZhao.setBounds(574, 114, 122, 121);
this.add(canZhao);
top = new JButton(new ImageIcon("day09\\images\\shang.png"));
bottom = new JButton(new ImageIcon("day09\\images\\xia.png"));
left = new JButton(new ImageIcon("day09\\images\\zuo.png"));
right = new JButton(new ImageIcon("day09\\images\\you.png"));
help = new JButton(new ImageIcon("day09\\images\\qiuzhu.png"));
clear = new JButton(new ImageIcon("day09\\images\\chongzhi.png"));
top.setBounds(732, 265, 57, 57);
bottom.setBounds(732, 347, 57, 57);
left.setBounds(650, 347, 57, 57);
right.setBounds(813, 347, 57, 57);
help.setBounds(626, 444, 108, 45);
clear.setBounds(786, 444, 108, 45);
this.add(top);
this.add(bottom);
this.add(left);
this.add(right);
this.add(help);
this.add(clear);
JLabel bgcImage = new JLabel(new ImageIcon("day09\\images\\background.png"));
bgcImage.setBounds(0, 0, 960, 530);
this.add(bgcImage);
}
/**
* 初始化窗口
*/
private void initFrame() {
this.setSize(960, 565);
this.setTitle("动漫拼图");
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(null);
}
/**
* 启动计时器,并更新计时器的秒数
*/
private void startTime() {
timer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
seconds++;
setTitle("动漫拼图 - 所用时间: " + seconds + "秒");
}
});
seconds = 0;
timer.start();
}
}
(2)程序入口:App
package com.game1;
public class App {
public static void main(String[] args) {
PictureFrame jf = new PictureFrame();
}
}