java实现连连看游戏(附带源码)

2025-09-26 17:56:15

Java 实现“连连看”游戏 —— 全面项目解析与实现详解

一、引言

“连连看”游戏作为一种经典的休闲益智游戏,曾风靡一时。游戏规则简单——在一个二维网格中,玩家需要找出所有相同图案的方块,并在保证两者之间能用不超过三个拐角的直线路径连通的前提下将它们消除。游戏不仅考验玩家的观察力和逻辑思维,同时也带来轻松愉快的游戏体验。

本项目旨在利用 Java 技术实现一款简易的连连看游戏,通过面向对象编程、GUI 编程以及算法设计等多方面知识的综合运用,帮助开发者从实际项目中体会设计思路与工程实现过程。本文将详细介绍项目的背景、核心需求、系统设计、关键算法、详细代码实现以及代码解读,并对项目的不足与未来改进方向做出展望。

二、项目背景与意义

2.1 项目背景

随着休闲游戏和益智小游戏在大众中广泛流行,连连看作为其中的经典代表,凭借简单明了的规则和无限的可能组合,成为众多程序员在学习 GUI 开发与算法设计时的热门项目。通过实现连连看游戏,不仅能深入学习 Java Swing 编程,还能锻炼二维数组、递归与路径搜索算法等方面的能力。

此外,连连看游戏的开发涉及到界面布局、用户交互、随机数生成、算法实现等多项内容,能帮助开发者了解项目的整体构建流程和模块化设计思想,从而为后续开发更复杂的游戏打下坚实基础。

2.2 项目意义

综合能力提升:

项目涵盖 Java 基础、面向对象设计、Swing 图形界面、事件监听以及路径搜索算法等多个知识点,通过完整项目实现,能全面提升开发者的编程综合能力。

工程实践与代码规范:

采用模块化、面向对象和分层设计思想,实现了游戏数据模型、界面显示、事件处理和逻辑算法的分离,培养工程化思维和良好的代码规范习惯。

趣味性与挑战性并存:

连连看游戏不仅简单有趣,而且需要设计一种算法来判断两个图案是否可以用不超过三个拐角的路径连通,该算法的实现对逻辑思维和算法设计具有较高的挑战性。

后续扩展性:

项目整体设计采用分层结构,易于扩展。例如可进一步增加关卡模式、时间限制、计分系统以及更多图形化特效,使项目具有更高的娱乐性和用户体验。

三、相关技术知识概述

在本项目中,主要会用到以下技术和知识点:

3.1 Java 基础

基本语法与数据结构:

变量、数组、集合、循环与条件语句等是编写程序的基础。游戏数据主要采用二维数组存储,每个元素代表一个图案(或称为“方块”)。

面向对象编程:

通过类和对象封装游戏中的各个模块,例如“Board”(棋盘)、“Tile”(方块)、“GameEngine”(游戏引擎)等,实现代码高内聚低耦合。

3.2 GUI 编程 —— Swing

Swing 组件:

利用 JFrame、JPanel、JButton 等组件搭建游戏界面。通过 GridLayout 实现棋盘的网格布局,利用 JButton 显示每个方块,并支持点击事件处理。

事件监听:

为每个按钮注册 ActionListener,捕捉用户点击事件,并根据用户的选择更新游戏状态,判断是否消除方块。

3.3 算法设计 —— 路径搜索

“连连看”游戏的核心在于判断两个相同图案是否可以连通。判断条件通常为:

两者之间能通过直线、一个拐角或两个拐角的路径连通路径上不能有其他障碍物

为此,常见的做法是将棋盘外围扩充一圈空白区域,然后设计以下三个判断方法:

直线连通检测(CheckDirect): 检查两点是否在同一行或同一列,中间是否全为空。一次转弯检测(CheckOneTurn): 寻找一个中间拐点,使得两段直线均连通。两次转弯检测(CheckTwoTurn): 枚举可能的两个转折点,判断是否存在满足条件的连通路径。

这些方法构成了判断两个方块是否可以消除的核心算法。

3.4 随机数生成与数据初始化

随机分布:

游戏开始时,需要将方块(图案)成对随机分布在棋盘中,保证每种图案均有偶数个,且随机排列。

数组与洗牌算法:

采用二维数组保存棋盘数据,然后利用洗牌算法随机打乱数组元素,保证游戏初始状态随机且公平。

四、系统需求分析与架构设计

4.1 系统需求分析

4.1.1 基本功能需求

游戏启动与退出:

玩家启动游戏后进入主界面,并可通过菜单或按钮选择退出游戏。

棋盘初始化:

