自学android一段时间了,自我感觉android基础学的差不多了,现在感觉也遇上了一些瓶颈,所以找来一些小项目练练手,实践一下,才知道自己到底会什么,到底不会什么,这篇文章是挺久之前做的一个小游戏——2048的一个个人总结,同时附上源代码和项目视频。
开发环境:windows 7 集成开发环境:eclipse/Android studio sdk:android 4.3
视频及源码链接:链接:https://pan.baidu.com/s/1mp07mO1WdFh6ImsWVUlSpQ 提取码:z8m0
如失效,麻烦联系我更新,强烈建议还是根据源码或视频,并以此文章为辅助来进行学习。
2048是一个曾经非常火爆的一个小游戏,本项目用到的主要知识点是:如何创建游戏项目,如何管理游戏布局,如何编写游戏主类GameView,如何在android平台触控交互设计,编写2048游戏的卡片类Card,将数字卡片类添加到场景,在游戏中添加随机数,实现游戏逻辑(最主要的部分),检查游戏计分,以及如何判断游戏是否结束。
2048的玩法这里就不介绍了(必须了解玩法才能去设计游戏),本博客主要是为了说明游戏的算法,有了这个算法后就可以很方便的移植到其他平台了。
为了避免文章过长,本文只介绍关键部分,具体的不是很关键的细节可以参考源代码(有详细注释)和视频。
首先创建一个项目,接下来设计2048的游戏布局(activity_main),上方是一个LinearLayout用于计分,下方是游戏布局GameView,继承自GridLayout,布局效果如下所示:
接下来实现游戏主类GameView(在xml文件中访问它),GameView继承自GridLayout,用于放置2048游戏的16个卡片
下面是关于2048游戏的触控交互设计,无非是检测手指的down和up动作,利用坐标的变化值来判断手指的上下左右滑动,这里有一个需要注意的地方:要解决好当手指斜向滑动的时候如何判断的问题。触控交互的具体代码如下,相信也比较好理解:
setOnTouchListener(new View.OnTouchListener() { // 设置交互方式
// 监听上下左右滑动的这几个动作,再由这几个动作去执行特定的代码,去实现游戏的逻辑
private float startX, startY, offsetX, offsetY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_UP:
offsetX = event.getX() - startX;
offsetY = event.getY() - startY;
if (Math.abs(offsetX) > Math.abs(offsetY)) { // 加此判断是为了解决当用户向斜方向滑动时程序应如何判断的问题
if (offsetX < -5) {
swipeLeft(); //向左滑动的处理
} else if (offsetX > 5) {
swipeRight();
}
} else { // 判断向上向下
if (offsetY < -5) {
swipeUp();
} else if (offsetY > 5) {
swipeDown();
}
}
break;
}
return true; // 此处必须返回true
}
});
接下来再去实现2048游戏的卡片类,将16个卡片对象添加到GameView(继承自GridLayout)中去,新建Card类,继承自Framelayout,每个卡片都要有显示的数字,所有这个类有成员变量:
private int num = 0;
private TextView label;
//卡片需要呈现文字
具体代码如下:
public class Card extends FrameLayout {
private int num = 0;
private TextView label; //卡片需要呈现文字
public Card(Context context) {
super(context);
label = new TextView(getContext()); //初始化
label.setTextSize(32); //设置文本大小
label.setBackgroundColor(0x33ffffff); //设置文字背景或颜色
label.setGravity(Gravity.CENTER); //居中文字显示 否则数字都在卡片的左上角
LayoutParams lp = new LayoutParams(-1, -1); //设置布局参数 填充满整个父级容器
lp.setMargins(10, 10, 0, 0); //设置文字间的间隔 用以区分各个card
addView(label, lp);
setNum(0); //默认情况下卡片数字为0 !!!!!!!!!顺序不能错 否则会出bug
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
if(num <= 0){
label.setText(""); //如果卡片中的数字是0,则不显示
}else {
label.setText(num+""); //如果卡片中的数字不是0,则显示,此时num(int)会变成一个字符串
}
}
public boolean equals(Card card) { //判断两个卡片上的数字是否相同
return this.getNum() == card.getNum();
}
}
接下来当然是往布局里面添加卡片类,将16个卡片添加到4*4方阵中,其中为了解决适配不同手机的分辨率的问题,所以不能给出具体的宽高值,需要动态的计算卡片的宽高,通过重写onSizeChanged(int w, int h, int oldw, int oldh)来实现,计算布局宽高并往GameView中添加16个卡片的具体代码如下:
// 只有第一次创建的时候才会执行一次 只可能会执行一次
// 手机横放的时候不会执行,因为布局宽高不会发生改变,在AndroidManifest文件中配置了横放手机布局不变的参数
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { // 动态计算卡片宽高
super.onSizeChanged(w, h, oldw, oldh);
int cardWidth = (Math.min(w, h) - 10) / 4; //留出图片和边缘的大小
int cardHeight = cardWidth;
addCards(cardWidth, cardHeight);
startGame(); // 这里开启游戏
// 因为第一次创建游戏时,onSizeChanged()会被调用,且仅被调用一次,所以在这里开启游戏很合适
}
private void addCards(int cardWidth, int cardHeight) {
Card card;
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
card = new Card(getContext());
card.setNum(0); // 刚开始全部添加0 此时并不会显示数字
addView(card, cardWidth, cardHeight); // card是一个继承自FrameLayout的View
// 在initGameView()中指明这个GridLayout是四列的方阵
cardsMap[x][y] = card; //用一个二维数组来记录下这16个卡片
}
}
}
这里需要注意一下,因为手机横放时手机的布局(宽高)可能会发生变化,这时候onSizeChanged(int w, int h, int oldw, int oldh)方法会被调用,但并不希望这时候它被调用,所以需要需要配置一下,在AndroidManifest文件中设置屏幕一直是直立状态,代码如下所示:
android:screenOrientation="portrait"
这样的话onSizeChanged(int w, int h, int oldw, int oldh)只会被执行一次,只有布局第一次被创建的时候才会被调用执行,手机横放的时候不会执行。
接下里分别设置GridLayout和Card的背景颜色,此时需要注意要给Card的背景颜色留出一定的margin,设置card的颜色的代码如下所示:
LayoutParams lp = new LayoutParams(-1, -1); //设置布局参数 填充满整个父级容器
lp.setMargins(10, 10, 0, 0); //设置文字间的间隔 用以区分各个card
addView(label, lp); //lable是一个TextView
此时各个card就可以明显区分了。
下面来添加随机数,首先把所有的位置(空点的位置)全放在一个数组里面,方便随机地去取,新建一个List用于存储空点位置,把所有的位置(空点的位置)全放在一个数组里面,方便随机地去取:
private List<Point> emptyPoints = new ArrayList<Point>();
添加随机数的代码如下:
private void addRandomNum() { // 添加随机数 首先需要遍历所有卡片
emptyPoints.clear(); // 添加随机数之前先清空emptyPoints,然后把每一个空点都添加进来
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (cardsMap[x][y].getNum() <= 0) {
emptyPoints.add(new Point(x, y)); // 把空点的位置添加进去
// 因为只有空点才能够去添加数字
// 已经有数字的话肯定不会去添加了
}
}
}
Point p = emptyPoints
.remove((int) (Math.random() * emptyPoints.size())); // 随机地移除一个点
// 注意这里仅仅是移除emptyPoints中记录的点了,并没有移除card
cardsMap[p.x][p.y].setNum(Math.random() > 0.1 ? 2 : 4); // 给这个空点添加一个数,2或4,概率为9:1
}
接下来是最重要的游戏逻辑部分!!!!
以手指向左滑动为例(其他方向类似),此处要实现游戏逻辑,包括游戏的玩法逻辑,游戏计分逻辑和判断游戏是否结束的逻辑。代码如下所示:
// 实现游戏逻辑 只要有位置的改变就添加新的 最重要的部分:游戏逻辑
private void swipeLeft() {
boolean merge = false; // 判断是否有合并,如果有的话就进行一些处理
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
for (int x1 = x + 1; x1 < 4; x1++) { // 从当前的位置往右边去遍历
if (cardsMap[x1][y].getNum() > 0) { // 如果往右去遍历得到的card的值(获取到的值)不是空的,则做如下逻辑判断
if (cardsMap[x][y].getNum() <= 0) { // 如果当前位置上的值是空的,则将获取到的值移动到当前位置上
cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
cardsMap[x1][y].setNum(0);
x--; // 这里非常重要!!!! 可以测试理解
merge = true;
} else if (cardsMap[x][y].equals(cardsMap[x1][y])) { // 如果当前位置上的值不是空的,而且获取到的值与当前位置上的值相等,则做相加处理,并将结果放在当前位置上
cardsMap[x][y].setNum(cardsMap[x][y].getNum() * 2);
cardsMap[x1][y].setNum(0);
// 合并时加分 有合并就有添加(分数)
MainActivity.getMainActivity().addScore(
cardsMap[x][y].getNum());
merge = true;
}
break; // 这个break的位置非常重要!!!!! 只能写在这里!! eg:方格最下面一行是2 32
// 64 2,然后往左滑动的情况!
}
}
}
}
if (merge) { // 在添加数字时判断游戏是否结束
addRandomNum();
checkComplete(); // 添加新项后都要检查游戏是否结束:没空位置,而且已经不能再合并
}
}
简要说一下此处实现的游戏玩法算法:首先用for循环一行一行地去遍历每一个card,然后从当前的位置往右去遍历,判断如果获取到了某一个值不是空的,此时有两种情况,一是当前位置上的值是空的,此时把获取到的值放到当前位置上,同时把获取到的位置上的数字清掉;二是当前位置上的值不是空的,并且获取到的值和当前位置上的值相同,则把合并这两个卡片,把当前位置上的值乘以二,同时把获取到的位置上的数字清掉。
还有一种情况是,如果我们当前位置上是空的,然后把右边的值放到当前的位置上去了,此时继续往后边(右边)去遍历,后边的位置还是空的,然后右边又有一个数字和之前放过去的数字是一样的情况的话,也是把它放到这个空位置上去了,这时会发生一个状况:这两张数字实际是一样的,但是它们并不合并。为了避免这种情况的发生,我们再让它去遍历一次,即让x-- ,这样这个问题就解决了。
这里还有一个非常关键的点:注意break的位置,写在别的地方是不对的,应该是只要检测到获取到的值不是空的,不管是否合并了,都应该break掉,这里可能比较难理解,最好亲手测试一下。
此处还设置了一个boolean类型的标志merge,用来记录卡片是否发生了合并,如果卡片发生了合并,就要添加一个随机数并且判断游戏是否结束,判断游戏是否结束的逻辑代码如下:
// 判断游戏是否结束的逻辑
private void checkComplete() {
boolean complete = true;
All: for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
// 游戏没有结束的判定情况 5种情况
if (cardsMap[x][y].getNum() == 0
|| (x > 0 && cardsMap[x][y].equals(cardsMap[x - 1][y]))
|| (x < 3 && cardsMap[x][y].equals(cardsMap[x + 1][y]))
|| (y > 0 && cardsMap[x][y].equals(cardsMap[x][y - 1]))
|| (y < 3 && cardsMap[x][y].equals(cardsMap[x][y + 1]))) {
complete = false;
break All; // 写一个标签,跳出所有循环
}
}
}
if (complete) {
AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
dialog.setTitle("你好")
.setMessage("游戏结束")
.setPositiveButton("重来",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
startGame();
}
});
dialog.setNegativeButton("关闭程序", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
MainActivity.getMainActivity().finish();
}
});
dialog.show();
}
}
这样的话游戏基本的逻辑代码就实现了,此时只是实现了游戏的基本逻辑,游戏还可以继续优化,美化,添加功能等。如果对上面文章感觉不是很清楚的话,建议还是结合上面链接中的视频和源码理解,如链接失效,麻烦联系我更新,相信这还是非常适合入门的一个安卓小项目的。