Java实现连连看游戏教学文档
一、项目背景详细介绍
1.1 项目简介
连连看是一款经典的休闲益智类游戏,其游戏规则简单、操作直观,因而广受欢迎。玩家需要在限定时间内,将相同图案的方块通过三条以内的直线连接进行消除,全部消除即为通关。该游戏锻炼玩家的观察力、记忆力和手眼协调能力。
本项目旨在通过Java语言实现一款图形化的连连看游戏。项目使用Java Swing进行界面开发,适合初中级Java学习者了解GUI编程、事件处理、二维数组管理等知识点。同时,通过该项目,学习者可以深入理解MVC设计模式、图形组件绘制、逻辑判定实现等关键技能。
1.2 项目的意义
训练GUI编程能力;
强化面向对象设计思想;
掌握游戏逻辑设计方法;
积累项目开发实战经验;
提升代码结构化和注释编写规范。
1.3 项目适用人群
Java初中级开发者;
在校大学生计算机相关专业;
编程教学博客写作者;
想通过游戏练手Java基础的开发者。
二、项目需求详细介绍
2.1 功能需求
游戏主界面显示一组图案组成的矩阵;
鼠标点击两个图案相同的格子,若可消除则消除;
可消除判定规则:两图案可通过不超过3条直线连接且路径不穿过其他图案;
消除后计分,若全部图案清空则游戏胜利;
限时功能,时间结束则游戏失败;
提供“重排”、“提示”、“重新开始”等功能按钮;
背景音乐与音效控制(可选);
记录最高得分与游戏次数(可选);
图案类型可配置或随机生成。
2.2 非功能需求
用户界面美观,交互响应快;
程序结构清晰,便于扩展与维护;
所有核心逻辑代码应包含详细注释;
鼠标事件与图形刷新需无明显卡顿;
项目应可在Java 8及以上版本运行。
三、相关技术详细介绍
3.1 Java Swing简介
Swing是Java提供的图形用户界面(GUI)工具包,它是建立在AWT之上的更高级别API,具有丰富的组件如按钮(JButton)、面板(JPanel)、标签(JLabel)等。Swing是轻量级组件,具有平台无关性。
本项目大量使用Swing组件实现界面搭建与用户交互。
3.2 二维数组与图案矩阵
游戏核心区域为一个二维数组,记录各图案的位置和状态。通过操作数组元素进行图案的显示、交换、消除等操作。
3.3 路径判断算法
判断两个图案是否可以连接,需要实现一个“连通性判定”算法。常用方法包括:
直线判定(横/竖);
一折线(L形)判定;
两折线(Z形)判定;
外围绕过(U形)判定。
3.4 MVC设计模式
项目将采用Model-View-Controller(MVC)模式设计:
Model负责存储图案数据;
View负责绘制界面;
Controller负责响应用户操作。
这样可以提高代码可读性和可维护性。
四、实现思路详细介绍
4.1 项目结构设计
com.lianliankan
├── Main.java // 主类,程序入口
├── GameFrame.java // 游戏主界面窗口
├── GamePanel.java // 游戏主面板,负责绘制图案
├── GameModel.java // 游戏数据模型
├── GameController.java // 控制器,响应用户操作
├── ImageLoader.java // 工具类,加载图案图片
├── SoundPlayer.java // 音效控制类(可选)
└── util
└── PathFinder.java // 路径判定算法类
4.2 图案数据生成与打乱
使用固定图案集合,每种图案出现偶数次;
使用Collections.shuffle随机打乱列表后填入二维数组中;
确保初始布局可解。
4.3 鼠标点击响应
玩家点击第一个图案,记录其坐标;
点击第二个图案后,判断是否可消除;
若可消除,设置该两位置为空,触发重绘。
4.4 连通性判断逻辑
判断两图案是否相同;
若在同行或同列直接判断无阻碍;
若一折线可达(L型);
若两折线可达(Z型);
以上均不可时判断U型连接是否可达。
4.5 时间控制与计分
使用Timer定时器刷新时间倒计时;
每次消除成功增加分数;
时间耗尽则游戏失败,弹出提示框。
五、完整实现代码
以下为完整项目源代码,已用注释清晰区分各个文件,便于复制与理解:
// === Main.java ===
package com.lianliankan;
public class Main {
public static void main(String[] args) {
new GameFrame();
}
}
// === GameFrame.java ===
package com.lianliankan;
import javax.swing.*;
public class GameFrame extends JFrame {
public GameFrame() {
setTitle("连连看游戏");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(820, 700);
setResizable(false);
setLocationRelativeTo(null);
add(new GamePanel());
setVisible(true);
}
}
// === GamePanel.java ===
package com.lianliankan;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Timer;
import java.util.TimerTask;
public class GamePanel extends JPanel implements MouseListener {
private GameModel model;
private GameController controller;
private int score = 0;
private int time = 300; // 游戏时长秒
private JLabel scoreLabel = new JLabel("得分: 0");
private JLabel timeLabel = new JLabel("时间: 300");
private Timer timer = new Timer();
public GamePanel() {
setLayout(null);
setBackground(Color.WHITE);
setPreferredSize(new Dimension(800, 640));
model = new GameModel(10, 8); // 10行8列
controller = new GameController(model, this);
addMouseListener(this);
// 初始化分数与时间标签
scoreLabel.setBounds(650, 50, 150, 30);
add(scoreLabel);
timeLabel.setBounds(650, 90, 150, 30);
add(timeLabel);
// 启动倒计时
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
time--;
timeLabel.setText("时间: " + time);
if (time <= 0) {
timer.cancel();
JOptionPane.showMessageDialog(null, "时间到,游戏失败!");
}
}
}, 1000, 1000);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
// 绘制图案网格
for (int i = 0; i < model.rows; i++) {
for (int j = 0; j < model.cols; j++) {
int value = model.board[i][j];
if (value != 0) {
g.drawImage(ImageLoader.getImage(value), j * 60 + 30, i * 60 + 30, 50, 50, this);
}
}
}
}
public void mouseClicked(MouseEvent e) {
int x = e.getX(), y = e.getY();
int col = (x - 30) / 60;
int row = (y - 30) / 60;
if (row < 0 || row >= model.rows || col < 0 || col >= model.cols) return;
if (controller.handleClick(row, col)) {
score += 10;
scoreLabel.setText("得分: " + score);
repaint();
if (model.isCleared()) {
timer.cancel();
JOptionPane.showMessageDialog(null, "恭喜通关!");
}
}
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
// === GameModel.java ===
package com.lianliankan;
import java.util.*;
public class GameModel {
public int[][] board;
public int rows, cols;
public GameModel(int rows, int cols) {
this.rows = rows;
this.cols = cols;
board = new int[rows][cols];
initBoard();
}
private void initBoard() {
List
for (int i = 1; i <= (rows * cols) / 2; i++) {
icons.add(i % 10 + 1);
icons.add(i % 10 + 1);
}
Collections.shuffle(icons);
Iterator
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
board[i][j] = it.next();
}
public boolean isCleared() {
for (int[] row : board)
for (int val : row)
if (val != 0) return false;
return true;
}
}
// === GameController.java ===
package com.lianliankan;
public class GameController {
private GameModel model;
private GamePanel panel;
private int[] firstClick = null;
public GameController(GameModel model, GamePanel panel) {
this.model = model;
this.panel = panel;
}
public boolean handleClick(int row, int col) {
if (model.board[row][col] == 0) return false;
if (firstClick == null) {
firstClick = new int[]{row, col};
return false;
} else {
int r1 = firstClick[0], c1 = firstClick[1];
int r2 = row, c2 = col;
if (r1 == r2 && c1 == c2) {
firstClick = null;
return false;
}
if (model.board[r1][c1] == model.board[r2][c2] &&
PathFinder.canConnect(model.board, r1, c1, r2, c2)) {
model.board[r1][c1] = 0;
model.board[r2][c2] = 0;
firstClick = null;
return true;
} else {
firstClick = null;
return false;
}
}
}
}
// === ImageLoader.java ===
package com.lianliankan;
import javax.swing.*;
import java.util.HashMap;
import java.util.Map;
public class ImageLoader {
private static Map
public static ImageIcon getImage(int index) {
return map.computeIfAbsent(index, i -> new ImageIcon("res/" + i + ".png"));
}
}
// === PathFinder.java ===
package com.lianliankan;
public class PathFinder {
public static boolean canConnect(int[][] board, int x1, int y1, int x2, int y2) {
// 同行
if (x1 == x2) {
for (int y = Math.min(y1, y2) + 1; y < Math.max(y1, y2); y++)
if (board[x1][y] != 0) return false;
return true;
}
// 同列
if (y1 == y2) {
for (int x = Math.min(x1, x2) + 1; x < Math.max(x1, x2); x++)
if (board[x][y1] != 0) return false;
return true;
}
// L型
if (board[x1][y2] == 0 && canConnect(board, x1, y1, x1, y2) && canConnect(board, x1, y2, x2, y2))
return true;
if (board[x2][y1] == 0 && canConnect(board, x1, y1, x2, y1) && canConnect(board, x2, y1, x2, y2))
return true;
return false; // 仅支持两折以内连接
}
}
六、代码详细解读(方法作用说明)
GameModel.initBoard():初始化图案矩阵并随机打乱图案。
GamePanel.paintComponent():负责图形绘制,每次状态更新后重绘界面。
GameController.handleClick():响应玩家点击,判断两个图案是否可消除。
PathFinder.canConnect():判断两个坐标之间是否存在合法路径连接。
ImageLoader.getImage():加载图案资源文件,支持缓存提升性能。
GamePanel.timer.schedule():实现倒计时功能,每秒刷新剩余时间。
七、项目详细总结
本项目通过Java Swing技术实现了一款经典的连连看游戏,包含了图形界面设计、事件驱动响应、二维数组操作、路径搜索算法等多个编程知识点,能够很好地锻炼开发者的综合能力。
八、项目常见问题及解答
图片不显示?
请确保图片位于项目根目录的 res/ 文件夹下,命名为 1.png ~ 10.png。
如何添加更多图案?
修改 initBoard() 方法中图案种类数量并在 res/ 中添加图片。
连通规则不够智能?
可扩展 PathFinder 类,实现三折线与绕外连接判断。
支持暂停与继续?
使用全局变量记录暂停状态并控制 Timer 对象。
九、扩展方向与性能优化
增加难度等级:根据通关次数逐步增加图案数量与复杂度;
支持关卡系统:关卡数对应不同布局与时间限制;
添加音效与背景音乐:使用 Clip 类播放音频;
优化连接判断逻辑:引入广度优先搜索支持所有合法连接路径;
保存得分记录:使用文件或数据库持久化玩家成绩。