Android项目实战 -- 2048小游戏
每学会一点知识,都要加以实战巩固,否则都很容易忘记。
这次我给大家带来一个简单的2048
小游戏项目实战练习,首先来看看项目的最终运行效果,如果觉得合你的胃口,就继续往下看,不然的话就可以提前溜咯:
都有哪些功能?
- 基本的4×4格子
- 5×5、6×6格子扩展
- 游戏模式:无限模式(就是没有了2048的上线,可以一直玩下去)
- 游戏进度保存(为了避免体积增大,使用的是原生的Sqlite)
- 外挂模式(手势识别GestureOverlayView)
本项目是用Java编写,本来是想用Kotlin写的,但这个其实是很久以前写的,只是功能还不完善,这次给他完善了下,由于之前使用Java写的,所有我就懒得改了。🙂
本项目已申请上线酷安平台,等通过审核后我把链接放出来,有兴趣的可以直接下载运行看效果。
项目已上线,点击下载体验😍
实现思想是参看徐宜生的《Android群英传》,并做了些许改变
《Android群英传》
确定布局
GameView
游戏的面板,即4x4的格子面板。它是本实例实现的关键。要实现这个布局,方法有很多,例如自定义一个View, 这个是万能方法,但是需要计算各个小方块的坐标,比较复杂。再比如用GridView,但是却不太好控制空格的小方块。因此,笔者最后选用了GridLayout 布局,这个布局是Android 4.0新增的布局。该布局的引人,极大地方便了Grid类型的布局开发,不熟悉该布局的读者朋友可以在Android开发者网站上找到相关的开发资料。
Cell
游戏中移动的小方块是2048最小的游戏对象。通过面向对象的设计方法,可以将这些小方块抽象成-个个对象。小方块的颜色、显示数字等属性都在对象中进行设置。对方块的合并、产生等操作,也是基于对象的操作,这样非常有利于程序逻辑的控制。
2048算法思路
玩家在进行上、下、左、右地滑动时,先去判断每行(列), 使用0来代表空格,如果某一行(列)的数字为2204,那么首先将这- -行(列)的非0数字存人-一个list, 即224。接下来,根据游戏规则,将2和2进行合并,即44。并将其作为该行(列)的返回值,从滑动的方向开始放置list中的数字。这样将每行(列)处理完毕后,就完成了-一次滑动。
GameView
继承自GridLayout,作为整个游戏的界面。
// 2048界面
public class GameView extends GridLayout {
...
}
再定义一个类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) {
...
LayoutParams params = new LayoutParams(-1, -1);
params.setMargins(leftMargin, topMargin, 0, bottomMargin);
addView(cellShowText, params);
setDigital(0);
}
...
/**
* 设置数字
*/
public void setDigital(int digital) {
this.digital = digital;
cellShowText.setBackgroundResource(getBackgroundResource(digital));
if (digital <= 0) {
cellShowText.setText("");
} else {
cellShowText.setText(String.valueOf(digital));
}
}
...
}
游戏初始化需要根据难度向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;
}
}
}
获取所有的空格子,也就是还没有数字的格子:
/**
* 获取空格子
*/
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));
}
}
}
}
添加一个数字:
/**
* 添加随机数字(2或4)或直接添加一个1024
*
* @param isCheat 是否是开挂
*/
public void addDigital(boolean isCheat) {
getEmptyCell();
if (emptyCellPoint.size() > 0) {
// 随机取出一个空格子的坐标位置
Point point = emptyCellPoint.get((int) (Math.random() * emptyCellPoint.size()));
if (isCheat) {
cells[point.x][point.y].setDigital(1024);
} else {
// 通过坐标位置获取到此空格子并以4:6的概率随机设置一个2或4
cells[point.x][point.y].setDigital(Math.random() > 0.4 ? 2 : 4);
}
// 设置动画
setAppearAnim(cells[point.x][point.y]);
}
}
右滑处理(其他几个方向的滑动我就不贴出来了,需要看的可以到我的Github上看源码):
private void swipeRight() {
// 判断是否需要添加数字
boolean needAddDigital = false;
for (int i = gridColumnCount - 1; i >= 0; i--) {
for (int j = gridColumnCount - 1; j >= 0; j--) {
// 获取当前位置数字
int currentDigital = cells[i][j].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
int temp = gridColumnCount - dataAfterSwipe.size();
for (int k = 0; k < temp; k++) {
dataAfterSwipe.add(0);
}
Collections.reverse(dataAfterSwipe);
// 若原始数据和移动后的数据不同,视为界面发生改变
Collections.reverse(someData);
if (!someData.equals(dataAfterSwipe)) {
needAddDigital = true;
}
someData.clear();
// 重新设置格子数据
int index = 0;
for (int p = 0; p < gridColumnCount; p++) {
cells[i][p].setDigital(dataAfterSwipe.get(index++));
}
// 重置数据
recordPreviousDigital = -1;
dataAfterSwipe.clear();
}
if (needAddDigital) {
// 添加一个随机数字(2或4)
addDigital(false);
playSound();
}
judgeOverOrAccomplish();
}
辅助方法:判断滑动的方向:
/**
* 获取滑动方向<br />
* 注:先依据在轴上滑动距离的大小,判断在哪个轴上滑动
*
* @param offsetX 在X轴上的移动距离
* @param offsetY 在Y轴上的移动距离
* @return 滑动方向
* <br />
* 注: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;
}
}
}
保存游戏进度
说一下思路:
首先定义一个实体类CellEntity,作为保存到数据库中的一个格子实体,代表游戏中某个位置的数据,然后通过遍历所有的格子,找出其中有数字的,并记录下每个格子的位置以及上面的数字作为一个实体类CellEntity,最后全部找完后通过ContentValues一个个的插入到数据库中就完事啦。下次进入APP时先去数据库中读,有数据的话就拿出保存的数据添加到界面中,否则就初始化游戏随机添加两个格子。 我的处理是每10S自动保存一次数据,然后用户主动退出时再保存一次数据。
就像这样:
private void startGame() {
// 将所有的格子重置
reset();
ArrayList<CellEntity> data = new ArrayList<>();
// 准备到数据库中读
SQLiteDatabase db = gameDatabaseHelper.getWritableDatabase();
Cursor cursor = db.query(Config.getTableName(), null, null, null,
null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
do {
int x = cursor.getInt(cursor.getColumnIndex("x"));
int y = cursor.getInt(cursor.getColumnIndex("y"));
int num = cursor.getInt(cursor.getColumnIndex("num"));
data.add(new CellEntity(x, y, num));
} while (cursor.moveToNext());
}
cursor.close();
}
if (data.size() <= 2) {
// 初始化游戏,随机添加两个格子到界面中
initGame();
} else {
// 恢复上次的游戏进度,将保存的数据添加到界面中
resumeGame(data);
}
}
总结:
基本上就是这样了,代码的话不算太多,给新手做练手项目吧!GitHub传送门
涉及到的知识点汇总:
- Sqlite
- SharedPreferences
- GestureOverlayView
- Animation
- Spannable
- Handler
- BroadcastReceiver
- Timer
- 自定义View
都是Android原生的东西,还是非常有学习价值的,没有用到任何第三方插件(本来想用RxJava的,想想算了,不能用任何库!😊)