com.badlogic.gdx.physics.bullet.demo.screens.SimulationScreen.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.physics.bullet.demo.screens.SimulationScreen.java

Source

package com.badlogic.gdx.physics.bullet.demo.screens;

import java.util.ArrayList;
import java.util.List;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.bullet.Bullet;
import com.badlogic.gdx.physics.bullet.btBroadphaseInterface;
import com.badlogic.gdx.physics.bullet.btCollisionDispatcher;
import com.badlogic.gdx.physics.bullet.btCollisionObject;
import com.badlogic.gdx.physics.bullet.btConstraintSolver;
import com.badlogic.gdx.physics.bullet.btDbvtBroadphase;
import com.badlogic.gdx.physics.bullet.btDefaultCollisionConfiguration;
import com.badlogic.gdx.physics.bullet.btDefaultMotionState;
import com.badlogic.gdx.physics.bullet.btDiscreteDynamicsWorld;
import com.badlogic.gdx.physics.bullet.btRigidBody;
import com.badlogic.gdx.physics.bullet.btSequentialImpulseConstraintSolver;
import com.badlogic.gdx.physics.bullet.btTransform;
import com.badlogic.gdx.physics.bullet.demo.WindowedStats;
import com.badlogic.gdx.physics.bullet.demo.simulationobjects.CollisionSimulationObject;

/**
 * An abstract screen that does physics simulation with gdx-bullet.
 * 
 * @author sterwill
 */
public abstract class SimulationScreen implements Screen {
    static {
        Bullet.init();
    }

    // Set to true after show() completes for the first time
    private boolean shownOnce;

    // Tracks whether this screen is paused
    private boolean paused;

    // Bullet physics
    private final btBroadphaseInterface broadphase;
    private final btCollisionDispatcher dispatcher;
    private final btDiscreteDynamicsWorld dynamicsWorld;
    private final btConstraintSolver solver;
    private final btDefaultCollisionConfiguration collisionConfiguration;

    // Bullet profiling
    private final WindowedStats stepSimulationTimes = new WindowedStats(30);
    private long stepSimulationLastAverage = 0;
    private long stepSimulationLastAverageMillis = System.currentTimeMillis();

    /*
     * Physics time (mostly computed in nanoseconds)
     * 
     * We have to pass a float to Bullet, so calculate the fixed Bullet step with float-level precision, then work out
     * the equivalent integer nanosecond step using floats (using doubles might give a different result).
     */
    protected static final float PHYSICS_TIME_STEP_SECONDS = 1f / 60f;
    protected static final long PHYSICS_TIME_STEP_NANOS = (long) (PHYSICS_TIME_STEP_SECONDS * 1000000000f);
    private long physicsCurrentTime;
    private long physicsAccumulator;

    // A list of all our scene objects
    private final List<CollisionSimulationObject> collisionSimulationObjects = new ArrayList<CollisionSimulationObject>();

    // OSD
    private final SpriteBatch osdSpriteBatch = new SpriteBatch();
    private final BitmapFont osdFont = new BitmapFont();
    private final StringBuilder osdStringBuilder = new StringBuilder(1024);

    // Perspective camera
    private final PerspectiveCamera perspectiveCamera = new PerspectiveCamera();

    // Orthographic OSD camera
    private final OrthographicCamera osdCamera = new OrthographicCamera();

    // Preallocated for use when rendering the physics objects
    private final btTransform transform = new btTransform();
    private final float[] glMatrix = new float[16];