游戏开始时,系统根据设定的行数和列数构建棋盘,将图案以成对随机分布的方式放入每个位置。

方块选择与消除:

玩家通过鼠标点击选择两个方块。若两者图案相同并且满足连通规则,则这对方块消除(在界面上隐藏),否则取消选择并给出提示。

连通性判断:

系统内置判断两个方块是否可连通的算法,支持直线、一转和二转判断,确保游戏规则正确执行。

胜利条件检测:

当所有方块全部消除后,系统判定玩家获胜,并给予提示。

4.1.2 扩展功能需求

计分与计时系统:

可扩展设计计分板和计时器,根据消除速度和连消次数进行评分,增加游戏竞技性。

提示功能:

当玩家长时间未能找到连通对时,系统可提供提示,帮助玩家寻找可消除的方块对。

重置与关卡选择:

增加重置游戏和多关卡选择功能,使游戏更加丰富和可玩性更高。

4.2 系统整体架构设计

整个“连连看”游戏主要分为以下几大模块,各模块之间通过明确接口进行通信:

界面显示层(UI):

利用 Swing 组件构建游戏主窗口、棋盘区域、状态显示区和操作按钮。界面负责显示棋盘中每个方块的状态,并响应用户点击事件。

数据模型层(Model):

包括棋盘数据模型(二维数组)、方块对象(Tile),负责存储每个位置的图案信息(用数字或图标表示)。

游戏引擎层(GameEngine):

控制游戏流程,包括棋盘初始化、用户操作响应、连通性判断、方块消除、胜利判断等核心业务逻辑。

算法层(Algorithm):

专门实现判断两个方块是否可以连通的算法,包括直线、一转和二转的路径检测。

辅助工具层(Util):

提供随机数生成、数组洗牌、界面更新等辅助功能,保证各模块数据交互的高效与正确。

4.3 模块划分与设计细节

4.3.1 棋盘数据模型

二维数组表示:

棋盘采用二维整型数组保存,数组元素的值表示不同的图案编号,值为 0 表示该位置为空。

扩充边界:

为简化路径判断,将棋盘外围扩充一圈空白区域,方便处理边缘方块的连通性问题。

4.3.2 图形界面设计

JFrame 主窗口:

游戏主窗口采用 JFrame 实现,包含菜单栏、状态栏和棋盘区域。

棋盘面板:

利用 JPanel 和 GridLayout 布局创建棋盘,将每个方块绘制为 JButton,按钮上显示图案编号或图标。

事件监听:

为每个按钮注册 ActionListener,捕获玩家点击,并将点击事件传递给游戏引擎进行处理。

4.3.3 连通性判断算法

直线判断(CheckDirect):

检查两方块是否在同一行或同一列,且中间所有单元均为空。

一次转弯判断(CheckOneTurn):

通过枚举一个拐点,分别检测两段直线是否通畅。

两次转弯判断(CheckTwoTurn):

枚举两个转折点,通过两次转弯来检查是否存在满足条件的连接路径。

4.3.4 游戏引擎

状态管理:

记录当前已选中的方块信息,当连续点击两块时调用连通性判断算法,若成功则更新数据模型和界面。

逻辑控制:

当玩家成功消除一对方块后,更新棋盘数据;若所有方块消除,则判定游戏胜利。

五、详细实现代码

下面给出完整的 Java 实现代码,所有代码整合在一个文件中。代码中包含了非常详细的注释,逐步说明每个类和方法的功能与实现思路。请将下面代码保存为一个 Java 文件(例如 LianLianKanGame.java),并在支持 Swing 的 Java 环境中编译运行。

import javax.swing.*;

import java.awt.*;

import java.awt.event.*;

import java.util.*;

/**

* 连连看游戏

*

* 本游戏采用 Swing 图形界面实现,将棋盘中的方块以按钮形式展示。

* 游戏规则:在棋盘中找出两块图案相同的方块,若它们之间可以用不超过三个转折点的直线路径连通,

* 则可以将它们消除。消除所有方块后游戏胜利。

*

* 系统模块包括:

* 1. 数据模型:使用二维数组 board 保存棋盘中每个位置的图案编号(0 表示空)。

* 2. 图形界面:使用 JFrame 作为主窗口,JPanel + GridLayout 布局构建棋盘,每个格子为 JButton。

* 3. 游戏引擎:处理用户点击,记录选中状态,并调用连通性判断算法消除匹配的方块。

* 4. 连通性判断算法:检查两个方块之间是否可以通过直线、一次转弯或两次转弯的路径连通。

*/

