The WorldRender Class

Recall what you have to render in 3D:

  • The ship, using the ship model and texture, and applying lighting.
  • The invaders, using the invader model and texture, again with lighting.
  • Any shots on the playfield, based on the shot model, this time without texturing but with lighting.
  • The shield blocks, based on the shield block model, again without texturing, but with lighting and transparency (see Figure 12–3).
  • Explosions instead of the ship or invader model, in case the ship or invader is exploding. The explosion is not lit, of course.

You know how to code the first four items on this list. But what about the explosions?

It turns out that you can abuse the SpriteBatcher for this. Based on the state time of the exploding ship or invader, you can fetch a TextureRegion from the Animation instance holding the explosion animation (see Assets class). The SpriteBatcher can only render textured rectangles in the x/y plane, so you have to find a way to move such a rectangle to an arbitrary position in space (where the exploding ship or invader is located). You can easily achieve this by using glTranslatef() on the model-view matrix before rendering the rectangle via the SpriteBatcher!

The rendering setup for the other objects is pretty straightforward. You have a directional light coming from the top right, and an ambient light can light all the objects a little, no matter their orientation. The camera is located a little above and behind the ship, and it will look at a point a little ahead of the ship. Use your LookAtCamera for this. To let the camera follow the ship, keep the x-coordinate of its position and the look-at point in sync with the ship's x-coordinate.

For some extra eye candy, rotate the invaders around the y-axis. Also, rotate the ship around the z-axis based on its current velocity, so that it appears to be leaning in the direction it is moving.

Let's put this into code! Listing 12–12 shows you the final class of Droid Invaders.

Listing 12–12. WorldRenderer.java, the World Renderer

package com.badlogic.androidgames.droidinvaders;

import java.util.List;

import javax.microedition.khronos.opengles.GL10;

import com.badlogic.androidgames.framework.gl.AmbientLight;
import com.badlogic.androidgames.framework.gl.Animation;
import com.badlogic.androidgames.framework.gl.DirectionalLight;
import com.badlogic.androidgames.framework.gl.LookAtCamera;
import com.badlogic.androidgames.framework.gl.SpriteBatcher;
import com.badlogic.androidgames.framework.gl.TextureRegion;
import com.badlogic.androidgames.framework.impl.GLGraphics;
import com.badlogic.androidgames.framework.math.Vector3;

public class WorldRenderer {
    GLGraphics glGraphics;
    LookAtCamera camera;
    AmbientLight ambientLight;
    DirectionalLight directionalLight;
    SpriteBatcher batcher;
    float invaderAngle = 0;

The WorldRenderer keeps track of the GLGraphics instance from which you'll fetch the GL10 instance. You also have a LookAtCamera, an AmbientLight, a DirectionLight, and a SpriteBatcher. Finally, use a member to keep track of the current rotation angle for all invaders.

    public WorldRenderer(GLGraphics glGraphics) {
        this.glGraphics = glGraphics;
        camera = new LookAtCamera(67, glGraphics.getWidth()
                / (float) glGraphics.getHeight(), 0.1f, 100);
        camera.getPosition().set(0, 6, 2);
        camera.getLookAt().set(0, 0, -4);
        ambientLight = new AmbientLight();
        ambientLight.setColor(0.2f, 0.2f, 0.2f, 1.0f);
        directionalLight = new DirectionalLight();
        directionalLight.setDirection(-1, -0.5f, 0);
        batcher = new SpriteBatcher(glGraphics, 10);
    }

In the constructor, set up all members, as usual. The camera has a field of view of 67°, a near clipping plane distance of 0.1 units, and a far clipping plane distance of 100 units. The view frustum will thus easily contain the entire game world. Position it above and behind the ship, and let it look at (0,0,−4). The ambient light is just a faint gray, and the directional light is white and comes from the top-right side. Finally, instantiate the SpriteBatcher so that you can render the explosion rectangles.

