简述

2048有着简单的游戏规则有趣的游戏过程,在早年的时候着实火了一把,以致在后来也出现了很多不同的版本。但主要的规则都是相似的,4*4的格子,数字随机出现2或者4,手指进行上下左右的滑动,所有数字向滑动方向靠拢,相同的相邻数字相加合并,合并成功或者移动后在剩下的空格中随机出现新的数字,直到合并出现2048,则挑战成功。

分析

游戏布局

游戏主布局为一个4*4的面板,想要实现该布局的方式有很多种,比如继承View手动计算每个格子的坐标,但是会比较复杂,鉴于GridLayout在4.0的引入,比较适用于本游戏的布局,然后只需要监控上下左右不同的手势方向执行不同的逻辑运算便可以达到效果。接下来的重点是每个格子的实现,它是根据不同的数值表现出来的不同的背景色值,其尺寸大小是根据不同的分辨率下动态设置的。

游戏算法

主要是小格子的产生、合并算法要求高点,如果过于麻烦效率太低势必会造成游戏卡顿,影响体验,于是这里采用的是思路比较传统的做法:

在进行上下左右滑动时,遍历每一行(列),如果没有数字用0来代替空格,如某一行的数字为4 4 0 2,则相同的合并后加入List中,否则直接加入,操作后的顺序即8 2,并从滑动的方向重新放置List中的数字,直到遍历所有行(列)。

实现

小格子设计

让GameBoxView继承FrameLayout,并根据设置的数字改变相应的颜色,对外提供setBoxNum()方法,并加入一个TextView设置数字背景颜色边距和数字值,整个实现起来较为简单:


import android.content.Context;
import android.graphics.Color;
import android.text.TextPaint;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.example.game2048.GameApp;
public class GameBoxView extends FrameLayout{
private int gBoxNumVal;
private TextView gBoxView;
private FrameLayout.LayoutParams gBoxParams;
// 小格子的初始化数字值
public GameBoxView(Context context, int gBoxNum){
super(context);
this.gBoxNumVal = gBoxNum;
initGameBoxView();
}
private void initGameBoxView(){
setBackgroundColor(Color.GRAY);
gBoxView = new TextView(getContext());
setBoxNumber(gBoxNumVal);
int gameLines = GameApp.mSp.getInt(GameApp.KEY_GAME_LINES, 4);
// 根据不同的宫格设置不同的字体大小
if (gameLines == 4) {
gBoxView.setTextSize(35);
} else if (gameLines == 5) {
gBoxView.setTextSize(25);
} else {
gBoxView.setTextSize(20);
}
TextPaint tp = gBoxView.getPaint();
tp.setFakeBoldText(true);
gBoxView.setGravity(Gravity.CENTER);
gBoxParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
gBoxParams.setMargins(5, 5, 5, 5);
addView(gBoxView, gBoxParams);
}
public View getBoxItemView(){
return gBoxView;
}
public int getBoxNumber(){
return gBoxNumVal;
}
public void setBoxNumber(int num){
this.gBoxNumVal = num;
if (num == 0) {
gBoxView.setText("");
} else {
gBoxView.setText("" + num);
}
switch (num) {
case 0:
gBoxView.setBackgroundColor(0x00000000);
break;
case 2:
gBoxView.setBackgroundColor(0xffeee5db);
break;
case 4:
gBoxView.setBackgroundColor(0xffeee0ca);
break;
case 8:
gBoxView.setBackgroundColor(0xfff2c17a);
break;
case 16:
gBoxView.setBackgroundColor(0xfff59667);
break;
case 32:
gBoxView.setBackgroundColor(0xfff68c6f);
break;
case 64:
gBoxView.setBackgroundColor(0xfff66e3c);
break;
case 128:
gBoxView.setBackgroundColor(0xffedcf74);
break;
case 256:
gBoxView.setBackgroundColor(0xffedcc64);
break;
case 512:
gBoxView.setBackgroundColor(0xffedc854);
break;
case 1024:
gBoxView.setBackgroundColor(0xffedc54f);
break;
case 2048:
gBoxView.setBackgroundColor(0xffedc32e);
break;
default:
gBoxView.setBackgroundColor(0xff3c4a34);
break;
}
}
}

