android 游戏开发之物理小球的应用

游戏开发不同于应用,一切都是自定义,所以对于控件什么的也没有什么好讲的,下面简单作一个案例,其实也是书本上的,最近在学习,顺便总结下.

首先我们要开发对象.Movable.java

package com.jj.ball;

import android.graphics.Bitmap;
import android.graphics.Canvas;

/***
 * 小球对象
 * 
 * @author zhangjia
 * 
 */
public class Moveable {
	int startX = 0;// 初始X坐标
	int startY = 0;// 初始Y坐标
	int x;// 时时X,
	int y;// 时时Y

	float startVX = 0f;// 初始竖直方向的速度
	float startVY = 0f;// 初始水平方向的速度

	float v_x = 0f;// 时时竖直方向速度
	float v_y = 0f;// 时时水平方向速度

	int r;// 半径

	double timeX;// 水平运动时间
	double timeY;// 竖直运动时间

	Bitmap bitmap = null;// 小球

	BallThread bt = null;

	boolean bFall = false;// 小球是否已经从木板下落

	float impactFactor = 0.25f;// 小球撞地后速度的损失系数.

	/***
	 * 构造方法
	 * 
	 * @param x
	 * @param y
	 * @param r
	 * @param bitmap
	 */
	public Moveable(int x, int y, int r, Bitmap bitmap) {
		super();
		this.x = x;
		this.startX = x;
		this.startY = y;
		this.y = y;
		this.r = r;
		this.bitmap = bitmap;
		timeX = System.nanoTime();
		this.v_x = BallView.V_MIN
				+ (int) ((BallView.V_MAX - BallView.V_MIN) * Math.random());// 初始水平速度
		bt = new BallThread(this);// 创建并启动BallThread
		bt.start();
	}

	/***
	 * 绘画
	 */
	public void drawSelf(Canvas canvas) {
		canvas.drawBitmap(bitmap, x, y, null);
	}

}
这里面无外乎就是一些小球的属性,涉及到了BallThread物理引擎,下面接着介绍.

首先你要明白:

微妙,纳秒,秒之间的换算
微秒,时间单位,符号μs
1,000,000 微秒 = 1秒 纳秒,时间单位,符号ns 1,000,000,000纳秒=1秒
另外你要明白:一些物理公式:比如说如何求:竖直方向的距离,水平方向移动距离,以及移动过程中的水平,竖直的速度等等,这些都是我们高一的物理知识,虽说我们N年没有触碰过了,但是现在回忆起来还是瞒不错的.在这里我感慨一下:中国教育课程五花八门,真正现实中我们用得到的又有几个?NND,坑爹青春啊.
还有一点:就是小球在撞击地面和上升到最高处都是极端位置,然后再次进行运动的话,都是一次新的运动.不过想大家都明白的,只是我看后感慨游戏确实比应用好多了,起码逻辑思维是应用不能媲美的.
package com.jj.ball;

/***
 * 小球物理引擎
 * 
 * @author zhangjia
 * 
 */
public class BallThread extends Thread {
	private Moveable moveable;// 小球对象
	private boolean flag = false;// 线程标识
	private int sleepSpan = 30;// 休眠时间
	private float g = 200;// 重力加速度
	private double currentTime;// 记录当前事件

	/***
	 * 构造方法
	 * 
	 * @param moveable
	 */
	public BallThread(Moveable moveable) {
		super();
		this.moveable = moveable;
		this.flag = true;
	}

