© Lee Stemkoski 2018
Lee StemkoskiJava Game Development with LibGDXhttps://doi.org/10.1007/978-1-4842-3324-5_16

16. Introduction to 3D Graphics and Games

Lee Stemkoski
(1)
DEPT OF MATH & CS, ADELPHI UNIVERSITY DEPT OF MATH & CS, Garden City, New York, USA
 
This chapter will introduce some of the 3D graphics capabilities of LibGDX. Along the way, you’ll learn about the concepts and classes necessary to describe and render a three-dimensional scene. To simplify and streamline this process, you’ll both adapt some old classes and write some new classes to accomplish the various tasks involved. Next, to understand 3D movement, you’ll create a simple interactive demo that enables players to control both an object within the scene and the camera viewing the scene. Finally, you’ll create the game Starfish Collector 3D, shown in Figure 16-1, which once again features a turtle on a quest to collect all the starfish that it can. For simplicity, this game will actually use 2.5D techniques: the game will render three-dimensional graphics, while the underlying gameplay (movement and collisions) will occur in two dimensions.
A352797_2_En_16_Fig1_HTML.jpg
Figure 16-1.
The Starfish Collector 3D game

Exploring 3D Concepts and Classes

As it turns out, all of the previously created games in this book exist in a three-dimensional space. You may have noticed that, for example, when setting the position of a camera object (in the alignCamera method of the BaseActor class), you have x, y, and z components to set. If the x-axis and the y-axis represent the horizontal and vertical directions on the screen, respectively, then the z-axis corresponds to a straight line pointing toward the viewer, perpendicular to the xy plane—the plane containing the x and y axes. The camera can be thought of as being positioned on the z axis, pointing straight toward the xy plane ; all of the game entities have implicitly had their z coordinate set to 0. This configuration is illustrated in Figure 16-2, which shows roughly how the camera sees the Starfish Collector game from previous chapters.
A352797_2_En_16_Fig2_HTML.jpg
Figure 16-2.
A camera looking down the z axis at the Starfish Collector game
Your previous projects have relied heavily on the Stage class, which manages the Camera and a Batch object (for rendering purposes). To create 3D scenes , you need the “3D versions” of these objects, provided by the PerspectiveCamera and ModelBatch classes, which will be covered in detail next. However, there is no corresponding stage-like object to manage them, and so you will create your own manager class (called Stage3D) in a later section.
To render a scene, you can use one of two types of cameras: an orthographic camera or a perspective camera . (The Stage class uses an OrthographicCamera object for rendering.) The difference between these two is in how they represent, or project, a 3D scene onto a 2D surface such as a computer screen. To illustrate the difference, consider one of the simplest 3D shapes: a cube. Figure 16-3 shows an orthographic projection and a perspective projection of a cube. In an orthogonal projection , if the edges of an object have the same length, then they will be drawn as having the same length in the projection, regardless of their distance from the viewer. This is in contrast to a perspective projection, in which objects with two edges of the same length may appear different in the projection; an edge that is farther away from the viewer will appear shorter. This also has the side effect that, if two edges of an object are parallel, then they remain parallel in an orthographic projection, but they appear to converge in a perspective projection . (In a perspective drawing, the point at which all such edges appear to converge is called the vanishing point .)
A352797_2_En_16_Fig3_HTML.jpg
Figure 16-3.
A cube drawn using orthographic projection (left) and perspective projection (right)
When initializing a PerspectiveCamera object, you have to define the region visible to the camera, which has the shape of a truncated pyramid, or frustum (illustrated in Figure 16-4). This is specified by five parameters: the field of view (an angle that represents how far the camera can see to either side), the width and height of the rectangle onto which the scene is being projected (determined by a Viewport object in LibGDX), and the near and far values (which represent the closest and farthest distances that the camera will include while rendering).
A352797_2_En_16_Fig4_HTML.jpg
Figure 16-4.
A region visible to a perspective camera; near and far distances are indicated by shaded planes
The next new class is ModelBatch . Just as a SpriteBatch object can be used to render two-dimensional Texture objects, ModelBatch is used to render three-dimensional objects. The data needed to describe the appearance of a three-dimensional object is contained in a Model object, which consists of two major components: Mesh and Material. A mesh is a collection of vertices, edges, and triangular faces that define the shape of an object . A material contains color or texture data that is applied to the mesh; the material defines the appearance of the mesh while rendering. Figure 16-5 contains two images of a teapot: a wireframe representation of the mesh, and its appearance after applying a material. This particular teapot is a classic model called the Utah teapot , created by the computer scientist Martin Newell in 1975. Models can be loaded from standard 3D object file formats.
A352797_2_En_16_Fig5_HTML.jpg
Figure 16-5.
The Utah teapot, rendered in wireframe (left) and with material applied (right)
Models can be created in two ways in LibGDX. Using the ModelLoader class, a model can be loaded from standard 3D object file formats (such as the Wavefront format, typically indicated by the .obj file extension), which may also contain references to image files used by the accompanying material. Alternatively, some basic shapes (such as spheres and boxes) can be generated at runtime using the ModelBuilder class. You will see examples of both of these approaches over the course of this chapter.
Finally, in order to give 3D models a realistic appearance, the effects of light sources need to be considered. In fact, if lights are not added to a scene, you will not be able to see anything at all! Lights are managed by the Environment class. The two types of lighting effects you will use are ambient light and directional light. Ambient light provides overall illumination and shines equally from all directions. Typically, it is important to include ambient light in a scene so that even the sides facing away from a light source will be somewhat visible (although this amount may vary depending on the type of location you are simulating). A directional light is used to simulate light shining throughout the scene in a particular direction. This helps provide a sense of depth in a scene, in particular allowing you to distinguish between different faces when an object’s material consists of just a single color. Figure 16-6 illustrates these effects with two renderings of a cube. In the image on the left, the scene contains only ambient light, which makes it difficult to see all the edges of the cube. The image on the right has a directional light, primarily aimed toward the left (and thus the right side of the cube appears brightest).
A352797_2_En_16_Fig6_HTML.jpg
Figure 16-6.
A cube illuminated with ambient light only (left) and directional light added (right)

Creating a Minimal 3D Demo