    public SimulationScreen() {
        /*
         * Allocate and initialize Bullet.
         * 
         * Things to know when using Bullet with libgdx:
         * 
         * Call Bullet.init() to load the native library before using any other Bullet types. This class does it in a
         * static initializer because it has Bullet types in fields.
         * 
         * When you're done with a Bullet object (class names start with "bt"), dispose of the native object by calling
         * .delete(). Remember to remove your collision objects from the world before deleting them.
         * 
         * It's important to prevent Java from garbage-collecting Bullet objects while they're in use. If you don't, the
         * JVM will almost certainly crash when you step your simulation (possibly seconds or minutes later). For this
         * class, objects will be in use as long as this screen is valid, so we store them in fields and delete them in
         * the dispose method. You could also store them in collections or use some other method to keep them around.
         * 
         * You'll want to read the Bullet API docs to know which methods take pointers vs. references to other objects.
         * Usually when a reference is taken, the parameter object's data is _copied_ into the target and you're free to
         * do whatever you want with the parameter object after the method returns. You don't have to keep the Java
         * object from getting GCed in this case.
         * 
         * If instead Bullet takes a pointer to an object, it probably needs it to be kept around (including the Java
         * object) until you unhook it or assign some other object in its place.
         * 
         * Some Bullet methods return libgdx types (Vector3, Matrix4, Quaternion) for convenience. These returned
         * objects are re-used by all Bullet methods that return those types, to avoid allocating new Java objects. You
         * should copy the data out of these objects before calling another Bullet method that returns one of these
         * types, or it will be overwritten during the next call.
         * 
         * libgdx types (Vector3, Matrix4, Quaternion) that are _inputs_ to Bullet methods are not used in any special
         * way. They are treated as value types (the data is always copied into Bullet) and no special access rules
         * apply.
         */

        collisionConfiguration = new btDefaultCollisionConfiguration();
        dispatcher = new btCollisionDispatcher(collisionConfiguration);
        broadphase = new btDbvtBroadphase();
        solver = new btSequentialImpulseConstraintSolver();

        dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
        dynamicsWorld.setGravity(new Vector3(0f, 0f, -9.8f));

        physicsCurrentTime = System.nanoTime();
        physicsAccumulator = 0;
    }

    public btDiscreteDynamicsWorld getDynamicsWorld() {
        return dynamicsWorld;
    }

    public PerspectiveCamera getPerspectiveCamera() {
        return perspectiveCamera;
    }

    public OrthographicCamera getOSDCamera() {
        return osdCamera;
    }

    @Override
    public void show() {
        if (!shownOnce) {
            shownOnce = true;

            // Resize does some more work on cameras, including update()
            resize(Gdx.app.getGraphics().getWidth(), Gdx.app.getGraphics().getHeight());

            // Add scene objects including player
            hookAddSimulationObjects();
        }
    }

    @Override
    public void hide() {
    }

    @Override
    public void pause() {
        paused = true;
    }

    public boolean isPaused() {
        return paused;
    }

    @Override
    public void resume() {
        paused = false;
    }

    // Required virtuals

    /**
     * Called each frame after the camera is positioned (projection and modelview matrixes set), but before any scene
     * objects have been rendered, to position any positionable lights. Directional lights may be set once in some other
     * hook and not updated by this method.
     */
    protected abstract void positionLights(float graphicsDelta, float physicsDelta);

    /**
     * Called at various times during {@link #render(float)} (but always after {@link #positionLights(float, float)} has
     * been called for that frame) to enable lighting.
     */
    public abstract void enableLights();

    /**
     * Called at various times during {@link #render(float)} (but always after {@link #positionLights(float, float)} has
     * been called for that frame) to disable lighting.
     */
    public abstract void disableLights();

    /**
     * Called by {@link #show()} the first time it executes so the derived class can add scene objects.
     */
    protected abstract void hookAddSimulationObjects();

    // Optional hooks

    /**
     * Called by {@link #render(float)} before the physics has been stepped for this frame.
     * 
     * @param graphicsDelta
     *            GDX's delta since last frame (may be smoothed, probably never 0)
     */
    protected void hookRenderPrePhysics(float graphicsDelta) {
    }

    /**
     * Called by {@link #render(float)} after the physics has been calculated. physicsDelta is the time the physics
     * engine was stepped this frame, which is useful for synchronizing things exactly with the physics engine.
     * 
     * @param graphicsDelta
     *            GDX's delta since last frame (may be smoothed, probably never 0)
     * @param physicsDelta
     *            the exact amount of time the physics world was just stepped (may be 0 when frame rate is much higher
     *            than fixed physics rate)
     */
    protected void hookRenderPostPhysics(float graphicsDelta, float physicsDelta) {
    }

