详解Java实现2048小游戏(JavaFX,完整源码+注释说明)

刚刚完成的一个小游戏,写文章记录一下,如果有任何错误或者可以改进的代码请提出
另一方面也是方便自己几个月或几年后忘记时,来这里翻一翻回顾思路

目录

基本界面:

类的组织:

_CardPane:

_CardMatrixPane:

_CardColor:

_GameMenuBar:

_2048Demo:

基本思路:

卡片:

卡片矩阵:

颜色:

游戏菜单:

控制器:


首先放一下效果图:

基本界面:

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第1张图片

 详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第2张图片

所有卡片

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第3张图片

分数统计

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第4张图片

尺寸为5x5或6x6

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第5张图片

 详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第6张图片

类的组织:

五个类,最基础的是_CardPane,继承自BorderPane,作为数字卡片。它里面有一个Rectangle,用来表示卡片的圆角矩形背景,以及一个Label来显示数字

然后是由数字卡片组成的矩阵_CardMatrixPane,继承自StackPane,它包含一个GridPane

_CardColor,里面只有一个静态的Color数组,用来搞卡片的背景颜色

_GameMenuBar作为游戏的菜单栏,继承自MenuBar

最后是_2048Demo,相当于控制器

这里类名前面加下划线是个人习惯,因为我的Eclipse项目名、包名、类名等等都会与图标重合一些,加下划线可以看的方便,如下:

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第7张图片

下面放代码:

_CardPane:

package _2048._node;

import _2048._model._CardColor;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

/**
 * 节点类——数字卡片
 * @author 邦邦拒绝魔抗
 * 
 */
//若继承自Pane类,缺少需要的setAlignment()方法
//若继承自StackPane类,会出现一些绘制错误
public class _CardPane extends BorderPane {
	private static final int RC=5;//矩形的圆角
	private int type;
	/* 类型
	 * type=0	number=0
	 * type=1	number=2
	 * type=2	number=4
	 * type=3	number=8
	 * ...
	 */
	private boolean merge=false;//是否被合并过,如果合并了,则不能继续合并,针对当前轮
	private Rectangle r;//圆角矩形
	private Label l;//数字标签
	
	/**无参构造方法*/
	public _CardPane() {
		this(0);
	}
	
	/**构造方法,通过下标和类型生成数字卡片*/
	public _CardPane(int type) {
		this.type=type;
		//圆角矩形
		r=new Rectangle();
		r.widthProperty().bind(this.widthProperty());//矩形的宽度绑定单元格宽度
		r.heightProperty().bind(this.heightProperty());//矩形的高度绑定单元格高度
		r.setArcWidth(RC);//圆角宽度
		r.setArcHeight(RC);//圆角高度
		r.setStroke(Color.BLACK);//边框颜色
		r.setStrokeWidth(3);//边框宽度
		getChildren().add(r);
		//数字标签
		l=new Label("65536");//65536是4*4情况下可能出现的最大数字
		setCenter(l);
		//绘制变化的部分
		draw();
	}
	
	/**获取数字标签对象*/
	public Label getLabel() {
		return l;
	}
	
	/**设置卡片类型*/
	public void setType(int type) {
		this.type=type;
	}
	
	/**获取卡片类型*/
	public int getType() {
		return type;
	}
	
	/**设置合并记录*/
	public void setMerge(boolean merge) {
		this.merge=merge;
	}
	
	/**获取合并记录*/
	public boolean isMerge() {
		return merge;
	}
	
	/**绘制单次操作中卡片变化的部分,包括颜色和显示的数字*/
	public void draw() {
		if(merge) {//突出显示已合并的卡片
			r.setStroke(Color.RED);//此次操作中合并,显示红色
		}else {
			r.setStroke(Color.BLACK);//此次操作中没有合并,显示黑色
		}
		r.setFill(_CardColor.CB[type]);
		drawNumber();
	}
	
	/**判断此卡片能否向调用者所给出的卡片移动或合并*/
	public boolean canMergeOrMove(_CardPane card) {
		if(type==0) {//空卡片不能移动或合并
			return false;
		}
		if(card.type==0) {//可以向空卡片移动
			return true;
		}
		return type==card.getType()&&!merge&&!card.isMerge();//不能二次合并
	}
	