You are now ready to create a minimal code example that renders a cube in LibGDX using the previously mentioned classes. The result is a single blue box, oriented and shaded as on the right side of Figure 16-6. To begin, create a new project in BlueJ called Project3D , copy the +libs folder (if not using the BlueJ userlib folder) and its contents from a previous project into this new project’s directory, and restart BlueJ so that the JAR files are loaded correctly. You don’t need to copy any classes or assets to this project at this time. Rather than starting with the Game class as usual (which implements the ApplicationListener interface methods), this example will be self-contained; you will implement the interface yourself.
To begin, create a new class called CubeDemo that contains the following code, which includes the core of a 3D application : import statements, variable declarations (those that are referenced in multiple methods), and the methods required by the ApplicationListener interface . As was explained in Chapter 2, the create method is used to initialize objects, while the render method handles the game loop; the code for each of these methods is presented in detail later. (The other methods required by the interface aren’t fundamental to this example and so are not discussed later.)
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.math.Vector3;
public class CubeDemo implements ApplicationListener
{
       public Environment environment;
       public PerspectiveCamera camera;
       public ModelBatch modelBatch;
       public ModelInstance boxInstance;
       public void create() {  }
       public void render() {  }
       public void dispose() {  }
       public void resize(int width, int height) {  }
       public void pause() {  }
       public void resume() {  }
}
The create method begins with initializing the Environment and adding a parameter (a subclass of the Attribute class) that defines the color of the ambient light in the scene. In general, shades of gray are used for lights (rather than, say, colors such as yellow or blue) so that your scene will not be tinted with unexpected colors. Then, an instance of a DirectionalLight is created using a brighter shade of gray, and its direction is specified (using a Vector3 object) to be primarily to the left and downward; after configuring its parameters, the light is added to the environment. A PerspectiveCamera is then initialized, with a field of view of 67 degrees and with near and far visibility set to 0.1 and 1000, respectively (these values have been chosen to guarantee that the view area contains the object you will add to the scene). The camera’s position is set, and the location it should initially be looking toward is specified via the lookAt method . Finally, a ModelBatch object is initialized, which will be used later when rendering. These steps “set the scene” and are accomplished by adding the following code to the create method :
environment = new Environment();
environment.set( new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f) );
DirectionalLight dLight = new DirectionalLight();
Color     lightColor = new Color(0.75f, 0.75f, 0.75f, 1);
Vector3  lightVector = new Vector3(-1.0f, -0.75f, -0.25f);
dLight.set( lightColor, lightVector );
environment.add( dLight ) ;
camera = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
camera.near = 0.1f;
camera.far  = 1000f;
camera.position.set(10f, 10f, 10f);
camera.lookAt(0,0,0);
camera.update();
modelBatch = new ModelBatch();
The next task is to create instances of models to add to your scene. For the sake of simplicity in this example, you will use the createBox method of the ModelBuilder class to construct a cube. You must also create a Material to give the cube its appearance on screen; here, a solid blue diffuse color is used. (The diffuse color of an object is the apparent color of the object when illuminated by pure white light.)
You must also determine what types of data each vertex of the model should contain: in every case, vertices should store a position, but for this example, they also store color data and a vector (called the normal vector) that is used to determine how light reflects off an object, thus providing shading effects. Each of these attributes has a corresponding constant value defined in the Usage class ; position data corresponds to Usage.Position, color data corresponds to Usage.ColorPacked, normal vector data corresponds to Usage.Normal, and so forth. When a combination of this data is needed, a value is generated by adding together the constant values for each of the desired attributes. The resulting value is passed as a parameter to the createBox method.
You also need to decide on the dimensions of the box itself. Because of the scale used by many modeling programs, these values are often in the range from 1 to 10, and so you should use similar ranges of values when creating objects with the ModelBuilder class. After creating the Model (which you can think of as a template object), a ModelInstance is initialized. This object contains a copy of the information from the model, as well as a transformation matrix that stores position, rotation, and scaling data for this particular instance. The following code performs all these tasks and should be added to the create method:
ModelBuilder modelBuilder = new ModelBuilder();
Material boxMaterial = new Material();
boxMaterial.set( ColorAttribute.createDiffuse(Color.BLUE) );
int usageCode = Usage.Position + Usage.ColorPacked + Usage.Normal;
Model boxModel = modelBuilder.createBox( 5, 5, 5, boxMaterial, usageCode );
boxInstance = new ModelInstance(boxModel);
Finally, the render method is given, which is where all the phases of the game loop happen. In this case, the program consists of a static scene, so there is no user input to process nor updating tasks to be done—just rendering to perform. The code for this method should appear relatively familiar. One difference is that the glClear function also needs to erase the depth information generated during the previous render, since the distance from the camera to each object in the scene may change if the camera moves around, in which case the depth values will need to be recalculated. Another difference is that the ModelBatch takes the PerspectiveCamera as input in its begin method. The corresponding code to add to the render method is as follows:
Gdx.gl.glClearColor(1,1,1,1);
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear( GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT );
modelBatch.begin(camera);
modelBatch.render( boxInstance, environment );
modelBatch.end();
As usual, you’ll also need a launcher-style class, as shown here:
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
public class Launcher1
{
    public static void main ()
    {
        CubeDemo myProgram = new CubeDemo();
        LwjglApplication launcher = new LwjglApplication( myProgram, "Cube Demo", 800, 600 );
    }
}
At this point, you should try out the code. Feel free to make some modifications and rerun the code to see the effects of your changes. For example, you could alter the color of the cube, the direction of the light source, or the location of the camera.

Recreating the Actor/Stage Framework

To facilitate and accelerate the development of future projects, in this section you’ll write some classes that function similarly to the BaseActor and Stage classes, but instead store data structures and methods useful for three-dimensional graphics. For convenience, you’ll continue adding code to the previously created project, which was called Project3D.

The BaseActor3D Class