	@Override
	public void run() {
		while (flag) {
			currentTime = System.nanoTime();// 获取当前时间,单位为纳秒
			double timeSpanX = (double) ((currentTime - moveable.timeX) / 1000 / 1000 / 1000);// 获取从玩家开始到现在水平方向走过的时间
			// 处理水平方向上的运动
			moveable.x = (int) (moveable.startX + moveable.v_x * timeSpanX);
			// 处理竖直方向上的运动
			if (moveable.bFall) {
				double timeSpanY = (double) ((currentTime - moveable.timeY) / 1000 / 1000 / 1000);
				moveable.y = (int) (moveable.startY + moveable.startVY
						* timeSpanY + timeSpanY * timeSpanY * g / 2);
				moveable.v_y = (float) (moveable.startVY + g * timeSpanY);
				// 判断小球是否到达最高点
				if (moveable.startVY < 0
						&& Math.abs(moveable.v_y) <= BallView.UP_ZERO) {
					moveable.timeY = System.nanoTime(); // 设置新的运动阶段竖直方向上的开始时间
					moveable.v_y = 0; // 设置新的运动阶段竖直方向上的实时速度
					moveable.startVY = 0; // 设置新的运动阶段竖直方向上的初始速度
					moveable.startY = moveable.y; // 设置新的运动阶段竖直方向上的初始位置
				}
				// 判断小球是否撞地
				if (moveable.y + moveable.r * 2 >= BallView.GROUND_LING
						&& moveable.v_y > 0) {// 判断撞地条件
					// 改变水平方向的速度
					moveable.v_x = moveable.v_x * (1 - moveable.impactFactor); // 衰减水平方向上的速度
					// 改变竖直方向的速度
					moveable.v_y = 0 - moveable.v_y
							* (1 - moveable.impactFactor); // 衰减竖直方向上的速度并改变方向
					if (Math.abs(moveable.v_y) < BallView.DOWN_ZERO) { // 判断撞地后的速度,太小就停止
						this.flag = false;
					} else { // 撞地后的速度还可以弹起继续下一阶段的运动
						// 撞地之后水平方向的变化
						moveable.startX = moveable.x; // 设置新的运动阶段的水平方向的起始位置
						moveable.timeX = System.nanoTime(); // 设置新的运动阶段的水平方向的开始时间
						// 撞地之后竖直方向的变化
						moveable.startY = moveable.y; // 设置新的运动阶段竖直方向上的起始位置
						moveable.timeY = System.nanoTime(); // 设置新的运动阶段竖直方向开始运动的时间
						moveable.startVY = moveable.v_y; // 设置新的运动阶段竖直方向上的初速度
					}
				}
			} else if (moveable.x + moveable.r / 2 >= BallView.WOOD_EDGE) {// 判断球是否移出了挡板
				moveable.timeY = System.nanoTime(); // 记录球竖直方向上的开始运动时间
				moveable.bFall = true; // 设置表示是否开始下落标志位
			}
			try {
				Thread.sleep(sleepSpan); // 休眠一段时间
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

}
在这里我要简单补充一点,或许大家都明白懂得,但是我在这里疑惑了,问题是:判断小球是否撞地moveable.y + moveable.r * 2 >= BallView.GROUND_LING在这里为什么半径要乘以2呢,如果这样想的话,那么你就是把中心放在小球的球心了,但是我们里面涉及的X,Y等等都是按照小球的左上角来计算的.就相当于最外面有一个矩形框在包裹着.其实这是用到碰撞检测技术中的(矩(圆柱)形检测).这样我们就很自然的明白了为什么半径乘以2了.判断小球是否移出挡板moveable.x + moveable.r / 2 >= BallView.WOOD_EDGE.在现实中,不可能小球还没有离开挡板就开始下落吧,如果是这样就不符合现实状况了,所以就在小球还差1/4之离开挡板之时开始下落,这样就和现实接轨了.(其实看起来没有什么区别,但是你要明白).
大家认真看的话一定可以理解的,毕竟代码都不难理解,关键是思维,你得在脑海中有小球运动的轨迹模型.

小球运动的物理引擎开发完毕后,得让小球显示出来吧,接下来我们要开发View 视图.
在这里我说明一点:在应用中,我们一般都不会自定义View,如果非要自定义的话,无外乎也是对继承已有的控件进行改造实现自己想要的效果,(特例除外),但是游戏不同,因为游戏的界面都不是android自带控件,都是通过Canvas画上去的.所以在游戏中我们一般直接继承自View和SurfaceView进行画图.
游戏的核心是不断地绘图和刷新界面,图我们已经通过onDraw 方法绘制了,下面来分析如何刷新界面。

View与SurfaceView的区别在哪里呢?
简单说明:
在View中android为我们提供了两种方法更新UI,invalidate postInvalidate,然而我们也要通过Handler,而postInvalidate不需要,直接可以在线程中更新,SurfaceView可以在自定义线程中进行更新视图.
说的有点模糊,给大家提供篇博客,强烈建议大家先看一下这篇博客:http://byandby.iteye.com/blog/824535.
接下来我们看BallView.java
package com.jj.ball;

import java.util.ArrayList;
import java.util.Random;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/***
 * 自定义View
 * 
 * @author zhangjia
 * 
 */
public class BallView extends SurfaceView implements SurfaceHolder.Callback {
	static final int V_MAX = 35;// 小球水平速度的最大值
	static final int V_MIN = 15;// 小球水平速度的最小值

	public static final int WOOD_EDGE = 60;// 木板右边沿的X坐标

	static final int GROUND_LING = 730;// 代表地面Y的坐标

	static final int UP_ZERO = 30;// 小球在上升的过程中,如果小于此数则为0,

	static final int DOWN_ZERO = 60;// 小球在下落的过程中,如果速度小于该值则为0.

	Bitmap bitmapArray[] = new Bitmap[6];// 图片数组
	Bitmap bmpBack;// 背景图片背景
	Bitmap bmpWood;// 模板图片背景

	String fps = "FPS:N/A";// 用于显示帧率的字符串

	int ballNumber = 8;// 小球个数

	ArrayList<Moveable> arrayList = new ArrayList<Moveable>();// 小球对象数组

	private SurfaceHolder surfaceHolder;
	private DrawThread drawThread;

	public BallView(Context context) {
		super(context);
		surfaceHolder = getHolder();
		surfaceHolder.addCallback(this);
		initBitmaps(getResources());
		initMovables();
		drawThread = new DrawThread(this, surfaceHolder);
	}

	public BallView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	/***
	 * 初始化图片数据
	 * 
	 * @param resources
	 */
	void initBitmaps(Resources resources) {
		bitmapArray[0] = BitmapFactory.decodeResource(resources,
				R.drawable.ball_red_small); // 红色较小球
		bitmapArray[1] = BitmapFactory.decodeResource(resources,
				R.drawable.ball_purple_small); // 紫色较小球
		bitmapArray[2] = BitmapFactory.decodeResource(resources,
				R.drawable.ball_green_small); // 绿色较小球
		bitmapArray[3] = BitmapFactory.decodeResource(resources,
				R.drawable.ball_red); // 红色较大球
		bitmapArray[4] = BitmapFactory.decodeResource(resources,
				R.drawable.ball_purple); // 紫色较大球
		bitmapArray[5] = BitmapFactory.decodeResource(resources,
				R.drawable.ball_green); // 绿色较大球
		bmpBack = BitmapFactory.decodeResource(resources, R.drawable.back); // 背景砖墙
		bmpWood = BitmapFactory.decodeResource(resources, R.drawable.wood); // 木板
	}

	/***
	 * 初始化小球
	 */
	public void initMovables() {
		Random random = new Random();
		for (int i = 0; i < ballNumber; i++) {
			int index = random.nextInt(32); // 产生随机数
			Bitmap tempBitmap = null; // 声明一个Bitmap图片引用
			if (i < ballNumber / 2) {
				tempBitmap = bitmapArray[3 + index % 3];// 如果是初始化前一半球,就从大球中随机找一个
			} else {
				tempBitmap = bitmapArray[index % 3];// 如果是初始化后一半球,就从小球中随机找一个
			}
			Moveable m = new Moveable(0, 70 - tempBitmap.getHeight(),
					tempBitmap.getWidth() / 2, tempBitmap); // 创建Movable对象
			arrayList.add(m); // 将新建的Movable对象添加到ArrayList列表中
		}
	}

	/***
	 * 绘制图片信息
	 */
	public void doDraw(Canvas canvas) {
		// 绘制全屏
		RectF rectF = new RectF(0, 0, getWidth(), getHeight());
		canvas.drawBitmap(bmpBack, null, rectF, null);// 绘制背景
		canvas.drawBitmap(bmpWood, 0, 60, null);// 绘制木板
		// 绘制一系列小球
		for (Moveable moveable : arrayList) {
			moveable.drawSelf(canvas);
		}
		Paint paint = new Paint();
		paint.setColor(Color.BLUE);
		paint.setTextSize(18);
		paint.setAntiAlias(true);
		canvas.drawText(fps, 30, 30, paint);// 绘制文字

	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		if (!drawThread.isAlive())
			drawThread.start();
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {

	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		drawThread.flag = false;
		drawThread = null;

	}

}
注释已经相当的清晰,相信大家都看的明白,这里不过多介绍,我们接下来看DrawThread.用于重绘屏幕和计算帧率.

DrawThread.java
package com.jj.ball;

import android.graphics.Canvas;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/***
 * 用于重绘屏幕和计算帧率
 * 
 * @author zhangjia
 * 
 */
public class DrawThread extends Thread {
	BallView ballView;// 自定义View
	SurfaceHolder surfaceHolder;
	boolean flag = false;// 线程标识
	int sleepSpan = 30;// 线程休眠
	long start = System.nanoTime();// 其实时间,用于计算帧速率
	int count = 0;// 计算帧率

	public DrawThread(BallView ballView, SurfaceHolder surfaceHolder) {
		super();
		this.ballView = ballView;
		this.surfaceHolder = surfaceHolder;
		this.flag = true;
	}

	@Override
	public void run() {
		Canvas canvas = null;
		while (flag) {
			try {
				canvas = surfaceHolder.lockCanvas();// 获取canvas.
				synchronized (surfaceHolder) {
					ballView.doDraw(canvas);// 进行绘制ballView.

				}
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (canvas != null) {
					surfaceHolder.unlockCanvasAndPost(canvas);// 解锁
				}
			}
			this.count++;
			if (count == 20) { // 如果计满20帧
				count = 0; // 清空计数器
				long tempStamp = System.nanoTime();// 获取当前时间
				long span = tempStamp - start; // 获取时间间隔
				start = tempStamp; // 为start重新赋值
				double fps = Math.round(100000000000.0 / span * 20) / 100.0;// 计算帧速率
				ballView.fps = "FPS:" + fps;// 将计算出的帧速率设置到BallView的相应字符串对象中
			}
			try {
				Thread.sleep(sleepSpan); // 线程休眠一段时间
			} catch (Exception e) {
				e.printStackTrace(); // 捕获并打印异常
			}
		}
	}

}
在这里获取BallView的surfaceHolder,我们要在这个线程中不停的进行绘制屏幕,已达到动画的效果.surfaceHolder的应用,至于什么时候lockCanvas和unlockCanvasAndPost等等,大家还是参考刚才推荐的那篇文章.

在这里我要详细讲解一个东东:帧速率.
this.count++;
			if (count == 20) { // 如果计满20帧
				count = 0; // 清空计数器
				long tempStamp = System.nanoTime();// 获取当前时间
				long span = tempStamp - start; // 获取时间间隔
				start = tempStamp; // 为start重新赋值
				double fps = Math.round(100000000000.0 / span * 20) / 100.0;// 计算帧速率
				ballView.fps = "FPS:" + fps;// 将计算出的帧速率设置到BallView的相应字符串对象中
			}
首先我们先获取20帧所消耗的时间span.然后在计算100s中包含了多少个span.然后在乘以20得到的就是100s中获得了多少帧,最后在除以100,就获得了1s钟得到的帧数,及帧速率(FPS).

这样我们就为小球准备好了所以工作,最后只是在Activity中调用即可.
requestWindowFeature(Window.FEATURE_NO_TITLE); // 设置不显示标题
		getWindow().setFlags(
				// 设置为全屏模式
				WindowManager.LayoutParams.FLAG_FULLSCREEN,
				WindowManager.LayoutParams.FLAG_FULLSCREEN);
		window_Height = getWindowManager().getDefaultDisplay().getHeight();
		window_Width = getWindowManager().getDefaultDisplay().getWidth();

		ballView = new BallView(this); // 创建BallView对象
		setContentView(ballView); // 将屏幕设置为BallView对象
最后给大家展示一下效果吧.(说来说去如果没有图的话,兴趣会减去一大半,一点也不夸张,我看别人博客也一样,如果没有图或许就不看了.呵呵)
示例如下:
    android 游戏开发之物理小球的应用_第1张图片      android 游戏开发之物理小球的应用_第2张图片
效果看起来不错吧.确实如此,游戏就是fun!!!
最后要说明一点:在游戏开发中,对屏幕分辨率要求很高,比如说这个案例,书上用的是320*480.而我的是480*800.没有处理运行起来很难看.
原因:给的图是一个整体.就是墙壁和地面,所以我无法判断地面所处的位置的Y值.获取有办法吧,在图片中算好地面占图片的比例,不过只是猜想,因为俺才初来咋到.如果你是搞游戏开发的话,那么还望你留下你们的解决方案.

就说到这里,如有疑问请留言。

另外,如果对您有帮助的话,记得赞一个哦.

在此:Thanks for you !



   

你可能感兴趣的:(android 游戏开发之物理小球的应用)