一个JFX版本俄罗斯方块写了半个月也是够了

目录

先截个图

算法

定义了三个数据结构

一个游戏逻辑类

界面渲染

用到的工具技术

Gradle构建工具

Eclipse IDE

jfx

多线程

java模块

声效


先截个图

一晃七月就过去了,本想写几篇教程的,不过看到各路大神的文章,有深度的、有浅显的,本来表达能力有限,再怎么组织语言也拼不过那些日日更新的博主们,自己再去写教程简直是多此一举。想想还是写一些小东西玩玩吧,也不枉自己当了十几年的程序员。

一时半会也不知道写点什么,就做个简易的俄罗斯方块吧。这个游戏的算法早就想出来了,但JFX是第一次接触,一边查资料,一边写,前前后后用了将近半个月时间,终于初见成效。目前完成了俄罗斯方块的基本操作,形状颜色区分、算分、升级加速、形状预览等功能。下图是游戏中的一个截图,玩法跟常规的俄罗斯方块无异:

俄罗斯方块

算法

这个俄罗斯方块的算法很简单,用20*10个球作为前端的呈现,后台放一个对应维数的数组来保存每个球的状态,每次移动完刷新界面,以呈现移动动画的效果。无键盘操作的时候,使用一个定时器,自动向下移动方块。移动到底部的时候,消去满行,将上部方块向移动,并计算分数。

定义了三个数据结构

一个是移动的形状,是一个4*4的矩阵,加上top、left、height、width、corlor五个属性。

public class ShapeStruct {
//省略代码...
	public int left;
	public int top;
	public boolean[][] data;
	public String color;
	private int width;
	private int height;
//省略代码...
}

表示格子的定义

public class BlockCell {
	public String color = "#FFFFFF";
	public boolean visable = false;
}

预定义形状的定义,比ShapeStruct 少了两个位置属性。

public class Block {
//省略代码...

	public boolean[][] data;
	public String color;
	private int width;
	private int height;
//省略代码...
}

一个游戏逻辑类

随机形状、移动、消行、算分等都在这个类里边实现。这个类里边不涉及UI元素,纯粹游戏逻辑。

比如消行部分的逻辑实现:

	private void removeRows() {
		if (preview != null) {
			for (var row = 0; row < preview.getHeight(); row++) {
				for (var col = 0; col < preview.getWidth(); col++) {
					if (preview.getCell(row, col)) {
						var cell = boardData[preview.getTop() + row][preview.getLeft() + col];
						cell.visable = true;
						cell.color = preview.color;
					}
				}
			}
		}
		preview = null;

		int removeCount = 0;
		for (int row = 0; row < 20; row++) {
			boolean b = true;
			for (int col = 0; col < 10; col++) {
				var cube = boardData[row][col].visable;
				b = b && cube;
			}

			if (b) {
				removed++;
				removeCount++;
				for (int ui = row - 1; ui >= 0; ui--) {
					for (int uj = 0; uj < 10; uj++) {
						var pre = boardData[ui][uj];
						boardData[ui + 1][uj].visable = pre.visable;
						boardData[ui + 1][uj].color = pre.color;
					}
				}
			}
		}

		var tempScore = 0;
		if (removeCount == 1)
			tempScore += 100;
		if (removeCount == 2)
			tempScore += 200;
		if (removeCount == 3)
			tempScore += 400;
		if (removeCount == 4)
			tempScore += 400;

		score += tempScore;
		levelScore += tempScore;

		if (levelScore >= 2000) {
			if (level < 12) {
				level++;
				ticks = 1065 - level * 65;
			} else
				level = 1;

			levelScore = 0;
		}
	}

界面渲染

预先画好20*10个球形,为了方便后边的操作,创建一个数据存放这些球形的引用。

for (int i = 0; i < 20; i++) {
			for (int j = 0; j < 10; j++) {
				Ellipse ellipse = new Ellipse();
				ellipse.setCenterX(j * 30.0f + 15);
				ellipse.setCenterY((19 - i) * 30.0f + 15);
				ellipse.setRadiusX(15);
				ellipse.setRadiusY(15.0f);
				ellipse.setFill(Color.WHITE);
				board.getChildren().add(ellipse);
				cubes[19 - i][j] = ellipse;
			}
		}

