基于JavaFX的扫雷游戏实现(二)——游戏界面
废话环节:看过上期文章的小伙伴现在可能还是一头雾水,怎么就完成了核心内容,界面呢?哎我说别急让我先急,博主这不夜以继日地肝出了界面部分嘛。还是老规矩,不会把所有地方都照顾到,只挑一些有代表性的内容介绍,您各位多担待。另外博主的JavaFX是跟着B站视频速成的,指路:https://www.bilibili.com/video/BV1Qf4y1F7Zv 有哪些地方讲的不对欢迎在评论区友好交流。
上期内容已经介绍了游戏初始数据,即地雷和数字分布情况的二维数组,那么如何把它与图形界面对应到一起呢?如果您熟悉JavaFX的各种布局和控件的话,很容易会联想到GridPane布局。至于可以点击的格子,用label或button也好,用rectangle绘制矩形也罢,只要看起来像那回事,能设置对应点击事件就OK。选完角儿后就是代码环节了,考虑到纯java代码实现界面不够直观,所以推荐使用fxml文件,因为有对应的可视化设计工具。这里我采用的是Scene Builder,建议大家也了解下。下面给出游戏界面设计图:
图中各部分内容所要承担的功能如下:
- 上方左右两侧的黑色格子是用于显示剩余标记计数和游戏用时的;
- 按钮是游戏重置按钮,不论游戏是否结束,点击就可以重新开局;
- 下方大片区域是要存放格子的GridPane布局;
设计完毕后生成的fxml文件如下(对于 controller 或 fx:id 等内容需要手动设置):
game.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="anchorPane"
prefWidth="400" prefHeight="500"
maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity"
xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="controllers.GameController">
<children>
<Label fx:id="labelTop" style="-fx-background-color: #8e7f7f; ">
<font>
<Font size="1.0"/>
</font>
</Label>
<Label fx:id="labelBottom" style="-fx-background-color: #8e7f7f;">
<font>
<Font size="1.0"/>
</font>
</Label>
<Label fx:id="labelLeft" style="-fx-background-color: #8e7f7f;">
<font>
<Font size="1.0"/>
</font>
</Label>
<Label fx:id="labelRight" style="-fx-background-color: #8e7f7f;">
<font>
<Font size="1.0"/>
</font>
</Label>
<Label fx:id="labelCenter" style="-fx-background-color: #8e7f7f;">
<font>
<Font size="1.0"/>
</font>
</Label>
<GridPane fx:id="grid"/>
<GridPane fx:id="mark" prefWidth="80.0" prefHeight="45.0" AnchorPane.topAnchor="35.0"
AnchorPane.leftAnchor="20.0" style="-fx-background-color: #000000; -fx-hgap: 5.0"/>
<GridPane fx:id="time" prefWidth="80.0" prefHeight="45.0" AnchorPane.topAnchor="35.0"
AnchorPane.rightAnchor="20.0" style="-fx-background-color: #000000; -fx-hgap: 5.0"/>
<Button fx:id="reset" prefHeight="50.0" prefWidth="50.0" AnchorPane.topAnchor="35.0" onAction="#onResetClick"/>
</children>
</AnchorPane>
你可能会有疑问,为什么图中没有格子按钮呢?原因很简单,以扫雷简单模式为例,9*9大小,一共要81个格子。这部分内容如果手动添加可太费时费力了,因为它们初始状态完全一致,所以建议在代码中通过循环来实现,如下:
for (int i = 0; i < GAME.height; ++i) {
for (int j = 0; j < GAME.width; ++j) {
Button button = new Button();
// 设置边界线的外观效果, 使按钮看起来更突出
button.setBorder(new Border(new BorderStroke(Color.web("#737373"), BorderStrokeStyle.SOLID, new CornerRadii(4), new BorderWidths(1))));
button.setPadding(new Insets(0));
// 设置按钮大小和点击事件
button.setPrefSize(GAME.buttonSize, GAME.buttonSize);
button.setOnMouseClicked(event -> {
handleEvent(event);
});
// 添加按钮到指定位置
grid.add(button, j, i);
}
}
而对于错位的重置按钮和暂时不可见的五个label边框,考虑到后续设置不同游戏难度的情况,这部分内容在代码中设置比较合适,我的做法如下:
/**
* 调整边框以及其他组件的位置和大小
*/
private void adjustControls() {
HashMap<String, Double> params = GAME.genParamsMap();
double thickness = params.get("thickness");
double offset = params.get("offset");
double lenVertical = params.get("lenVertical");
double lenHorizontal = params.get("lenHorizontal");
// 计算实际窗口宽高
WIDTH_OFFSET += lenHorizontal + thickness * 2;
HEIGHT_OFFSET += lenVertical;
// 设置窗口大小
anchorPane.setPrefSize(WIDTH_OFFSET, lenVertical);
// 设置网格布局位置
AnchorPane.setTopAnchor(grid, offset + thickness);
AnchorPane.setLeftAnchor(grid, thickness);
// 设置重置按钮的位置
reset.setStyle("-fx-background-size: contain; -fx-background-image: url(" + SMILE_IMG + ")");
AnchorPane.setLeftAnchor(reset, thickness + (lenHorizontal - 50) / 2);
// 设置边框标签的大小和位置
labelTop.setPrefSize(lenHorizontal, thickness);
AnchorPane.setLeftAnchor(labelTop, thickness);
AnchorPane.setTopAnchor(labelTop, 0.0);
labelCenter.setPrefSize(lenHorizontal, thickness);
AnchorPane.setLeftAnchor(labelCenter, thickness);
AnchorPane.setTopAnchor(labelCenter, offset);
labelBottom.setPrefSize(lenHorizontal, thickness);
AnchorPane.setLeftAnchor(labelBottom, thickness);
AnchorPane.setTopAnchor(labelBottom, lenVertical - thickness);
labelLeft.setPrefSize(thickness, lenVertical);
AnchorPane.setLeftAnchor(labelLeft, 0.0);
AnchorPane.setTopAnchor(labelLeft, 0.0);
labelRight.setPrefSize(thickness, lenVertical);
AnchorPane.setLeftAnchor(labelRight, lenHorizontal + thickness);
AnchorPane.setTopAnchor(labelRight, 0.0);
}
注:GAME为游戏难度枚举类实例,genParamsMap是用于生成计算所需数据的静态方法
完整的枚举类代码如下:
GameEnum
package components;
import java.util.HashMap;
/**
* @description: 游戏难度枚举
* @author: 郭小柒w
* @time: 2023/6/11
*/
public enum GameEnum {
EASY(9, 9, 10, 40.0, 30.0),
MEDIUM(16, 16, 40, 35.0, 25.0),
HARD(30, 16, 99, 30.0, 20.0),
CUSTOM();
// 游戏难度规格[宽 x 高], 相应地雷个数
public int width, height, bomb;
// 网格按钮尺寸, 数字字体大小
public double buttonSize, numSize;
GameEnum(int width, int height, int bomb, double buttonSize, double numSize) {
this.width = width;
this.height = height;
this.bomb = bomb;
this.buttonSize = buttonSize;
this.numSize = numSize;
}
GameEnum() {
this.buttonSize = 35.0;
this.numSize = 25.0;
}
// 宽和高限制在简单和困难之间
public void setWidth(int width) {
if (width < EASY.width) {
this.width = EASY.width;
} else if (width > HARD.width) {
this.width = HARD.width;
} else {
this.width = width;
}
}
public void setHeight(int height) {
if (height < EASY.height) {
this.height = EASY.height;
} else if (height > HARD.height) {
this.height = HARD.height;
} else {
this.height = height;
}
}
// 地雷数介于格子数之间
public void setBomb(int bomb) {
if (bomb < 0) {
this.bomb = 0;
} else if (bomb > width * height) {
this.bomb = width * height;
} else {
this.bomb = bomb;
}
}
/**
* 生成游戏窗口和边框大小计算需要用到的参数
* @return 参数集合
*/
public HashMap<String, Double> genParamsMap() {
HashMap<String, Double> params = new HashMap();
// 标签宽度, 固定值10
double thickness = 10.0;
params.put("thickness", thickness);
// 中间位置的标签框相对于布局顶部的偏移量, 固定值110
double offset = 110.0;
params.put("offset", offset);
// 边框标签边的水平和竖直长度, 宽度为固定值10
double lenVertical = height * buttonSize + thickness * 2 + offset;
double lenHorizontal = width * buttonSize;
params.put("lenVertical", lenVertical);
params.put("lenHorizontal", lenHorizontal);
return params;
}
}
为什么要使用枚举类对游戏难度进行区分呢?如果您完整地阅读过我的代码,就会发现MineSweeper类仅负责对接游戏进行中的各种逻辑,对于游戏难度、计时判断、排行计算等功能可以说完全不参与。这是因为和win7自带的扫雷不同,我打算新增一个菜单页,而不是运行程序直接开始游戏。这就需要我合理划分每个类负责的功能,不然就要全部塞进MineSweeper类里,显得过于臃肿(事实上大二时期我用awt和swing干过这种蠢事,那一版扫雷几百行的代码全在一个类里,没有注释还bug百出)。你也可以把难度作为MineSweeper类的一个属性来处理,不过这会导致和难度有关的逻辑修改起来比较麻烦,比如下面的代码是我进行游戏初始化的部分:
public void initialize() {
// 重置剩余可用标记数
REST_FLAG = GAME.bomb;
// 重置点击状态
CLICKED = NO;
// 重置游戏状态
STATE = UNSURE;
// 重置计时器
if (TIMELINE != null) {
TIMELINE.stop();
TIMELINE = null;
}
// 生成新游戏的用到的数据
mineSweeper = new MineSweeper(GAME.width, GAME.height, GAME.bomb, new int[GAME.height][GAME.width]);
// 设置监听
addListener();
// 绘制界面
adjustControls();
// 填充网格布局
addToGrid();
}
很显然,如果没有使用枚举类,创建minesweeper对象的语句将会更繁琐。因为那需要你根据一个难度全局变量,使用if-else或者switch语句对其进行判断,然后才能设置对应长宽地雷数,另外想要增加一个新的难度时也不可避免地要修改多处代码。而现在仅需要这个全局变量是枚举类实例。
至于图中计数和计时两个黑框框为什么不显示内容,这是因为我想实现液晶数字显示的效果,就像计算器(时代眼泪)的显示风格那样。这种情况没有官方类库可以使用,只能魔改大神轮子做一个自定义控件来满足我的需求,内容较多放在下期再说。
有了fxml文件和初始化代码(下期展开讲),通过这段代码来生成界面:
/**
* 打开新窗口
*
* @param filePath fxml文件相对路径
* @param method 方法名
*/
public void openNewWindow(String filePath, String method) {
try {
parent = (Stage) anchorPane.getScene().getWindow();
// 加载设置界面布局文件
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource(filePath));
Parent root = loader.load();
Scene scene = new Scene(root);
// 设置Stage
Stage stage = new Stage();
stage.setResizable(false);
if ("onPlayClick".equals(method)) {
// 根据实际效果重置窗口大小
stage.setOnShown(event -> {
stage.setWidth(WIDTH_OFFSET);
stage.setHeight(HEIGHT_OFFSET);
});
}
// 设置左上角图标
stage.getIcons().add(new Image(ICON_IMG));
stage.setScene(scene);
// 设置父窗体
stage.initOwner(anchorPane.getScene().getWindow());
// 设置除当前窗体外其他窗体均不可编辑
stage.initModality(Modality.WINDOW_MODAL);
// 隐藏父窗口
parent.hide();
stage.setOnCloseRequest(event -> {
if(TIMELINE != null) {
TIMELINE.stop();
TIMELINE = null;
}
// 显示父窗口
parent.show();
// 还原更改的值
WIDTH_OFFSET = 6.0;
HEIGHT_OFFSET = 35.0;
});
stage.showAndWait();
} catch (IOException e) {
System.out.println("Error on [Class:MenuController, Method:" + method + "]=>");
e.printStackTrace();
}
}
打开游戏界面:
/**
* 点击开始新游戏
*/
public void onPlayClick() { openNewWindow("/fxmls/game.fxml", "onPlayClick"); }
最终效果图如下(以简单模式为例):
——————————————我———是———分———割———线——————————————
不知道本期的介绍有没有让您对项目更加了解呢?是否对没有讲的部分更加期待呢?如果看完所有代码后仍有不清楚地方,请在评论区中指出。我会抽时间回复或者出一期答疑。下期的话打算讲讲交互的实现,网格按钮点击事件第一期已经介绍过了所以下期不会着重说明。感谢各位阅读,我们下期不见不散
基于JavaFX的扫雷游戏实现(二)——游戏界面的更多相关文章
- 基于jQuery经典扫雷游戏源码
分享一款基于jQuery经典扫雷游戏源码.这是一款网页版扫雷小游戏特效代码下载.效果图如下: 在线预览 源码下载 实现的代码. html代码: <center> <h1>j ...
- 基于HTML5的SLG游戏开发( 二):创建HTML5页面
HTML5游戏的开发过程中是在浏览器上进行运行调试的,所以首先我们需要建立一个html页面. 其中,我们把所有的canvas都放到一个viewporter(视图)里面,因此,在body中放置了一个id ...
- web版扫雷小游戏(二)
接上篇~~第一次写这种技术博客,发现把自己做的东西介绍出来还是一件脑力活,不是那么轻松啊,好吧,想到哪写到哪,流水记录之,待完成之后再根据大家的意见进行修改吧. 游戏实现 根据对扫雷游戏的体验和分析, ...
- C语言二维数组实现扫雷游戏
#include<stdio.h> //使用二维数组实现 扫雷 int main() { char ui[8][8]={ '+','+','+','+','+','+','+','+', ...
- (转载)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+编程或对相关知识感兴趣的读者做一个入门讲解.游戏尚且未完善,但基本功能都有,完整源码在文章结尾的附件中. 整体思路: 扫雷的游 ...
- C#编写扫雷游戏
翻看了下以前大学学习的一些小项目,突然发现有个项目比较有意思,觉得有必要把它分享出来.当然现在看来,里面有很多的不足之处,但因博主现在已经工作,没有时间再去优化.这个项目就是利用C#编写一个Windo ...
- 基于第二次数独游戏,添加GUI界面
高级软件工程第三次作业:基于第二次数独游戏,添加GUI界面.GUI界面代码如下: package firstGui; import java.awt.*; import java.awt.event. ...
- Java练习(模拟扫雷游戏)
要为扫雷游戏布置地雷,扫雷游戏的扫雷面板可以用二维int数组表示.如某位置为地雷,则该位置用数字-1表示, 如该位置不是地雷,则暂时用数字0表示. 编写程序完成在该二维数组中随机布雷的操作,程序读入3 ...
随机推荐
- 正则表达式、datetime
1.正则表达式就是用来匹配字符串的 2.常用\d表示一个数字,\w表示数字或者字母,'.'表示任意字符 3.如果要匹配边长的字符串,使用*表示任意个字符,+表示至少一个字符,?表示0个或者1个字符,{ ...
- 带你揭开神秘的javascript AST面纱之AST 基础与功能
作者:京东科技 周明亮 AST 基础与功能 在前端里面有一个很重要的概念,也是最原子化的内容,就是 AST ,几乎所有的框架,都是基于 AST 进行改造运行,比如:React / Vue /Taro ...
- 32-webpack详细配置-entry
const HtmlWebpackPlugin = require('html-webpack-plugin') const {resolve} = require('path') /** * ent ...
- ARL:资产侦察灯塔系统
资产灯塔,不仅仅是域名收集 功能简介 "挖洞神器"资产安全灯塔(ARL),旨在快速侦察与目标关联的互联网资产,构建基础资产信息库. 协助甲方安全团队或者渗透测试人员有效侦察和检索资 ...
- MySQL-(InnoDB)事务和锁
在事务并行处理背景下,不同的事务之间因数据共享的状态变化,存在着某种依赖/隔离影响.即事务隔离级别. 事务隔离级别,官网的解释在这里. InnoDB提供 SQL:1992 标准描述的所有四种事务隔离级 ...
- SpringBoot开启日志级别
#开启logging logging.level.org.springframework.boot.autoconfigure: error logging: level: main.blog.map ...
- [Pytorch框架] 3.3 通过Sin预测Cos
文章目录 3.3 通过Sin预测Cos 3.3 通过Sin预测Cos %matplotlib inline import torch import torch.nn as nn from torch. ...
- Hugging News #0428: HuggingChat 来啦
每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...
- 【必知必会的MySQL知识】④DCL语言
目录 一.概述 二 .授权 2.1 语法格式 2.2 语法说明 2.3 权限类型 2.4 权限级别 三. 回收权限 3.1 语法格式 3.2 语法说明 3.3 注意事项 四 .实践操作 一.概述 数据 ...
- SQL Server数据库判断最近一次的备份执行结果
1 麻烦的地方 在SQL Server的官方文档里面可以看到备份和还原的表,但是这些表里面只能找到备份成功的相关信息,无法找到备份失败的记录,比如msdb.dbo.backupset.对于一些监控系统 ...