首先在构造时确定显示的数字,从而确定小格子的背景颜色,默认都是灰色,然后设置小格子的边距,整个的实现效果如下图:

android studio开发2048小游戏app项目介绍 安卓2048游戏_初始化

小格子效果图

可以看到的是对于不同的数字对比效果还是很明显的,也达到了我们的目的。

游戏面板设计

在初始化游戏矩阵时,首先需要移除之前的游戏布局,并获取最高成绩,并随机添加2个小格子作为初始格子。

// 初始化游戏矩阵
private void initGameMatrix(){
removeAllViews();
mHistoryScore = 0;
GameApp.CURRENT_SCORE = 0;
GameApp.mGameLines = GameApp.mSp.getInt(GameApp.KEY_GAME_LINES,4);
mGameLines = GameApp.mGameLines;
mGameMatrixViews = new GameBoxView[mGameLines][mGameLines];
mGameMatrixHistory = new int[mGameLines][mGameLines];
mTmpBoxNumList = new ArrayList<>();
mBlankGameBoxes = new ArrayList<>();
mHighestScore = GameApp.mSp.getInt(GameApp.KEY_HIGH_SCORE , 0);
setColumnCount(mGameLines);
setRowCount(mGameLines);
// 设置触摸事件监听用于处理滑动事件
setOnTouchListener(this);
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
display.getMetrics(metrics);
GameApp.mGameBoxWidth = metrics.widthPixels / GameApp.mGameLines;
//根据小格子宽度初始化格子
initGameView(GameApp.mGameBoxWidth);
}
public void initGameView(int cardSize){
removeAllViews();
GameBoxView gameBoxView;
for (int i = 0; i < mGameLines; i ++){
for (int j = 0 ; j < mGameLines ; j++){
gameBoxView = new GameBoxView(getContext(),0);
addView(gameBoxView,cardSize,cardSize);
mGameMatrixViews[i][j] = gameBoxView;
mBlankGameBoxes.add(new Point(i,j));
}
}
// 初始化随机2个数字
addRandomNum();
addRandomNum();
}
// 根据剩余的格子位置坐标初始化小格子
private void addRandomNum(){
getBlankGameBoxes();
if (mBlankGameBoxes.size() > 0){
int randomNumPos = (int) Math.random() * mBlankGameBoxes.size();
Point randomPoint = mBlankGameBoxes.get(randomNumPos);
mGameMatrixViews[randomPoint.x][randomPoint.y]
// 只会出现2或者4
.setBoxNumber(Math.random() > 0.2d ? 2: 4);
// 增加缩放动画过渡自然些
animCreate(mGameMatrixViews[randomPoint.x][randomPoint.y]);
}
}

滑动事件处理

游戏中只存在着上下左右四个方向的滑动,整个处理流程大致如下:滑动事件产生->滑动事件结束->判断滑动方向->游戏布局重新处理->添加新的随机数字->游戏是否结束。

下面是对移动方向的处理:

@Override
public boolean onTouch(View v, MotionEvent event){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
saveHistoryMatrix();
mStartX = (int) event.getX();
mStartY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
mEndX = (int) event.getX();
mEndY = (int) event.getY();
judgeDirection(mEndX - mStartX , mEndY - mStartY);
if (isMoved()) {
addRandomNum();
GameActivity.getGameActivity().setScore(GameApp.CURRENT_SCORE, 0);
}
checkCompleted();
break;
default:
break;
}
return true;
}

在onTouch方法中捕获ACTION_DOWN事件,预先保存目前状态的游戏数据,记录起点坐标,在手指抬起后,捕获ACTION_UP事件,记录终点坐标随之判断滑动的方向,合并游戏数据,添加一个新的随机数字并检查游戏是否符合达到结束的条件。