	/**尝试向调用者所给出的卡片移动或合并,这一函数可能会修改两个卡片的属性*/
	public boolean tryMergeOrMoveInto(_CardPane card) {
		boolean canMergeOrMove=canMergeOrMove(card);
		if(canMergeOrMove) {//可以移动或合并
			if(card.getType()==0) {//移动
				card.setType(type);//移动数字
				card.setMerge(merge);//移动合并记录
				this.toVoid();//this成为空卡片
			}else {//合并
				card.setType(card.getType()+1);//合并数字
				card.setMerge(true);//设置合并记录
				this.toVoid();//this成为空卡片
			}
		}
		return canMergeOrMove;
	}
	
	/**刷新为空卡片*/
	private void toVoid() {
		type=0;
		merge=false;
	}
	
	private void drawNumber() {
		if(type==0) {//空卡片
			l.setText("");
		}else {//非空卡片需要显示数字
			l.setText(""+getNumber());
		}
	}
	
	/**计算需显示的数字*/
	public int getNumber() {
		return (int)Math.pow(2,type);
	}
	
	@Override
	public String toString() {
		return "[type="+type+", merge="+merge+"]";
	}
}

_CardMatrixPane:

package _2048._node;

import java.util.ArrayList;
import java.util.List;

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;

/**
 * 节点类——卡片矩阵
 * @author 邦邦拒绝魔抗
 * 
 */
//若继承自Pane类,缺少需要的setAlignment()方法
public class _CardMatrixPane extends StackPane {
	private Callbacks mCallbacks;
	private int cols;//卡片矩阵列数
	private int rows;//卡片矩阵行数
	private GridPane gridPane;//卡片矩阵容器
	private _CardPane[][] cps;//卡片矩阵
	private long score=0;//分数,初始为0
	private int[] mcQuantities=new int[15];//合并过的卡片数字数量,包括4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536
	
	/**回调接口*/
	public interface Callbacks {
		void afterScoreChange();//分数变化
	}
	
	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)));//测试用
		init();
		getChildren().add(gridPane);
	}
	
	/**获取分数*/
	public long getScore() {
		return score;
	}
	
	/**获取合并过的卡片数字数量*/
	public int[] getMcQuantities() {
		return mcQuantities;
	}
	
	private void init() {
		initGridPane();//初始化GridPane
		createRandomNumber();//在随机的空卡片上生成数字,这个方法会返回布尔值,但这里不需要
	}
	
	/**初始化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{//键盘按下事件
			_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()+"更大的数字卡片,终点已至");
				alert.show();
				return;
			}
			KeyCode kc=e.getCode();
			switch(kc) {
			case UP:
			case W:
				goUp();//↑
				break;
			case DOWN:
			case S:
				goDown();//↓
				break;
			case LEFT:
			case A:
				goLeft();//←
				break;
			case RIGHT:
			case D:
				goRight();//→
				break;
			default:
				return;//未定义的操作
			}
			redrawAllCardsAndResetIsMergeAndSetScore();//重绘所有的卡片,并重设合并记录,更新分数
			boolean isFull=!createRandomNumber();//生成新的随机数字卡片,并判满,这包含了生成数字后满的情况
			if(isFull) {//矩阵已满,可能已经游戏结束
				boolean testOpe=false;//是否还能进行横向或竖向操作
				testOpe|=testUp();//还能进行竖向操作
				testOpe|=testLeft();//还能进行横向操作
				if(!testOpe) {//游戏结束
					Alert alert=new Alert(AlertType.INFORMATION);
					alert.setTitle(alert.getAlertType().toString());
					alert.setContentText("游戏结束,本次最大数字为"+maxCard.getNumber()+",可在菜单栏选择重新开始\n");
					alert.show();
				}
			}
		});
	}
	
	/**向上操作*/
	private void goUp() {
		boolean mergeOrMoveExist;//矩阵的这次操作的一次遍历中是否存在移动或合并
		do {
			mergeOrMoveExist=false;//初始为false
			for(int i=0;i=0;j--) {//从倒数第二行起向上,遍历卡片矩阵的行
					_CardPane card=cps[i][j];
					_CardPane preCard=cps[i][j+1];//前一个卡片
					boolean isChanged=card.tryMergeOrMoveInto(preCard);//记录两张卡片间是否进行了移动或合并
					mergeOrMoveExist|=isChanged;//只要有一次移动或合并记录,就记存在为true
				}
			}
		}while(mergeOrMoveExist);//如果存在移动或合并,就可能需要再次遍历,继续移动或合并
	}
	
	/**向左操作*/
	private void goLeft() {
		boolean mergeOrMoveExist;//矩阵的这次操作的一次遍历中是否存在移动或合并
		do {
			mergeOrMoveExist=false;//初始为false
			for(int i=1;i=0;i--) {//从倒数第二列起向左,遍历卡片矩阵的列
				for(int j=0;jmaxCard.getType()) {
					maxCard=card;
				}
			}
		}
		return maxCard;
	}
	
	/**在随机的空卡片上生成新的数字,若矩阵已满,或生成数字后满,则返回false*/
	public boolean createRandomNumber() {
		List<_CardPane> voidCards=new ArrayList<>();//空卡片列表
		
		for(int i=0;i16) {
					return;
				}
				card.setType(i*4+j+1);
				card.draw();//重绘
			}
		}
	}
}

