Game Development in Java
Introduction
In my early days as a software developer I worked at a small game studio. This was back in the days when ActionScript and Flash were still a thing.
At JCore during Corona times we’ve spent part of the JCore Fast Track looking at game development in Unity3D and the Unreal engine. These engines work on C#/JavaScript and C++ respectively.
Nowadays the language I’m most comfortable with is Java. A little while ago I was wondering whether it would be possible to create a game in Java.
Of course almost all languages support some sort of drawing, so technically the answer would be a straight yes. Modern engines and libraries offer support for OpenGL rendering and provide a lot of tools out of the box.
Though there are 3D engines for Java I’ll be focusing on a 2D game, mostly because the complexity of 3D and its assets makes creating anything resembling a 3D game a daunting task.
Definitions
In this blog I will use some jargon which might need some explanation.
Texture
A texture is an image which can be loaded into the graphics card. In 3D games a texture is applied to the surface of a 3D object to give it a certain look and feel. In 2D games a texture is wrapped in a Sprite. A Sprite contains the bitmap information and the location of the points defining its position and rotation point. Sometimes behaviour of the Sprite can also be included in the definition. A sprite can be a static image or a series of images forming an animation.
In this blog I’ll be using plain textures.
Rendering
Rendering is the process of drawing the scene onto the screen. The term is mostly used for drawing using the graphics card.
UI
UI refers to the user interface of the game. The UI is rendered on top of the game and contains scores, menus, buttons, etc.
The framework of choice
There are several frameworks which can be used to develop games. Most frameworks are based on LWJGL or JOGL. As far as I know all game libraries support rendering on graphics cards which is very important for the visual style of the game. The graphics card is (logically) much better at rendering graphics and/by doing matrix calculations. If you want to draw a lot of images, apply transformations and enable lighting a CPU would be very restrictive.
While JOGL and LWJGL can be used to create 2D and 3D games, these frameworks offer little more than a low level binding of OpenGL.
For 3D games there are some higher level engines like jMonkeyEngine, which uses LWJGL for rendering by default but can also use JOGL, and Ogre4j which uses JNI to connect Java to the C++ Ogre engine.
Another framework on top of LWJGL is LibGDX. LibGDX is suited for 2D and 3D game development and includes several supporting libraries easing the creation of games.
There are other libraries like Slick2D. A comprehensive list of other frameworks can be found here. I’ve found the other frameworks that I tried more cumbersome to work with, both in the setup and the APIs they offer.
Because LibGDX is very easy to setup, offers just the right abstraction to retain flexibility and can also be used for 3D game development I’ve chosen LibGDX for this blog.
The Game
Ever had a BadConnection
or no connection at all?
If you’re using Chrome you might be familiar with the game it offers to pass the time until your internet returns.
This is the game I’ll be recreating in LibGDX. The game is extremely simple, but it can be used to demo several of the libraries LibGDX offers.
During the blog I’ll show you almost all the code. I’ve only omitted some unimplemented methods from interfaces in the examples. I’ll revisit existing classes to add new features.
The full project is available on git.
Setup
LibGDX provides a tool to generate a Gradle based project: https://libgdx.badlogicgames.com/download.html
When running it, it shows a screen to configure the game project.
After generation it will create a project with the following structure. The generated project is based on Java 7, which might also result in some errors while generating the project with a newer JDK installed. This wasn’t a problem for my project.
In this blog I would like to introduce you to the framework and the supporting libraries.
Follow along?
If you’d like to follow along, download the following zip file and place its contents in the core/assets directory.
I’ve used Java 11.
Change sourceCompatibility = 11
in the build.gradle files in the generated project.
You can probably use any Java version above 7, if you match the sourceCompatibility.
Launching the game is done through the DesktopLauncher
.
Since I like the game to be fullscreen I’ve made a minor config change to the class.
package nl.jdriven.desktop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import nl.jdriven.BadConnection;
public class DesktopLauncher {
public static void main (String[] arg) {
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.height = 1050; (1)
config.fullscreen = true;
new LwjglApplication(new BadConnection(), config);
}
}
1 | Set the height of the screen to 1050 pixels, and start the game fullscreen. |
All other development in the blog will be done in the core
project.
Screens
Supporting several screens
The generated class BadConnection
extends ApplicationAdapter
.
To support screens, it needs to extend from Game
.
In LibGDX a Game
supports screens and will defer rendering to the current screen.
I’ve added a menu screen and a game screen.
When the game is created it should immediately enter the menu screen.
The game will be using system resources not controlled by the JVM.
An example of this is a texture currently rendered to the screen.
This texture will be loaded into the memory of the graphics card.
To correctly release these resources you can’t put your trust in the Java garbage collector.
Before the game exits it will call the dispose
method.
This call must be propagated to all assets that implement the Disposable
interface.
package nl.jdriven.screens;
import nl.jdriven.BadConnection;
import com.badlogic.gdx.Screen;
public class MenuScreen implements Screen {
private BadConnection badConnection;
public MenuScreen(BadConnection badConnection) {
this.badConnection = badConnection;
}
// methods from interface Screen
}
package nl.jdriven.screens;
import com.badlogic.gdx.Screen;
import nl.jdriven.BadConnection;
public class GameScreen implements Screen {
private BadConnection badConnection;
public GameScreen(BadConnection badConnection) {
this.badConnection = badConnection;
}
// methods from interface Screen
}
package nl.jdriven;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Screen;
import nl.jdriven.screens.GameScreen;
import nl.jdriven.screens.MenuScreen;
public class BadConnection extends Game { (1)
private final Screen menuScreen; (2)
private final Screen gameScreen;
public BadConnection() {
menuScreen = new MenuScreen(this); (3)
gameScreen = new GameScreen(this);
}
@Override
public void create () {
enterMenu(); (4)
}
@Override
public void dispose () {
menuScreen.dispose(); (5)
gameScreen.dispose();
}
public void startGame() { (6)
setScreen(gameScreen);
}
public void enterMenu() {
setScreen(menuScreen);
}
}
1 | Extend Game instead of ApplicationAdapter . |
2 | Create fields for both of the screens. |
3 | Initialise the screens in the constructor. |
4 | When starting the game, navigate to the menu. |
5 | Dispose both screens. |
6 | Create utility methods to navigate to the screens. |
The MenuScreen & Scene2D
Scene2D is one of the supporting libraries in LibGDX. While you can create complete games using Scene2D I’ve chosen to use plain LibGDX for the game itself. This way I can introduce the other libraries separately instead of combining Scene2D with other tools. Scene2D is very useful for creating UI elements on top of the game.
Scene2D operates on a Stage
which binds to the default viewport of the game (being a viewport which covers the entire window).
The Stage
is a container holding all objects currently in the scene.
The UI elements of Scene2D need a Skin
to render the components.
The skin can be found in the assets directory and contains images and json files describing how to render UI elements.
Scene2D offers an HTML-like approach to create and layout a UI. I’ve used a table the size of the screen and added two buttons, one to start the game, and one to exit it.
I’ve assigned the stage as InputProcessor to GDX, so the UI receives all input events.
In the render method I’ve used two OpenGL methods to clear the screen to a certain color before drawing the stage.
In the dispose
method I dispose of all disposables I’ve created.
package nl.jdriven.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import nl.jdriven.BadConnection;
import com.badlogic.gdx.Screen;
public class MenuScreen implements Screen {
private BadConnection badConnection;
private Stage stage; (1)
private Skin skin;
public MenuScreen(BadConnection badConnection) {
this.badConnection = badConnection;
}
@Override
public void show() {
stage = new Stage(); (2)
skin = new Skin(Gdx.files.internal("skin/glassy-ui.json"));
Table table = new Table(skin); (3)
table.setWidth(Gdx.graphics.getWidth());
table.setHeight(Gdx.graphics.getHeight());
Button start = new TextButton("Start", skin); (4)
start.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
badConnection.startGame();
}
});
Button exit = new TextButton("Exit", skin);
exit.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
Gdx.app.exit();
}
});
table.add(start).pad(10f); (5)
table.add(exit).pad(10f);
stage.addActor(table);
Gdx.input.setInputProcessor(stage); (6)
}
@Override
public void render(float delta) { (7)
Gdx.gl.glClearColor(.5f, .7f, .9f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
stage.draw();
}
@Override
public void hide() { (8)
stage.dispose();
skin.dispose();
}
// other methods from interface Screen
}
1 | Create fields for the Stage and the Skin . |
2 | Initialise the Stage and the Skin in the constructor. The Skin needs a path to the resource. |
3 | Create a table the size of the screen. |
4 | Create two buttons for starting and exiting the game. |
5 | Add the buttons to the table with some padding, add the table to the stage. |
6 | Assign the Stage as InputProcessor to LibGDX. |
7 | In the render method, clear the screen and draw the Stage . |
8 | Dispose of the resources. |
When starting the game the menu is shown, and the exit button works as expected.
Creating the Game
Creating the GameWorld
The GameWorld
is the root of the game itself.
It will contain all the objects and systems in the Game.
I’ve created an abstract GameObject
class to hold some shared functionality and utility for the objects.
I’ve added a List
of disposables, so the GameObject
can dispose them all.
I’ve also added two utilities to load a texture, and create an animation based on textures.
These textures will also be disposed by the abstract class.
The GameObject
has an abstract update- and render method making the implementation responsible for what happens during these steps.
package nl.jdriven.game;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Disposable;
import java.util.ArrayList;
import java.util.List;
public abstract class GameObject implements Disposable {
private List<Disposable> disposables; (1)
protected GameObject() {
disposables = new ArrayList<>();
}
public final void dispose() {
disposables.forEach(Disposable::dispose); (2)
}
public final Texture loadTexture(String path) { (3)
Texture texture = new Texture(path);
disposables.add(texture);
return texture;
}
public final Animation<Texture> createTextureAnimation( (4)
String filenameFormat,
int frames,
float frameDuration,
Animation.PlayMode playMode) {
Texture[] textures = new Texture[frames];
for (int i = 0; i < frames; i++) {
textures[i] = loadTexture(String.format(filenameFormat, i));
}
Animation<Texture> animation = new Animation<>(frameDuration, textures);
animation.setPlayMode(playMode);
return animation;
}
public abstract void update(float delta); (5)
public abstract void render(SpriteBatch spriteBatch);
}
1 | Create a list of `Disposable`s. |
2 | In the dispose method, dispose all loaded disposables. |
3 | Create a utility method to load a texture, also adding it to the list. |
4 | Create a utility method to create an animation based on several textures. |
5 | Create abstract methods to update and render the object. |
The first GameObject
I’m adding is the player.
I skipped ahead a little, so there might be some unused variables in the class, but they will be used later.
The player has 4 animations.
One of the animations will be the current animation.
In the update method I increase the elapsed time and in the render method I use a SpriteBatch
to render the current frame of the current Animation
to the screen.
In the next section I will explain the SpriteBatch and the update and render steps.
package nl.jdriven.game.objects;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import nl.jdriven.BadConnection;
import nl.jdriven.game.GameObject;
public class Player extends GameObject { (1)
private BadConnection badConnection;
private final Animation<Texture> walkingAnimation; (2)
private final Animation<Texture> jumpStartAnimation;
private final Animation<Texture> jumpingAnimation;
private final Animation<Texture> jumpEndAnimation;
private Animation<Texture> currentAnimation;
private float elapsed_time = 0f;
public Player(BadConnection badConnection) {
super();
this.badConnection = badConnection;
this.walkingAnimation = createTextureAnimation("dino/Run (%d).png", 8, 1f / 12f, Animation.PlayMode.LOOP); (3)
this.jumpStartAnimation = createTextureAnimation("dino/JumpStart (%d).png", 2, 1f / 12f, Animation.PlayMode.NORMAL);
this.jumpingAnimation = createTextureAnimation("dino/Jump (%d).png", 5, 1 / 12f, Animation.PlayMode.LOOP);
this.jumpEndAnimation = createTextureAnimation("dino/JumpEnd (%d).png", 3, 1f / 12f, Animation.PlayMode.NORMAL);
this.currentAnimation = walkingAnimation;
}
@Override
public void update(float delta) { (4)
elapsed_time += delta;
}
@Override
public void render(SpriteBatch spriteBatch) { (5)
spriteBatch.draw(currentAnimation.getKeyFrame(elapsed_time),0, 0, 680, 472);
}
}
1 | Player extends GameObject . |
2 | Create field for 4 animations, the current animation and the elapsed time. |
3 | Use the utility from GameObject to load the animations. |
4 | Increment the elapsed_time field with the time since the previous frame (delta). |
5 | Render the current animation. |
The GameWorld
manages the complete gamestate.
I’ve added just the player, but the number of GameObjects
will increase.
I’ve also added a SpriteBatch
.
A spritebatch can be used to draw images to the screen.
Only one SpriteBatch can be active at any given moment, because the SpriteBatch loads the textures it draws to the graphics card.
Repeatedly drawing the same texture to the screen is a very fast operation, while loading different texture to the graphics card is a relatively slow operation. |
In the update method I’ll update the entire world. Right now there’s only one object to update, so I’ve created an updateGameObjects method to update it. The update method returns a boolean indicating whether the screen should render after the update.
In the render method I need to clear the screen and draw the game objects using the SpriteBatch
.
package nl.jdriven.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Disposable;
import nl.jdriven.BadConnection;
import nl.jdriven.game.objects.*;
public class GameWorld implements Disposable { (1)
private final Player player;(2)
private final SpriteBatch spriteBatch;
private BadConnection badConnection;
public GameWorld(BadConnection badConnection) {
this.badConnection = badConnection;
this.spriteBatch = new SpriteBatch();
this.player = new Player(badConnection);
}
public boolean update(float delta) { (3)
updateGameObjects(delta);
return true;
}
private void updateGameObjects(float delta) { (4)
player.update(delta);
}
public void render() { (5)
Gdx.gl.glClearColor(.5f, .7f, .9f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
renderGameObjects();
}
private void renderGameObjects() { (6)
spriteBatch.begin();
player.render(spriteBatch);
spriteBatch.end();
}
@Override
public void dispose() { (7)
player.dispose();
}
}
1 | GameWorld implements Disposable . |
2 | Create a SpriteBatch and a Player and initialise them in the constructor. |
3 | In the update method add a call to update the game objects. |
4 | Add the method to update the game objects.
For now it only updates the player.
It return true, so the GameWorld will always be rendered.
I’ll add exit conditions later. |
5 | In the render method clear the screen and add a call to render the gameobject. |
6 | Add the method to render the game objects. For now it only renders the player. |
7 | Dispose of the assets. |
When the gamescreen is shown the world is created. The render method of the screen updates the world, and when the world returns true the world is rendered. Should the world determine (in the update method) the game is over, the world should not be rendered anymore.
When the game leaves the gamescreen the gameworld must be disposed.
package nl.jdriven.screens;
import com.badlogic.gdx.Screen;
import nl.jdriven.BadConnection;
import nl.jdriven.game.GameWorld;
public class GameScreen implements Screen {
private BadConnection game;
private GameWorld gameWorld; (1)
public GameScreen(BadConnection game) {
this.game = game;
}
@Override
public void show() { (2)
gameWorld = new GameWorld(game);
}
@Override
public void render(float delta) { (3)
if(gameWorld.update(delta)) {
gameWorld.render();
}
}
@Override
public void hide() { (4)
gameWorld.dispose();
}
// other methods from interface Screen
}
1 | Add a field for the GameWorld . |
2 | Every time the screen is shown it will create a new GameWorld . |
3 | In the render method, update the GameWorld .
If the update returns true, also render the GameWorld . |
4 | When the screen is hidden it will dispose of the GameWorld . |
When you run the game you should see the dino running on the screen.
Adding a camera
Right now the game is rendering the texture directly on the pane which is our screen.
To navigate the world I will add a Camera
.
When given to the SpriteBatch
, the spritebatch can render textures as if it were looking through the camera.
It does this by transforming positions using a set of matrices in the camera.
Generally speaking, there are two types of cameras.
An OrthographicCamera
and a PerspectiveCamera
. I’ll be using an OrthographicCamera
, which does not account for distance.
When using a PerspectiveCamera
(mostly used in 3D games) objects further from the camera will be rendered smaller.
The camera needs a size for the viewport. The purpose of the setting will become obvious later in the blog, but take it from me it’s useful to start thinking of positions and sizes as if they were in meters.
Since I set the screen width to 8 meters, I can calculate the height. Lastly, position the camera "half a viewport" up, so y = 0 will still be the bottom of the screen.
The player will be controlling our camera, so I’ve added the camera as a constructor parameter for the player.
In the update method I need to update the scene after I’ve updated the game objects. In the updateScene I’ll update the camera, and assign its matrices to the spritebatch.
package nl.jdriven.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Disposable;
import nl.jdriven.BadConnection;
import nl.jdriven.game.objects.*;
public class GameWorld implements Disposable {
private final Player player;
private final SpriteBatch spriteBatch;
private final OrthographicCamera camera; (1)
private BadConnection badConnection;
public GameWorld(BadConnection badConnection) {
this.badConnection = badConnection;
this.spriteBatch = new SpriteBatch();
float w = (float) Gdx.graphics.getWidth(); (2)
float h = (float) Gdx.graphics.getHeight();
this.camera = new OrthographicCamera(8, 8 * (h / w));
this.camera.position.y = camera.viewportHeight / 2;
this.player = new Player(badConnection, camera); (3)
}
public boolean update(float delta) {
updateGameObjects(delta);
updateScene(); (4)
return true;
}
private void updateGameObjects(float delta) {
player.update(delta);
}
private void updateScene() { (5)
camera.update();
spriteBatch.setProjectionMatrix(camera.combined);
}
public void render() {
Gdx.gl.glClearColor(.5f, .7f, .9f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
renderGameObjects();
}
private void renderGameObjects() {
spriteBatch.begin();
player.render(spriteBatch);
spriteBatch.end();
}
@Override
public void dispose() {
player.dispose();
}
}
1 | Add an OrthographicCamera (Later in the blog the Camera is passed to another system which needs the class instead of the Camera interface). |
2 | Calculate the viewport, initialise the OrtographicCamera and position it. |
3 | Pass the camera to the Player . |
4 | Add a call to update the scene to the update method. |
5 | Implement the updateScene method to update the camera, and pass its matrices to the SpriteBatch . |
Next I need to alter the player to render its width and height in meters. I’ve hardcoded the width and height of the texture for simplicity.
package nl.jdriven.game.objects;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import nl.jdriven.BadConnection;
import nl.jdriven.game.GameObject;
public class Player extends GameObject {
private final float height; (1)
private final float width;
private BadConnection badConnection;
private Camera camera;
private final Animation<Texture> walkingAnimation;
private final Animation<Texture> jumpStartAnimation;
private final Animation<Texture> jumpingAnimation;
private final Animation<Texture> jumpEndAnimation;
private Animation<Texture> currentAnimation;
private float elapsed_time = 0f;
public Player(BadConnection badConnection, OrthographicCamera camera) { (2)
super();
this.badConnection = badConnection;
this.camera = camera;
this.walkingAnimation = createTextureAnimation("dino/Run (%d).png", 8, 1f / 12f, Animation.PlayMode.LOOP);
this.jumpStartAnimation = createTextureAnimation("dino/JumpStart (%d).png", 2, 1f / 12f, Animation.PlayMode.NORMAL);
this.jumpingAnimation = createTextureAnimation("dino/Jump (%d).png", 5, 1 / 12f, Animation.PlayMode.LOOP);
this.jumpEndAnimation = createTextureAnimation("dino/JumpEnd (%d).png", 3, 1f / 12f, Animation.PlayMode.NORMAL);
this.currentAnimation = walkingAnimation;
this.height = 1.0f; (3)
this.width = height * 680f / 472f;
}
@Override
public void update(float delta) {
elapsed_time += delta;
}
@Override
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(currentAnimation.getKeyFrame(elapsed_time), 0,0, width, height); (4)
}
}
1 | Add fields for the width and the height. |
2 | Change the constructor of the Player to receive the OrthographicCamera . |
3 | The dino’s height is 1 meter, calculate its width based on the texture size. |
4 | Render the current frame with the new width and height. |
Because the camera looks at point (0, viewportheight / 2) and the player is rendered at point (0,0), the player appears at the center bottom of the screen.
Adding physics
To move the player I can use several approaches. I can just move texture in the world by setting its position based on input, but LibGDX comes with Box2D which adds a physics engine to the game.
The physics engine creates a World
parallel to the world you see on the screen.
The physics world deals with bodies with certain attributes and polygons defining their shape.
The physics world is instantiated with the gravity vector. I’ve used earth’s gravity. In the update method I’ve added a call to update the physics world. Don’t forget the physics world needs to be disposed.
I’ve added the physics world as a constructor parameter to the player.
Because every GameObject
in the scene will create a body for itself and add it to the world, the Player
will pass the World
to its super constructor.
For debug purposes I’ve also added a Box2DDebugRenderer. This object can render the polygons describing the physics bodies.
The physics world calculates everything in "units". The maximum number of units anything can move in the physics world is 2. When you define distances in meters, this means in a 60 FPS game the maximum speed is 120 meters per second. If you would define distances in pixels, moving more than 120 pixels per second would become impossible.
I’m not exactly sure why the limit is in place.
The best explanation I could find is that because the framework is based on meters, using pixels would result in really weird objects for a physics engine. If you would use pixels, a brick of 100x30 pixels would seem like an entire house to the physics engine because it would "see" something with the characteristics of a brick, but being 100 meters wide and 30 meters tall. I can imagine this might throw the calculation off.
package nl.jdriven.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Disposable;
import nl.jdriven.BadConnection;
import nl.jdriven.game.objects.*;
public class GameWorld implements Disposable {
private final Player player;
private final SpriteBatch spriteBatch;
private final OrthographicCamera camera;
private final World physicsWorld; (1)
private final Box2DDebugRenderer box2DDebugRenderer;
private BadConnection badConnection;
public GameWorld(BadConnection badConnection) {
this.badConnection = badConnection;
this.spriteBatch = new SpriteBatch();
this.physicsWorld = new World(new Vector2(0, -9.81f), false); (2)
this.box2DDebugRenderer = new Box2DDebugRenderer();
float w = (float) Gdx.graphics.getWidth();
float h = (float) Gdx.graphics.getHeight();
this.camera = new OrthographicCamera(8, 8 * (h / w));
this.camera.position.y = camera.viewportHeight / 2;
this.player = new Player(badConnection, camera, physicsWorld);
}
public boolean update(float delta) {
physicsWorld.step(delta, 1, 1); (3)
updateGameObjects(delta);
updateScene();
return true;
}
private void updateGameObjects(float delta) {
player.update(delta);
}
private void updateScene() {
camera.update();
spriteBatch.setProjectionMatrix(camera.combined);
}
public void render() {
Gdx.gl.glClearColor(.5f, .7f, .9f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
renderGameObjects();
box2DDebugRenderer.render(physicsWorld, camera.combined); (4)
}
private void renderGameObjects() {
spriteBatch.begin();
player.render(spriteBatch);
spriteBatch.end();
}
@Override
public void dispose() {
player.dispose();
physicsWorld.dispose(); (5)
box2DDebugRenderer.dispose();
}
}
1 | Add fields for the physics World and the Box2DDebugRenderer . |
2 | Initialise the World and Box2DDebugRenderer .
The World receives a vector containing earths gravity. |
3 | In the update method call the step method on the world. This "steps" the physics simulation forward with delta time |
4 | In the render method the Box2DDebugRenderer should render the physics world. |
5 | Dispose of the assets. |
Since every GameObject
has a body I’ve added the functionality to create it to the GameObject
class.
In its constructor I now need the physics world.
The take away from this class is that in Box2D a body consists of the body itself, which has certain properties and one or more fixtures (I’ll just be using one fixture). The fixtures define the shape of the body, and where it interacts with the world.
The physics world can create a body for us, but it doesn’t know the properties of the body. The body can create fixtures for us, but again, it doesn’t know the properties. Since the GameObject also doesn’t know the properties of the specific objects, I’ve used some abstract methods to get them from the specific implementations.
Note that I’ve also added a method to get the x position of the object. Since the end product will be a side scroller, I only care about the x.
package nl.jdriven.game;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.physics.box2d.*;
import com.badlogic.gdx.utils.Disposable;
import java.util.ArrayList;
import java.util.List;
public abstract class GameObject implements Disposable {
protected final World physicsWorld; (1)
protected final Body body;
private Fixture fixture;
private List<Disposable> disposables;
protected GameObject(World physicsWorld) { (2)
this.physicsWorld = physicsWorld;
this.body = createPhysicsBody();
disposables = new ArrayList<>();
}
public final float getXPos() { (3)
return body.getPosition().x;
}
public final void dispose() {
body.destroyFixture(fixture); (4)
physicsWorld.destroyBody(body);
disposables.forEach(Disposable::dispose);
}
public final Body createPhysicsBody() { (5)
Body body = physicsWorld.createBody(getBodyDef());
createFixture(body);
return body;
}
public final Fixture createFixture(Body body) { (6)
FixtureDef fixtureDef = getFixtureDef();
fixture = body.createFixture(fixtureDef);
fixture.setUserData(getFixtureType());
fixtureDef.shape.dispose();
return fixture;
}
public final Texture loadTexture(String path) {
Texture texture = new Texture(path);
disposables.add(texture);
return texture;
}
public final Animation<Texture> createTextureAnimation(String filenameFormat, int frames, float frameDuration, Animation.PlayMode playMode) {
Texture[] textures = new Texture[frames];
for (int i = 0; i < frames; i++) {
textures[i] = loadTexture(String.format(filenameFormat, i));
}
Animation<Texture> animation = new Animation<>(frameDuration, textures);
animation.setPlayMode(playMode);
return animation;
}
public abstract void update(float delta);
public abstract void render(SpriteBatch spriteBatch);
public abstract BodyDef getBodyDef(); (7)
public abstract FixtureDef getFixtureDef();
public abstract String getFixtureType();
}
1 | Add fields for the World , a Body and a Fixture . |
2 | The World is passed in the constructor, the body must be created when instantiating the GameObject . |
3 | Add a method to get the x position of the Body of the GameObject . |
4 | When the GameObject is disposed, the Body and Fixture should be removed from the World . |
5 | Create a utility method to create a Body in the World . |
6 | Create a utility method to create a Fixture for the Body . |
7 | Create three abstract methods to provide information about the Body and the Fixture . |
Before I return to the player I’d like to add some ground. Our physics world has gravity so without ground the player will just start falling, something I will prove at the end of the section. I’ll add 2 instances to the scene. Every time an instance is behind the player, I’ll move it forward two spots. This will effectively create an infinite ground to walk on. Notice that for the position of the object I refer to the position of the body.
Reusing objects in a pool is a common practise in game development, because it’s much cheaper compared to instantiating objects and makes the performance of the game more predictable. I will also give an example of creating new objects, but you’ll need to remember to correctly dispose them. |
In the body definition I’ve defined a static body. A static body will interact with other bodies, but is unaffected by forces. This way, the ground won’t fall under the effect of gravity.
The fixture for this body is a shape around the actual ground texture. I’ve created some slopes around the edges so the player can traverse to the next ground without bumps.
The fixture type will be used for collision detection. This fixture describes the shape of "Ground".
I’ve added information to the new concepts in the class. Loading textures and drawing them is covered in previous sections.
package nl.jdriven.game.objects;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import nl.jdriven.game.GameObject;
public class Ground extends GameObject {
private final Texture texture;
private final float width;
private final float height;
private int index;
private float playerXPos;
public Ground(World physicsWorld, int index) {
super(physicsWorld);
this.index = index;
texture = loadTexture("ground.png");
height = 2.1f;
width = height * texture.getWidth() / texture.getHeight();
body.setTransform(index * width, body.getPosition().y, 0); (1)
}
public void setPlayerXPos(float x) { (2)
playerXPos = x;
}
@Override
public void update(float delta) {
if ((index + 1) * width < playerXPos - 1f) { (3)
index += 2;
body.setTransform(index * width, body.getPosition().y, 0);
}
}
@Override
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(texture, body.getPosition().x, body.getPosition().y, width, height);
}
@Override
public BodyDef getBodyDef() { (4)
BodyDef def = new BodyDef();
def.type = BodyDef.BodyType.StaticBody;
def.fixedRotation = true;
return def;
}
@Override
public FixtureDef getFixtureDef() { (5)
PolygonShape shape = new PolygonShape();
shape.set(new Vector2[]{
new Vector2(-0.1f, 0),
new Vector2(-0.1f, 1.845f),
new Vector2(0.0f, 1.85f),
new Vector2(10.24f, 1.85f),
new Vector2(10.24f + 0.1f, 1.845f),
new Vector2(10.24f + 0.1f, 0),
});
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 1.0f;
return fd;
}
@Override
public String getFixtureType() { (6)
return "Ground";
}
}
1 | Position the body based on the index. |
2 | Add a method to receive the x position of the Player . |
3 | When the ground is behind the player, update its position. |
4 | Define the body to be static. |
5 | Define the fixture for the body, the polygon describes its shape. |
6 | Assign a type to fixture. This will be used for collision detection. |
The player also needs some modification to interact with the physics world. In the update method I’ve set a linear velocity to the player. This way the player will always be moving forward. I’ll also update the camera position to be 3 meters in front of the player.
In the render method I now render the texture at the location of the physics body.
The body type of the player is dynamic. This means forces in the physics world will interact with the player.
package nl.jdriven.game.objects;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.*;
import nl.jdriven.BadConnection;
import nl.jdriven.game.GameObject;
public class Player extends GameObject {
private final float height;
private final float width;
private BadConnection badConnection;
private Camera camera;
private final Animation<Texture> walkingAnimation;
private final Animation<Texture> jumpStartAnimation;
private final Animation<Texture> jumpingAnimation;
private final Animation<Texture> jumpEndAnimation;
private Animation<Texture> currentAnimation;
private float elapsed_time = 0f;
public Player(BadConnection badConnection, OrthographicCamera camera, World physicsWorld) {
super(physicsWorld);
this.badConnection = badConnection;
this.camera = camera;
this.walkingAnimation = createTextureAnimation("dino/Run (%d).png", 8, 1f / 12f, Animation.PlayMode.LOOP);
this.jumpStartAnimation = createTextureAnimation("dino/JumpStart (%d).png", 2, 1f / 12f, Animation.PlayMode.NORMAL);
this.jumpingAnimation = createTextureAnimation("dino/Jump (%d).png", 5, 1 / 12f, Animation.PlayMode.LOOP);
this.jumpEndAnimation = createTextureAnimation("dino/JumpEnd (%d).png", 3, 1f / 12f, Animation.PlayMode.NORMAL);
this.currentAnimation = walkingAnimation;
this.height = 1.0f;
this.width = height * 680f / 472f;
}
@Override
public void update(float delta) {
body.setLinearVelocity(2f, body.getLinearVelocity().y); (1)
elapsed_time += delta;
camera.position.x = body.getPosition().x + 3f; (2)
}
@Override
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(currentAnimation.getKeyFrame(elapsed_time), body.getPosition().x, body.getPosition().y, width, height); (3)
}
@Override
public BodyDef getBodyDef() { (4)
BodyDef def = new BodyDef();
def.type = BodyDef.BodyType.DynamicBody;
def.position.set(1f, 1.85f);
def.fixedRotation = true;
return def;
}
@Override
public FixtureDef getFixtureDef() {
PolygonShape shape = new PolygonShape();
shape.set(new Vector2[]{
new Vector2(.2f, .1f),
new Vector2(.1f, .3f),
new Vector2(.5f, .96f),
new Vector2(.8f, .96f),
new Vector2(.7f, .1f)
});
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 1.0f;
fd.friction = 0.0f;
return fd;
}
@Override
public String getFixtureType() {
return "Player";
}
}
1 | Apply a linear velocity to the`Body` of the Player . |
2 | Position the camera in front of the Player . |
3 | Draw the texture on the location of the Body . |
4 | Define the players Body , Fixture and fixture type. |
When running the game you should see the running dinosaur plummeting down. If you pay close attention you’ll notice the fixture shape is drawn around the player.
GameLogic
It’s now time to add the gamelogic to the game. I’ll add two more objects to the world. These objects don’t contain any new concepts, so here they are. It might be interesting to note the animation of the bird. The animation of the player consists of different textures in different image files, while the animation of the bird consists of different parts of the same texture. This technique (sometime refered to as "SpriteSheets") doesn’t do much for our game.
Remember the tip that loading images to the graphics card is a relatively costly operation, but drawing the same texture repeatedly is cheap? If our game would have hundreds of flying birds in different frames of the animation, it would be very costly to render those using the method I’ve used for the dino. Using the method I’ve used for the bird I could optimize by drawing all the birds in a single loop. The texture would be loaded in the graphics card, then for each bird it would stamp out a part of the texture to the screen without the need to load other textures to the graphics card. These are optimisations you as a game developer are responsible for and can have a lot of impact on the performance of the game.
package nl.jdriven.game.objects;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import nl.jdriven.game.GameObject;
public class Cactus extends GameObject {
private final Texture texture;
private final float width;
private final float height;
public Cactus(World physicsWorld, float x) {
super(physicsWorld);
texture = loadTexture("cactus.png");
body.setTransform(x, 1.75f, 0);
width = 1.0f;
height = 1.0f;
}
@Override
public void update(float delta) {
}
@Override
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(texture, body.getPosition().x, body.getPosition().y, width, height);
}
@Override
public BodyDef getBodyDef() {
BodyDef def = new BodyDef();
def.type = BodyDef.BodyType.StaticBody;
def.fixedRotation = true;
return def;
}
@Override
public FixtureDef getFixtureDef() {
PolygonShape shape = new PolygonShape();
shape.set(new Vector2[]{
new Vector2(.3f, 0f),
new Vector2(.3f, .8f),
new Vector2(.45f, .95f),
new Vector2(.5f, .95f),
new Vector2(.7f, .65f),
new Vector2(.6f, 0f)
});
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 1.0f;
return fd;
}
@Override
public String getFixtureType() {
return "Enemy";
}
}
package nl.jdriven.game.objects;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import nl.jdriven.game.GameObject;
public class Bird extends GameObject {
private Texture texture;
private Animation<TextureRegion> animation;
private float elapsed_time = 0f;
private final float width;
private final float height;
public Bird(World physicsWorld, float x) {
super(physicsWorld);
body.setTransform(x, 3.0f, 0f);
body.setLinearVelocity(-1f, 0f);
texture = loadTexture("bird.png");
TextureRegion[][] textures = TextureRegion.split(texture, texture.getWidth() / 3, texture.getHeight() / 3);
animation = new Animation<>(1f / 10f, textures[0]);
animation.setPlayMode(Animation.PlayMode.LOOP);
width = 0.5f;
height = 0.5f;
}
@Override
public void update(float delta) {
elapsed_time += delta;
}
@Override
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(animation.getKeyFrame(elapsed_time), body.getPosition().x, body.getPosition().y, width, height);
}
@Override
public BodyDef getBodyDef() {
BodyDef def = new BodyDef();
def.type = BodyDef.BodyType.DynamicBody;
def.fixedRotation = true;
def.gravityScale = 0f;
return def;
}
@Override
public FixtureDef getFixtureDef() {
PolygonShape shape = new PolygonShape();
shape.set(new Vector2[]{
new Vector2(0f, .3f),
new Vector2(.1f, .35f),
new Vector2(.5f, .35f),
new Vector2(.6f, .3f),
new Vector2(.5f, .1f),
new Vector2(.1f, .1f)
});
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 1.0f;
return fd;
}
@Override
public String getFixtureType() {
return "Enemy";
}
}
To actually play a game the player needs to be controlled. I’ve implemented the InputProcessor interface. In the keyUp method I check if the escape key was pressed. If it was I set a boolean for the game to be over to true.
Next I want the player to be able to jump, if he’s not yet jumping. On the keyDown of the spacebar I’ll initiate the jump. The update method has some checks to switch animations during the jump.
How do I detect the player has landed again? I’ve also implemented the contact listener. This means when two bodies collide in the world, the player will be notified of the collision. If the player collides with the ground, the jump is over. I’ve also added handling for collisions with enemies, this will set the gameOver boolean to "true", so the game will end.
package nl.jdriven.game.objects;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.*;
import nl.jdriven.BadConnection;
import nl.jdriven.game.GameObject;
public class Player extends GameObject implements InputProcessor, ContactListener { (1)
private final float height;
private final float width;
private BadConnection badConnection;
private Camera camera;
private final Animation<Texture> walkingAnimation;
private final Animation<Texture> jumpStartAnimation;
private final Animation<Texture> jumpingAnimation;
private final Animation<Texture> jumpEndAnimation;
private Animation<Texture> currentAnimation;
private float elapsed_time = 0f;
private boolean isJumping; (2)
private boolean gameOver;
public Player(BadConnection badConnection, OrthographicCamera camera, World physicsWorld) {
super(physicsWorld);
this.badConnection = badConnection;
this.camera = camera;
this.walkingAnimation = createTextureAnimation("dino/Run (%d).png", 8, 1f / 12f, Animation.PlayMode.LOOP);
this.jumpStartAnimation = createTextureAnimation("dino/JumpStart (%d).png", 2, 1f / 12f, Animation.PlayMode.NORMAL);
this.jumpingAnimation = createTextureAnimation("dino/Jump (%d).png", 5, 1 / 12f, Animation.PlayMode.LOOP);
this.jumpEndAnimation = createTextureAnimation("dino/JumpEnd (%d).png", 3, 1f / 12f, Animation.PlayMode.NORMAL);
this.currentAnimation = walkingAnimation;
this.height = 1.0f;
this.width = height * 680f / 472f;
}
@Override
public void update(float delta) {
body.setLinearVelocity(2f, body.getLinearVelocity().y);
elapsed_time += delta;
camera.position.x = body.getPosition().x + 3f;
if (currentAnimation == jumpStartAnimation) { (3)
if (currentAnimation.getKeyFrameIndex(elapsed_time) == 1 && !isJumping) {
body.applyLinearImpulse(new Vector2(0, 5.5f * body.getMass()), body.getLocalCenter(), true);
isJumping = true;
} else if (currentAnimation.isAnimationFinished(elapsed_time)) {
currentAnimation = jumpingAnimation;
elapsed_time = 0;
}
} else if (currentAnimation == jumpEndAnimation && currentAnimation.isAnimationFinished(elapsed_time)) {
currentAnimation = walkingAnimation;
elapsed_time = 0;
}
}
@Override
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(currentAnimation.getKeyFrame(elapsed_time), body.getPosition().x, body.getPosition().y, width, height);
}
@Override
public BodyDef getBodyDef() {
BodyDef def = new BodyDef();
def.type = BodyDef.BodyType.DynamicBody;
def.position.set(1f, 1.85f);
def.fixedRotation = true;
return def;
}
@Override
public FixtureDef getFixtureDef() {
PolygonShape shape = new PolygonShape();
shape.set(new Vector2[]{
new Vector2(.2f, .1f),
new Vector2(.1f, .3f),
new Vector2(.5f, .96f),
new Vector2(.8f, .96f),
new Vector2(.7f, .1f)
});
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 1.0f;
fd.friction = 0.0f;
return fd;
}
@Override
public String getFixtureType() {
return "Player";
}
public boolean isGameOver() { (4)
return gameOver;
}
@Override
public boolean keyDown(int keycode) { (5)
if (keycode == Input.Keys.SPACE && !isJumping) {
currentAnimation = jumpStartAnimation;
elapsed_time = 0;
}
return false;
}
@Override
public boolean keyUp(int keycode) { (6)
if (keycode == Input.Keys.ESCAPE) {
gameOver = true;
}
return false;
}
@Override
public void beginContact(Contact contact) { (7)
if (isJumping &&
(("Player".equals(contact.getFixtureA().getUserData()) && "Ground".equals(contact.getFixtureB().getUserData())) ||
(("Ground".equals(contact.getFixtureA().getUserData()) && "Player".equals(contact.getFixtureB().getUserData()))))) {
currentAnimation = jumpEndAnimation;
elapsed_time = 0;
isJumping = false;
}
if (("Player".equals(contact.getFixtureA().getUserData()) && "Enemy".equals(contact.getFixtureB().getUserData())) ||
("Enemy".equals(contact.getFixtureA().getUserData()) && "Player".equals(contact.getFixtureB().getUserData()))) {
gameOver = true;
}
}
// other methods from interface
}
1 | Implement the ContactListener and InputProcessor interfaces. |
2 | Add two booleans for the state of the player. The player can be "game over" or "jumping". |
3 | Add logic to change the animation for the different phases of a jump. Apply a linear impulse to propel the player upwards. |
4 | Add a method to return whether the player is game over. |
5 | In the keyDown method, add logic to start jumping when the spacebar is pressed. |
6 | In the keyUp method, add logic to end the game when the escape key is released. |
7 | In the beginContact method, add logic to stop jumping when the player is jumping an collides with the ground. Also add logic to end the game when the player collides with an enemy. |
The next part of the gamelogic will be added to the world.
I’ve added a list of game objects containing enemies. I’ve also added some logic to destroy game objects that are behind the player, and spawn new ones in front of the player regularly.
Note that I’ve assigned the player as ContactListener
to the physics world, and as InputProcessor
to GDX.
package nl.jdriven.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Disposable;
import nl.jdriven.BadConnection;
import nl.jdriven.game.objects.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
public class GameWorld implements Disposable {
private final Player player;
private final Ground ground0; (1)
private final Ground ground1;
private final List<GameObject> gameObjectList;
private final Random random;
private float spawnCountDown = 3.0f;
private final World physicsWorld;
private final OrthographicCamera camera;
private final SpriteBatch spriteBatch;
private BadConnection badConnection;
public GameWorld(BadConnection badConnection) {
this.badConnection = badConnection;
this.random = new Random();
this.spriteBatch = new SpriteBatch();
this.physicsWorld = new World(new Vector2(0, -9.81f), false);
float w = (float) Gdx.graphics.getWidth();
float h = (float) Gdx.graphics.getHeight();
this.camera = new OrthographicCamera(8, 8 * (h / w));
this.camera.position.y = camera.viewportHeight / 2;
this.player = new Player(badConnection, camera, physicsWorld);
this.ground0 = new Ground(physicsWorld, 0); (2)
this.ground1 = new Ground(physicsWorld, 1);
this.gameObjectList = new ArrayList<>();
this.physicsWorld.setContactListener(player);
Gdx.input.setInputProcessor(player);
}
public boolean update(float delta) {
if (player.isGameOver()) { (3)
if (!physicsWorld.isLocked())
badConnection.enterMenu();
return false;
} else {
physicsWorld.step(delta, 1, 1);
destroyOffScreenEnemies(); (4)
spawnNewEnemies(delta);
updateGameObjects(delta);
updateScene();
return true;
}
}
private void destroyOffScreenEnemies() { (5)
List<GameObject> enemiesToDestroy = gameObjectList.stream()
.filter(gameObject -> gameObject.getXPos() < player.getXPos() - 2f)
.collect(Collectors.toList());
enemiesToDestroy.forEach(gameObject -> {
gameObjectList.remove(gameObject);
gameObject.dispose();
});
}
private void spawnNewEnemies(float delta) { (6)
spawnCountDown -= delta;
if (spawnCountDown <= 0.0f) {
spawnCountDown = 2.0f + random.nextFloat() * 2.0f;
int i = random.nextInt(5);
if (i == 4) {
gameObjectList.add(new Bird(physicsWorld, player.getXPos() + 10f));
} else {
gameObjectList.add(new Cactus(physicsWorld, player.getXPos() + 10f));
}
}
}
private void updateGameObjects(float delta) {
player.update(delta);
ground0.setPlayerXPos(player.getXPos()); (7)
ground1.setPlayerXPos(player.getXPos());
ground0.update(delta);
ground1.update(delta);
gameObjectList.forEach(gameObject -> gameObject.update(delta));
}
private void updateScene() {
camera.update();
spriteBatch.setProjectionMatrix(camera.combined);
}
public void render() {
Gdx.gl.glClearColor(.5f, .7f, .9f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
renderGameObjects();
}
private void renderGameObjects() {
spriteBatch.begin();
ground0.render(spriteBatch); (8)
ground1.render(spriteBatch);
gameObjectList.forEach(gameObject -> gameObject.render(spriteBatch));
player.render(spriteBatch);
spriteBatch.end();
}
@Override
public void dispose() {
gameObjectList.forEach(Disposable::dispose); (9)
ground0.dispose();
ground1.dispose();
player.dispose();
physicsWorld.dispose();
}
}
1 | Add fields for two instances of Ground , a list of game objects, a Random and a countdown. |
2 | Initialise the fields, and assign the Player as contact listener to the physics world and as inputprocessor to Gdx. |
3 | In the update method, if the Player is game over and the physics world is not locked, end the game.
Otherwise update the GameWorld . |
4 | In the update method, call methods to destroy off screen game objects, and create new ones. |
5 | Create the method to destroy objects which are more then 2 meters behind the player. |
6 | Create the method to create new objects in front of the player when the countdown reaches zero. |
7 | In the updateGameObjects method, set the x position of the player to the grounds, then update the grounds and the game objects. |
8 | In the renderGameObjects method, render the grounds and the game objects. |
9 | Dispose of the assets. |
When running the game you should see something resembling what I tried to achieve.
From Dusk till Dawn
Box2D also comes with a lighting module. It is very easy to add dynamic lighting to the game world.
Let’s change the scene to a night scene by adding a moon and lights.
The moon doesn’t extend from GameObject which might be a bit of a smell. For this simple example I decided to just make a simple class for it.
The moon consists of a Texture and two lights, a small one for the direct glow of the moon, and a large one which will cast shadows.
They take a RayHandler
as argument.
The rayhandler is the bridge between the light sources and the physics world.
The second parameter is the number of rays which will be cast from each light source.
The higher the number, the better the light and its shadows will look.
package nl.jdriven.game.objects;
import box2dLight.PointLight;
import box2dLight.RayHandler;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Disposable;
public class Moon implements Disposable { (1)
private final PointLight pointLight;
private final PointLight pointLight2;
private final Texture texture;
public Moon(RayHandler rayHandler) { (2)
texture = new Texture("moon.png");
pointLight = new PointLight(rayHandler, 100, new Color(.4f, .4f, .3f, 1.0f), 1.0f, 0, 4.5f); (3)
pointLight2 = new PointLight(rayHandler, 500, new Color(.1f, .1f, .05f, 0.5f), 12.0f, 0, 4.5f);
}
public void update(float x) {
pointLight.setPosition(x + 4.5f, pointLight.getY()); (4)
pointLight2.setPosition(x + 4.5f, pointLight.getY());
pointLight.update();
pointLight2.update();
}
public void render(SpriteBatch spriteBatch) {
spriteBatch.draw(texture, pointLight.getX() - .35f, pointLight.getY() - .35f, .7f, 0.7f);
}
@Override
public void dispose() {
texture.dispose(); (5)
}
}
1 | The Moon implements Disposable . |
2 | The constructor takes a RayHandler . |
3 | Create two lights passing the RayHandler , the other parameters define the quality, color, size and x,y of the light. |
4 | In the update method set the lights position and update the lights. |
5 | Dispose of the assets. |
In the gameworld I’ve added the RayHandler
.
I’ve also removed the Box2DDebugRenderer
.
After initialising the RayHandler I’ve set its ambient light.
I’ve added the moon object to the scene and added both instances to the correct update method.
Notice that when rendering, the moon is rendered after the rayhandler so it’s not affected by lights and shadows.
package nl.jdriven.game;
import box2dLight.RayHandler;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Disposable;
import nl.jdriven.BadConnection;
import nl.jdriven.game.objects.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
public class GameWorld implements Disposable {
private final Player player;
private final Ground ground0;
private final Ground ground1;
private final Moon moon;
private final List<GameObject> gameObjectList;
private final Random random;
private float spawnCountDown = 3.0f;
private final World physicsWorld;
private final OrthographicCamera camera;
private final SpriteBatch spriteBatch;
private final RayHandler rayHandler; (1)
private BadConnection badConnection;
public GameWorld(BadConnection badConnection) {
this.badConnection = badConnection;
this.random = new Random();
this.spriteBatch = new SpriteBatch();
this.physicsWorld = new World(new Vector2(0, -9.81f), false);
this.rayHandler = new RayHandler(physicsWorld); (2)
this.rayHandler.setAmbientLight(new Color(.01f, .01f, 0.3f, 0.1f));
float w = (float) Gdx.graphics.getWidth();
float h = (float) Gdx.graphics.getHeight();
this.camera = new OrthographicCamera(8, 8 * (h / w));
this.camera.position.y = camera.viewportHeight / 2;
this.player = new Player(badConnection, camera, physicsWorld);
this.ground0 = new Ground(physicsWorld, 0);
this.ground1 = new Ground(physicsWorld, 1);
this.gameObjectList = new ArrayList<>();
this.moon = new Moon(rayHandler); (3)
this.physicsWorld.setContactListener(player);
Gdx.input.setInputProcessor(player);
}
public boolean update(float delta) {
if (player.isGameOver()) {
if (!physicsWorld.isLocked())
badConnection.enterMenu();
return false;
} else {
physicsWorld.step(delta, 1, 1);
destroyOffScreenEnemies();
spawnNewEnemies(delta);
updateGameObjects(delta);
updateScene();
return true;
}
}
private void destroyOffScreenEnemies() {
List<GameObject> enemiesToDestroy = gameObjectList.stream()
.filter(gameObject -> gameObject.getXPos() < player.getXPos() - 2f)
.collect(Collectors.toList());
enemiesToDestroy.forEach(gameObject -> {
gameObjectList.remove(gameObject);
gameObject.dispose();
});
}
private void spawnNewEnemies(float delta) {
spawnCountDown -= delta;
if (spawnCountDown <= 0.0f) {
spawnCountDown = 2.0f + random.nextFloat() * 2.0f;
int i = random.nextInt(5);
if (i == 4) {
gameObjectList.add(new Bird(physicsWorld, player.getXPos() + 10f));
} else {
gameObjectList.add(new Cactus(physicsWorld, player.getXPos() + 10f));
}
}
}
private void updateGameObjects(float delta) {
player.update(delta);
ground0.setPlayerXPos(player.getXPos());
ground1.setPlayerXPos(player.getXPos());
ground0.update(delta);
ground1.update(delta);
moon.update(player.getXPos()); (4)
gameObjectList.forEach(gameObject -> gameObject.update(delta));
}
private void updateScene() {
camera.update();
spriteBatch.setProjectionMatrix(camera.combined);
rayHandler.setCombinedMatrix(camera); (5)
rayHandler.update();
}
public void render() {
Gdx.gl.glClearColor(.5f, .7f, .9f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
renderGameObjects();
rayHandler.render(); (6)
renderMoon();
}
private void renderGameObjects() {
spriteBatch.begin();
ground0.render(spriteBatch);
ground1.render(spriteBatch);
gameObjectList.forEach(gameObject -> gameObject.render(spriteBatch));
player.render(spriteBatch);
spriteBatch.end();
}
private void renderMoon() { (7)
spriteBatch.begin();
moon.render(spriteBatch);
spriteBatch.end();
}
@Override
public void dispose() {
gameObjectList.forEach(Disposable::dispose);
ground0.dispose();
ground1.dispose();
player.dispose();
moon.dispose(); (8)
rayHandler.dispose();
physicsWorld.dispose();
}
}
1 | Add a field for the RayHandler . |
2 | Initialise the RayHandler with the physics world and set the ambient color. |
3 | Add the moon. |
4 | Update the moon. |
5 | Update the RayHandler with the Camera . |
6 | Render the RayHandler after the game objects, but before the moon.
Notice that when rendering the RayHandler, there can’t be an active SpriteBatch . |
7 | In the renderMoon method begin, and end the SpriteBatch |
8 | Dispose of the assets. |
Conclusion
Creating 2D (and 3D) games using Java is a perfectly valid option. Though engines like the Unreal Engine can probably squeeze more performance out of your system resources, the Java frameworks binding to OpenGL provide more than enough performance for decent looking games. Creating a simple game like this one, would probably be faster and easier using Unity3D but I prefer a well-structured Java project. To be fair, I also had a lot more fun programming the game in Java, then configuring it in Unity.
The APIs of the libraries are a bit quirky sometimes, but they are relatively simple to use.
If you’re interested in creating games I can recommend LibGDX. The setup is relatively easy, and running the repository on different systems works like a charm, even though Windows, OS X and Linux require different native bindings.
The complete game can be fetched from git, feel free to fork it and expand the game.