    public void render(World world, float deltaTime) {
        GL10 gl = glGraphics.getGL();
        camera.getPosition().x = world.ship.position.x;
        camera.getLookAt().x = world.ship.position.x;
        camera.setMatrices(gl);

        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glEnable(GL10.GL_TEXTURE_2D);
        gl.glEnable(GL10.GL_LIGHTING);
        gl.glEnable(GL10.GL_COLOR_MATERIAL);
        ambientLight.enable(gl);
        directionalLight.enable(gl, GL10.GL_LIGHT0);

        renderShip(gl, world.ship);
        renderInvaders(gl, world.invaders, deltaTime);

        gl.glDisable(GL10.GL_TEXTURE_2D);

        renderShields(gl, world.shields);
        renderShots(gl, world.shots);

        gl.glDisable(GL10.GL_COLOR_MATERIAL);
        gl.glDisable(GL10.GL_LIGHTING);
        gl.glDisable(GL10.GL_DEPTH_TEST);
    }

In the render() method, start off by setting the camera's x-coordinate to the ship's x-coordinate. Of course, also set the x-coordinate of the camera's look-at point accordingly. This way, the camera will follow the ship. Once the position and look-at point are updated, set the projection and model-view matrix via a call to LookAtCamera.setMatrices().

Next, set up all the states that you need for rendering. You'll need depth-testing, texturing, lighting, and the color material functionality so that you don't have to specify a material for the objects via glMaterial(). The next two statements activate the ambient and directional light. With these calls, you have everything set up and you can start rendering the objects.

The first thing you render is the ship via a call to renderShip(). Next, render the invaders with a call to renderInvaders().

Since the shield blocks and shots don't need texturing, simply disable that to save some computations. Once texturing is turned off, render the shots and shields via calls to renderShots() and renderShields().

Finally, disable the other states you set so that you return a clean OpenGL ES state to whoever called you.