界面更新的核心代码,迭代后台数据,并更新到界面上来。

	/**
	 *
	 * 刷新界面
	 */
	private void render() {

		var datas = data.getBoardData();

		for (int row = 0; row < 20; row++) {
			for (int col = 0; col < 10; col++) {
				var cell = datas[row][col];
				Ellipse cube = cubes[row][col];
				if (!cell.visable) {
					cube.setFill(Color.WHITE);
				} else {
					var c = cell.color;
					Stop[] stops = { new Stop(0, Color.WHITE), new Stop(1, Color.valueOf(c)) };

					RadialGradient gradient = new RadialGradient(0.5, 0.5, 0.5, 0.5, 0.5, true, CycleMethod.NO_CYCLE,
							stops);
					cube.setFill(gradient);
				}
			}
		}

		if (data.getCurrent() == null)
			return;
		var arr = data.getCurrent().getData();

		int sh = data.getCurrent().getHeight();
		int sw = data.getCurrent().getWidth();

		for (int row = 0; row < sh; row++) {
			for (var col = 0; col < sw; col++) {
				if (arr[row][col]) {
					var cube = cubes[data.getCurrent().getTop() + row][data.getCurrent().getLeft() + col];
					var c = data.getCurrent().getColor();

					// cube.setFill(Color.valueOf(c));
					Stop[] stops = { new Stop(0, Color.WHITE), new Stop(1, Color.valueOf(c)) };

					RadialGradient gradient = new RadialGradient(0.5, 0.5, 0.5, 0.5, 0.5, true, CycleMethod.NO_CYCLE,
							stops);
					cube.setFill(gradient);
				}

			}

			for (int i = 0; i < 4; i++) {
				for (int j = 0; j < 4; j++) {
					next[i][j].setFill(
							data.getNext().getData()[i][j] ? Color.valueOf(data.getNext().getColor()) : Color.DARKGRAY);
				}
			}
		}

		Platform.runLater(new Runnable() {
			@Override
			public void run() {
				score.setText(String.format("分数:%d", data.getScore()));
				level.setText(String.format("级别:1", data.getLevel()));

				if (data.isFull()) {
					started = false;

					ButtonType loginButtonType = new ButtonType("OK", ButtonData.OK_DONE);
					Dialog<String> dialog = new Dialog<>();
					dialog.setTitle("Game Over!");
					dialog.setContentText("Game Over!");
					dialog.getDialogPane().getButtonTypes().add(loginButtonType);

					boolean disabled = false; // computed based on content of text fields, for example
					dialog.getDialogPane().lookupButton(loginButtonType).setDisable(disabled);
					dialog.showAndWait();
				}
			}
		});

	}

 

用到的工具技术

Gradle构建工具

使用Gradle的目的是依赖管理和编译,Java9以后,Jfx从JDK中分离出来,成了独立的项目。为了使用jfx,可以下载jfx的sdk,也可以使用依赖管理工具自动下载,并加入到引用包中,此时使用Gradle就很方便了。下面是build.gradle的内容:

/*
 * This file was generated by the Gradle 'init' task.
 */
plugins {
	id 'application'
	id 'org.openjfx.javafxplugin' version '0.0.9'
	id 'org.beryx.jlink' version '2.20.0'
}
javafx {
	version = "14"
	modules = ['javafx.controls','javafx.fxml','javafx.base']
}
jlink {
	launcher {
		name = 'tetris'
	}
	mergedModule {
		additive = true  // redundant, because excludeXXX() methods are also present
		//requires 'java.ws.rs'

	}
}
java {
	modularity.inferModulePath = true
	
}

mainClassName = "tetris.Main"

dependencies {

}

Eclipse IDE

IDE就不用讲了,强大的代码编辑功能,为写程序提供了很多方便。

jfx

jfx是个兼容并包的东西,如果你有微软WPF的经验,会发现jfx很容易上手,它们在设计上有着相似的理念。另外,jfx可以用html+css3来渲染,因而有web开发经验的同学上手jfx也不算难。如果有awt和swing的经验的同学,会发现jfx基本就是复制了一份类库出来,因而上手也比较容易。

多线程

游戏中多线程用法很简单,目的也很单纯,就是用定时器自动下移方块。为了保证键盘操作与定时器操作的同步,使用了少量的锁。比如:

	public synchronized void moveDown() {

		if (!this.isBottom()) {
			current.moveDown();
		} else {

			remove();

		}
	}

定时器用的JDK中自带的Timer类,因为这个Timer不能动态更改时间间隔,便使用了类似javascript中的setTimeout函数的用法,使用嵌套的方式无限循环,直到游戏结束。

	private void timerTask() {
		render();

		timer.schedule(new TimerTask() {

			@Override
			public void run() {
				data.moveDown();
				if (!data.isFull())
					timerTask();

			}
		}, data.getTicks());
	}

java模块

使用java模块,可以在打包的时候,打包最小依赖集。使用jlink生成独立的可执行程序,并且不需要单独安装独立的jre环境。

声效

声音使用的是Mp3格式的音频,使用JFX自带的媒体播放类。声音采用异步播放。

var audioSrc = getClass().getResource("1.mp3");
		var m = new Media(audioSrc.toString());

		audio1 = new MediaPlayer(m);
		audioSrc = getClass().getResource("2.mp3");
		m = new Media(audioSrc.toString());
		audio2 = new MediaPlayer(m);

		data.setOnRemove(() -> {
			audio2.stop();
			audio2.play();
		});

项目地址:https://gitee.com/abiao_cn/frontend4j

源码下载(不包含项目文件):https://download.csdn.net/download/icoolno1/12671938

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页