物理引擎开发是游戏开发中的一环,许多游戏都需要程序来模拟物理环境
研究这个简单的引擎研究了一天,主要实现的是一个小球从高处落下,然后弹起的,知道速度为0 ,静止的过程。
什么都不说了,放出源代码,我做了非常详细的注释,请大家看代码吧!
小球类,具体见代码
/*这个类是定义一个小球具有的属性和方法*/
public class Movable {
int startX = 0;
int startY = 0; //小球的初始坐标X,Y,小球的实时坐标应该等于初始坐标加位移
int currentX;
int currentY; //实时坐标
float startVX = 0f;
float startVY = 0f; //初始时水平和竖直方向的速度
float currentVX = 0f;
float currentVY = 0f; //实时速度
int r; //可移动物体的半径
double timeX;
double timeY; //物体在X,Y上移动的时间,当物体从一个阶段进入到另一个阶段,此属性被重置
Bitmap bitmap = null; //声明一个要使用的图片
BallThread bt = null; //自己写的物理引擎,为一个线程,将根据物理公式对小球的位置坐标等属性修改
boolean bFall = false; //判断小球是否从木板上落下来了
float impactFactory = 0.25f; //小球撞地后的速度衰减系数
public Movable(int x, int y, int r, Bitmap bitmap){
this.startX = x;
this.startY = y;
this.currentX = x;
this.currentY = y; //构造函数初始化的时候,初始位置和实时位置相同
this.r = r;
this.bitmap = bitmap;
/*
* System.nanoTime是专门用来算间隔的(衡量时间段)。System.nanoTime()返回的是纳秒,nanoTime而返回的可能是任意时间,
* 甚至可能是负数
* System.currentTimeMillis()返回的毫秒,这个毫秒其实就是自1970年1月1日0时起的毫秒数,Date()其实就是相当于
* Date(System.currentTimeMillis());因为Date类还有构造Date(long date),用来计算long秒与1970年1月1日之间的毫秒差。
*/
timeX = System.nanoTime();
this.currentVX = BallView.V_MIN + (int)((BallView.V_MAX - BallView.V_MIN) * Math.random());
//上面这行是随机在水平向的最小和最大速度之间随便取个数作为水平初速度,正的,小球向右运动
bt = new BallThread(this);
bt.start(); //创建并启动物理引擎线程
}
public void drawSelf(Canvas canvas){ //将自己绘制(Bitmap贴图)画到画布上
canvas.drawBitmap(bitmap, currentX, currentY, null);
}
}
下面这个类是这整篇博客的精华所在,物理引擎类
/*
* 该类为一个物理引擎,主要功能是改变小球运动轨迹!另,在android中,坐标系以Y向下为正
* 全是初中就学过的物理,我觉得大家应该能看懂
* 这个类就是这整个程序的核心了~~~,物理引擎啊,好好看吧,我尽量注释详细点
*/
public class BallThread extends Thread {
Movable father; //声明一个小球的实例
boolean flag = false; //线程是否执行的标记
int sleepSpan = 40; //休眠时间
float g = 200; //求落下的加速度
double current; //记录当前时间
public BallThread(Movable father){ //让每个小球都要自己的物理引擎,按自己的引擎去运动
this.father = father;
this.flag = true; //一旦初始化线程就开始执行
}
//run方法进行两项工作:(run方法又是整个这个类的核心)
//(1)按照物理公式移动改变小球位置
//(2)检测并处理小球达到最高点以及撞击地面的事件
//而且,请各位注意,每个阶段一开始的时候,要把所有的时间的记录啊,X,Y的速度啊,X,Y位置啊之类的全更新
public void run(){
while(flag){ //只要线程在,就一直循环
current = System.nanoTime(); //取得当前的时间
//开始处理水平方向的运动
//获取水平方向走过的时间,当前时间减掉小球初始化时候的时间,就是时间差,除以3个1000就得到s,nanotime得到的是ns
double timeSpanX = (double)((current - father.timeX)/1000/1000/1000);
father.currentX = (int)(father.startX + father.currentVX * timeSpanX); //初始位置+速度*时间=当前位置,水平向是匀速的
//下面处理竖直方向的运动
if(father.bFall){ //bFall是判断小球有没有从木板上落下来,落下来了才有竖向速度
double timeSpanY = (double)((current - father.timeY)/1000/1000/1000);
father.currentY = (int)(father.startY + father.startVY * timeSpanY + timeSpanY * timeSpanY * g/2); //s=vt+1/2at
father.currentVY = (float)(father.startVY + g * timeSpanY); //求竖向当前速度,当前速度=v + at(高中物理)
//下面这个是判断小球是否达到最高点
if(father.startVY < 0 && Math.abs(father.currentVY) <= BallView.UP_ZERO){ //startVY<0证明小球是向上运动的
father.timeY = System.nanoTime(); //设置新的运动阶段竖直方向的开始时间
father.currentVY = 0; //设置新的运动阶段竖直方向的实时速度,达到最高点是时,速度肯定为0
father.startVY = 0; //设置新的运动阶段竖直方向的初始速度,达到最高点以后开始改变方向,起始速度为0
father.startY = father.currentY; //设置新的运动阶段竖直方向的初始位置,这时刚刚要改变运动方向,所以初始位置=当前位置
}
//判断小球是否落地
if(father.currentY + father.r*2 >= BallView.GROUND_LING && father.currentVY > 0){
father.currentVX = father.currentVX * (1 - father.impactFactory); //衰减水平方向的速度
father.currentVY = 0 - father.currentVY * (1 - father.impactFactory); //衰减竖向的速度,并改变速度的方向(反弹)
if(Math.abs(father.currentVY) < BallView.DOWN_ZERO){ //如果衰减后的速度小于DOWN_ZERO,就停止线程的执行,Math.adb求绝对值
this.flag = false;
}else{ //如果衰减后速度还够,就弹起
father.startX = father.currentX; //弹起时水平方向的起始位置
father.timeX = System.nanoTime(); //记录弹起时的时间
father.startY = father.currentY;
father.timeY = System.nanoTime();
father.startVY = father.currentVY;
}
}
}else if(father.currentX + father.r/2 >= BallView.WOODEDGE){ //当前位置加上小球半径的一半,大于这个值,小球就掉落了
//BallView.WOODEDGE是木板的长度
father.timeY = System.nanoTime(); //记录竖直方向上的开始运动时间
father.bFall = true; //一定会先执行这个else,才执行上面这个if,即timeY会先有值。看判断条件
}
try{
Thread.sleep(sleepSpan);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
可能有的行比较长,自动换行了,大家克服一下~
下面是显示用的类
/*
* 这个类的作用有以下几个
* (1)作为一个主界面显示
* (2)在此类中声明物理计算时使用的静态常量
* (3)初始化图片资源
* (4)初始化小球
* 多说一点,其实这里可以把刷新画面的线程直接写在这个类里面,只要implements Runnable就行了,但是单独重写了一个
* 线程类,也是为了把功能分开,尽量不要把功能都放在一个类里面,分开放,这才是面向对象不是?
* 另,这些
* 代码都不是我原创,不过看完书以后理解了,详细注释以后来给大家分享分享
*/
public class BallView extends SurfaceView implements SurfaceHolder.Callback{
public static final int V_MAX = 55; //小球水平速度最大值
public static final int V_MIN = 30; //小球水平速度的最小值
public static final int WOODEDGE = 140; //木板的长度
public static int GROUND_LING; //屏幕的底部,即屏幕最下方的Y坐标,表示小球落到底了
//根据你用的手机来设定
public static final int UP_ZERO = 30; //小球上升过程中,如果小于这个值,速度就为算为0
public static final int DOWN_ZERO = 60; //小球撞击地面后,如果速度小于该值就算为0
Bitmap[] bitmapArray = new Bitmap[6]; //各种小球的引用
Bitmap bmpBack; //背景图片
Bitmap bmpWood; //木板图片
String fps = "FPS:N/A"; //用于显示帧速率的字符串,初始时为:"FPS:N/A",调试程序用
int ballNumber = 8; //小球的数量
ArrayList<Movable> alMovable = new ArrayList<Movable>(); //小球对象的列表
DrawThread dt; //后台屏幕绘制线程
private SurfaceHolder sfh;
public BallView(Context Activity) {
super(Activity);
sfh = this.getHolder();
sfh.addCallback(this);
initBitmaps(getResources()); //自己写的方法,初始化图片
initMovables(); //自己写的方法,初始化小球
dt = new DrawThread(this, sfh); //初始化重绘线程
// TODO Auto-generated constructor stub
}
//初始化小球
public void initMovables(){
Random r = new Random(); //创建一个随机对象
for(int i = 0; i < ballNumber; i++){
int index = r.nextInt(32); //取得一个随机数
Bitmap tempBitmap = null;
if(i < ballNumber/2){
tempBitmap = bitmapArray[3 + index % 3]; //如果初始化前一半的球,就从大球中随机找一个
}else{ //关于大球还是小球,请看initBitmaps()这个函数
tempBitmap = bitmapArray[index % 3]; //如果初始化后一半的球,就从小球中随机找一个
}
Movable m = new Movable(0, 70 - tempBitmap.getHeight(), tempBitmap.getWidth()/2, tempBitmap);
alMovable.add(m); //每次循环放一个到arrayList里
}
}
//初始化图片
public void initBitmaps(Resources r){
bitmapArray[0] = BitmapFactory.decodeResource(r, R.drawable.ball_red_small); //红色小球
bitmapArray[1] = BitmapFactory.decodeResource(r, R.drawable.ball_purplev_small); //紫色小球
bitmapArray[2] = BitmapFactory.decodeResource(r, R.drawable.ball_green_small); //绿色小球
bitmapArray[3] = BitmapFactory.decodeResource(r, R.drawable.ball_red); //红色大球
bitmapArray[4] = BitmapFactory.decodeResource(r, R.drawable.ball_purple); //紫色大球
bitmapArray[5] = BitmapFactory.decodeResource(r, R.drawable.ball_green); //绿色大球
bmpBack = BitmapFactory.decodeResource(r, R.drawable.back);
bmpWood = BitmapFactory.decodeResource(r, R.drawable.wood);
}
//绘制程序中所需要的图片
public void doDraw(Canvas canvas){
canvas.drawBitmap(bmpBack, 0, 0, null); //绘制背景,中间两个参数是图片的左边和上边的位置
canvas.drawBitmap(bmpWood, 0, 60, null); //绘制木板
for(Movable m : alMovable){ //遍历所以小球,挨个绘制
m.drawSelf(canvas);
}
Paint p = new Paint(); //初始化一个画笔
p.setColor(Color.BLUE); //设置画笔颜色
p.setTextSize(18); //设置字体大小
p.setAntiAlias(true); //设置画笔抗锯齿
canvas.drawText(fps, 30, 30, p); //绘制fps到画布上
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
// TODO Auto-generated method stub
}
//surfaceView被创建时调用,所以类似启动后台刷屏线程的代码就写在这
@Override
public void surfaceCreated(SurfaceHolder holder) {
// TODO Auto-generated method stub
GROUND_LING = this.getHeight(); //只有surface创建以后,才能取到屏幕宽高
if(!dt.isAlive()){
dt.start(); //启动后台刷屏线程
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// TODO Auto-generated method stub
dt.flag = false;
dt = null;
}
}
画图的线程类
/*
* 这个类的作用有两个
* (1)定时重绘屏幕
* (2)计算帧速率
*/
public class DrawThread extends Thread {
BallView bv;
SurfaceHolder surfaceHolder;
boolean flag = false; //线程执行标志位
int sleepSpan = 30; //休眠时间
long start = System.nanoTime(); //记录起始时间,该变量用于计算帧速率
int count = 0; //记录帧数,该变量用于计算帧速率
public DrawThread(BallView bv, SurfaceHolder surfaceHolder){
this.bv = bv;
this.surfaceHolder = surfaceHolder;
this.flag = true;
}
public void run(){
Canvas canvas = null;
while(flag){
try{
canvas = surfaceHolder.lockCanvas(null); //获取Ballview的画布
synchronized(surfaceHolder){ //对象锁
bv.doDraw(canvas); //调用BallView的doDraw方法进行绘制
}
}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; //记录时间间隔,即记满20帧需要的时间
start = tempStamp;
double fps = Math.round(100000000000.0 / span * 20)/100.0; //计算帧速率
//(100s内包含的时间间隔(span)个数*20,即100s能绘制的帧数)
bv.fps = "FPS:" + fps; //将帧速率
}
try{
Thread.sleep(sleepSpan);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
最后是android必须有的mainActivity类,我们把显示画面设置为BallView类
public class MainActivity extends Activity {
BallView bv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); //设置不显示标题
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //设置全屏
bv = new BallView(this);
setContentView(bv);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
源码地址:http://download.csdn.net/detail/lxtalx/6544169