To begin, recall that the Actor class stored transformation data (position, rotation, and scale) and methods to get, set, and change these values. All Actor objects contained an act method, which could be used to update their internal state, and a draw method, which the actor could use to render itself with a given Batch object. You then wrote an extension of the Actor class, called the BaseActor class, which additionally stored a Texture, a Polygon for collision detection, and related methods. Here, the BaseActor3D class will be presented, which will provide similar functionality in a 3D setting.
Some of the most complicated underlying concepts in 3D graphics are the mathematical structures used to store the transformation data. The technical details will not be covered in depth here,1 but to understand the code for this example, it’s important to know what the objects are and how to use their associated methods.
The transformation data for a ModelInstance object is stored in its transform field as a Matrix4 object : a four-by-four grid of numbers. From this object, you can extract a Vector3 that contains the position of the object. You can also extract another Vector3 that contains the scaling factor in each direction (initialized to 1 in all directions, which results in no change in the default size). The transformation also stores the orientation of the model, which cannot be stored with a single number (in contrast to the rotation value of an Actor), because an object in three-dimensional space can be rotated any amount around any combination of the x, y, and z axes. For many technical reasons (such as computation, performance, and avoiding a phenomena known as gimbal lock 2), an object called a Quaternion (corresponding to a mathematical object of the same name) is used to store orientation data. For convenience, rather than work with the Matrix4 directly, you’ll maintain separate objects to store the position, rotation, and scale data for each BaseActor3D object and combine them into a Matrix4 that will be stored in the ModelInstance when needed.
Next, create a new class named BaseActor3D that contains the following code, which includes import statements, variable declarations, and the fundamental methods. This first set of methods includes the constructor; a method to set the ModelInstance for this actor; the calculateTransform method to combine the position, rotation, and scale data into a Matrix4; methods to set the Color and load a Texture used by the associated Material; the act method to update the transformation data of the model instance; and the draw method to render the model instance using the supplied ModelBatch and Environment.
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Matrix4;
public class BaseActor3D
{
    private ModelInstance modelData;
    private final Vector3 position;
    private final Quaternion rotation;
    private final Vector3 scale;
    public BaseActor3D(float x, float y, float z)
    {
        modelData = null;
        position  = new Vector3(x,y,z);
        rotation  = new Quaternion();
        scale     = new Vector3(1,1,1);
    }
    public void setModelInstance(ModelInstance m)
    {  modelData = m;  }
    public Matrix4 calculateTransform()
    {  return new Matrix4(position, rotation, scale);  }
    public void setColor(Color c)
    {
        for (Material m : modelData.materials)
            m.set( ColorAttribute.createDiffuse(c) );
    }
    public void loadTexture(String fileName)
    {
        Texture tex = new Texture(Gdx.files.internal(fileName), true);
        tex.setFilter( TextureFilter.Linear, TextureFilter.Linear );
        for (Material m : modelData.materials)
            m.set( TextureAttribute.createDiffuse(tex) );
    }
    public void act(float dt)
    {  modelData.transform.set( calculateTransform() );  }
    public void draw(ModelBatch batch, Environment env)
    {  batch.render(modelData, env);  }
}
Next are a variety of methods related to the position variable : get and set methods and methods to add values to the current position coordinates. For convenience, this code includes overloaded variations of the methods; the variations allow either a Vector3 or individual float inputs to be used.
public Vector3 getPosition()
{  return position;  }
public void setPosition(Vector3 v)
{  position.set(v);  }
public void setPosition(float x, float y, float z)
{  position.set(x,y,z);  }
public void moveBy(Vector3 v)
{  position.add(v);  }
public void moveBy(float x, float y, float z)
{  moveBy( new Vector3(x,y,z) );  }
The next functionality you will incorporate is the ability to rotate . While in theory a three-dimensional object can rotate around the x-axis, y-axis, or z-axis, for simplicity you will limit the actor to “turning” left and right, which corresponds to rotating around the y-axis, which points upward in this 3D world, as illustrated in Figure 8-2.3 The amount of rotation around the y-axis will be referred to as the turn angle . 4 There will be methods to get, set, and adjust this value, each of which is implemented using methods from the Quaternion class; add these methods to the BaseActor3D class as well.
public float getTurnAngle()
{  return rotation.getAngleAround(0,-1,0);  }
public void setTurnAngle(float degrees)
{  rotation.set( new Quaternion(Vector3.Y,degrees) );  }
public void turn(float degrees)
{  rotation.mul( new Quaternion(Vector3.Y,-degrees) );  }
Also, methods must be written that enable an actor to move in directions relative to its current orientation. When a BaseActor3D is first initialized, it will be assumed that the forward direction is represented by the vector (0, 0, –1), since the initial position of the camera will have a positive z coordinate and the actor will be facing away from the camera. Similarly, the initial upward direction is the vector (0, 1, 0), and the rightward direction is the vector (1, 0, 0). After the actor has been rotated, the relative forward, upward, and rightward directions can be determined by transforming these original vectors by the actor’s current rotation. Then, to move a given distance in one of these relative directions, you can scale the corresponding vector by the desired distance and add the result to the current position. The methods that enable the actor to move in these ways are given here:
public void moveForward(float dist)
{  moveBy( rotation.transform( new Vector3(0,0,-1) ).scl( dist ) );  }
public void moveUp(float dist)
{  moveBy( rotation.transform( new Vector3(0,1,0) ).scl( dist ) );  }
public void moveRight(float dist)
{  moveBy( rotation.transform( new Vector3(1,0,0) ).scl( dist ) );  }
Finally, a method will be added to set the scale of the object, which is useful for resizing a model :
public void setScale(float x, float y, float z)
{  scale.set(x,y,z);  }
This completes a large portion of the BaseActor3D class. A later section will discuss and present code for collision-detection and list-management methods (similar to the overlaps and getList methods from the BaseActor class). Next, you will create a complementary class that will be used to manage all these actors: the Stage3D class.

The Stage3D Class

