Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!
关于2048,我看到很多大神,比如医生,郭神,所以我也研究了一段时间,还好是研究了一套逻辑,这是一整套的2048游戏从设计到逻辑再到编写的全部过程,小伙伴们看仔细咯,刚好今天是礼拜天,一天应该了一把这篇博客发表了,其实2048开发起来还是有点难度的,并且他的逻辑挺强的,我也是看了很多的资料偷学的,很适合来锻炼自己的逻辑性
我们首先先来选择开发环境,这里我们就以Eclipse为IDE,新建一个工程——Game2048
一.Score分数
既然是2048游戏,我们也就做一个简单的,他有一个分数,然后就是一个游戏的布局,我们也做一个简单的4*4的游戏,大概的设计图就是这样
二.游戏类:GameView
因为我们的游戏所使用到的布局就是GridLayout,所以我们新建一个GameView继承自GridLayout,然后通过算法动态添加方块,并且监听手势进行操作,这个重写的GridLayout就是游戏的布局了
<com.lgl.game2048.GameView
android:id="@+id/game_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
三.实现交互逻辑
我们这里铁定是手势操作啦,这里我们就得区分手势是往上,往下,往左,往右,的手势,这时候,我们就需要用到手势监听——OnTouchListener了,也为了确保是实时监听,我们直接在initView的初始方法中处理
我们其实只要知道两点,用户手指按下的坐标点和手指离开的坐标点,然后进行比对,就能识别出用户的意图了
其实关于MotionEvent的几个方法,大家估计都见怪不怪了,因为用的太多了
// 初始化
private void initView() {
// 识别手势
setOnTouchListener(new OnTouchListener() {
// 起始点和偏移点
private float startX, startY, offsetX, offsetY;
@Override
public boolean onTouch(View v, MotionEvent event) {
/**
* 交互逻辑 :我们其实只要知道两点,用户手指按下的坐标点和手指离开的坐标点,然后进行比对,就能识别出用户的意图了
*/
switch (event.getAction()) {
// 手指按下
case MotionEvent.ACTION_DOWN:
// 记录按下的x,y坐标
startX = event.getX();
startY = event.getY();
break;
// 手指离开
case MotionEvent.ACTION_UP:
// 手指离开之后计算偏移量(离开的位置-按下的位置在进行判断是往哪个方向移动)
offsetX = event.getX() - startX;
offsetY = event.getY() - startY;
// 开始识别方向
// offsetX 的绝对值大于offsetY的绝对值 说明在水平方向
if (Math.abs(offsetX) > Math.abs(offsetY)) {
// (直接<0 会有些许误差,我们可以 <-5)
if (offsetX < -5) {
// 左
System.out.println("左");
} else if (offsetX > 5) {
// 右
System.out.println("右");
}
// 开始计算垂直方向上下的滑动
} else {
if (offsetY < -5) {
// 上
System.out.println("上");
} else if (offsetY > 5) {
// 下
System.out.println("下");
}
}
break;
}
return true;
}
});
}
上面的逻辑是不是非常的简单,然后我们操作一下,看log
现在手势识别也是很精准了,当然,我们的代码设计也不能太过臃肿,所以,我们的操作逻辑就不在里面编写了,我们分别实现四个方向的方法
// 左
private void isLeft() {
}
// 右
private void isRight() {
}
// 上
private void isTop() {
}
// 下
private void isButtom() {
}
然后把输出语句替换掉,监听到哪个方向就执行哪个方法
//System.out.println("上,下,左,右");
private void isXXX() {
}
四.实现方块类CardView
我们可以把这一个个卡片看作是一个对象,我们每次操作,他都要进行实例化
首先,我们新建一个类CardView继承自FrameLayout,再里面我们要考虑三点
1.卡片
2.卡片上的数字
卡片相同的比较
package com.lgl.game2048;
import android.content.Context;
import android.widget.FrameLayout;
import android.widget.TextView;
public class CardView extends FrameLayout {
// 卡片数量
private int num = 0;
// 卡片文字
private TextView tv_num;
public CardView(Context context) {
super(context);
// 初始化TextView
tv_num = new TextView(getContext());
// 卡片文字大小
tv_num.setTextSize(20);
// 布局控制器,填充满整个父容器
LayoutParams lp = new LayoutParams(-1, -1);
addView(tv_num, lp);
setNum(0);
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
// 要呈现出来的文字(这里要注意是String类型的)
tv_num.setText(num + "");
}
// 两卡片相同的比较方法
public boolean equals(CardView card) {
return getNum() == card.getNum();
}
}
五.动态分配方块的宽高以及添加方块
1.动态分配方块的宽高
写到这里,就有一个梗了,还是Android的老毛病,屏幕的适配问题,所以我们队卡牌的宽高是不能做限定的,也就是说我们要去根据手机屏幕动态分配卡片的width和height,在这里我们就得用到我之前一篇博客
Android绘图机制(一)——自定义View的基础属性和方法
中提到的一个方法了
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// TODO Auto-generated method stub
super.onSizeChanged(w, h, oldw, oldh);
}
他负责我们的View在父容器的位置,所以我们动态分配高宽也是在他这里面完成,首先,位置发生改变之后,我们得到的位置是一个确定数,但是为了防止用户是横放这手机,这就蛋疼了,所以我们得进行一个设置了
我们打开AndroidManifest.xml的activity标签中加入
//禁止屏幕横屏
android:screenOrientation="portrait"
好的,现在可以计算了
宽高求最小值 因为考虑到,我们的方阵他是正方形的,而手机屏幕是长方形的,这样,我们的正方形要设置变长就得求长方形的宽,也就是最小值了
并且我们也不需要他填满宽度,我们需要一点空隙,所以我们减去10个像素
再让他除以4,通过这种方式,我们就可以动态平分这个宽度了
int cardWidth = (Math.min(w, h)-10)/4;
2.添加方块
-1.添加卡片
// 添加卡片,参数为卡片的宽高,因为他是正方形,所以宽高都是cardWidth
private void addCard(int cardWidth, int cardHeight) {
// 创建方块
CardView c;
// 循环添加
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
c = new CardView(getContext());
// num为随机数
c.setNum(2);
addView(c, cardWidth, cardHeight);
}
}
}
好的,我们来运行一下(换个AS2.0的模拟器感觉萌萌哒)
-2.方块换行
你会发现,并没有4*4,而且一排没有换行,我们回到GameView的initView()方法中添加
//换行
setColumnCount(4);
现在再看看
-3.文字居中
现在我们调整一下,让文字居中,在CardView中
//文字居中
tv_num.setGravity(Gravity.CENTER);
现在是不是好看多了
-4.方块颜色
既然是玩2048我们怎么能少了颜色尼,我们就根据这
Android高效率编码-细节,控件,架包,功能,工具,开源汇总
中的色彩表来,自己觉得什么颜色好看可以自行替换,我们直接来到CardView里面
//设置文字背景(暗卡其色)
tv_num.setBackgroundColor(0xffBDB76A);
-5.方块间距
既然是方块间隔,我们还是回到CardView里面,还记得我们设置的LayoutParams吗?你可能想到了吧,我们用Margins
//设置间距
lp.setMargins(10, 10, 0, 0);
-6.记忆方块
我们所操作之后,会有新生成的卡片,为了不重合,我们得做一个记忆功能
// 记录卡片的二维数组
private CardView[][] cards = new CardView[4][4];
然后在addCard()方法中
//记忆
cards[j][i] = c;
六.随机数
我们先来思考一下这个随机数的逻辑,我们玩2048的时候,是不是开始新游戏的时候会随机出现两个方块,而这两个方块,他是随机出现在4*4的任意位置的,所以,我们确定下来,一开始是两个方块的随机出现,再接下来,我们会发现,他有时候是两个2,但是有时候是一个2,一个4,这个4出现的几率有点小,而且我们作为游戏规则制定者,这个也是我们控制的,这里,我不想他出现的很容易,所以我这里的逻辑就设置成1-9,这样4出现的概率会小很多,好了,基本确定了,我们就开始写代码了,我们写一个方法,在此之前,我们要对之前的代码进行调整一下,在CardView中setNum方法中,我们默认为0就占一格,
// 要呈现出来的文字(这里要注意是String类型的)
if (num <= 0) {
tv_num.setText("");
} else {
tv_num.setText(num + "");
}
然后把刚才的文字设置换成0
// c.setNum(2);
c.setNum(0);
然后我们就可以添加随机数了,我们新建一个方法addRandom();
// 随机数
private void addRandom() {
// 我们新建一个lsit存放空的方块,操作之前清空
point.clear();
// 对所有的位置进行遍历
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
// 空方块才可以添加数字,有值我们就不添加
if (cards[j][i].getNum() <= 0) {
point.add(new Point(j, i));
}
}
}
// for循环走完之后我们要取方块
Point p = point.remove((int) (Math.random() * point.size()));
// 我们用Math.random()返回一个0-1的数,当大于0.1的时候是2否则就是4,也就是4出现的概率为十分之一
cards[p.x][p.y].setNum(Math.random() > 0.1 ? 2 : 4);
}
这个时候我们就可以开始游戏了,为了方便等下我们需要重新开始游戏,我们就新建一个startGame()方法,让他在onSizeChanged()调用
// 开启游戏
private void startGame() {
// 既然是开始游戏,我们就要对所有的值进行清理
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
cards[j][i].setNum(0);
}
}
// 重新添加随机数
addRandom();
// 我们要添加两个
addRandom();
}
好的,我们现在运行一下啊
这正是我们要的效果,每次进入游戏随机生成两个方块,他会出现在不同的位置,而且会出现2和4,4的概率小很多
七.实现方块滑动,递加逻辑
好的,终于到了核心的东西了,这次我们就要用到之前所写的上下左右方向方法了
// 左
private void isLeft() {
/**
* 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
*/
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
// 往左滑是一行一行去遍历的
for (int j2 = j + 1; j2 < 4; j2++) {
// 如果说遍历到值
if (cards[j2][i].getNum() > 0) {
// 如果当前位置上为0,就放在这个位置上去
if (cards[j][i].getNum() <= 0) {
cards[j][i].setNum(cards[j2][i].getNum());
// 把原来位置上的数字清除
cards[j2][i].setNum(0);
// 让图形继续遍历
j--;
// 有值,并且还相同
} else if (cards[j][i].equals(cards[j2][i])) {
// 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
cards[j][i].setNum(cards[j][i].getNum() * 2);
// 把原来位置上的数字清除
cards[j2][i].setNum(0);
}
break;
}
}
}
}
}
// 右
private void isRight() {
/**
* 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
*/
for (int i = 0; i < 4; i++) {
for (int j = 3; j >= 0; j--) {
// 往左滑是一行一行去遍历的
for (int j2 = j - 1; j2 >= 0; j2--) {
// 如果说遍历到值
if (cards[j2][i].getNum() > 0) {
// 如果当前位置上为0,就放在这个位置上去
if (cards[j][i].getNum() <= 0) {
cards[j][i].setNum(cards[j2][i].getNum());
// 把原来位置上的数字清除
cards[j2][i].setNum(0);
// 让图形继续遍历
j++;
// 有值,并且还相同
} else if (cards[j][i].equals(cards[j2][i])) {
// 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
cards[j][i].setNum(cards[j][i].getNum() * 2);
// 把原来位置上的数字清除
cards[j2][i].setNum(0);
}
break;
}
}
}
}
}
// 上
private void isTop() {
/**
* 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
*/
for (int j = 0; j < 4; j++) {
for (int i = 0; i < 4; i++) {
// 往左滑是一行一行去遍历的
for (int i2 = i + 1; i2 < 4; i2++) {
// 如果说遍历到值
if (cards[j][i2].getNum() > 0) {
// 如果当前位置上为0,就放在这个位置上去
if (cards[j][i].getNum() <= 0) {
cards[j][i].setNum(cards[j][i2].getNum());
// 把原来位置上的数字清除
cards[j][i2].setNum(0);
// 让图形继续遍历
i--;
// 有值,并且还相同
} else if (cards[j][i].equals(cards[j][i2])) {
// 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
cards[j][i].setNum(cards[j][i].getNum() * 2);
// 把原来位置上的数字清除
cards[j][i2].setNum(0);
}
break;
}
}
}
}
}
// 下
private void isButtom() {
/**
* 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
*/
for (int j = 0; j < 4; j++) {
for (int i = 3; i >= 0; i--) {
// 往左滑是一行一行去遍历的
for (int i2 = i - 1; i2 >= 0; i2--) {
// 如果说遍历到值
if (cards[j][i2].getNum() > 0) {
// 如果当前位置上为0,就放在这个位置上去
if (cards[j][i].getNum() <= 0) {
cards[j][i].setNum(cards[j][i2].getNum());
// 把原来位置上的数字清除
cards[j][i2].setNum(0);
// 让图形继续遍历
i++;
// 有值,并且还相同
} else if (cards[j][i].equals(cards[j][i2])) {
// 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
cards[j][i].setNum(cards[j][i].getNum() * 2);
// 把原来位置上的数字清除
cards[j][i2].setNum(0);
}
break;
}
}
}
}
}
四个方法的逻辑都是大同小异的,不过逻辑性还是很强的,大家可以适当的去研究一下然后我们多增加几个方块先来模拟下效果
八.计分
方块的逻辑差不多写完了,我们先来就在MainActivity里面来实现我们的Score计分
MainActivity
package com.lgl.game2048;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity {
private TextView tv_score;
//外界可以访问的实例
private static MainActivity mainActivity = null;
//积分器
private int score = 0;
public MainActivity() {
mainActivity = this;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_score = (TextView) findViewById(R.id.tv_score);
}
public static MainActivity getMainActivity() {
return mainActivity;
}
public static void setMainActivity(MainActivity mainActivity) {
MainActivity.mainActivity = mainActivity;
}
//清除分数
public void clearScore(){
score = 0;
showScore();
}
//分数
public void showScore(){
tv_score.setText("分数:"+score);
}
public void addScore(int s){
score += s;
showScore();
}
}
这是我们计分的过程,我们思考一下在什么时候计分呢?想想就知道在滑动的时候俩值相加的时候开始计分,所以我们在四个滑动方法有值的判断句中添加
//开始计分 MainActivity.getMainActivity().addScore(cards[j][i].getNum());
同时,我们在开始游戏的时候要清零,所以我们在startGame方法中要添加
//计分清零
MainActivity.getMainActivity().clearScore();
九.滑动后增加方块
我们默认进来是两个方块,但是滑动之后我们应该也要随机增加方块才能达到游戏的逻辑,你说是吧!
所以,只要你滑动了,我们就要添加,一直到gameover结束为止,那我们依然在那四个方向方法里写
private void isxx(){
// 加个判断是否可以添加
boolean isAdd = false;
for(....){
for(....){
for(....){
if(....){
if(....){
....
// 可以添加
isAdd = true;
}else if(....){
....
// 可以添加
isAdd = true;
}
}
}
}
}
// 开始进行判断
if (isAdd) {
// 如果可以合并,我们添加随机数
addRandom();
}
}
好的,我们现在来运行一下
游戏现在大体上是OK的了
十.游戏结束
游戏有始有终,我们现在就来判断游戏结束,游戏结束有两个前提
1.16个格子都是满的
2.上下左右相邻的格子都不相同
这样的话我们就可以写一个endGame方法,然后让他在每次增加方块的时候调用了
// 游戏结束
private void endGame() {
// 在每次添加新的方块的时候判断一下
// 是否结束?
boolean isEnd = true;
ALL: // 标签,让break跳出整个循环
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
// 等于0的话游戏没有结束,或者上下左右还是有相同的数
if (cards[j][i].getNum() == 0
// 左
|| (j > 0 && cards[j][i].equals(cards[j - 1][i]))
// 右
|| (j < 3) && cards[j][i].equals(cards[j + 1][i])
// 上
|| (i > 0 && cards[j][i].equals(cards[j][i - 1]))
// 下
|| (i < 3 && cards[j][i].equals(cards[j][i + 1]))) {
// 说明游戏没有结束
isEnd = false;
break ALL;
}
}
}
if (isEnd) {
// 当isEnd = true的时候游戏结束
new AlertDialog.Builder(getContext())
.setTitle("Sorry,游戏结束!")
.setMessage("是否重新开始?")
.setPositiveButton("是",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// 重新开始
startGame();
}
})
.setNegativeButton("否",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
}
}).show();
}
}
好的,现在我们可以来检测一下了
十一.优化之色块
我们的数字每个数字代表一种颜色,这里我就简单的写点颜色,你们要是喜欢可以自己想改什么就改什么
在CardView中setnum方法里
switch (num) {
case 0:
tv_num.setBackgroundColor(0xffBDB76A);
break;
case 2:
tv_num.setBackgroundColor(0xffeee4da);
break;
case 4:
tv_num.setBackgroundColor(0xffede0c8);
break;
case 8:
tv_num.setBackgroundColor(0xfff2b179);
break;
case 16:
tv_num.setBackgroundColor(0xfff59563);
break;
case 32:
tv_num.setBackgroundColor(0xfff67c5f);
break;
case 64:
tv_num.setBackgroundColor(0xfff65e3b);
break;
case 128:
tv_num.setBackgroundColor(0xffedcf72);
break;
case 256:
tv_num.setBackgroundColor(0xffedcc61);
break;
case 512:
tv_num.setBackgroundColor(0xffedc850);
break;
case 1024:
tv_num.setBackgroundColor(0xffedc53f);
break;
case 2048:
tv_num.setBackgroundColor(0xffedc22e);
break;
default:
tv_num.setBackgroundColor(0xff3c3a32);
break;
}
我们运行下