public class LianLianKanGame extends JFrame {

// 棋盘尺寸(包含扩充边界),例如原始棋盘 8x8,扩充边界后为 10x10

private final int rows = 10;

private final int cols = 10;

// 实际游戏区域为 board[1..rows-2][1..cols-2]

// 棋盘数据模型,board[i][j] 表示位置 (i, j) 的图案编号,0 表示空

private int[][] board = new int[rows][cols];

// 按钮数组,与 board 一一对应

private JButton[][] buttons = new JButton[rows][cols];

// 记录第一次点击的位置

private Point firstSelected = null;

// 图案种类数(按实际需要设置,保证总数为偶数)

private final int tileTypes = 10;

// 随机数生成器

private Random random = new Random();

// 主界面面板

private JPanel boardPanel;

// 状态信息显示区

private JLabel statusLabel;

/**

* 构造方法,初始化窗口、棋盘数据及界面

*/

public LianLianKanGame() {

setTitle("Java 连连看游戏");

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

setLayout(new BorderLayout());

// 初始化状态标签

statusLabel = new JLabel("请选择两个方块进行匹配。");

add(statusLabel, BorderLayout.SOUTH);

// 初始化棋盘面板,使用 GridLayout 布局

boardPanel = new JPanel(new GridLayout(rows, cols));

add(boardPanel, BorderLayout.CENTER);

// 初始化棋盘数据:扩充边界均设为 0,内部填充图案

initBoardData();

// 初始化按钮并添加到面板上

initBoardButtons();

pack();

setLocationRelativeTo(null);

setVisible(true);

}

/**

* 初始化棋盘数据:

* - 扩充边界(第一行、最后一行、第一列、最后一列)均设置为 0(空)

* - 内部区域随机分配图案,保证每个图案成对出现

*/

private void initBoardData() {

// 内部区域行数和列数

int innerRows = rows - 2;

int innerCols = cols - 2;

int totalTiles = innerRows * innerCols;

// 保证总数为偶数

if (totalTiles % 2 != 0) {

System.err.println("内部区域格子数必须为偶数!");

System.exit(1);

}

// 创建数组保存所有图案对,图案编号范围 1 ~ tileTypes

int[] tiles = new int[totalTiles];

// 分配成对数据

for (int i = 0; i < totalTiles; i++) {

tiles[i] = (i % (totalTiles / 2)) % tileTypes + 1;

}

// 打乱数组顺序

shuffleArray(tiles);

// 填充 board 内部区域

int index = 0;

for (int i = 1; i < rows - 1; i++) {

for (int j = 1; j < cols - 1; j++) {

board[i][j] = tiles[index++];

}

}

// 扩充边界默认已为 0

}

/**

* 使用 Fisher-Yates 算法随机打乱数组

*/

private void shuffleArray(int[] array) {

for (int i = array.length - 1; i >= 0; i--) {

int j = random.nextInt(i + 1);

int temp = array[i];

array[i] = array[j];

array[j] = temp;

}

}

/**

* 初始化棋盘按钮,根据 board 数据创建 JButton,并注册点击事件

*/

private void initBoardButtons() {

for (int i = 0; i < rows; i++) {

for (int j = 0; j < cols; j++) {

JButton btn = new JButton();

btn.setPreferredSize(new Dimension(50, 50));

// 如果是边界区域或空格子,不显示内容

if (board[i][j] != 0) {

btn.setText(String.valueOf(board[i][j]));

btn.setFont(new Font("Arial", Font.BOLD, 20));

} else {

btn.setEnabled(false);

}

final int r = i, c = j;

// 注册按钮点击事件

btn.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {

handleButtonClick(r, c);

}

});

buttons[i][j] = btn;

boardPanel.add(btn);

}

}

}

/**

* 处理棋盘上按钮的点击事件

* @param r 按钮所在行

* @param c 按钮所在列

*/