Recall that the LibGDX Stage object handles rendering tasks (using its internal Camera and Batch objects) and manages a list of Actor objects. There are also act and draw methods in the Stage class, which call the act and draw methods of all attached actors. You will create similar functionality with the Stage3D class . To begin, create a new class named Stage3D with the following code. This includes a set of import statements, the variables required for rendering (Environment, PerspectiveCamera, and ModelBatch), and an ArrayList to store the BaseActor3D objects. These variables are initialized in the constructor, with nearly identical code to the previous example.
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import java.util.ArrayList;
public class Stage3D
{
    private Environment environment;
    private PerspectiveCamera camera;
    private final ModelBatch modelBatch;
    private ArrayList<BaseActor3D> actorList;
    public Stage3D()
    {
        environment = new Environment();
        environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.7f, 0.7f, 0.7f, 1));
        DirectionalLight dLight = new DirectionalLight();
        Color lightColor = new Color(0.9f, 0.9f, 0.9f, 1);
        Vector3 lightVector = new Vector3(-1.0f, -0.75f, -0.25f);
        dLight.set( lightColor, lightVector );
        environment.add( dLight ) ;
        camera = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        camera.position.set(10f, 10f, 10f);
        camera.lookAt(0,0,0);
        camera.near = 0.01f;
        camera.far = 1000f;
        camera.update();
        modelBatch = new ModelBatch();
        actorList = new ArrayList<BaseActor3D>();
    }
}
Next, are the act and draw methods, which invoke the corresponding methods on all the BaseActor3D objects contained in the ArrayList. In addition, the camera is updated in the act method .
public void act(float dt)
{
    camera.update();
    for (BaseActor3D ba : actorList)
        ba.act(dt);
}
public void draw()
{
    modelBatch.begin(camera);
    for (BaseActor3D ba : actorList)
        ba.draw(modelBatch, environment);
    modelBatch.end();
}
There are methods to add and remove actors and retrieve the list, given by the following code:
public void addActor(BaseActor3D ba)
{  actorList.add( ba );  }
public void removeActor(BaseActor3D ba)
{  actorList.remove( ba );  }
public ArrayList<BaseActor3D> getActors()
{  return actorList;  }
The final part of this class is an extensive set of methods to adjust the camera , which is a much more involved process than in a 2D game. First are the methods to set the camera position and to move the camera by a given amount; these values may be specified by either a Vector3 object or three float values:
public void setCameraPosition(float x, float y, float z)
{  camera.position.set(x,y,z);  }
public void setCameraPosition(Vector3 v)
{  camera.position.set(v);  }
public void moveCamera(float x, float y, float z)
{  camera.position.add(x,y,z);  }
public void moveCamera(Vector3 v)
{  camera.position.add(v);  }
Next, building on these methods are additional methods that move the camera relative to its current position. A Camera object stores two internal Vector3 objects: direction, which determines where the camera is currently facing, and up, which determines the direction that should be oriented toward the top of the screen. When moving the camera forward and backward in this program, the camera should maintain a constant height (even if the camera is tilted at an angle), and so the y component of the vector direction can be set to 0 in order to yield a vector that moves you forward in this way. Once the vector has been determined, it needs to be scaled by the distance you want the camera to travel, and then the vector should be added to the camera’s current position via the moveCamera function. For moving to the left and right, you will similarly discard the y component of the vector; to transform the direction vector into a vector pointing to the right, interchange the x and z values and negate the z value, as illustrated by the example in Figure 16-7. In this picture, keep in mind that the values displayed refer to the change in direction represented by each of the vectors .
A352797_2_En_16_Fig7_HTML.jpg
Figure 16-7.
Converting a forward-facing vector to a rightward-facing vector
Moving the camera upward is a straightforward task. In this case, movement will always be in the direction of the y-axis and not the camera’s up vector, since when the camera is tilted, its up vector will no longer be pointing in the same orientation as the y-axis. The methods for moving the camera in these ways are as follows:
public void moveCameraForward(float dist)
{
    Vector3 forward = new Vector3(camera.direction.x, 0, camera.direction.z).nor();
    moveCamera( forward.scl( dist ) );
}
public void moveCameraRight(float dist)
{
    Vector3 right = new Vector3(camera.direction.z, 0, -camera.direction.x).nor();
    moveCamera( right.scl( dist ) );
}
public void moveCameraUp(float dist)
{  moveCamera( 0,dist,0 );  }
Functionality should also be provided for rotating the camera, and, once again, restricting the types of possible camera movement will make the navigation easier for the user to visualize. As with BaseActor3D objects, the camera will be able to turn to the left and right, which corresponds to rotating it around the y-axis. In addition, it would be convenient to be able to tilt the camera up and down to look higher and lower. This can be done by determining the vector that points to the right, as before, and then rotating the direction vector of the camera around the vector pointing to the right. These two methods, turnCamera and tiltCamera , are given here:
public void turnCamera(float angle)
{  camera.rotate( Vector3.Y, -angle );  }
public void tiltCamera(float angle)
{
    Vector3 right = new Vector3(camera.direction.z, 0, -camera.direction.x);
    camera.direction.rotate(right, angle);
}
Finally, it is important to be able to orient the camera to look at a particular position. This is accomplished with a camera method called lookAt, but this method may have the undesired result of tilting the camera to the left or right, making the horizon no longer level, which can be disorienting to the player. So, after calling the camera’s lookAt method , the camera’s up axis needs to be reset to the direction of the y-axis to correct this problem; this method will be called setCameraDirection . As before, this method will be overloaded to take either a Vector3 or three float values as input.
public void setCameraDirection(Vector3 v)
{
    camera.lookAt(v);
    camera.up.set(0,1,0);
}
public void setCameraDirection(float x, float y, float z)
{   setCameraDirection( new Vector3(x,y,z) );   }
This is all the functionality you’ll need for the Stage3D class. Now that you are finished writing this class, you can return to the BaseActor3D class to add some stage-related functionality. You will modify the constructor by adding a Stage3D parameter to which the actor will be added when it is created, and which will store a reference to the stage, which will be convenient later. In the BaseActor3D class , add the following variable declaration:
protected Stage3D stage;
Also, change the constructor of this class to the following:
public BaseActor3D(float x, float y, float z, Stage3D s)
    {
        modelData = null;
        position  = new Vector3(x,y,z);
        rotation  = new Quaternion();
        scale     = new Vector3(1,1,1);
        stage = s;
        s.addActor(this);
    }
You’re now ready to move on to using these classes to create your first interactive 3D demo.

Creating an Interactive 3D Demo

