引言
《2048Numberpuzzlegame》是一款数字益智游戏,而《2048》的初始数字则是由2+2组成的基数4。在操作方面的不同则表现为一步一格的移动,变成更为爽快的一次到底。相同数字的方框在靠拢、相撞时会相加。系统给予的数字方块不是2就是4,玩家要想办法在这小小的16格范围中凑出「2048」这个数字方块。
游戏规则很简单,每次可以选择上下左右其中一个方向去滑动,每滑动一次,所有的数字方块都会往滑动的方向靠拢外,系统也会在空白的地方乱数出现一个数字方块,相同数字的方块在靠拢、相撞时会相加。系统给予的数字方块不是2就是4,玩家要想办法在这小小的16格范围中凑出“2048”这个数字方块。
游戏的画面很简单,一开整体16个方格大部分都是灰色的,当玩家拼图出现数字之后就会改变颜色,整体格调很是简单。
在玩法规则也非常的简单,一开始方格内会出现2或者4等这两个小数字,玩家只需要上下左右其中一个方向来移动出现的数字,所有的数字就会向滑动的方向靠拢,而滑出的空白方块就会随机出现一个数字,相同的数字相撞时会叠加靠拢,然后一直这样,不断的叠加最终拼凑出2048这个数字就算成功。
分析
类:
最基础的是CardPane,继承自BorderPane,作为数字卡片。
然后是由数字卡片组成的矩阵CardMatrixPane,继承自StackPane
CardColor,里面只有一个静态的Color数组,用来搞卡片的背景颜色
settings类,红框标识工具
源代码:
CardPane:
Settings:
import javafx.beans.property.SimpleBooleanProperty;
public final class _Settings {
public static SimpleBooleanProperty NEEDHL=new SimpleBooleanProperty();//默认合并卡片红色边框
}
CardMatrixPane:
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
public class _CardMatrixPane extends StackPane {
private Callbacks mCallbacks;
private int cols;//卡片矩阵列数
private int rows;//卡片矩阵行数
private GridPane gridPane;//卡片矩阵容器
private _CardPane[][] cps;//卡片矩阵
private final Random rand=new Random();
private int[] mcQuantities=new int[15];//合并过的卡片数字数量,包括4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536
/**回调接口*/
public _CardMatrixPane(Application application) {
this(4,4,application);//默认4*4
}
public _CardMatrixPane(int cols,int rows,Application application) {//application供回调方法使用
mCallbacks=(Callbacks)application;
this.cols=cols;
this.rows=rows;
// this.setBackground(new Background(new BackgroundFill(Color.BLUE,CornerRadii.EMPTY,Insets.EMPTY)));//测试用
initGridPane();//初始化GridPane
createRandomNumber();//在随机的空卡片上生成数字,这个方法会返回布尔值
getChildren().add(gridPane);
}
/**初始化GridPane*/
private void initGridPane() {
gridPane=new GridPane();
// gridPane.setBackground(new Background(new BackgroundFill(Color.YELLOW,CornerRadii.EMPTY,Insets.EMPTY)));//测试用
// gridPane.setGridLinesVisible(true);//单元格边框可见,测试用
//对this尺寸监听
widthProperty().addListener(ov->setGridSizeAndCardFont());//宽度变化,更新边长和字号
heightProperty().addListener(ov->setGridSizeAndCardFont());//高度变化,更新边长和字号
//单元格间隙
gridPane.setHgap(5);
gridPane.setVgap(5);
//绘制每个单元格
cps=new _CardPane[cols][rows];
for(int i=0;i<cols;i++) {//遍历卡片矩阵的列
for(int j=0;j<rows;j++) {//遍历卡片矩阵的行
_CardPane card=new _CardPane(0);
gridPane.add(card,i,j);
cps[i][j]=card;
}
}
}
/**设置GridPane的边长,其内部单元格的尺寸和CardPane的字号*/
private void setGridSizeAndCardFont(){
double minSide=Math.min(widthProperty().get(),heightProperty().get());
gridPane.setMaxWidth(minSide);
gridPane.setMaxHeight(minSide);
for(_CardPane[] row:cps) {
for(_CardPane card:row) {
card.getLabel().setFont(new Font((minSide/14)/cols*4));//设置显示数字的尺寸
//由于下面两行代码主动设置了每个单元格内cardPane的尺寸,gridPane不需要自动扩张
card.setPrefWidth(minSide-5*(cols-1));//设置单元格内cardPane的宽度,否则它会随其内容变化,进而影响单元格宽度
card.setPrefHeight(minSide-5*(rows-1));//设置单元格内cardPane的高度,否则它会随其内容变化,进而影响单元格高度
}
}
}
/**添加键盘监听*/
public void createKeyListener() {
setOnKeyPressed(e->{
_CardPane maxCard=getMaxCard();//最大卡片
if(maxCard.getType()==16) {//出现最大数字
Alert alert=new Alert(AlertType.INFORMATION);
alert.setTitle(alert.getAlertType().toString());
alert.setContentText("恭喜你,游戏的最大数字为"+maxCard.getNumber()+",可在菜单栏选择重新开始\n"+
"事实上,我们还尚未准备比"+maxCard.getNumber()+"更大的数字卡片,终点已至");
return;
}
KeyCode kc=e.getCode();
boolean suc=false;
switch(kc) {
case UP:
case W:
suc=goUp();//↑
break;
case DOWN:
case S:
suc=goDown();//↓
break;
case LEFT:
case A:
suc=goLeft();//←
break;
case RIGHT:
case D:
suc=goRight();//→
break;
default://尚未定义的操作
return;
}
redrawAllCardsAndResetIsMergeAndSetScore();//重绘所有的卡片,并重设合并记录,更新分数
if(!suc) {//失败的操作
return;
}
boolean isFull=!createRandomNumber();//生成新的随机数字卡片,并判满,这包含了生成数字后满的情况
if(isFull) {//矩阵已满,可能已经游戏结束
boolean canMove=testUp()||testLeft();
if(!canMove) {//游戏结束
Alert alert=new Alert(AlertType.INFORMATION);
alert.setTitle(alert.getAlertType().toString());
alert.setContentText("游戏结束,本次最大数字为"+maxCard.getNumber()+",可在菜单栏选择重新开始\n");
}
}
});
}
/**向上操作,返回操作成功与否*/
private boolean goUp() {
boolean suc=false;
boolean mergeOrMoveExist;//矩阵的这次操作的一次遍历中是否存在移动或合并
do {
mergeOrMoveExist=false;//初始为false
for(int i=0;i<cols;i++) {//遍历卡片矩阵的列
for(int j=1;j<rows;j++) {//从第二行起向下,遍历卡片矩阵的行
_CardPane card=cps[i][j];
_CardPane preCard=cps[i][j-1];//前一个卡片
boolean isChanged=card.tryMergeOrMoveInto(preCard);//记录两张卡片间是否进行了移动或合并
mergeOrMoveExist|=isChanged;//只要有一次移动或合并记录,就记存在为true
}
}
suc|=mergeOrMoveExist;
}while(mergeOrMoveExist);//如果存在移动或合并,就可能需要再次遍历,继续移动或合并
return suc;
}
/**测试是否能向上操作*/
private boolean testUp() {
for(int i=0;i<cols;i++) {//遍历卡片矩阵的列
for(int j=1;j<rows;j++) {//从第二行起向下,遍历卡片矩阵的行
_CardPane card=cps[i][j];
_CardPane preCard=cps[i][j-1];//前一个卡片
if(card.canMergeOrMove(preCard)) {
return true;//能
}
}
}
return false;//不能
}
/**向下操作,返回操作成功与否*/
private boolean goDown() {
boolean suc=false;
boolean mergeOrMoveExist;//矩阵的这次操作的一次遍历中是否存在移动或合并
do {
mergeOrMoveExist=false;//初始为false
for(int i=0;i<cols;i++) {//遍历卡片矩阵的列
for(int j=rows-2;j>=0;j--) {//从倒数第二行起向上,遍历卡片矩阵的行
_CardPane card=cps[i][j];
_CardPane preCard=cps[i][j+1];//前一个卡片
boolean isChanged=card.tryMergeOrMoveInto(preCard);//记录两张卡片间是否进行了移动或合并
mergeOrMoveExist|=isChanged;//只要有一次移动或合并记录,就记存在为true
}
}
suc|=mergeOrMoveExist;
}while(mergeOrMoveExist);//如果存在移动或合并,就可能需要再次遍历,继续移动或合并
return suc;
}
/**向左操作,返回操作成功与否*/
private boolean goLeft() {
boolean suc=false;
boolean mergeOrMoveExist;//矩阵的这次操作的一次遍历中是否存在移动或合并
do {
mergeOrMoveExist=false;//初始为false
for(int i=1;i<cols;i++) {//从第二列起向右,遍历卡片矩阵的列
for(int j=0;j<rows;j++) {//遍历卡片矩阵的行
_CardPane card=cps[i][j];
_CardPane preCard=cps[i-1][j];//前一个卡片
boolean isChanged=card.tryMergeOrMoveInto(preCard);//记录两张卡片间是否进行了移动或合并
mergeOrMoveExist|=isChanged;//只要有一次移动或合并记录,就记存在为true
}
}
suc|=mergeOrMoveExist;
}while(mergeOrMoveExist);//如果存在移动或合并,就可能需要再次遍历,继续移动或合并
return suc;
}
/**测试是否能向左操作*/
private boolean testLeft() {
for(int i=1;i<cols;i++) {//从第二列起向右,遍历卡片矩阵的列
for(int j=0;j<rows;j++) {//遍历卡片矩阵的行
_CardPane card=cps[i][j];
_CardPane preCard=cps[i-1][j];//前一个卡片
if(card.canMergeOrMove(preCard)) {
return true;//能
}
}
}
return false;//不能
}
/**向右操作,返回操作成功与否*/
private boolean goRight() {
boolean suc=false;
boolean mergeOrMoveExist;//矩阵的这次操作的一次遍历中是否存在移动或合并
do {
mergeOrMoveExist=false;//初始为false
for(int i=cols-2;i>=0;i--) {//从倒数第二列起向左,遍历卡片矩阵的列
for(int j=0;j<rows;j++) {//遍历卡片矩阵的行
_CardPane card=cps[i][j];
_CardPane preCard=cps[i+1][j];//前一个卡片
boolean isChanged=card.tryMergeOrMoveInto(preCard);//记录两张卡片间是否进行了移动或合并
mergeOrMoveExist|=isChanged;//只要有一次移动或合并记录,就记存在为true
}
}
suc|=mergeOrMoveExist;
}while(mergeOrMoveExist);//如果存在移动或合并,就可能需要再次遍历,继续移动或合并
return suc;
}
mCallbacks.afterScoreChange();
}
/**获取卡片矩阵中的最大卡片*/
private _CardPane getMaxCard() {
_CardPane maxCard=new _CardPane();//type=0的新卡片
for(_CardPane[] row:cps) {
for(_CardPane card:row) {
if(card.getType()>maxCard.getType()) {
maxCard=card;
}
}
}
return maxCard;
}
/**在随机的空卡片上生成新的数字,若矩阵已满,或生成数字后满,则返回false*/
public boolean createRandomNumber() {
List<_CardPane> voidCards=new ArrayList<>();//空卡片列表
for(_CardPane[] row:cps) {
for(_CardPane card:row) {
if(card.getType()==0) {//是空卡片
voidCards.add(card);//添加到列表中
}
}
}
int len=voidCards.size();
if(len==0) {//没有空卡片了,返回
return false;//判满
}
_CardPane card=voidCards.get((int)(rand.nextDouble()*len));
card.setType(rand.nextInt(5)!=0?1:2);//设置type
card.draw();
return len!=1;//len==1,也满
}
/**重启卡片矩阵,并在随机的空卡片上生成数字*/
public void restartMatrix() {
for(_CardPane[] row:cps) {
for(_CardPane card:row) {
card.setType(0);
card.draw();//重绘
}
}
mcQuantities=new int[15];//重设合并过的卡片数字数量
mCallbacks.afterScoreChange();
createRandomNumber();//在随机的空卡片上生成数字,这个方法会返回布尔值,但这里不需要
}
/**进行颜色测试,可在4*4矩阵中显示2至65536*/
public void testColors() {
for(int i=0;i<cols;i++) {//遍历卡片矩阵的列
for(int j=0;j<rows;j++) {//遍历卡片矩阵的行
_CardPane card=cps[i][j];
int type=i*4+j+1;
if(type>16) {
return;
}
card.setType(i*4+j+1);
card.draw();//重绘
}
}
}
}
CardColor:
原来的游戏页面
不足
只有单纯的游戏,并且画面过于简洁,增加了分数的计算和记录
额外增加了菜单栏类和卡片的美化
改动:
卡片的美化:
分数的记录:
菜单栏的增设:
最终的画面呈现:
项目结语:
通过对2048小游戏的二次开发,我发现即使很简单的小游戏里面也有很大的学问,需要注意操作的流畅性,用户界面的美化,相关内容的记录;在多次查找相关内容后进行综合,才能够利用好已学知识。在修改的过程,我也询问了原作者相关的内容。获得了很大的帮助,对项目有了更深层次的掌握。当修改时,我们要注意到前后的衔接,类名、成员、方法的命名规范,最好借助思维图帮助自身梳理。这次的项目改进令我受益匪浅,对JAVA语言的使用也有了提升。