private void handleButtonClick(int r, int c) {

// 如果当前按钮已为空,则忽略

if (board[r][c] == 0) {

return;

}

// 第一次点击,记录位置并高亮显示

if (firstSelected == null) {

firstSelected = new Point(r, c);

buttons[r][c].setBackground(Color.YELLOW);

statusLabel.setText("请再选择一个与 [" + r + "," + c + "] 图案相同的方块。");

} else {

// 第二次点击,若点击同一按钮则取消选择

if (firstSelected.x == r && firstSelected.y == c) {

buttons[r][c].setBackground(null);

firstSelected = null;

statusLabel.setText("取消选择,请重新选择一个方块。");

return;

}

// 如果两次点击的图案不同,重置选择

if (board[firstSelected.x][firstSelected.y] != board[r][c]) {

buttons[firstSelected.x][firstSelected.y].setBackground(null);

firstSelected = null;

statusLabel.setText("选择的两个方块图案不同,请重新选择。");

return;

}

// 两个方块图案相同,判断是否能连通

if (canConnect(firstSelected.x, firstSelected.y, r, c)) {

// 可连通,消除这两个方块

removeTile(firstSelected.x, firstSelected.y);

removeTile(r, c);

statusLabel.setText("成功消除!");

// 判断是否全部消除

if (isGameOver()) {

statusLabel.setText("恭喜你,已全部消除,游戏胜利!");

}

} else {

statusLabel.setText("这两个方块无法连通,请重新选择。");

}

// 恢复第一个按钮背景色,重置选择

buttons[firstSelected.x][firstSelected.y].setBackground(null);

firstSelected = null;

}

}

/**

* 消除指定位置的方块,更新 board 数据和按钮显示

*/

private void removeTile(int r, int c) {

board[r][c] = 0;

buttons[r][c].setText("");

buttons[r][c].setEnabled(false);

}

/**

* 判断游戏是否结束:检查内部区域是否全部为空

*/

private boolean isGameOver() {

for (int i = 1; i < rows - 1; i++) {

for (int j = 1; j < cols - 1; j++) {

if (board[i][j] != 0) {

return false;

}

}

}

return true;

}

/**

* 判断两个位置的方块是否可以通过规定规则连通

* 先判断是否直接连通,再判断是否一次转弯连通,最后判断是否两次转弯连通

* @param r1 第一个方块行

* @param c1 第一个方块列

* @param r2 第二个方块行

* @param c2 第二个方块列

* @return 如果可以连通返回 true,否则返回 false

*/

private boolean canConnect(int r1, int c1, int r2, int c2) {

// 如果直接连通

if (checkDirect(r1, c1, r2, c2)) {

return true;

}

// 一次转弯

if (checkOneTurn(r1, c1, r2, c2)) {

return true;

}

// 两次转弯

if (checkTwoTurn(r1, c1, r2, c2)) {

return true;

}

return false;

}

/**

* 判断两个点是否在同一行或同一列,且之间的所有单元均为空

*/

private boolean checkDirect(int r1, int c1, int r2, int c2) {

// 如果在同一行

if (r1 == r2) {

int start = Math.min(c1, c2) + 1;

int end = Math.max(c1, c2);

for (int i = start; i < end; i++) {

if (board[r1][i] != 0) return false;

}

return true;

}

// 如果在同一列

if (c1 == c2) {

int start = Math.min(r1, r2) + 1;

int end = Math.max(r1, r2);

for (int i = start; i < end; i++) {

if (board[i][c1] != 0) return false;

}

return true;

}

return false;

}

/**

* 判断是否存在一次转弯的连通路径

* 思路:寻找一个中间拐点 (r1, c2) 或 (r2, c1),检查两段直线是否通畅

*/

private boolean checkOneTurn(int r1, int c1, int r2, int c2) {

// 拐点 1: (r1, c2)

if (board[r1][c2] == 0) {

if (checkDirect(r1, c1, r1, c2) && checkDirect(r1, c2, r2, c2)) {

return true;

}

}

// 拐点 2: (r2, c1)

if (board[r2][c1] == 0) {

if (checkDirect(r1, c1, r2, c1) && checkDirect(r2, c1, r2, c2)) {

return true;

}

}

return false;

}

/**

* 判断是否存在两次转弯的连通路径

* 思路:枚举所有可能的行和列,寻找满足条件的中间两个拐点

*/

private boolean checkTwoTurn(int r1, int c1, int r2, int c2) {

// 枚举所有可能的行

for (int i = 0; i < rows; i++) {

if (board[i][c1] == 0 && board[i][c2] == 0) {

if (checkDirect(r1, c1, i, c1) &&

checkDirect(i, c1, i, c2) &&

checkDirect(i, c2, r2, c2)) {

return true;

}

}

}

// 枚举所有可能的列

for (int j = 0; j < cols; j++) {

if (board[r1][j] == 0 && board[r2][j] == 0) {

if (checkDirect(r1, c1, r1, j) &&

checkDirect(r1, j, r2, j) &&

checkDirect(r2, j, r2, c2)) {

return true;

}

}

}

return false;

}

/**

* 主方法,启动游戏

*/

public static void main(String[] args) {

SwingUtilities.invokeLater(new Runnable() {

public void run() {

new LianLianKanGame();

}

});

}

}

六、代码解读

下面对上述代码中关键部分的实现逻辑进行详细解读,帮助读者理解整个程序的设计思路:

6.1 数据模型与界面初始化

棋盘数据结构:

整个棋盘使用一个二维数组 board 表示。为方便算法处理,在原始棋盘外扩充一圈空白区域(边界均为 0),这样即使在边缘也能统一处理路径判断。

内部区域的格子随机填充图案编号(1~tileTypes),保证总数为偶数,每个图案均成对出现。

按钮初始化:

每个棋盘格子对应一个 JButton,通过 GridLayout 布局排列在 JPanel 上。若该位置不为空,则按钮显示图案编号(可以扩展为图标),否则按钮禁用。

6.2 用户交互逻辑

点击事件处理:

当玩家点击按钮时,程序先判断当前是否已有选中项。如果是第一次点击,则高亮显示并记录位置;如果是第二次点击,则判断两个按钮的图案是否相同。

若图案不同,取消选择;若图案相同,则调用连通性判断算法判断是否能连通,若成功则“消除”这两个按钮(清空文字并禁用按钮)。

6.3 连通性判断算法

直线检测(checkDirect):

检查两个点是否在同一行或同一列,并判断两个点之间的所有单元是否为空。

一次转弯检测(checkOneTurn):

通过在两个候选拐点((r1, c2) 和 (r2, c1))上判断,检查是否存在一转弯的连通路径。

两次转弯检测(checkTwoTurn):

枚举所有可能的行和列,寻找能够构成两次转弯连通路径的中间拐点组合。

当以上三种方式都不满足时,则认为两个方块无法连通。

6.4 游戏状态更新与结束判断

消除方块:

当两个方块可以连通时,调用 removeTile() 方法,将对应 board 元素设为 0,同时更新按钮显示为空,禁用该按钮。胜利判断:

每次消除后,程序遍历内部区域,若所有方块均已消除,则显示胜利提示。

七、项目总结与未来展望

7.1 项目实现亮点

模块化设计:

采用数据模型、界面显示、游戏引擎和算法判断四个主要模块,使得代码结构清晰、职责分明,便于后续功能扩展和维护。

算法实现:

通过直线、一次转弯和两次转弯检测等多重判断,实现了连连看核心“连通性判断”的功能,解决了复杂的路径搜索问题。

用户体验:

利用 Swing 构建了简洁直观的游戏界面,通过按钮点击和状态提示,使玩家能够轻松理解并参与游戏。

7.2 项目中遇到的挑战

路径判断算法复杂性:

连连看中判断两个点是否可连通是关键难点,如何高效判断路径中不受阻碍,需要对边界情况进行充分考虑。本文采用枚举行和列的方式解决问题,虽然效率上可以进一步优化,但对于初学者来说已足够说明思路。

界面布局与数据同步:

在 Swing 界面中,需要保证数据模型 board 与按钮显示状态实时同步,避免因数据不一致导致游戏逻辑错误。项目中通过统一的事件处理和界面刷新机制解决了这一问题。

7.3 未来改进方向

图形与音效优化:

可进一步引入图片资源代替数字显示,使游戏画面更加美观;同时增加背景音乐和音效,提升游戏体验。

算法优化:

当前连通性判断算法虽能满足基本需求,但在棋盘规模较大时效率可能下降。可以考虑采用更高效的搜索算法(如广度优先搜索)进行优化。

提示和求助功能:

在玩家长时间未能找到匹配对时,系统可自动提供提示或标记,增加游戏友好性。

关卡与计分系统:

增加多个难度关卡和计时计分机制,根据玩家消除速度和连续成功次数计算得分,增强游戏竞技性和重玩价值。

存档与排行榜:

支持游戏进度存档和读取,同时可以引入本地排行榜记录最佳成绩,增加玩家粘性。

八、结束语

本文详细介绍了如何使用 Java 实现一款经典的“连连看”游戏。从项目背景、相关技术知识、系统需求与架构设计,到详细的实现思路和完整代码,再到代码解读和项目总结,力求为读者呈现一个全面、系统的开发案例。

通过本项目,开发者不仅能深入理解 Java Swing 编程和面向对象设计,还能掌握二维数组数据处理、随机数生成、路径搜索算法等实用技巧。希望这篇博客能为你在游戏开发和项目实践中提供宝贵的参考和启发,同时也欢迎大家在此基础上不断扩展、完善,创造出更富创意和乐趣的作品!

未来,随着新技术的不断发展,你可以尝试将该项目改造为图形化版本、加入联网对战、增加多种互动元素,打造一款更具吸引力的连连看游戏。愿你在编程的道路上不断探索、勇于创新,收获更多乐趣与成就!