This section presents an interactive demo inspired by Figure 16-2. This demo consists of a screenshot of the Starfish Collector game on a flattened box shape and cubes with colored crate textures to represent the origin of the scene and points on the x, y, and z axes. There is also a sphere that can be moved forward and backward, left and right, up and down, and turned to the left or right; the sphere is textured to help the user visualize these directions relative to the sphere. Finally, you will enable the user to turn, tilt, and move the camera in any direction. Figure 16-8 shows this demo in action .
A352797_2_En_16_Fig8_HTML.jpg
Figure 16-8.
The 3D movement demo program
Continuing with the Project3D project, you should first download the source code files for this chapter and copy the assets folder into your project, as it contains all the images you will need for this project. You will then create a simplified version of the BaseGame class that you have used in previous chapters. Create a new class called BaseGame with the following code:
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputMultiplexer;
public abstract class BaseGame extends Game
{
    private static BaseGame game;
    public BaseGame()
    {
        game = this;
    }
    public void create()
    {
        InputMultiplexer im = new InputMultiplexer();
        Gdx.input.setInputProcessor( im );
    }
    public static void setActiveScreen(BaseScreen s)
    {
        game.setScreen(s);
    }
}
Next, you need a new version of the BaseScreen class that uses a Stage3D to contain the game entities, rather than a normal Stage object. However, a standard Stage object remains sufficient for the user interface, and most parts of this class should be familiar from earlier code development in this book, such as implementing the Screen and InputProcessor interfaces. Create a new class called BaseScreen with the following code:
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
public abstract class BaseScreen implements Screen, InputProcessor
{
    protected Stage3D mainStage3D;
    protected Stage uiStage;
    protected Table uiTable;
    public BaseScreen()
    {
        mainStage3D = new Stage3D();
        uiStage   = new Stage();
        uiTable = new Table();
        uiTable.setFillParent(true);
        uiStage.addActor(uiTable);
        initialize();
    }
    public abstract void initialize();
    public abstract void update(float dt);
    // gameloop method
    public void render(float dt)
    {
         // limit amount of time that can pass while window is being dragged
        dt = Math.min(dt, 1/30f);
        // act methods
        uiStage.act(dt);
        mainStage3D.act(dt);
        // defined by game-specific classes
        update(dt);
        // render
        Gdx.gl.glClearColor(0.5f,0.5f,0.5f,1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT + GL20.GL_DEPTH_BUFFER_BIT);
        // draw the graphics
        mainStage3D.draw();
        uiStage.draw();
    }
    // methods required by Screen interface
    public void resize(int width, int height)
    {   uiStage.getViewport().update(width, height, true);  }
    public void pause()   {  }
    public void resume()  {  }
    public void dispose() {  }
    public void show()
    {
        InputMultiplexer im = (InputMultiplexer)Gdx.input.getInputProcessor();
        im.addProcessor(this);
        im.addProcessor(uiStage);
    }
    public void hide()
    {
        InputMultiplexer im = (InputMultiplexer)Gdx.input.getInputProcessor();
        im.removeProcessor(this);
        im.removeProcessor(uiStage);
    }
    // methods required by InputProcessor interface
    public boolean keyDown(int keycode)
    {  return false;  }
    public boolean keyUp(int keycode)
    {  return false;  }
    public boolean keyTyped(char c)
    {  return false;  }
    public boolean mouseMoved(int screenX, int screenY)
    {  return false;  }
    public boolean scrolled(int amount)
    {  return false;  }
    public boolean touchDown(int screenX, int screenY, int pointer, int button)
    {  return false;  }
    public boolean touchDragged(int screenX, int screenY, int pointer)
    {  return false;  }
    public boolean touchUp(int screenX, int screenY, int pointer, int button)
    {  return false;  }
}
Since this program—and potentially others—will use box and sphere shapes, you will next create some classes that extend the BaseActor3D class and create these shapes for you, using the functionality of the ModelBuilder class introduced previously. First, create a new class named Box with the following code:
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.math.Vector3;
public class Box extends BaseActor3D
{
    public Box(float x, float y, float z, Stage3D s)
    {
        super(x,y,z,s);
        ModelBuilder modelBuilder = new ModelBuilder();
        Material boxMaterial = new Material();
        int usageCode = Usage.Position + Usage.ColorPacked
                      + Usage.Normal   + Usage.TextureCoordinates;
        Model boxModel = modelBuilder.createBox(1,1,1, boxMaterial, usageCode);
        Vector3 position = new Vector3(0,0,0);
        setModelInstance( new ModelInstance(boxModel, position) );
    }
}
Next, you also need a class to create a sphere . The ModelBuilder class contains a method named createSphere, similar to createBox, which allows you to specify the radius of the sphere in the x, y, and z directions,5 the resolution of the sphere in terms of the number of subdivisions in the latitudinal and longitudinal directions (for a smooth sphere, you’ll set these both to 32), and the associated Material and usage code value. Create a new class called Sphere that contains the following code:
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.math.Vector3;
public class Sphere extends BaseActor3D
{
    public Sphere(float x, float y, float z, Stage3D s)
    {
        super(x,y,z,s);
        ModelBuilder modelBuilder = new ModelBuilder();
        Material mat = new Material();
        int usageCode = Usage.Position + Usage.ColorPacked
                      + Usage.Normal   + Usage.TextureCoordinates;
        int r = 1;
        Model mod = modelBuilder.createSphere(r,r,r, 32,32, mat, usageCode);
        Vector3 pos = new Vector3(0,0,0);
        setModelInstance( new ModelInstance(mod, pos) );
    }
}
Now you are ready to create the class that sets up the scene pictured in Figure 16-8. Create a new class named DemoScreen with the following code. The object named player will be controlled by the user.
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
public class DemoScreen extends BaseScreen
{
    BaseActor3D player;
    public void initialize()
    {    }
    public void update(float dt)
    {    }
}
Next, to set up the objects in the scene, you will create a flat box with the image of the Starfish Collector game as its texture, four cubical boxes with the same texture as each other but different colors, and a sphere that will be controlled by the user. You also need to set the starting position and direction of the camera. To accomplish these tasks, add the following code to the initialize method :
Box screen = new Box(0,0,0, mainStage3D);
screen.setScale(16, 12, 0.1f);
screen.loadTexture("assets/starfish-collector.png");
Box markerO = new Box(0,0,0, mainStage3D);
markerO.setColor(Color.BROWN);
markerO.loadTexture("assets/crate.jpg");
Box markerX = new Box(5,0,0, mainStage3D);
markerX.setColor(Color.RED);
markerX.loadTexture("assets/crate.jpg");
Box markerY = new Box(0,5,0, mainStage3D);
markerY.setColor(Color.GREEN);
markerY.loadTexture("assets/crate.jpg");
Box markerZ = new Box(0,0,5, mainStage3D);
markerZ.setColor(Color.BLUE);
markerZ.loadTexture("assets/crate.jpg");
player = new Sphere(0,1,8, mainStage3D);
player.loadTexture("assets/sphere-pos-neg.png");
mainStage3D.setCameraPosition(3,4,10);
mainStage3D.setCameraDirection(0,0,0);
Finally, there is the update method to consider, which processes lots of potential player input. The player is controlled using the keyboard keys W/A/S/D, which correspond to moving forward/left/backward/right, a standard configuration in many computer games. To this standard, you also add the R and F keys for moving up and down (which we think of as the Rise and Fall directions). You also use the Q and E keys to turn left and right (which also seems memorable because these keys are positioned above the keys for moving left and right). The camera can be controlled in the same way, using the same keys, when the Shift key is being pressed simultaneously. The camera can also be tilted upward and downward using the T and G keys (which you can remember with the mnemonic words Top and Ground). The following is the code that accomplishes all of these tasks, which, as mentioned previously, should be included in the update method:
float speed = 3.0f;
float rotateSpeed = 45.0f;
if ( !(Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)
       || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT)) )
{
    if ( Gdx.input.isKeyPressed(Keys.W) )
        player.moveForward( speed * dt );
    if ( Gdx.input.isKeyPressed(Keys.S) )
        player.moveForward( -speed * dt );
    if ( Gdx.input.isKeyPressed(Keys.A) )
        player.moveRight( -speed * dt );
    if ( Gdx.input.isKeyPressed(Keys.D) )
        player.moveRight( speed * dt );
    if ( Gdx.input.isKeyPressed(Keys.Q) )
        player.turn( -rotateSpeed * dt );
    if ( Gdx.input.isKeyPressed(Keys.E) )
        player.turn( rotateSpeed * dt );
    if ( Gdx.input.isKeyPressed(Keys.R) )
        player.moveUp( speed * dt );
    if ( Gdx.input.isKeyPressed(Keys.F) )
        player.moveUp( -speed * dt );
}
if ( Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)
     || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT) )
{
    if (Gdx.input.isKeyPressed(Keys.W))
        mainStage3D.moveCameraForward( speed * dt );
    if (Gdx.input.isKeyPressed(Keys.S))
        mainStage3D.moveCameraForward( -speed * dt );
    if (Gdx.input.isKeyPressed(Keys.A))
        mainStage3D.moveCameraRight( -speed * dt );
    if (Gdx.input.isKeyPressed(Keys.D))
        mainStage3D.moveCameraRight( speed * dt );
    if (Gdx.input.isKeyPressed(Keys.R))
        mainStage3D.moveCameraUp( speed * dt );
    if (Gdx.input.isKeyPressed(Keys.F))
        mainStage3D.moveCameraUp( -speed * dt );
    if (Gdx.input.isKeyPressed(Keys.Q))
        mainStage3D.turnCamera(-rotateSpeed * dt);
    if (Gdx.input.isKeyPressed(Keys.E))
        mainStage3D.turnCamera(rotateSpeed * dt);
    if (Gdx.input.isKeyPressed(Keys.T))
        mainStage3D.tiltCamera(rotateSpeed * dt);
    if (Gdx.input.isKeyPressed(Keys.G))
        mainStage3D.tiltCamera(-rotateSpeed * dt);
}
This completes the code for the update method. Before you can run this demo, you will need to write a launcher-style class and a class that extends BaseGame, as you have in previous projects. First, create a new class called MoveDemo with the following code:
public class MoveDemo extends BaseGame
{
    public void create()
    {
        super.create();
        setActiveScreen( new DemoScreen() );
    }
}
Next, create a class called Launcher2 that contains the following code:
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
public class Launcher2
{
    public static void main ()
    {
        MoveDemo myProgram = new MoveDemo();
        LwjglApplication launcher = new LwjglApplication(
            myProgram, "Movement Demo", 800, 600 );
    }
}
Now, try out the program and get a feel for moving around in three-dimensional space!