_CardColor:

package _2048._model;

import javafx.scene.paint.Color;

public class _CardColor {
	public static Color[] CB= {//卡片颜色
			Color.rgb(255,255,255),//0
			//235
			//195*2
			Color.rgb(235,195,195),//2
			Color.rgb(195,235,195),//4
			Color.rgb(195,195,235),//8
			//195
			//215*2
			Color.rgb(195,215,215),//16
			Color.rgb(215,195,215),//32
			Color.rgb(215,215,195),//64
			//175
			//225*2
			Color.rgb(175,225,225),//128
			Color.rgb(225,175,225),//256
			Color.rgb(225,225,175),//512
			//235
			//155*2
			Color.rgb(235,155,155),//1024
			Color.rgb(155,235,155),//2048
			Color.rgb(155,155,235),//4096
			//115
			//255*2
			Color.rgb(115,255,255),//8192
			Color.rgb(255,115,255),//16384
			Color.rgb(255,255,115),//32768
			
			Color.rgb(195,195,195),//65536
	};
	public static Color[] CF= {//数字颜色
			
	};
}

_GameMenuBar:

package _2048._node;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Alert.AlertType;

/**
 * 节点类——2048游戏菜单栏
 * @author 邦邦拒绝魔抗
 *
 */
public class _GameMenuBar extends MenuBar {
	private Callbacks mCallbacks;
	private Menu scoreMenu;
	
	/**回调接口*/
	public interface Callbacks {
		void afterRestart();//重新开始
		void afterResetGridSize(int cols,int rows);//重设表格尺寸
		void afterGetMoreScoreInfo();//获取更详细的分数信息
	}
	