    /**
     * Called by {@link #render(float)} after the screen has been cleared. This is a good time to turn on fixed function
     * pipeline options like fog.
     * <p>
     * Time delta values are exactly the same as passed to {@link #hookRenderPostPhysics(float, float)} for the same
     * frame.
     */
    protected void hookRenderPostClear(float graphicsDelta, float physicsDelta) {
    }

    /**
     * Called by {@link #render(float)} with OpenGL in a perspective projection, after simulation objects have been
     * rendered.
     * <p>
     * Time delta values are exactly the same as passed to {@link #hookRenderPostPhysics(float, float)} for the same
     * frame.
     */
    protected void hookRenderScene(float graphicsDelta, float physicsDelta) {
    }

    /**
     * Called by {@link #render(float)} with OpenGL in an orthographic projection, after the on screen display text has
     * been rendered, which is always done after {@link #hookRenderScene(float, float)} has completed.
     * <p>
     * Time delta values are exactly the same as passed to {@link #hookRenderPostPhysics(float, float)} for the same
     * frame.
     */
    protected void hookRenderOSD(float graphicsDelta, float physicsDelta) {
    }

    // Misc

    protected CharSequence getOSDText() {
        osdStringBuilder.setLength(0);

        if (paused) {
            osdStringBuilder.append("[PAUSED] ");
        }

        osdStringBuilder.append("fps: ");
        osdStringBuilder.append(Gdx.graphics.getFramesPerSecond());
        osdStringBuilder.append(" objects: ");
        osdStringBuilder.append(collisionSimulationObjects.size());

        long now = System.currentTimeMillis();
        if (now > stepSimulationLastAverageMillis + 1000) {
            stepSimulationLastAverage = stepSimulationTimes.average();
            stepSimulationLastAverageMillis = now;
        }

        osdStringBuilder.append(" stepSimulation: ");
        osdStringBuilder.append(stepSimulationLastAverage);
        osdStringBuilder.append(" ");

        return osdStringBuilder;
    }

    /**
     * Adds a {@link CollisionSimulationObject} to the dynamics world. All objects added to the dynamics world that are
     * still there when {@link #dispose()} is called will be disposed.
     * 
     * @param object
     *            the object to add
     */
    protected void addCollisionSimulationObject(CollisionSimulationObject object) {
        object.addToDynamicsWorld(dynamicsWorld);
        collisionSimulationObjects.add(object);
    }

    /**
     * Removes a {@link CollisionSimulationObject} from the dynamics world.
     * 
     * @param object
     *            the object to remove
     */
    protected void removeCollisionSimulationObject(CollisionSimulationObject object) {
        object.removeFromDynamicsWorld(dynamicsWorld);
        collisionSimulationObjects.remove(object);
    }

    @Override
    public final void render(float graphicsDelta) {
        // Physics
        hookRenderPrePhysics(graphicsDelta);
        final float physicsDelta = stepPhysics();
        hookRenderPostPhysics(graphicsDelta, physicsDelta);

        // Clear frame and enable model styles
        Gdx.gl10.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        hookRenderPostClear(graphicsDelta, physicsDelta);

        // Apply perspective player camera
        perspectiveCamera.apply(Gdx.gl10);

        Gdx.gl10.glEnable(GL10.GL_DITHER);
        Gdx.gl10.glEnable(GL10.GL_DEPTH_TEST);
        Gdx.gl10.glEnable(GL10.GL_CULL_FACE);
        Gdx.gl10.glEnable(GL10.GL_BLEND);
        Gdx.gl10.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);

        // Position lights so lighting calculations for objects are correct
        positionLights(graphicsDelta, physicsDelta);

        renderDynamicsWorld();

        hookRenderScene(graphicsDelta, physicsDelta);

        // Apply orthographic OSD camera
        osdCamera.apply(Gdx.gl10);

        // Disable face culling so we draw everything
        Gdx.gl10.glDisable(GL10.GL_CULL_FACE);