Game Project: Starfish Collector 3D

In this section, you’ll create the game Starfish Collector 3D, which, as the name suggests, is a 3D version of the Starfish Collector game introduced at the beginning of this book; a side-by-side comparison of these games is shown in Figure 16-9. As you may expect, the goal in this new game will be to help the turtle collect all the starfish . You control the turtle using the arrow keys: pressing the up arrow key moves the turtle forward, while pressing the left and right arrow keys turns the turtle to the left and to the right. Most of the difficult groundwork has been laid in the previous section. The remaining topics include loading complex models from external files, displaying an image that surrounds the game world, and performing simplified collision detection. As before, you will continue adding code to Project3D, as it already contains many of the classes you will need (BaseGame, BaseScreen, BaseActor3D, and Stage3D).
A352797_2_En_16_Fig9_HTML.jpg
Figure 16-9.
Starfish Collector in 2D (left) and 3D (right)
The first task, loading a model, is relatively straightforward. To do so, you need to use an extension of the ModelLoader class, called ObjLoader , which can import 3D models from files that use the Wavefront (*.obj) file format, and then you need to use ObjLoader class loadModel method, which takes a FileHandle as input and returns a Model. You can then use the model to create a ModelInstance that you use in a BaseActor3D object, as you did for the Box and Sphere classes. Since you will be importing multiple models in this game, it makes sense to make a new class that contains this functionality. Create a new class called ObjModel that contains the following code:
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.loader.ObjLoader;
public class ObjModel extends BaseActor3D
{
    public ObjModel(float x, float y, float z, Stage3D s)
    {
        super(x,y,z,s);
    }
    public void loadObjModel(String fileName)
    {
        ObjLoader loader = new ObjLoader();
        Model objModel = loader.loadModel(Gdx.files.internal(fileName), true);
        setModelInstance( new ModelInstance(objModel) );
    }
}
Next, you will need to surround your game world with an image so as to give the appearance of a sky in the background. In the previous 2D games in this book, you created a rectangular object that simply displayed an image of the sky. Because you’re in a 3D environment, here you’ll create a spherical object that is significantly larger than and surrounds your game world, and then you will apply a texture to it, such as the one shown in Figure 16-10. This is often referred to as a sky sphere or a sky dome . You may notice that the image appears slightly stretched near the top (and it would on the bottom, too, were the bottom not simply a gray color). This is because the image has been spherically distorted: while it looks strange as a rectangle, when the image is applied to a sphere, everything will appear to have the correct proportions. This the same phenomena that occurs when trying to make a flat, rectangular map of the Earth, which is roughly spherical; the map will inevitably contain distorted areas corresponding to the regions near the poles. There is a second difficulty that will arise when trying to implement a sky dome: textures applied to the material of an object are typically only visible when viewed from the outside of the object. (This convention is to increase the efficiency of 3D programs; there is no need to render objects from a perspective that the user will not see.) Fortunately, you can perform a geometric trick to resolve this problem: after creating the sphere, you will scale the mesh by –1 in the z direction; this will cause the sphere to turn itself “inside-out,” reversing the sides on which the image will be displayed.
A352797_2_En_16_Fig10_HTML.jpg
Figure 16-10.
A spherically distorted image of the sky
The third and final concept to discuss is collision detection . To keep the level of complexity manageable, the motion and placement of your three-dimensional objects will be restricted to a two-dimensional plane, thus allowing this project to reuse collision code from the original BaseActor class. This technique is well-known in game development. Games that use this approach (those that have 3D graphics but restrict game play to a 2D plane and have restricted camera movement) are called 2.5D games . Figure 16-11 illustrates how the game will appear to the player, while on the right you can see the water represented by a grid and the collision polygons that will correspond to the pictured game entities (the two rocks and the turtle).
A352797_2_En_16_Fig11_HTML.jpg
Figure 16-11.
The game world rendered in 3D, and the corresponding 2D collision polygons
To incorporate collision into your project, you need to make some additions to the BaseActor3D class . First, add the following import statements:
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.math.Polygon;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Intersector.MinimumTranslationVector;
Next, add the following variable declaration to the class:
private Polygon boundingPolygon;
Next are a pair of methods used to set the polygon to either a rectangular or an eight-sided polygon (octagon) shape. In both cases, you need to determine the dimensions of the object in the x and z dimensions; these quantities are analogous to the width and height in the two-dimensional case. These values can be determined by calculating the BoundingBox associated with the model, which is the smallest box that contains the entire model. A bounding box stores the dimensions of the model using two Vector3 objects, min and max, which store the values of the smallest and largest coordinates contained by the model, respectively. These values are used to create the array of vertices that is passed to the polygon object, as illustrated here:
public void setBaseRectangle ()
{
    BoundingBox modelBounds = modelData.calculateBoundingBox( new BoundingBox() );
    Vector3 max = modelBounds.max;
    Vector3 min = modelBounds.min;
    float[] vertices =
        {max.x, max.z, min.x, max.z, min.x, min.z, max.x, min.z};
    boundingPolygon = new Polygon(vertices);
    boundingPolygon.setOrigin(0,0);
}
public void setBasePolygon()
{
    BoundingBox modelBounds = modelData.calculateBoundingBox( new BoundingBox() );
    Vector3 max = modelBounds.max;
    Vector3 min = modelBounds.min;
    float a = 0.75f; // offset amount.
    float[] vertices =
        {max.x,0, a*max.x,a*max.z, 0,max.z, a*min.x,a*max.z,
         min.x,0, a*min.x,a*min.z, 0,min.z, a*max.x,a*min.z };
    boundingPolygon = new Polygon(vertices);
    boundingPolygon.setOrigin(0,0);
}
Once the polygon has been set up, you need a method that returns the boundary polygon that has been updated to take the position, rotation, and scale into account. Recall that because of the orientation of the coordinate axes, the horizontal plane contains the x-axis and the z-axis, and so these values will be used when updating the position and scale of the polygon, while the turn angle (the rotation around the y-axis) will be used to set its rotation. Add the following code to the BaseActor3D class:
public Polygon getBoundaryPolygon()
{
    boundingPolygon.setPosition( position.x, position.z );
    boundingPolygon.setRotation( getTurnAngle() );
    boundingPolygon.setScale( scale.x, scale.z );
    return boundingPolygon;
}
Next, you need methods to detect overlap (to check if the turtle has collected a starfish) and prevent overlap (so that the rocks behave as solids and the turtle cannot pass through them). These methods, which you also need to add to the BaseActor3D class, are identical to those from the BaseActor class, except that a BaseActor3D object must be passed in as a parameter. Add the following code :
public boolean overlaps(BaseActor3D other)
{
    Polygon poly1 = this.getBoundaryPolygon();
    Polygon poly2 = other.getBoundaryPolygon();
    if ( !poly1.getBoundingRectangle().overlaps(poly2.getBoundingRectangle()) )
        return false;
    MinimumTranslationVector mtv = new MinimumTranslationVector();
    return Intersector.overlapConvexPolygons(poly1, poly2, mtv);
}
public void preventOverlap(BaseActor3D other)
{
    Polygon poly1 = this.getBoundaryPolygon();
    Polygon poly2 = other.getBoundaryPolygon();
    // initial test to improve performance
    if ( !poly1.getBoundingRectangle().overlaps(poly2.getBoundingRectangle()) )
        return;
    MinimumTranslationVector mtv = new MinimumTranslationVector();
    boolean polygonOverlap = Intersector.overlapConvexPolygons(poly1, poly2, mtv);
    if ( polygonOverlap )
        this.moveBy( mtv.normal.x * mtv.depth, 0, mtv.normal.y * mtv.depth );
}
Finally, you will recreate some methods from the Actor and BaseActor classes to work with lists: getList, count, and remove. First, add the following import statement to the BaseActor3D class:
import java.util.ArrayList;
Then, add the following code to the BaseActor3D class :
public static ArrayList<BaseActor3D> getList(Stage3D stage, String className)
{
    ArrayList<BaseActor3D> list = new ArrayList<BaseActor3D>();
    Class theClass = null;
    try
    {  theClass = Class.forName(className);  }
    catch (Exception error)
    {  error.printStackTrace();  }
    for (BaseActor3D ba3d : stage.getActors())
    {
        if ( theClass.isInstance( ba3d ) )
            list.add(ba3d);
    }
    return list;
}
public static int count(Stage3D stage, String className)
{
    return getList(stage, className).size();
}
public void remove()
{
    stage.removeActor(this);
}
At this point, the BaseActor3D class has all the core functionality you will need for the Starfish Collector 3D game. In this game, the water will be displayed using a Box object, the sky dome will be displayed using a Sphere object, and the turtle, starfish, and rocks will use imported model data from the ObjModel class. Create a new class called Turtle with the following code:
public class Turtle extends ObjModel
{
    public Turtle(float x, float y, float z, Stage3D s)
    {
        super(x,y,z,s);
        loadObjModel("assets/turtle.obj");
        setBasePolygon();
    }
}
Next, create a class called Starfish with the following code. Note that the act method is being used to make the starfish rotate.
public class Starfish extends ObjModel
{
    public Starfish(float x, float y, float z, Stage3D s)
    {
        super(x,y,z,s);
        loadObjModel("assets/star.obj");
        setScale(3,1,3);
        setBasePolygon();
    }
    public void act(float dt)
    {
        super.act(dt);
        turn( 90 * dt );
    }
}
Next, create a class called Rock with the following code :
public class Rock extends ObjModel
{
    public Rock(float x, float y, float z, Stage3D s)
    {
        super(x,y,z,s);
        loadObjModel("assets/rock.obj");
        setBasePolygon();
        setScale(3,3,3);
    }
}
To display the number of remaining starfish in a label, you will set up a LabelStyle object as you have done previously. In the BaseGame class, add the following import statements:
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
Then, add the following variable declaration:
public static LabelStyle labelStyle;
To initialize the labelStyle object, add the following code to the create method:
FreeTypeFontGenerator fontGenerator = new FreeTypeFontGenerator(Gdx.files.internal("assets/OpenSans.ttf"));
FreeTypeFontParameter fontParameters = new FreeTypeFontParameter();
fontParameters.size = 36;
fontParameters.color = Color.WHITE;
fontParameters.borderWidth = 2;
fontParameters.borderColor = Color.BLACK;
fontParameters.borderStraight = true;
fontParameters.minFilter = TextureFilter.Linear;
fontParameters.magFilter = TextureFilter.Linear;
BitmapFont customFont = fontGenerator.generateFont(fontParameters);
labelStyle = new LabelStyle();
labelStyle.font = customFont;
Now you can set up the screen that contains the actual game. Begin by creating a new class called LevelScreen with the following code:
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
public class LevelScreen extends BaseScreen
{
    Turtle turtle;
    Label starfishLabel;
    Label messageLabel;
    public void initialize()
    {    }
    public void update(float dt)
    {    }
}
In the initialize method, you need to set up the floor and the sky dome, which will be scaled to a very large size. You also need to initialize the turtle, as well as a number of rocks and starfish. As before, you should also set the camera position and direction so that many of the objects you have added are in view when the game begins. Finally, you should initialize the labels and add them to the user-interface table . To accomplish these tasks, add the following code to the initialize method:
Box floor = new Box(0,0,0, mainStage3D);
floor.loadTexture( "assets/water.jpg" );
floor.setScale(500, 0.1f, 500);
Sphere skydome = new Sphere(0,0,0, mainStage3D);
skydome.loadTexture( "assets/sky-sphere.png" );
// when scaling, the negative z-value inverts the sphere
//   so that the texture is rendered on the inside
skydome.setScale(500,500,-500);
turtle = new Turtle(0, 0, 15, mainStage3D);
turtle.setTurnAngle(90);
new Rock(-15, 1,  0, mainStage3D);
new Rock(-15, 1, 15, mainStage3D);
new Rock(-15, 1, 30, mainStage3D);
new Rock(  0, 1,  0, mainStage3D);
new Rock(  0, 1, 30, mainStage3D);
new Rock( 15, 1,  0, mainStage3D);
new Rock( 15, 1, 15, mainStage3D);
new Rock( 15, 1, 30, mainStage3D);
new Starfish( 10, 0, 10, mainStage3D);
new Starfish( 10, 0, 20, mainStage3D);
new Starfish(-10, 0, 10, mainStage3D);
new Starfish(-10, 0, 20, mainStage3D);
mainStage3D.setCameraPosition(0,10,0);
mainStage3D.setCameraDirection( new Vector3(0,0,0) );
starfishLabel = new Label("Starfish left: 4", BaseGame.labelStyle);
starfishLabel.setColor( Color.CYAN );
messageLabel = new Label("You Win!", BaseGame.labelStyle);
messageLabel.setColor( Color.LIME );
messageLabel.setFontScale(2);
messageLabel.setVisible(false);
uiTable.pad(20);
uiTable.add(starfishLabel);
uiTable.row();
uiTable.add(messageLabel).expandY();
In the update method , you need to check for user input and move the turtle accordingly, keeping the camera directed toward the turtle. You also need to prevent overlap between the turtle and the rock objects, and if the turtle overlaps a starfish, the starfish should be “collected” and removed from the game. Finally, you need to update the label that displays the number of starfish left, and if there are none left, display the “You Win!” message. To implement all of this, add the following code to the update method:
float speed = 3.0f;
float rotateSpeed = 45.0f;
if ( Gdx.input.isKeyPressed(Keys.UP) )
    turtle.moveForward( speed * dt );
