刚刚完成的一个小游戏,写文章记录一下,如果有任何错误或者可以改进的代码请提出
另一方面也是方便自己几个月或几年后忘记时,来这里翻一翻回顾思路
目录
基本界面:
类的组织:
_CardPane:
_CardMatrixPane:
_CardColor:
_GameMenuBar:
_2048Demo:
基本思路:
卡片:
卡片矩阵:
颜色:
游戏菜单:
控制器:
首先放一下效果图:
所有卡片
分数统计
尺寸为5x5或6x6
五个类,最基础的是_CardPane,继承自BorderPane,作为数字卡片。它里面有一个Rectangle,用来表示卡片的圆角矩形背景,以及一个Label来显示数字
然后是由数字卡片组成的矩阵_CardMatrixPane,继承自StackPane,它包含一个GridPane
_CardColor,里面只有一个静态的Color数组,用来搞卡片的背景颜色
_GameMenuBar作为游戏的菜单栏,继承自MenuBar
最后是_2048Demo,相当于控制器
这里类名前面加下划线是个人习惯,因为我的Eclipse项目名、包名、类名等等都会与图标重合一些,加下划线可以看的方便,如下:
下面放代码:
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+"]";
}
}
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();//重绘
}
}
}
}
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= {//数字颜色
};
}
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;
}
}
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()方法会返回一个布尔值来表示矩阵里还有没有空卡片,有时并不需要这个返回值,是因为我们认为矩阵里肯定还有空卡片,比如重新开始游戏的时候
一开始考虑了做卡片的数字颜色,后来偷懒都用黑色了
同样,菜单栏越职去访问和修改卡片矩阵是不好的,用回调方法把这些工作交给了控制器来完成
实现回调接口中的各个回调方法,在恰当的时机控制各个节点
更新:对部分代码优化,修改了分数统计的形式(改为了用表格来展示)
更新:最近在对代码进行重构,并尝试加入ai,完成之后会补充到这里(也可能另外写一篇)
……
更新:补图