最近把box2d研究了一遍,总体上差不多是了解了,但是在运行那个小球下落的demo时候发现移动速度与物理情况不一样,仔细研究了半天才发现原来有这么多细节概念要注意。这必须记录下来,对以后有同样问题的人肯定会有所启发。
物理模型:
一个边长为1m的正方形盒子悬停在20m高的位置,然后自由落体。根据公式h=gt^2/2可知下落到0m高时应该是2s。
对应box2d的模型:
一个形状为边长为1m的正四角型,不存在任何阻尼,放在20m高的空中,然后设置向下引力为9.8m/s^2。
注意一:单位换算
这个地方是新手最容易出错的。由于libgdx中的距离单位都是px,但box2d的距离单位都是m,所以你程序中所有要计算的值都要确保单位正确。
具体来说,比如我的显示器分辨率是1440px*900px,那我创建一个游戏界面为200px*200px的,如new JoglApplication(new DemoGame(), "Demo Demo", 200, 200, false);。这个很好理解,你的整个游戏界面就是这个大小,与显示器的分辨率是正比关系。
然后,游戏中会有一个camera来看具体的界面内容,这里只考虑正投影摄像机(不会因为距离对界面放大或者缩小),那么定义其能看到的也是200px*200px的画面new OrthographicCamera(200, 200),这 样就真的是画面上的1px代表你显示器的1px了。camera大小的变化是你游戏中的1px与实际显示器1px是成反比的。我觉得这个与游戏界面设置一样最好理解了。
那如果我希望10个像素代表1m(scale=10px/m),那计算出来的每个物体位移1m,你对应的actor都要移动10个像素。就是这么个换算关系。
注1:box2d中的单位都是标准单位,比如长度就是m,重量就是kg。
注2:box2d的物理计算与画面上的actor是分开的,需要把物理数据再通过同步来反应到actor上。但是在Box2DDebugRenderer可以直观的看这个物理世界。
注意二:最大速度限制
这个时候物体移动起来有点意思了,当把世界引力加到很大,诡异的事情发生了,开始还是加速运动,突然到后面明显变成匀速运动了。查了一遍资料才发现,原来box2d把每帧位移限制在2m/frame,所以在60Hz的刷新率情况下物体最大移动速度就是120m/s了。悲剧的是这个限制是hard coding,这可能是让物体看起来移动比较连续吧。所以要把速度加上去,那就只能提高帧数了,比如world.step(1/100f, 3, 3);能提高最大速度就是200m/s,但这么修改会有一个严重的问题,就是让物体位移的时间刻度和实际时间不一致了,画面看上去会和人感知不一样。
注:timestep是时间步长,这是动画里面一个重要概念,物理世界每次计算出来新的位置就必须要知道过了多久时间,这就让离散的时间点计算变成一个连续的动画。
注意三:初始化的影响
以上工作都做好了,发现下落时间怎么统计都不对。弄了半天才发反应过来原来第一次render的时候就已经开始计算物体移动了,并且把这个deltaTime带入进去计算了。由于初始化速度会比较慢,程序远达不到60Hz的刷新度,所以我把那个时间作为第一次下落时间其实是已经晚了的。所以干脆把第一次render再让每个物体唤醒,这就得到期望的结果了。
附上测试代码:
- public class DemoGame implements ApplicationListener {
- protected OrthographicCamera camera;
- protected Box2DDebugRenderer renderer; // 测试用绘制器
- private World world;
- private float scale = 10f; // 屏幕的缩放比例,10像素/米
- private long start = 0;
- private Body body;
- private boolean isFinished = true;
- private int count = 0;
- private float totalTime = 0;
- @Override
- public void create() {
- Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
- camera = new OrthographicCamera(200/scale, 200/scale);// 这里为了展示物理世界,把视野也转换成m
- camera.position.set(100/scale, 100/scale, 0); // 摄像机的postion是画面的中心点
- renderer = new Box2DDebugRenderer();
- world = new World(new Vector2(0, -9.8f), true); // 一般标准重力场
- BodyDef bd = new BodyDef(); //声明物体定义
- bd.position.set(10, 20);
- bd.type = BodyType.DynamicBody;
- body = world.createBody(bd); //通过world创建一个物体
- PolygonShape box = new PolygonShape();
- box.setAsBox(0.5f, 0.5f); // 注意单位是边长的一半
- FixtureDef fd = new FixtureDef();
- fd.shape = box;
- fd.friction = 0;
- fd.restitution = 0;
- fd.density = 1;
- body.createFixture(fd); //将形状和密度赋给物体
- body.setLinearDamping(0f); // 没有线性阻尼
- body.setAngularDamping(0f); // 没有旋转阻尼
- body.setAwake(false);
- }
- @Override
- public void render() {
- world.step(Gdx.app.getGraphics().getDeltaTime(), 3, 3);
- if(start == 0) {
- count = 1;
- start = System.currentTimeMillis();
- System.out.println("start postion:" + body.getPosition().y);
- body.setAwake(true); // 让渲染的第一帧再让物体运动,这样时间获得才准确,
- // 否则第一个deltaTime有初始化时间,会让整个计算不准
- } else {
- if(isFinished){
- System.out.println("动画渲染次数: " + count++);
- System.out.println("物体移动速度: " + body.getLinearVelocity().y);
- System.out.println("---------------------------------");
- totalTime += Gdx.app.getGraphics().getDeltaTime();
- }
- if(body.getPosition().y <= 0 && isFinished) {
- System.out.println("实际时间差:" + (System.currentTimeMillis() - start)/1000f);
- System.out.println("图像增量统计时间差:" + totalTime);
- System.out.println("end postion:" + body.getPosition().y);
- isFinished = false;
- world.destroyBody(body);
- }
- }
- GL10 gl = Gdx.app.getGraphics().getGL10();
- gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
- camera.update();
- camera.apply(gl);
- renderer.render(world, camera.combined);
- }
- @Override
- public void dispose() {
- renderer.dispose();
- world.dispose();
- renderer = null;
- world = null;
- }
- @Override
- public void pause() {
- }
- @Override
- public void resize(int width, int height) {
- }
- @Override
- public void resume() {
- }
- }