if ( Gdx.input.isKeyPressed(Keys.LEFT) )
    turtle.turn( -rotateSpeed * dt );
if ( Gdx.input.isKeyPressed(Keys.RIGHT) )
    turtle.turn( rotateSpeed * dt );
mainStage3D.setCameraDirection( turtle.getPosition() );
for ( BaseActor3D rock : BaseActor3D.getList( mainStage3D, "Rock") )
    turtle.preventOverlap(rock);
for ( BaseActor3D starfish : BaseActor3D.getList( mainStage3D, "Starfish") )
    if (turtle.overlaps(starfish) )
        starfish.remove();
int starfishCount = BaseActor3D.count(mainStage3D, "Starfish");
starfishLabel.setText( "Starfish left: " + starfishCount );
if (starfishCount == 0)
    messageLabel.setVisible(true);
At this point, the Starfish Collector 3D gameplay code is complete. To play this game, you need to create a new class called StarfishCollector3DGame as follows:
public class StarfishCollector3DGame extends BaseGame
{
    public void create()
    {
        super.create();
        setActiveScreen( new LevelScreen() );
    }
}
Also, create a new class called Launcher3 as follows:
import com.badlogic.gdx.Game;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
public class Launcher3
{
    public static void main ()
    {
        Game myGame = new StarfishCollector3DGame();
        LwjglApplication launcher = new LwjglApplication(
                                          myGame, "Starfish Collector 3D", 800, 600 );
    }
}
Now, run your project, help the turtle collect all the starfish, and enjoy the 3D graphics as you play!

