蜡笔物理学小游戏:基于JBox2D物理引擎开发的“雷电”小游戏(三)——模拟并显示世界

上一篇文章里讲到了如何创建世界及其边界,这次我将给大家讲讲如何模拟这个世界。

什么是模拟世界

虽然我们创建世界以及作为边界的刚体,但是这个世界是需要我们不断的去计算各个刚体的状态如速度、加速度、受力情况等,而这个不断的计算就是所谓的模拟世界,所以我们需要用一个定时器不断的模拟世界,使得世界能够运转。

模拟世界

ScheduledThreadPoolExecutor定时器

在这里,我选择使用ScheduledThreadPoolExecutor定时器,因为它比Timer的功能强大、性能优越,所以推荐大家使用ScheduledThreadPoolExecutor进行世界的模拟而不是Timer。

创建模拟世界任务MyTask

ScheduledThreadPoolExecutor的用法与Timer类似,所以也需要自己写一个MyTask让定时器去跑这个任务,这次我们的任务便是模拟世界,下面来看看代码:

//用作复制blpublic 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,到这里,我们的世界就完完整整的模拟并被绘制出来了,有木有很激动!有木有!有木有!
咳咳~~现在显示是显示出来了,不过现在这个世界也就只有边界,其余的物体是没有的,所以即使显示出了世界没有什么可以看到的东西,不过不要方,相信看过上一篇文章后,你也会自己向世界中添加刚体了。当然,后面也会接着讲如何创建其他刚体的。

这次就先讲这么多,咱们下篇文章再见~

相关推荐

相关文章