自学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,布局效果如下所示:

                                               

android studio制作游戏 android studio2048小游戏_android studio制作游戏

    接下来实现游戏主类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();
		}

	}

 

    这样的话游戏基本的逻辑代码就实现了,此时只是实现了游戏的基本逻辑,游戏还可以继续优化,美化,添加功能等。如果对上面文章感觉不是很清楚的话,建议还是结合上面链接中的视频和源码理解,如链接失效,麻烦联系我更新,相信这还是非常适合入门的一个安卓小项目的。