Summary and Next Steps

This chapter may have only scratched the surface of 3D game programming, but it’s a topic that entails a lot of material. You explored the components of 3D scenes, perspective cameras, and lighting. You learned that 3D models contain meshes and materials, and that instances of models store transformation data (position, rotation, and scale) using matrices. You adapted and extended your custom game development framework to include 3D versions of actors and stages and learned the many ways you can move objects around in a three-dimensional world. Finally, you put your skills (and your code) to the test by creating a pair of interactive demo programs.
With the foundation laid in this chapter, you are now ready to create some games with 3D graphics (and 2.5D gameplay, for simplicity). A great place to start is recreating some of the earlier projects from this book. Games such as Rectangle Destroyer can be recreated simply using boxes and spheres. For other games, you will likely want to use pre-created model files (similar to the turtle, starfish, and rock). Some websites from which you can download model files (in a variety of formats) include the following:
Once you have downloaded a 3D model, and before loading it into LibGDX, you can view and modify it if desired using 3D graphics software such as Blender, which is freely available at www.blender.org , or, for the artistically inclined, Blender can even be used to create 3D models from scratch.
Once again, congratulations on finishing the final game project in this book! The next chapter concludes with some general advice and possible next steps in game development.
Footnotes
1
For additional information, two excellent books about the mathematical details of 3D graphics are 3D Math Primer for Graphics and Game Development by Fletcher Dunn and Ian Parberry (A K Peters/CRC Press, 2011) and Mathematics for 3D Game Programming and Computer Graphics by Eric Lengyel (Cengage Learning PTR, 2011).
 
2
When using three values to represent the rotations of an object around three axes, gimbal lock refers to the problem that occurs when an object is in one of a few particular orientations and two axes of rotation line up, making it impossible for the object to rotate in certain ways while in the given orientation.
 
3
In theory, this choice of the y-axis as the “up” direction is somewhat arbitrary, as you could orient yourself in the game world so that any axis corresponds to the up direction.
 
4
The amount of rotation around the upward-pointing axis is also called the yaw angle . Similarly, the rotation around the sideways-pointing axis (the motion from tilting your head up and down) is called the pitch angle , and the rotation around the forward-pointing axis (the motion from tilting your head to the left and to the right) is called the roll angle .
 
5
Technically, a sphere has only one radius value; the figures created by the createSphere method are more accurately referred to as ellipsoids . However, in the class you are creating, all the radius values will be set to the same number, so it truly is a spherical object.
 
..................Content has been hidden....................

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