下面是向上滑动后进行的操作:

private void swipeUp(){
for (int i = 0 ; i < mGameLines ; i ++){
for (int j = 0 ; j < mGameLines ; j ++) {
int currentNum = mGameMatrixViews[j][i].getBoxNumber();
if (currentNum != 0) {
if (mLastBoxNum == -1) {
mLastBoxNum = currentNum;
} else {
if (mLastBoxNum == currentNum){
mTmpBoxNumList.add(mLastBoxNum * 2);
GameApp.CURRENT_SCORE += mLastBoxNum * 2;
mLastBoxNum = -1;
} else {
mTmpBoxNumList.add(mLastBoxNum);
mLastBoxNum = currentNum;
}
}
} else {
continue;
}
}
if (mLastBoxNum != -1){
mTmpBoxNumList.add(mLastBoxNum);
}
for (int j = 0; j < mTmpBoxNumList.size() ; j ++){
mGameMatrixViews[j][i].setBoxNumber(mTmpBoxNumList.get(j));
}
for (int m = mTmpBoxNumList.size(); m < mGameLines; m ++){
mGameMatrixViews[m][i].setBoxNumber(0);
}
mLastBoxNum = -1;
mTmpBoxNumList.clear();
}
}

通过外层循环遍历每一列,再通过内层循环遍历每一行,对相同的数字进行合并,并通过 mLastBoxNum 标识区分是否已经进行了合并,对于已经合并后的数字并不会与下个数字再次进行合并,最后将处理后的数字重新设置到游戏面板,从而完成了整个滑动过程。

判断游戏结束

游戏结束意味着达到了设定的目标分数值,或者失败表示着所有的方块无法再次移动。对于游戏数据来说,成功即意味着游戏矩阵含有了设定分数值的方块,而失败意味着每个小方块四个方向都不存在与其相同的小方块。

主要操作如下:

private void checkCompleted(){
int result = checkNums();
if (result == 0) {
if (GameApp.CURRENT_SCORE > mHighestScore){
SharedPreferences.Editor editor = GameApp.mSp.edit();
editor.putInt(GameApp.KEY_HIGH_SCORE, GameApp.CURRENT_SCORE);
editor.apply();
GameActivity.getGameActivity().setScore(GameApp.CURRENT_SCORE, 1);
GameApp.CURRENT_SCORE = 0;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("GameOver")
.setPositiveButton("Again",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which){
startGame();
}
}).create().show();
GameApp.CURRENT_SCORE = 0;
} else if (result == 2) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("Mission Accomplished")
.setPositiveButton("Again",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which){
startGame();
}
})
.setNegativeButton("Continue",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which){
SharedPreferences.Editor editor = GameApp.mSp.edit();
if (mTargetScore == 1024){
editor.putInt(GameApp.KEY_GAME_GOAL,2048);
mTargetScore = 2048;
GameActivity.getGameActivity().setGoal(2048);
} else if (mTargetScore == 2048){
editor.putInt(GameApp.KEY_GAME_GOAL,4096);
mTargetScore = 4096;
GameActivity.getGameActivity().setGoal(4096);
} else {
editor.putInt(GameApp.KEY_GAME_GOAL,4096);
mTargetScore = 4096;
GameActivity.getGameActivity().setGoal(4096);
}
editor.apply();
}
}).create().show();
GameApp.CURRENT_SCORE = 0;
}
}

最终游戏实现效果图:

android studio开发2048小游戏app项目介绍 安卓2048游戏_android网格布局2048_02

最终效果图

总结

一步步分析来看,2048游戏的核心就是随机数字的生成,合并同行/同列数字,重新布局,检查游戏是否结束,关键还是对于游戏玩法转化成代码的理解能力,需要加强对日常业务的思考,对于问题转化成解决问题的模型的能力。