        // Render OSD
        disableLights();
        osdSpriteBatch.begin();
        osdFont.setColor(1, 1, 1, 1f);
        osdFont.draw(osdSpriteBatch, getOSDText(), 10, 10 + osdFont.getCapHeight());
        osdSpriteBatch.end();
        hookRenderOSD(graphicsDelta, physicsDelta);
    }

    /**
     * Steps the physics world (or possibly doesn't if the frame rate is super awesomely fast and it's not needed yet).
     * 
     * @return the amount of time the physics world was stepped (may be 0)
     */
    private float stepPhysics() {
        /*
         * Fixed step physics. Basic premise is we always step by exactly PHYSICS_TIME_STEP, sometimes stepping more
         * than once to catch up if we were behind, and if there's not enough time yet for another simulation, leave the
         * remainder in the accumulator for next time.
         * 
         * Keeps our physics updating regularly even through terribly slow and blazingly fast framerates.
         * 
         * http://gafferongames.com/game-physics/fix-your-timestep/ (specifically "Free the physics")
         */
        long newTime = System.nanoTime();
        long frameTime = newTime - physicsCurrentTime;
        physicsCurrentTime = newTime;
        physicsAccumulator += frameTime;

        float physicsDelta = 0;
        while (physicsAccumulator >= PHYSICS_TIME_STEP_NANOS) {
            if (!paused) {
                /*
                 * Pass maxSubSteps = 0 for exactly one integration over the time specified by the third parameter.
                 * Bullet documentation warns against this, but we're doing our own make-up logic.
                 */
                long start = System.nanoTime();
                dynamicsWorld.stepSimulation(PHYSICS_TIME_STEP_SECONDS, 0, PHYSICS_TIME_STEP_SECONDS);
                long elapsed = System.nanoTime() - start;
                stepSimulationTimes.add(elapsed);
                physicsDelta += PHYSICS_TIME_STEP_SECONDS;
            }

            physicsAccumulator -= PHYSICS_TIME_STEP_NANOS;
        }

        return physicsDelta;
    }

    @Override
    public void resize(int width, int height) {
        perspectiveCamera.viewportWidth = width;
        perspectiveCamera.viewportHeight = height;
        perspectiveCamera.update();

        osdCamera.viewportWidth = width;
        osdCamera.viewportHeight = height;
        osdCamera.position.set(osdCamera.viewportWidth / 2, osdCamera.viewportHeight / 2, 0);
        osdCamera.update();
    }

    /**
     * {@inheritDoc}
     * <p>
     * {@link SimulationScreen} will dispose any {@link CollisionSimulationObject}s that it currently manages (were
     * added with {@link #addCollisionSimulationObject(CollisionSimulationObject)} and were not removed with
     * {@link #removeCollisionSimulationObject(CollisionSimulationObject)} ).
     */
    @Override
    public void dispose() {
        // Remove all the objects from the world, then delete them.
        for (CollisionSimulationObject object : collisionSimulationObjects) {
            object.removeFromDynamicsWorld(dynamicsWorld);
            object.dispose();
        }

        collisionSimulationObjects.clear();

        // Delete the native bullet objects
        dynamicsWorld.delete();
        broadphase.delete();
        dispatcher.delete();
        solver.delete();
        collisionConfiguration.delete();
    }

    private void renderDynamicsWorld() {
        for (int i = 0; i < collisionSimulationObjects.size(); i++) {
            final CollisionSimulationObject simulationObject = collisionSimulationObjects.get(i);
            final btCollisionObject collisionObject = simulationObject.getCollisionObject();

            /*
             * Prefer the (interpolated) transform of the motion state.
             * 
             * Bullet offers native upcast methods, but the wrappers allocate new Java objects to hold the upcasted
             * object result, so simply cast to the types we know we use in our scene to avoid the allocation.
             */
            boolean handled = false;
            if (collisionObject instanceof btRigidBody) {
                final btDefaultMotionState ms = (btDefaultMotionState) ((btRigidBody) collisionObject)
                        .getMotionState();
                if (ms != null) {
                    ms.getGraphicsWorldTrans(transform);
                    handled = true;
                }
            }

            // Fall back to the world transform
            if (!handled) {
                collisionObject.getWorldTransform(transform);
            }

            Gdx.gl10.glPushMatrix();

            // Apply the object's transform to the OpenGL world
            transform.getOpenGLMatrix(glMatrix);
            Gdx.gl10.glMultMatrixf(glMatrix, 0);

            simulationObject.render(this);

            Gdx.gl10.glPopMatrix();
        }
    }
}