Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写。加上色彩。分数等深度剖析开发过程!
关于2048,我看到非常多大神,比方医生。郭神。所以我也研究了一段时间。还好是研究了一套逻辑,这是一整套的2048游戏从设计到逻辑再到编写的全部过程,小伙伴们看细致咯,刚好今天是礼拜天,一天应该了一把这篇博客发表了,事实上2048开发起来还是有点难度的。而且他的逻辑挺强的,我也是看了非常多的资料偷学的,非常适合来锻炼自己的逻辑性
我们首先先来选择开发环境,这里我们就以Eclipse为IDE,新建一个project——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出现的几率有点小,而且我们作为游戏规则制定者,这个也是我们控制的,这里。我不想他出现的非常easy。所以我这里的逻辑就设置成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;
}
我们执行下