    private void renderShip(GL10 gl, Ship ship) {
        if (ship.state == Ship.SHIP_EXPLODING) {
            gl.glDisable(GL10.GL_LIGHTING);
            renderExplosion(gl, ship.position, ship.stateTime);
            gl.glEnable(GL10.GL_LIGHTING);
        } else {
            Assets.shipTexture.bind();
            Assets.shipModel.bind();
            gl.glPushMatrix();
            gl.glTranslatef(ship.position.x, ship.position.y, ship.position.z);
            gl.glRotatef(ship.velocity.x / Ship.SHIP_VELOCITY * 90, 0, 0, -1);
            Assets.shipModel.draw(GL10.GL_TRIANGLES, 0,
                    Assets.shipModel.getNumVertices());
            gl.glPopMatrix();
            Assets.shipModel.unbind();
        }
    }

The renderShip() method starts off by checking the state of the ship. If it is exploding, disable lighting, call renderExplosion() to render an explosion at the position of the ship, and enable lighting again.

If the ship is alive, bind its texture and model, push the model-view matrix, move it to its position and rotate it around the z-axis based on its velocity, and draw its model. Finally, pop the model-view matrix again (leaving only the camera's view), and unbind the ship model's vertices.

    private void renderInvaders(GL10 gl, List<Invader> invaders, float deltaTime) {
        invaderAngle += 45 * deltaTime;

        Assets.invaderTexture.bind();
        Assets.invaderModel.bind();
        int len = invaders.size();
        for (int i = 0; i < len; i++) {
            Invader invader = invaders.get(i);
            if (invader.state == Invader.INVADER_DEAD) {
                gl.glDisable(GL10.GL_LIGHTING);
                Assets.invaderModel.unbind();
                renderExplosion(gl, invader.position, invader.stateTime);
                Assets.invaderTexture.bind();
                Assets.invaderModel.bind();
                gl.glEnable(GL10.GL_LIGHTING);
            } else {
                gl.glPushMatrix();
                gl.glTranslatef(invader.position.x, invader.position.y,
                        invader.position.z);
                gl.glRotatef(invaderAngle, 0, 1, 0);
                Assets.invaderModel.draw(GL10.GL_TRIANGLES, 0,
                        Assets.invaderModel.getNumVertices());
                gl.glPopMatrix();
            }
        }
        Assets.invaderModel.unbind();
    }

The renderInvaders() method is pretty much the same as the renderShip() method. The only difference is that you loop through the list of invaders and bind the texture and mesh before you do so. This considerably reduces the number of binds and speeds up the rendering. For each invader, check its state again, and render either an explosion or the normal invader model. Since you bind the model and texture outside the for loop, you have to unbind and rebind them before you can render an explosion instead of an invader.

    private void renderShields(GL10 gl, List<Shield> shields) {
        gl.glEnable(GL10.GL_BLEND);
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
        gl.glColor4f(0, 0, 1, 0.4f);
        Assets.shieldModel.bind();
        int len = shields.size();
        for (int i = 0; i < len; i++) {
            Shield shield = shields.get(i);
            gl.glPushMatrix();
            gl.glTranslatef(shield.position.x, shield.position.y,
                    shield.position.z);
            Assets.shieldModel.draw(GL10.GL_TRIANGLES, 0,
                    Assets.shieldModel.getNumVertices());
            gl.glPopMatrix();
        }
        Assets.shieldModel.unbind();
        gl.glColor4f(1, 1, 1, 1f);
        gl.glDisable(GL10.GL_BLEND);
    }

The renderShields() method renders, you guessed it, the shield blocks. Apply the same principle as in the case of rendering invaders. You only bind the model once. Since you have no texture, you don't need to bind one. However, you need to enable blending. Set the global vertex color to blue, with the alpha component set to 0.4. This will make the shield blocks a little transparent.

    private void renderShots(GL10 gl, List<Shot> shots) {
        gl.glColor4f(1, 1, 0, 1);
        Assets.shotModel.bind();
        int len = shots.size();
        for (int i = 0; i < len; i++) {
            Shot shot = shots.get(i);
            gl.glPushMatrix();
            gl.glTranslatef(shot.position.x, shot.position.y, shot.position.z);
            Assets.shotModel.draw(GL10.GL_TRIANGLES, 0,
                    Assets.shotModel.getNumVertices());
            gl.glPopMatrix();
        }
        Assets.shotModel.unbind();
        gl.glColor4f(1, 1, 1, 1);
    }

Rendering the shots in renderShots() is the same as rendering the shields, except that you don't use blending and you use a different vertex color (yellow).

    private void renderExplosion(GL10 gl, Vector3 position, float stateTime) {
        TextureRegion frame = Assets.explosionAnim.getKeyFrame(stateTime,
                Animation.ANIMATION_NONLOOPING);

        gl.glEnable(GL10.GL_BLEND);
        gl.glPushMatrix();
        gl.glTranslatef(position.x, position.y, position.z);
        batcher.beginBatch(Assets.explosionTexture);
        batcher.drawSprite(0, 0, 2, 2, frame);
        batcher.endBatch();
        gl.glPopMatrix();
        gl.glDisable(GL10.GL_BLEND);
    }
}

Finally, the mysterious renderExplosion() method. Get the position at which you want to render the explosion, as well as the state time of the object that is exploding. The latter is used to fetch the correct TextureRegion from the explosion Animation, just as you did for Bob in Super Jumper.

The first thing you do is fetch the explosion animation frame based on the state time. Next, enable blending, since the explosion has transparent pixels that you don't want to render. Push the current model-view matrix and call glTranslatef() so that anything you render after that call will be positioned at the given location. Tell the SpriteBatcher that you are about to render a rectangle using the explosion texture.

The next call is where the magic happens. Tell the SpriteBatcher to render a rectangle at (0,0,0) (the z-coordinate is not given but implicitly zero, remember?), with a width and height of 2 units. Because you used glTranslatef(), that rectangle will not be centered around the origin, but rather around the position you specified to glTranslatef(), which is exactly the position of the ship or invader that exploded. Finally, pop the model-view matrix and disable blending again.

That's it. Twelve classes, forming a full 3D game, parroting the classic Space Invaders game. Try it out. When you come back, you can have a look at the performance characteristics.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.184.126