	public _GameMenuBar(Application application) {//application供回调方法使用
		mCallbacks=(Callbacks)application;
		//Game菜单
		Menu gameMenu=new Menu("游戏");//游戏
		MenuItem restartMenuItem=new MenuItem("重新开始");//重新开始
		restartMenuItem.setOnAction(e->mCallbacks.afterRestart());
		MenuItem exitMenuItem=new MenuItem("退出");//退出
		exitMenuItem.setOnAction(e->Platform.exit());
		gameMenu.getItems().addAll(restartMenuItem,exitMenuItem);
		
		//Setting菜单
		Menu settingMenu=new Menu("设置");//设置
		ToggleGroup tg=new ToggleGroup();//组
		
		RadioMenuItem r44MenuItem=new RadioMenuItem("尺寸:4x4");
		r44MenuItem.setOnAction(e->mCallbacks.afterResetGridSize(4,4));
		RadioMenuItem r55MenuItem=new RadioMenuItem("尺寸:5x5");
		r55MenuItem.setOnAction(e->mCallbacks.afterResetGridSize(5,5));
		RadioMenuItem r66MenuItem=new RadioMenuItem("尺寸:6x6");
		r66MenuItem.setOnAction(e->mCallbacks.afterResetGridSize(6 ,6));
		
		r44MenuItem.setToggleGroup(tg);
		r55MenuItem.setToggleGroup(tg);
		r66MenuItem.setToggleGroup(tg);
		settingMenu.getItems().addAll(r44MenuItem,r55MenuItem,r66MenuItem);
		r44MenuItem.setSelected(true);//默认选中4x4
		
		//Info菜单
		Menu infoMenu=new Menu("信息");//信息
		MenuItem helpMenuItem=new MenuItem("帮助");//帮助
		helpMenuItem.setOnAction(e->{
			Alert alert=new Alert(AlertType.INFORMATION);
			alert.setTitle(alert.getAlertType().toString());
			alert.setContentText("操作方式:\n"+
					"向上滑动:方向键↑或键W\n"+
					"向下滑动:方向键↓或键S\n"+
					"向左滑动:方向键←或键A\n"+
					"向右滑动:方向键→或键D\n"+
					"\n游戏规则:\n"+
					"相同数字的卡片在靠拢、相撞时会合并\n"+
					"在操作中合并的卡片会以红色边框凸显\n尽可能获得更大的数字!");
			alert.show();
		});
		MenuItem aboutUsMenuItem=new MenuItem("关于我们");//关于我们
		aboutUsMenuItem.setOnAction(e->{
			Alert alert=new Alert(AlertType.INFORMATION);
			alert.setTitle(alert.getAlertType().toString());
			alert.setContentText("游戏作者:邦邦拒绝魔抗\n他的邮箱:[email protected]\n\n感谢你的游玩!");
			alert.show();
		});
		infoMenu.getItems().addAll(helpMenuItem,aboutUsMenuItem);
		
		//Record菜单
		Menu recordMenu=new Menu("记录");//记录
		MenuItem historyScoreMenuItem=new MenuItem("历史分数");//历史分数
		historyScoreMenuItem.setOnAction(e->{
			Alert alert=new Alert(AlertType.INFORMATION);
			alert.setTitle(alert.getAlertType().toString());
			alert.setContentText("还没有制作喵");
			alert.show();
		});
		recordMenu.getItems().addAll(historyScoreMenuItem);
		
		
		//Score菜单
		scoreMenu=new Menu("分数");//分数
		MenuItem moreScoreInfo=new MenuItem("更多分数信息");//更多分数信息
		moreScoreInfo.setOnAction(e->mCallbacks.afterGetMoreScoreInfo());
		scoreMenu.getItems().addAll(moreScoreInfo);
		getMenus().addAll(gameMenu,settingMenu,infoMenu,recordMenu,scoreMenu);
	}
	
	/**获取分数菜单*/
	public Menu getScoreMenu() {
		return scoreMenu;
	}
}

_2048Demo:

import _2048._node._CardMatrixPane;
import _2048._node._GameMenuBar;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

/**
 * 2048运行类
 * @author 邦邦拒绝魔抗
 *
 */
public class _2048Demo extends Application implements _GameMenuBar.Callbacks,_CardMatrixPane.Callbacks {
	private BorderPane borderPane;
	private _GameMenuBar menuBar;
	private _CardMatrixPane cardMatrixPane;
	
	@Override
	public void start(Stage primaryStage) {
		borderPane=new BorderPane();
		Scene scene=new Scene(borderPane,1000,600);
		
		//Top菜单栏
		menuBar=new _GameMenuBar(this);//创建菜单栏,并传入Application供调用
		borderPane.setTop(menuBar);//顶部
		
		//Center2048卡片矩阵
		cardMatrixPane=new _CardMatrixPane(this);
		cardMatrixPane.setPadding(new Insets(5,5,5,5));//外边距
		borderPane.setCenter(cardMatrixPane);//中心
		
		primaryStage.setTitle("2048");
		primaryStage.setScene(scene);
		primaryStage.show();
		
		startGame();
//		cardMatrixPane.testColors();//颜色测试
	}
	
	public static void main(String[] args) {
		Application.launch(args);
	}
	
	/**开始游戏*/
	private void startGame() {
		cardMatrixPane.requestFocus();//添加焦点
		cardMatrixPane.createKeyListener();//添加键盘监听
		afterScoreChange();
	}

	@Override
	public void afterRestart() {
		cardMatrixPane.restartMatrix();
	}

	@Override
	public void afterResetGridSize(int cols,int rows) {
		cardMatrixPane=new _CardMatrixPane(cols,rows,this);
		cardMatrixPane.setPadding(new Insets(5,5,5,5));//外边距
		borderPane.setCenter(cardMatrixPane);
		startGame();
//		cardMatrixPane.testColors();//颜色测试
	}

