基于JAVA的2048小游戏的二次开发

引言
《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语言的使用也有了提升。

posted @ 2024-03-05 16:13  祝一夏  阅读(40)  评论(0编辑  收藏  举报