上一篇文章里讲到了如何创建世界及其边界,这次我将给大家讲讲如何模拟这个世界。
什么是模拟世界
虽然我们创建世界以及作为边界的刚体,但是这个世界是需要我们不断的去计算各个刚体的状态如速度、加速度、受力情况等,而这个不断的计算就是所谓的模拟世界,所以我们需要用一个定时器不断的模拟世界,使得世界能够运转。
模拟世界
ScheduledThreadPoolExecutor定时器
在这里,我选择使用ScheduledThreadPoolExecutor定时器,因为它比Timer的功能强大、性能优越,所以推荐大家使用ScheduledThreadPoolExecutor进行世界的模拟而不是Timer。
创建模拟世界任务MyTask
ScheduledThreadPoolExecutor的用法与Timer类似,所以也需要自己写一个MyTask让定时器去跑这个任务,这次我们的任务便是模拟世界,下面来看看代码:
//用作复制bl
public ArrayList<MyBody> _bl = new ArrayList<MyBody>();
//模拟的的频率
private float timeStep = 1.0f / 60.0f;
//迭代越大,模拟约精确,但性能越低
private int iterations = 10;
...
private class MyTask implements Runnable {
private GameView gv;//游戏视图
public MyTask(GameView gameView) {
this.gv = gameView;//初始化成员变量
}
@Override
public void run() {
try {
if (!isGameOver) {//判断游戏是否进行中
gv.activity.world.step(timeStep, iterations); //开始模拟
_bl.clear();//清空_bl
for(MyBody mpi:bl){
_bl.add(mpi);//复制bl到_bl中
}
gv.repaint(); //绘制整个世界
}
...
}catch (Exception e){
}
}
}
代码中的GameView是一个继承SurfaceView类的一个类,gv.repaint()为绘制世界,在下面会讲到。
_bl用于复制bl中的刚体,目的在于将动态改动的bl复制到一个静态的 _bl中,便于以后创建、销毁刚体时不会出错。
timeStep即为模拟频率,1/60表示60Hz。
iterations为迭代,具体的意思我也不是太清楚,这个值的大小对我的项目没有什么影响,设置为10就好了。
gv.activity.world.step()即为我们所需要的模拟了,这个函数让我们的世界正常运转,所以一定不要忘了。
设置MyTask
创建好了MyTask,我们需要将其添加至定时器中:
private ScheduledThreadPoolExecutor stpe;
private MyTask myTask = new MyTask(this);
private void playGame(){
//参数表示打开的线程池大小
stpe = new ScheduledThreadPoolExecutor(5);
stpe.scheduleAtFixedRate(myTask, 0, 1000/60, TimeUnit.MILLISECONDS);
...
}
ScheduledThreadPoolExecutor可以指定线程池的大小,即指定可以并发的线程数量。
scheduleAtFixedRate()四个参数意思为:任务、开始任务的起始时间、任务间间隔、时间单位。
TimeUnit.MILLISECONDS表示单位为毫秒。
scheduleAtFixedRate表示会在每一个myTask执行完毕后再开始计算时间间隔。
显示世界
经过我们的努力,总算是让世界运转起来了,不过虽然是运转起来了,但是却并没有把世界的“模样”显示给用户,所以我们接下来要做的便是将世界显示出来。
在创建刚体根类的时候,我给大家讲过,我们是通过canvas将刚体画出来的,所以我们采用的方法即为用画布时时画出世界的状态,以呈现出世界的模样。
GameView
在项目中,我创建了一个GameView用作显示游戏界面,GameView类是继承SurfaceView的一个类。在这里继承SurfaceView是因为SurfaceView很适合用作游戏界面,它具有双画布机制,两张画布轮流显示,即在显示一张画布时,另一张画布会画即将显示的界面,保证了界面的刷新的流畅。
下面就先看看部分代码:
public class GameView extends SurfaceView implements SurfaceHolder.Callback {
private MainActivity activity;//父activity
private Paint paint; //画笔
...
public GameView(MainActivity activity) {//构造器
super(activity);//调用父类
this.activity = activity; //初始化成员变量
this.getHolder().addCallback(this);//设置生命周期回调接口的实现者
paint = new Paint(); //创建画笔
paint.setAntiAlias(true); //打开抗锯齿
playGame();//开始游戏
...
}
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {}
//创建时被调用
public void surfaceCreated(SurfaceHolder holder) {repaint();}
//销毁时被调用
public void surfaceDestroyed(SurfaceHolder arg0) {}
public void repaint() {
SurfaceHolder holder = this.getHolder();//得到回调接口的对象
Canvas canvas = holder.lockCanvas(); //获取并锁定画布
try {
synchronized (holder) { //同步处理
OnDraw(canvas); //绘制
}
} catch (Exception e) { //捕获异常
e.printStackTrace(); //打印堆栈信息
} finally {
if (canvas != null) {//判断canvas是否为空
holder.unlockCanvasAndPost(canvas);//解锁画布
}
}
}
...
holder.lockCanvas()即在获取未显示的那张画布,并锁定这张画布开始绘制,绘制完毕后通过holder.unlockCanvasAndPost(canvas)解锁画布并显示,这即为SurfaceView的双画布机制。
当然,以上只是一些简单的构造,真正开始绘制世界的是onDraw()函数。
onDraw
做了这么多的准备工作后,我们现在终于可以开始绘制世界了,下面先来看看onDraw()函数的代码吧:
public void OnDraw(Canvas canvas) {//绘制方法
if (canvas == null) {//判断canvas是否为空
return;//canvas为空则返回
}
canvas.drawARGB(255, 123, 123, 123); //设置背景颜色
for (MyBody mb : _bl) { //遍历所有刚体
mb.drawSelf(canvas, paint); //绘制
}
...
}
设置背景颜色的目的在于用背景色将画布之前的内容覆盖掉。
通过遍历复制bl后的 _bl的刚体,对每个刚体进行绘制。
OK,到这里,我们的世界就完完整整的模拟并被绘制出来了,有木有很激动!有木有!有木有!
咳咳~~现在显示是显示出来了,不过现在这个世界也就只有边界,其余的物体是没有的,所以即使显示出了世界没有什么可以看到的东西,不过不要方,相信看过上一篇文章后,你也会自己向世界中添加刚体了。当然,后面也会接着讲如何创建其他刚体的。
这次就先讲这么多,咱们下篇文章再见~