	@Override
	public void afterScoreChange() {
		menuBar.getScoreMenu().setText("分数: "+cardMatrixPane.getScore());
	}

	@Override
	public void afterGetMoreScoreInfo() {
		int[] temp=cardMatrixPane.getMcQuantities();
		Alert alert=new Alert(AlertType.INFORMATION);
		alert.setTitle(alert.getAlertType().toString());
		alert.setContentText(
				"4的合并次数: 		"+temp[0]+"\n"+
				"8的合并次数: 		"+temp[1]+"\n"+
				"16的合并次数: 		"+temp[2]+"\n"+
				"32的合并次数: 		"+temp[3]+"\n"+
				"64的合并次数: 		"+temp[4]+"\n"+
				"128的合并次数:		"+temp[5]+"\n"+
				"256的合并次数:		"+temp[6]+"\n"+
				"512的合并次数:		"+temp[7]+"\n"+
				"1024的合并次数: 	"+temp[8]+"\n"+
				"2048的合并次数: 	"+temp[9]+"\n"+
				"4096的合并次数: 	"+temp[10]+"\n"+
				"8192的合并次数: 	"+temp[11]+"\n"+
				"16384的合并次数: 	"+temp[12]+"\n"+
				"32768的合并次数: 	"+temp[13]+"\n"+
				"65536的合并次数: 	"+temp[14]+"\n");
		alert.show();
	}
}

一共700多行吧,里面有几行是注释掉的,它们在测试程序时候用过,方便调试,所以没删

_CardMatrixPane的testColors()方法正常流程是用不到的,测试程序时候查看所有卡片的颜色和字号用,可以删掉

基本思路:

代码里注释已经比较详细了,所以这里并不会涉及到很多的细节

卡片:

卡片的类型和显示的数字间关系很简单,在换算方法getNumber()中也有体现,数字就是2的type次方。其中的例外是2的0次方,这时候类型0即空卡片,不显示数字

为什么要加一个类型呢,举例的话用类型作为数组的下标取颜色就很方便,数字就不够紧凑了。当然,显示时的换算是一个开销,可以搞一个专门的存储数字的数组,用类型作为下标取数字,那就不用换算了

merge用来记录在一次操作中(向上、向下、向左、向右)已经合并了的卡片,逻辑上不希望卡片间连续地合并,这也符合2048游戏的基本规则

数据绑定,矩形的尺寸和GridPane的单元格尺寸绑定,缩放页面时候会跟随变化

用红色边框突出显示已合并的卡片,这里就是方便看的,可以删掉

重写了Object类的toString()方法,它是为了debug时方便看而设置的,可以删掉,用不到

卡片矩阵:

考虑到5x5和6x6的情况,卡片矩阵的行列数是变量而非常量

写了回调方法afterScoreChange(),这是因为卡片矩阵越职去修改菜单栏的分数是不好的,所以把这项工作交给了控制器来完成

对于卡片矩阵的宽高变化设有监听器,它随之修改卡片矩阵中GridPane的尺寸,还有单元格的尺寸和显示数字的尺寸。因为GridPane需要是正方形的,它的边长便取卡片矩阵宽高中的最小值。而卡片矩阵的宽高接近于窗口的宽高

这里有一个逻辑上的问题,按照2048游戏的基本规则,如果一次操作中没有出现任何卡片的移动或合并(矩阵中还有空卡片),就不应该生成新的2或4了,但这个程序的表现是会生成的,大家可以自行修改

createRandomNumber()方法会返回一个布尔值来表示矩阵里还有没有空卡片,有时并不需要这个返回值,是因为我们认为矩阵里肯定还有空卡片,比如重新开始游戏的时候

颜色:

一开始考虑了做卡片的数字颜色,后来偷懒都用黑色了

游戏菜单:

同样,菜单栏越职去访问和修改卡片矩阵是不好的,用回调方法把这些工作交给了控制器来完成

控制器:

实现回调接口中的各个回调方法,在恰当的时机控制各个节点


更新:对部分代码优化,修改了分数统计的形式(改为了用表格来展示)

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第8张图片


更新:最近在对代码进行重构,并尝试加入ai,完成之后会补充到这里(也可能另外写一篇)

……

更新:补图

详解Java实现2048小游戏(JavaFX,完整源码+注释说明)_第9张图片

你可能感兴趣的:(java)