基于JavaFX的扫雷游戏实现(一)——整体概述
我在不要更新挑战中坚持了一年,你也来试试吧(咕咕咕)!
好言归正传,本次更新带来的是经典游戏扫雷,基于JavaFX实现。篇幅有限,文章主要介绍核心操作实现,不会列出所有代码。需要完整源码或是想预览最终效果,可以点击下方链接。后续会逐步更新细节实现方面的内容,将来吧反正(肯定不鸽!)
视频演示:
https://www.bilibili.com/video/BV1jh4y1u7ad
源码(项目所使用的JDK为1.8版本):
如果您已经看过视频,或是成功运行代码,相信对本项目和扫雷已经有了初步认知。如果您是直接阅读的本篇文章,这里也提供了在线的扫雷入口,方便您快速了解:扫雷游戏网页版 - Minesweeper(非本人制作,仅分享)
怎么样,是否找回了那些年在微机课上偷偷玩扫雷的快乐。总之不管您之前有没有玩过,我建议先熟悉下它的规则和操作,本项目主要是围绕这些内容编写。
规则:
- 扫雷游戏是在一个方格网格中进行的,其中包含了地雷和数字。
- 目标是清除所有非地雷方格而不触发地雷。
操作:
- 游戏开始时,你会看到一个方格网格,其中的方格是覆盖的。
- 你可以通过鼠标左键点击一个方格来揭开它。如果揭开的方格是地雷,游戏结束,你输了。
- 如果揭开的方格是数字,它会显示周围相邻方格中地雷的数量。
- 如果揭开的方格是空白方格(数字为0),它会自动揭开相邻的空白方格和数字方格,直到边界或者遇到数字方格为止。
- 如果你认为某个方格是地雷,你可以使用鼠标右键进行标记。标记的方格会显示一个旗帜图标,表示你认为该方格是地雷。
- 如果你揭开了所有非地雷方格,游戏胜利。
了解完这些,让我们尝试使用代码来实现它。
首先是数据来源的问题。每生成一局新游戏,都有对应的地雷数字分布记录,用于指导你推断哪些地方是数字,哪些地方是地雷。考虑到游戏界面行列整齐排放的格子,用二维数组存取对应数据最直观易懂。那么选定数据结构后,如何生成初始数据呢?鉴于每局游戏的数据几乎不会重复,如果只靠我们预输入的数据,没玩几局就腻了。为此可以采用随机生成数据的方式,我的做法如下:
/**
* 生成新游戏的地图数据
*/
public void init() {
// 用于记录地雷的位置, 避免重复选择
HashSet<Integer> set = new HashSet();
// 确定随机数据范围
int count = height * width;
// 开始随机
for (int rest = bomb; rest > 0; ) {
int index = rand.nextInt(count);
// 如果当前位置可以设置为地雷, 标记该位置, 地雷剩余个数减一
if (!set.contains(index)) {
set.add(index);
map[index / width][index % width] = BOOM;
rest -= 1;
}
}
// 统计地雷分布情况
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
if (map[i][j] != BOOM) {
map[i][j] = countBomb(i, j);
}
}
}
}
注:map是用于存储数据的二维数组;width和height分别表示横向和纵向的格子数,即map每个维度的长
仅生成地雷位置还不够,我们还需要知道地雷周围对应的数字,上面代码中的countBomb方法负责完成这部分工作,具体实现如下:
/**
* 统计当前格子周围的地雷个数
*
* @param x 横坐标
* @param y 纵坐标
* @return count 地雷个数
*/
public int countBomb(int x, int y) {
int count = 0;
// 依次判断周围格子是否存在地雷
for (int i = 0; i < 8; ++i) {
int newX = x + positions[i][0];
int newY = y + positions[i][1];
if (newX > -1 && newX < height && newY > -1 && newY < width && map[newX][newY] == BOMB) {
count += 1;
}
}
return count;
}
注:positions为相对方位坐标数组,用于计算周围八个格子的坐标;BOMB是int常量,值为9,表示地雷
这样就有了初始游戏数据,仅有这个还不够,我们最终要把它展示在屏幕上。不妨想一下,在绘制界面的过程中,我们可以根据数据的不同来确定某个格子具体显示为地雷,数字或是空白。比如0-8表示周围地雷个数统计,9表示地雷。可是游戏一开始全是未知的格子,难道我们要再设置一个相应的boolean数组记录格子是否被点开吗?这样做虽然可行,但我觉得较为麻烦,所以我是这样设计的:
// 数字常量 [0:空白格, 9:地雷]
public static final byte BLANK = 0;
public static final byte BOMB = 9;
// [20:旗帜标记判断, 40:问号标记判断]
public static final byte FLAG = 20;
public static final byte GUESS = 40;
// [99:边界标记, 超过这个数字代表当前格子已被点开]
public static final byte BOUND = 99;
对于可能用于逻辑判断的量,将它们定义为常量,这样在代码中就不会出现 if ( 变量 == 9 ),却不清楚‘9’是什么含义的情况,避免降低可读性。其次是格子是否被点击过的问题,可以设置一个边界值进行区分。因为地雷和周围数字只占用了很少一部分整型数据,所以可以根据数据是否超过某个范围来判断是否被点击过。最后是右键标记问题,我印象里的操作是右键一次采用旗帜标记,两次采用问号标记,所以设置两个对应常量用于判断。下面是点击过程中的逻辑判断代码:
// 获取按钮
Button button = (Button) buttons.get(row * GAME.width + column);
// 根据左右键设置不同响应逻辑
if (event.getButton() == MouseButton.SECONDARY) {
// 定义图片路径
String imagePath = null;
// 右键对应行为
if (map[row][column] >= GUESS) {
// 不设置图片, 还原雷的数目
map[row][column] -= GUESS;
REST_FLAG += 1;
} else if (map[row][column] >= FLAG) {
// 如果已经被标记, 路径更换为问号图片, 表示不确定
imagePath = GUESS_IMG;
map[row][column] = map[row][column] - FLAG + GUESS;
} else {
// 未被标记过, 判断是否还有可用标记
if (REST_FLAG > 0) {
imagePath = FLAG_IMG;
map[row][column] += FLAG;
REST_FLAG -= 1;
}
}
button.setStyle("-fx-background-size: contain; -fx-background-image: url(" + imagePath + ")");
} else {
// 左键对应行为
if (map[row][column] <= BOUND && map[row][column] >= FLAG) {
// 如果被标记, 则先清空标记
map[row][column] -= map[row][column] >= GUESS ? GUESS : FLAG;
REST_FLAG += 1;
button.setStyle("-fx-background-size: contain; -fx-background-image: url(" + null + ")");
} else {
// 更新点击过的数据
mineSweeper.clickCell(row, column);
if (STATE == UNSURE) {
// 统计非雷格子已点开数目
int count = 0;
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
if (map[i][j] > BOUND) {
Button btn = (Button) buttons.get(i * GAME.width + j);
count += 1;
int value = map[i][j] - 100;
if (value != BLANK) {
// 消除空白填充
btn.setPadding(new Insets(0.0));
// 设置粗体和字体颜色
btn.setFont(Font.font("Arial", FontWeight.BOLD, GAME.numSize));
btn.setTextFill(NUMS[value - 1]);
btn.setText(value + "");
}
btn.setStyle("-fx-border-color: #737373; -fx-opacity: 1; -fx-background-color: #ffffff");
btn.setDisable(true);
}
}
}
// 判断全部非雷格子是否全部点开
if (count + GAME.bomb == GAME.width * GAME.height) {
STATE = WIN;
}
} else if (STATE == LOSS) {
// 游戏失败, 显示所有地雷位置
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
if (map[i][j] == BOMB) {
Button btn = (Button) buttons.get(i * GAME.width + j);
btn.setStyle("-fx-background-color:#ffffff; -fx-background-size: contain; -fx-background-image: url(" + UNEXPLODED_IMG + ")");
}
}
}
button.setStyle("-fx-background-color:#ffffff; -fx-background-size: contain; -fx-background-image: url(" + EXPLODED_IMG + ")");
}
}
}
注:阅读时请先忽略掉界面控件相关的操作,仅需关注map数据的变化
这段代码根据左或右键点击来进行对应的操作,同时引出了新的问题,点击格子后不总是只更新它自身的数据,像操作中说的,如果它是空白格 (数据为0),还需要展开它周围的格子,这个过程是怎么进行的呢?它在上述代码中体现为mineSweeper.clickCell(row, column); 具体实现如下:
/**
* 展开与当前位置相连的所有空白区域, 包括包裹这层空白区域数字边界
*
* @param x 横坐标
* @param y 纵坐标
*/
public void clickCell(int x, int y) {
if (map[x][y] == BLANK) {
map[x][y] += 100;
// 点击到空白区域, 递归判断周围8个方向
for (int i = 0; i < 8; i += 1) {
int newX = x + positions[i][0];
int newY = y + positions[i][1];
if (newX > -1 && newX < height && newY > -1 && newY < width
&& map[newX][newY] != BOMB && map[newX][newY] < FLAG) {
// 递归展开非雷和未标记区域
clickCell(newX, newY);
}
}
} else if (map[x][y] == BOMB) {
// 点击到地雷, 游戏状态设置为失败
STATE = LOSS;
} else if (map[x][y] < BOUND) {
// 点击到数字格, 数值加100用于区分是否已被点开
map[x][y] += 100;
}
}
至此,我们基本完成了扫雷的核心内容,剩余的功能如计时,成绩排行,难度设置,胜负判定等只能说是使这个玩法更像是完整的游戏。因为本文是概述性质的,所以这些功能和界面统一放在后续文章里结合着讲。
———————————————我———是———分———割———线——————————————
隔了这么久再次写博客,都不知道从何写起讲些什么了。如果文章或者演示里有哪些不清楚的地方,还请留意后续更新。另外GitHub的代码我应该还会更新,如果有不足之处欢迎在issue里指出。这次的项目拖拖拉拉大概进行了一个月吧,实际用来写代码的时间也不能算多,拖延症大抵是没救了(悲)希望下次更新不是明年吧
基于JavaFX的扫雷游戏实现(一)——整体概述的更多相关文章
- 基于jQuery经典扫雷游戏源码
分享一款基于jQuery经典扫雷游戏源码.这是一款网页版扫雷小游戏特效代码下载.效果图如下: 在线预览 源码下载 实现的代码. html代码: <center> <h1>j ...
- (转载)WinformGDI+入门级实例——扫雷游戏(附源码)
本文将作为一个入门级的.结合源码的文章,旨在为刚刚接触GDI+编程或对相关知识感兴趣的读者做一个入门讲解.游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中. 整体思路: 扫雷的游戏界面让我从 ...
- C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式
C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...
- WinformGDI+入门级实例——扫雷游戏(附源码)
写在前面: 本文将作为一个入门级的.结合源码的文章,旨在为刚刚接触GDI+编程或对相关知识感兴趣的读者做一个入门讲解.游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中. 整体思路: 扫雷的游 ...
- 基于JavaFX图形界面演示的迷宫创建与路径寻找
事情的起因是收到了一位网友的请求,他的java课设需要设计实现迷宫相关的程序--如标题概括. 我这边不方便透露相关信息,就只把任务要求写出来. 演示视频指路: 视频过审后就更新链接 完整代码链接: 网 ...
- Pomelo:网易开源基于 Node.js 的游戏服务端框架
Pomelo:网易开源基于 Node.js 的游戏服务端框架 https://github.com/NetEase/pomelo/wiki/Home-in-Chinese
- 洛谷 P2670 扫雷游戏==Codevs 5129 扫雷游戏
题目描述 扫雷游戏是一款十分经典的单机小游戏.在n行m列的雷区中有一些格子含有地雷(称之为地雷格),其他格子不含地雷(称之为非地雷格).玩家翻开一个非地雷格时,该格将会出现一个数字——提示周围格子中有 ...
- 基于HTML5的SLG游戏开发(序)
2012年前后,HTML5游戏凭借跨平台.易移植.部署简单.节省成本等优点被炒的火热,经过一两年的快速发展,市场出现了一些成功地HTML5游戏产品,像磊友的<修仙三国>,神奇时 ...
- [置顶] 使用红孩儿工具箱完成基于Cocos2d-x的简单游戏动画界面
[Cocos2d-x相关教程来源于红孩儿的游戏编程之路CSDN博客地址:http://blog.csdn.net/honghaier 红孩儿Cocos2d-X学习园地QQ3群:205100149,47 ...
- 原生javascript扫雷游戏
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
随机推荐
- python入门教程之六运算符
什么是运算符? 本章节主要说明Python的运算符.举个简单的例子 4 +5 = 9 . 例子中,4 和 5 被称为操作数,"+" 称为运算符. Python语言支持以下类型的运算 ...
- [智能制造] 如何利用生产软件(MES)进行生产信息收集?
1 如何保证生产管理软件所收集信息的准确性? 1.1 当前制造企业使用MES系统收集信息的现状 原以为使用了MES生产管理系统后,会得到稽核员的肯定. 但没想到,在实际的稽核过程中,稽核员还是发现目前 ...
- [数据库/Java SE]MySQL驱动包(mysql-connector-java.jar)问题[com.mysql.jdbc.Driver/org.gjt.mm.mysql.Driver/com.mysql.cj.jdbc.Driver]
MySQL的驱动JAR包----mysql-connector-java.jar,不同版本,其JBDC驱动类Driver的路径均有可能变化. 日后使用时,可根据本文的思路,有依据地进行检查(而不是随便 ...
- 4.测试类mapper报错
1.总结:前几天还有今天一直在弄测试类报错的原因,想着项目是一个大整体,写一个mappe测试类,测试一个mapper,这样后面不会出错: 但是在测试mapper的时候一直,出现mapper值为空的异常 ...
- 一分钟使用Gitee,把本地项目放入gitee仓库中
一.先创建一个Gitee账号 首先需要自己去别的地方看创建一个空仓库,然后复制仓库的地址 省略... 现有本地有项目代码,远程空仓库一个,如何把本地项目代码推到远程仓库? 1.在项目根目录初始化 Gi ...
- 详解Redis三大集群模式,轻松实现高可用!
1. Redis集群简介 1.1 什么是Redis集群 Redis集群是一种通过将多个Redis节点连接在一起以实现高可用性.数据分片和负载均衡的技术.它允许Redis在不同节点上同时提供服务,提高整 ...
- CSS6大种选择器
一.常用的css基本选择器(4种) 1.标签选择器 结构: 标签名{css属性名:属性值}作用:通过标签名,找到页面中所有的这类标签,设置样式 注意:1.标签选择器选择的是一类标签,而不是单独的一个2 ...
- 一文梳理z-index和层叠上下文
前言 最近参与某前端项目架构改造,发现项目中滥用z-index,设置的值有几十种并且不统一.在对项目的z-index进行梳理和统一过程中也深入学习了一下z-index,并撰写成文,希望也能帮助到陌生的 ...
- ET–异步协程使用–TimerComponent篇
之前可能也有群友写过一些关于ET框架中TimerComponent的使用教程,我这里写下关于TimerComponent的常规使用跟一些不常规使用的方法以及一些情况下需要使用到的不同的函数. 先来看看 ...
- sh: vue-cli-service: command not found
mac环境下运行vue项目报错sh: vue-cli-service: command not found 解决方法:cd到项目目录下,执行命令sudo rm -rf node_modules pac ...