Android小项目--2048小游戏
16lz
2021-01-23
Android小项目–2048小游戏
文章目录
- Android小项目--2048小游戏
-
- 1.摘要
- 2.实现的功能
- 3.完成的界面展示
- 4.功能具体实现的过程
-
- 1. 游戏面板GameView的设计(采用GridLayout布局)
- 2. Cell类的设计:Cell用来表示游戏中的小格子
- 3. 将cell添加到GameView中:
- 4. 游戏模式的改变:
- 5. 记录当前分数及历史最高分数
- 6. 判断游戏是否结束
- 5. 总结
1.摘要
现如今,电子游戏已慢慢渗透进人们生活中,并在扮演着越来越重的角色。2048小游戏属于益智类小游戏,它做到了娱乐性、趣味性、教育性相统一。益智类的游戏即是需要去开动大脑去思考从而获得游戏的胜利。简单的益智类游戏可以使玩家在娱乐中不断的开发大脑。这样一来就实现了在娱乐中学习。
这篇文章主要为大家介绍了Android实现2048小游戏的相关内容,感兴趣的小伙伴们可以参考一下!
2.实现的功能
- 确认布局
- UI界面
- 2048游戏逻辑的实现
- 游戏界面:
- 基本的4×4格子
- 扩展的5×5格子
- 扩展的6×6格子
- 记录当前得分和历史最高分数
- 游戏模式:
- 经典模式:达到2048即为“成功”,游戏结束。
- 无限模式:没有2048的上限,在没有失败的情况下,玩家可以一直玩下去.
- 最后游戏结束时“You Win!”或“You Lose!”的判定
3.完成的界面展示
!!
4.功能具体实现的过程
1. 游戏面板GameView的设计(采用GridLayout布局)
- 先自定义一个GameView类,继承GridLayout,添加两个构造方法(GridLayout布局是Android 4.0新增的布局。引入该布局极大地方便了Grid类型的布局开发,不熟悉该布局的读者朋友可以在Android开发者网站上寻找相关的开发资料进行学习)
public class GameView extends GridLayout { //两个必要的构造方法 public GameView(Context context) { super(context); initView(); } public GameView(Context context, AttributeSet attrs) { super(context, attrs); initView(); }}
- 在initView()中定义格子的宽和高,并且添加触摸事件监听
public void initView(int mode) { gameMode = mode; canSwipe = true; // 移除所有视图,以便更改游戏难度 removeAllViews(); // 初始化格子 if (mode == Constant.MODE_CLASSIC) { // 经典模式 gridColumnCount = Config.GRIDColumnCount; } else if (mode == Constant.MODE_INFINITE) { // 无限模式 gridColumnCount = 6; } cells = new Cell[gridColumnCount][gridColumnCount]; // 设置界面大小 setColumnCount(gridColumnCount); // 获取格子的宽 int cellWidth = getCellSize(); // 获取格子的高 int cellHeight = getCellSize(); addCell(cellWidth, cellHeight); startGame(); setOnTouchListener((v, event) -> { // 通知父控件不要拦截此控件的onTouch事件 v.getParent().requestDisallowInterceptTouchEvent(true); if (canSwipe) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setX = event.getX(); setY = event.getY(); break; case MotionEvent.ACTION_UP: offsetX = event.getX() - setX; offsetY = event.getY() - setY; // 判断滑动方向 int orientation = getOrientation(offsetX, offsetY); switch (orientation) { case 0: // 向右滑动 swipeRight(); break; case 1: // 向左滑动 swipeLeft(); break; case 2: // 向下滑动 swipeDown(); break; case 3: // 向上滑动 swipeUp(); break; default: break; } default: break; } } return true; }); }
- 添加滑动事件(这里举例上滑):
- 简要说一下此处实现的游戏玩法算法:首先用for循环一行一行地去遍历每一个cell,然后从当前的位置往右去遍历,判断如果获取到了某一个值不是空的,此时有两种情况,一是当前位置上的值是空的,此时把获取到的值放到当前位置上,同时把获取到的位置上的数字清掉;二是当前位置上的值不是空的,并且获取到的值和当前位置上的值相同,则把合并这两个卡片,把当前位置上的值乘以二,同时把获取到的位置上的数字清掉。
- 还有一种情况是,如果我们当前位置上是空的,然后把右边的值放到当前的位置上去了,此时继续往后边(右边)去遍历,后边的位置还是空的,然后右边又有一个数字和之前放过去的数字是一样的情况的话,也是把它放到这个空位置上去了,这时会发生一个状况:这两张数字实际是一样的,但是它们并不合并。为了避免这种情况的发生,我们再让它去遍历一次,即让x-- ,这样这个问题就解决了。
//上滑事件 private void swipeUp() { // 判断是否需要添加数字 boolean needAddDigital = false; for (int i = 0; i < gridColumnCount; i++) { for (int j = 0; j < gridColumnCount; j++) { // 获取当前位置数字 int currentDigital = cells[j][i].getDigital(); someData.add(currentDigital); if (currentDigital != 0) { // 记录数字 if (recordPreviousDigital == -1) { recordPreviousDigital = currentDigital; } else { // 记录的之前的数字和当前数字不同 if (recordPreviousDigital != currentDigital) { // 加入记录的数字 dataAfterSwipe.add(recordPreviousDigital); recordPreviousDigital = currentDigital; } else { // 记录的之前的数字和当前的数字相同 // 加入*2 dataAfterSwipe.add(recordPreviousDigital * 2); // 记录得分 recordScore(recordPreviousDigital * 2); // 重置记录数字 recordPreviousDigital = -1; } } } } if (recordPreviousDigital != -1) { dataAfterSwipe.add(recordPreviousDigital); } // 补0 for (int p = dataAfterSwipe.size(); p < gridColumnCount; p++) { dataAfterSwipe.add(0); } // 若原始数据和移动后的数据不同,视为界面发生改变 if (!someData.equals(dataAfterSwipe)) { needAddDigital = true; } someData.clear(); // 重新设置格子数据 for (int k = 0; k < dataAfterSwipe.size(); k++) { cells[k][i].setDigital(dataAfterSwipe.get(k)); } // 重置数据 recordPreviousDigital = -1; dataAfterSwipe.clear(); } if (needAddDigital) { // 添加一个随机数字(2或4) addDigital(false); playSound(); } judgeOverOrAccomplish(); }
- 判断滑动方向:
/** * 注:先依据在轴上滑动距离的大小,判断在哪个轴上滑动 * @param offsetX 在X轴上的移动距离 * @param offsetY 在Y轴上的移动距离 * @return 滑动方向 * 注:0右滑、1左滑、2下滑、3上滑、-1未构成滑动 */ private int getOrientation(float offsetX, float offsetY) { // X轴移动 if (Math.abs(offsetX) > Math.abs(offsetY)) { if (offsetX > MIN_DIS) { return 0; } else if (offsetX < -MIN_DIS) { return 1; } else { return -1; } } else { // Y轴移动 if (offsetY > MIN_DIS) { return 2; } else if (offsetY < -MIN_DIS) { return 3; } else { return -1; } } }
2. Cell类的设计:Cell用来表示游戏中的小格子
- Cell表示游戏中移动的小格子,格子的颜色、显示数字等属性都在对象中进行设置,Cell类如下:
public class Cell extends FrameLayout { //显示数字的TextView private TextView cellShowText; //显示的数字 private int digital; public Cell(Context context) { super(context); } public Cell(@NonNull Context context, int leftMargin, int topMargin, int bottomMargin) { super(context); init(context, leftMargin, topMargin, bottomMargin); } //初始化 private void init(@NonNull Context context, int leftMargin, int topMargin, int bottomMargin) { cellShowText = new TextView(context); // 不同难度设置不同字体大小 switch (Config.GRIDColumnCount) { case 4: cellShowText.setTextSize(36); break; case 5: cellShowText.setTextSize(28); break; case 6: cellShowText.setTextSize(20); break; default: cellShowText.setTextSize(36); break; } cellShowText.setGravity(Gravity.CENTER); // 抗锯齿 cellShowText.getPaint().setAntiAlias(true); // 粗体 cellShowText.getPaint().setFakeBoldText(true); // 颜色 cellShowText.setTextColor(ContextCompat.getColor(context, R.color.colorWhite)); // 填充整个父容器 LayoutParams params = new LayoutParams(-1, -1); params.setMargins(leftMargin, topMargin, 0, bottomMargin); addView(cellShowText, params); setDigital(0); } //获取卡片 public TextView getItemCell() { return cellShowText; } //获取数字 public int getDigital() { return digital; } //设置数字 public void setDigital(int digital) { this.digital = digital; cellShowText.setBackgroundResource(getBackgroundResource(digital)); if (digital <= 0) { cellShowText.setText(""); } else { cellShowText.setText(String.valueOf(digital)); } } //根据数字获取相应的背景 private int getBackgroundResource(int number) { switch (number) { case 0: return R.drawable.bg_cell_0; case 2: return R.drawable.bg_cell_2; case 4: return R.drawable.bg_cell_4; case 8: return R.drawable.bg_cell_8; case 16: return R.drawable.bg_cell_16; case 32: return R.drawable.bg_cell_32; case 64: return R.drawable.bg_cell_64; case 128: return R.drawable.bg_cell_128; case 256: return R.drawable.bg_cell_256; case 512: return R.drawable.bg_cell_512; case 1024: return R.drawable.bg_cell_1024; case 2048: return R.drawable.bg_cell_2048; default: return R.drawable.bg_cell_default; } }}
3. 将cell添加到GameView中:
- 游戏初始化需要根据难度向GameView添加所有的Cell
/** * 初始化向布局中添加空格子 * @param cellWidth 格子宽 * @param cellHeight 格子高 */ private void addCell(int cellWidth, int cellHeight) { Cell cell; for (int i = 0; i < gridColumnCount; i++) { for (int j = 0; j < gridColumnCount; j++) { if (i == gridColumnCount - 1) { // 为最底下的格子加上bottomMargin cell = new Cell(getContext(), 16, 16, 16); } else { cell = new Cell(getContext(), 16, 16, 0); } cell.setDigital(0); addView(cell, cellWidth, cellHeight); cells[i][j] = cell; } } }
- 所有格子需要获取数字,最初全部设为0,即所有格子为空
//获取空格子 private void getEmptyCell() { // 清空 emptyCellPoint.clear(); // 遍历所有格子,记录所有空格子的坐标位置 for (int i = 0; i < gridColumnCount; i++) { for (int j = 0; j < gridColumnCount; j++) { // 空格子 if (cells[i][j].getDigital() <= 0) { emptyCellPoint.add(new Point(i, j)); } } } }
- 以4:6的概率随机获取一个数字2或4
public void addDigital(boolean isCheat) { getEmptyCell(); if (emptyCellPoint.size() > 0) { // 随机取出一个空格子的坐标位置 Point point = emptyCellPoint.get((int) (Math.random() * emptyCellPoint.size())); cells[point.x][point.y].setDigital(Math.random() > 0.4 ? 2 : 4); // 设置动画 setAppearAnim(cells[point.x][point.y]); } }
4. 游戏模式的改变:
- 扩展功能:无限模式和经典模式的切换:
//打开切换模式对话框 private void showChangeModeDialog() { String subject = ""; if (Config.CurrentGameMode == Constant.MODE_CLASSIC) { subject = "无限模式"; } else if (Config.CurrentGameMode == Constant.MODE_INFINITE) { subject = "经典模式"; } CommonDialog dialog = new CommonDialog(this, R.style.CustomDialog); dialog.setCancelable(true); dialog.setTitle(getResources().getString(R.string.tip)) .setMessage("是否要切换到" + subject) .setOnPositiveClickedListener("", v -> { if (Config.CurrentGameMode == Constant.MODE_CLASSIC) { Toast.makeText(GameActivity.this, "已进入无限模式", Toast.LENGTH_SHORT).show(); enterInfiniteMode(); } else { Toast.makeText(GameActivity.this, "已进入经典模式", Toast.LENGTH_SHORT).show(); enterClassicsMode(); } dialog.cancel(); }) .setOnNegativeClickListener("", v -> dialog.cancel()) .show(); } // 进入无限模式 private void enterInfiniteMode() { Config.haveCheat = false; Config.CurrentGameMode = Constant.MODE_INFINITE; // 保存游戏模式 ConfigManager.putCurrentGameMode(this, Constant.MODE_INFINITE); titleDescribe.setText(getResources().getString(R.string.game_mode_infinite)); bestScores.setText(String.valueOf(ConfigManager.getBestScoreWithinInfinite(this))); bestScoresRank.setText(getResources().getText(R.string.tv_best_score_infinite)); currentScores.setText(String.valueOf(ConfigManager.getCurrentScoreWithinInfinite(this))); modeDescribe.setText(getResources().getString(R.string.tv_describe_infinite)); setTextStyle(titleDescribe); gameView.initView(Constant.MODE_INFINITE); } //进入经典模式 private void enterClassicsMode() { Config.haveCheat = false; Config.CurrentGameMode = Constant.MODE_CLASSIC; // 保存游戏模式 ConfigManager.putCurrentGameMode(this, Constant.MODE_CLASSIC); titleDescribe.setText(getResources().getString(R.string.game_mode_classics)); // 读取到历史最高分 bestScores.setText(String.valueOf(Config.BestScore)); bestScoresRank.setText(getString(R.string.best_score_rank, Config.GRIDColumnCount)); currentScores.setText(String.valueOf(ConfigManager.getCurrentScore(this))); modeDescribe.setText(getResources().getString(R.string.tv_describe)); setTextStyle(titleDescribe); gameView.initView(Constant.MODE_CLASSIC); }
5. 记录当前分数及历史最高分数
//记录得分 private void recordScore(int score) { currentScores.setText(String.valueOf(score)); // 当前分数大于最高分 if (Config.CurrentGameMode == Constant.MODE_CLASSIC) { if (score > ConfigManager.getBestScore(this)) { updateBestScore(score); } } else if (Config.CurrentGameMode == Constant.MODE_INFINITE) { if (score > ConfigManager.getBestScoreWithinInfinite(this)) { updateBestScore(score); } } } //更新历史最高分 private void updateBestScore(int newScore) { bestScores.setText(String.valueOf(newScore)); if (Config.CurrentGameMode == Constant.MODE_CLASSIC) { Config.BestScore = newScore; ConfigManager.putBestScore(this, newScore); } else if (Config.CurrentGameMode == Constant.MODE_INFINITE) { Config.BestScoreWithinInfinite = newScore; ConfigManager.putBestScoreWithinInfinite(this, newScore); } }
6. 判断游戏是否结束
- 每次获取数字时需要判断游戏是否结束
private void judgeOverOrAccomplish() { // 判断游戏结束的标识 boolean isOver = true; // 判断游戏是否结束:格子都不为空且相邻的格子数字不同 over: for (int i = 0; i < gridColumnCount; i++) { for (int j = 0; j < gridColumnCount; j++) { // 有空格子,游戏还可以继续 if (cells[i][j].getDigital() == 0) { isOver = false; break over; } // 判断左右上下有没有相同的 if (j < gridColumnCount - 1) { if (cells[i][j].getDigital() == cells[i][j + 1].getDigital()) { isOver = false; break over; } } if (i < gridColumnCount - 1) { if (cells[i][j].getDigital() == cells[i + 1][j].getDigital()) { isOver = false; break over; } } } } // 游戏结束,弹出提示框 if (isOver) { canSwipe = false; sendGameOverMsg(ACTION_LOSE); } // 经典模式下才判赢 if (gameMode == 0) { // 判断是否达成游戏目标 for (int i = 0; i < gridColumnCount; i++) { for (int j = 0; j < gridColumnCount; j++) { // 有一个格子数字到达2048则视为达成目标 if (cells[i][j].getDigital() == 2048) { canSwipe = false; int currentTime = ConfigManager.getGoalTime(getContext()) + 1; ConfigManager.putGoalTime(getContext(), currentTime); Config.GetGoalTime = currentTime; sendGameOverMsg(ACTION_WIN); } } } } }
5. 总结
- 涉及到的知识点汇总:
- Sqlite
- SharedPreferences
- GestureOverlayView
- Animation
- Spannable
- Handler
- BroadcastReceiver
- Timer
- 自定义View
- 本文代码是根据极客学院2048小游戏代码视频一步步编写并完善实现,过程中遇到的困难主要是思考游戏思路和键盘事件的理解(即手指滑动时的事件)。游戏思路在上文中滑动事件前已有详细解释,这里给出一个不错的讲解键盘事件的链接,有遇到相同困难的同学可以查看。
- 键盘事件讲解链接
作者:宋迎新
参考文章:参考文章
原文链接:原文链接
更多相关文章
- html5游戏移植到android并打包成apk,加广告《一》
- 简单的Android游戏测试
- Chapter3-运行cocos2dx游戏在android设备上
- 十五开源的Android(2D或3D)Android开发游戏引擎
- 为什么没有好用的Android游戏引擎?