LibGDX Zombie Bird Tutorial (Flappy Bird Clone/Remake)
Day 4 - GameWorld and GameRenderer and the Orthographic Camera
Welcome to Day 4! Long title, huh? In this section, we will create two helper classes for our GameScreen, so that we can start implementing our gameplay. Then we will add an orthographic camera and add some shapes to our game!
Quick Reminder
We have five Java projects that we have generated using libGDX's setup tool; however, we only have to worry about three of them while we are implementing our game:
1. Whenever I say open a class or create a new package, I will be asking you to modify the ZombieBird project.
2. If I ask you to run your code, you will open your ZombieBird-desktop project and run Main.java.
3. When we add assets (images and sounds), we will add to the ZombieBird-android project. All other projects will receive a copy of the assets that are in this project.
1. Whenever I say open a class or create a new package, I will be asking you to modify the ZombieBird project.
2. If I ask you to run your code, you will open your ZombieBird-desktop project and run Main.java.
3. When we add assets (images and sounds), we will add to the ZombieBird-android project. All other projects will receive a copy of the assets that are in this project.
Examining the GameScreen
Fire up Eclipse and open the GameScreen class. In Day 3, we discussed how and when each of its methods are called. I want you to make a small change to your code:
Have a look at your render() method. It has one parameter called delta, a float. To see what this is for, I want you to add the following to your method body: Gdx.app.log("GameScreen FPS", (1 / delta) + " ");
Have a look at your render() method. It has one parameter called delta, a float. To see what this is for, I want you to add the following to your method body: Gdx.app.log("GameScreen FPS", (1 / delta) + " ");
@Override public void render(float delta) { // Sets a Color to Fill the Screen with (RGB = 10, 15, 230), Opacity of 1 (100%) Gdx.gl.glClearColor(10/255.0f, 15/255.0f, 230/255.0f, 1f); // Fills the screen with the selected color Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); // Covert Frame rate to String, print it Gdx.app.log("GameScreen FPS", (1/delta) + ""); }
Try running the game (DesktopLauncher.java inside your desktop project). You will see the following:
The float delta is the number of seconds (usually a small fraction) that has passed since the last time that the render method was called. When I asked you to print 1/delta, I was asking you to print out how many times that the render method would be called in one second if that rate was sustained. This is equivalent to our FPS.
Okay, so at this point it should be clear that our render method can be treated as our game loop. In our game loop, we will do two things:
Firstly, we will update all our game objects. Secondly, we will render those game objects.
To apply good object-oriented programming principles and design patterns, we should follow these guidelines:
1. GameScreen should only do one thing well, so...
2. Updating the game objects should be the responsibility of a helper class.
3. Rendering these game objects should be the responsibility of another helper class.
Alright! We need two helper classes. We will give these helper classes some descriptive names: GameWorld and GameRenderer.
Create a new package called com.kilobolt.gameworld and create these two classes. They can be empty for now:
Okay, so at this point it should be clear that our render method can be treated as our game loop. In our game loop, we will do two things:
Firstly, we will update all our game objects. Secondly, we will render those game objects.
To apply good object-oriented programming principles and design patterns, we should follow these guidelines:
1. GameScreen should only do one thing well, so...
2. Updating the game objects should be the responsibility of a helper class.
3. Rendering these game objects should be the responsibility of another helper class.
Alright! We need two helper classes. We will give these helper classes some descriptive names: GameWorld and GameRenderer.
Create a new package called com.kilobolt.gameworld and create these two classes. They can be empty for now:
GameWorld.java
package com.kilobolt.gameworld; public class GameWorld { } |
GameRenderer.java
package com.kilobolt.gameworld; public class GameRenderer { } |
In our GameScreen, we are going to delegate the updating and rendering tasks to the GameWorld and GameRenderer classes, respectively. To do this, we must do the following:
1. When GameScreen is created, we must create a new GameWorld object and a new GameRenderer object.
2. Inside the render method of the GameScreen class, we must ask our GameWorld object and the GameRenderer object to update and render.
I will ask you to make these changes yourself. If you get stuck, scroll down.
1. When GameScreen is created, we must create a new GameWorld object and a new GameRenderer object.
2. Inside the render method of the GameScreen class, we must ask our GameWorld object and the GameRenderer object to update and render.
I will ask you to make these changes yourself. If you get stuck, scroll down.
1. Creating the GameWorld and GameRenderer
Open up GameScreen. We are going to create GameWorld and GameRenderer objects in our constructor. We will call their methods in our render() method. To do this:
- We need two instance variables (variables accessible anywhere inside the class). Declare the following in the class header:
private GameWorld world;
private GameRenderer renderer;
- The GameScreen is created in the constructor. Add these lines to our constructor to initialize the above variables:
world = new GameWorld(); // initialize world
renderer = new GameRenderer(); // initialize renderer
Add the appropriate imports:
import com.kilobolt.gameworld.GameRenderer;
import com.kilobolt.gameworld.GameWorld;
- We need two instance variables (variables accessible anywhere inside the class). Declare the following in the class header:
private GameWorld world;
private GameRenderer renderer;
- The GameScreen is created in the constructor. Add these lines to our constructor to initialize the above variables:
world = new GameWorld(); // initialize world
renderer = new GameRenderer(); // initialize renderer
Add the appropriate imports:
import com.kilobolt.gameworld.GameRenderer;
import com.kilobolt.gameworld.GameWorld;
2. Asking GameWorld to update and GameRenderer to render
The whole point of having the GameWorld and GameRenderer classes is so that our GameScreen doesn't have to do the updating and rendering itself. It can simply ask two other classes to do those tasks.
- Replace all the code in the render method with the following:
// We are passing in delta to our update method so that we can perform frame-rate independent movement.
world.update(delta); // GameWorld updates
renderer.render(); // GameRenderer renders
Your completed GameScreen should look like this:
- Replace all the code in the render method with the following:
// We are passing in delta to our update method so that we can perform frame-rate independent movement.
world.update(delta); // GameWorld updates
renderer.render(); // GameRenderer renders
Your completed GameScreen should look like this:
package com.kilobolt.screens; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL20; import com.kilobolt.gameworld.GameRenderer; import com.kilobolt.gameworld.GameWorld; public class GameScreen implements Screen { private GameWorld world; private GameRenderer renderer; public GameScreen() { Gdx.app.log("GameScreen", "Attached"); world = new GameWorld(); renderer = new GameRenderer(); } @Override public void render(float delta) { world.update(delta); renderer.render(); } @Override public void resize(int width, int height) { } @Override public void show() { Gdx.app.log("GameScreen", "show called"); } @Override public void hide() { Gdx.app.log("GameScreen", "hide called"); } @Override public void pause() { Gdx.app.log("GameScreen", "pause called"); } @Override public void resume() { Gdx.app.log("GameScreen", "resume called"); } @Override public void dispose() { // Leave blank } }
Eclipse will give you errors because we haven't declared the update method in the GameWorld and the render method in GameRenderer. Let's do that:
GameWorldpackage com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; public class GameWorld { public void update(float delta) { Gdx.app.log("GameWorld", "update"); } } |
GameRendererpackage com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; public class GameRenderer { public void render() { Gdx.app.log("GameRenderer", "render"); } } |
Try running your code (Main.java in Desktop project).
Warning: your game may flicker (we are not drawing anything).
You should see the following:
Warning: your game may flicker (we are not drawing anything).
You should see the following:
Awesome. To summarize what we have done so far, we have delegated two complex tasks (updating the game and rendering the game), so that our GameScreen does not have to worry about these two things itself. Have a look at this diagram again (can you see where we are?):
We need to make one change. Our GameRenderer needs to have a reference to the GameWorld that it will be drawing. To do this, we have to ask, "Who has a reference to both the GameRenderer and the GameWorld?" Looking at the diagram, it's pretty obvious. Let's open up our GameScreen and make the following changes to the constructor:
// This is the constructor, not the class declaration public GameScreen() { Gdx.app.log("GameScreen", "Attached"); world = new GameWorld(); renderer = new GameRenderer(world); }
This will give us an error, as the GameRenderer class does not have a constructor that takes in one input of type GameWorld. So we will create this.
Open up the GameRenderer class.
We need to store world as a variable inside our GameRenderer, so that whenever we want to refer to an object inside our GameWorld, we can retrieve it.
- Create an instance variable:
private GameWorld myWorld;
- Inside the constructor, initialize this variable with the GameWorld object received from GameScreen.
We need to store world as a variable inside our GameRenderer, so that whenever we want to refer to an object inside our GameWorld, we can retrieve it.
- Create an instance variable:
private GameWorld myWorld;
- Inside the constructor, initialize this variable with the GameWorld object received from GameScreen.
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; public class GameRenderer { private GameWorld myWorld; public GameRenderer(GameWorld world) { myWorld = world; } public void render() { Gdx.app.log("GameRenderer", "render"); } }
Step away from the code for a minute.
Spend some time making sure that you understand what we have just done. Look over your code and make sure you can see the 3-way relationship of the three classes we have dealt with. By this point, my hope is that you understand the roles of the GameScreen, GameWorld and GameRenderer, and how they work together.
Ready to move on?
We will do one more thing in this lesson to illustrate how we would go about creating GameObjects and implementing them in the game. Before we do that, we are going to talk about the Orthographic Camera.
Orthographic Camera
libGDX is a 3D game development framework; however, our game will be in 2D. So what does this mean for us?
Nothing really, because we can make use of something called the orthographic camera. A lot of "2D" games that you see are actually built in 3D. Many modern platformers (even those that employ pixel art) are rendered by a 3D engine, on which developers create scenes in 3D space, rather than in 2D. For example, have a look at this fan-made Mario game to the left, in which the world has been reconstructed using 3D models.
Looking at Mario 2.5D above, it is clear that the game is in 3D. The characters have "depth."
To make this game 2D, we might try to rotate the camera so that we are looking at the game directly from the front. Believe it or not, the game will still appear 3D! Have a look for yourself:
Why does this happen? Because in a 3D environment (look around you), objects that are farther away appear smaller to the viewer. Even though we are looking at Mario from a perpendicular angle, some objects in this 3D world, such as the bricks, will appear smaller than those that are closer to us (the camera).
This where an orthographic camera comes in. When we use an orthographic projection, all objects in your scene, regardless of their distance, are projected onto a single plane. Imagine taking a big canvas and pushing it through the scene of the game in the second image above. When each object hits the canvas, it will be a flat, fixed-sized image. This is what an orthographic camera provides for us, and this is how we can create a 2D game in 3D space. This is how the game might look if it used an orthographic camera:
I hope I'm not scaring you with all this talk of 3D space and camera projections. It is much simpler in code than I am making it seem. So let's implement this into our game.
I'm going to ask you to make one change in the DesktopLauncher.java inside the Desktop Project (that we use to run our game). We are going to change the screen resolution to the following: DesktopLauncher.javapackage com.kilobolt.zombiebird.desktop; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.kilobolt.zombiebird.ZBGame; public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); config.title = "Zombie Bird"; config.width = 272; config.height = 408; new LwjglApplication(new ZBGame(), config); } } Creating our Camera |
Open up GameRenderer. We are going to create a new Orthographic Camera object.
- Declare the instance variable:
private OrthographicCamera cam;
- Import the following:
import com.badlogic.gdx.graphics.OrthographicCamera;
- Initialize the instance variable inside the constructor:
cam = new OrthographicCamera();
cam.setToOrtho(true, 136, 204);
The three arguments are asking: 1. whether you want an Orthographic projection (we do), 2. what the width should be and 3. what the height should be. This is the size of our game world. We will make changes to this at a later time. This is simply for illustration. Remember that we set our screen resolution in DesktopLauncher.java to 272 x 408. This means that everything in our game will scale by a factor of 2x when drawn.
- Declare the instance variable:
private OrthographicCamera cam;
- Import the following:
import com.badlogic.gdx.graphics.OrthographicCamera;
- Initialize the instance variable inside the constructor:
cam = new OrthographicCamera();
cam.setToOrtho(true, 136, 204);
The three arguments are asking: 1. whether you want an Orthographic projection (we do), 2. what the width should be and 3. what the height should be. This is the size of our game world. We will make changes to this at a later time. This is simply for illustration. Remember that we set our screen resolution in DesktopLauncher.java to 272 x 408. This means that everything in our game will scale by a factor of 2x when drawn.
Creating a ShapeRenderer
To test our camera, we are going to create a ShapeRenderer object, which will draw shapes and lines for us. This is provided by libGDX!
Inside the GameRenderer,
- Declare the instance variable:
private ShapeRenderer shapeRenderer;
- Import the following:
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
- Initialize the shapeRenderer, and attach it to our camera inside the constructor:
shapeRenderer = new ShapeRenderer();
shapeRenderer.setProjectionMatrix(cam.combined);
This is what you should have so far:
Inside the GameRenderer,
- Declare the instance variable:
private ShapeRenderer shapeRenderer;
- Import the following:
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
- Initialize the shapeRenderer, and attach it to our camera inside the constructor:
shapeRenderer = new ShapeRenderer();
shapeRenderer.setProjectionMatrix(cam.combined);
This is what you should have so far:
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; public class GameRenderer { private GameWorld myWorld; private OrthographicCamera cam; private ShapeRenderer shapeRenderer; public GameRenderer(GameWorld world) { myWorld = world; cam = new OrthographicCamera(); cam.setToOrtho(true, 136, 204); shapeRenderer = new ShapeRenderer(); shapeRenderer.setProjectionMatrix(cam.combined); } public void render() { Gdx.app.log("GameRenderer", "render"); } }
Now that our ShapeRenderer is ready, we need to create something for it to render! We could create a rectangle object inside our GameRenderer, but that violates our design pattern. We need to create all Game Objects inside our GameWorld and retrieve it in GameRenderer to draw it.
Open up GameWorld and make these changes:
Open up GameWorld and make these changes:
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.math.Rectangle; public class GameWorld { private Rectangle rect = new Rectangle(0, 0, 17, 12); public void update(float delta) { Gdx.app.log("GameWorld", "update"); rect.x++; if (rect.x > 137) { rect.x = 0; } } public Rectangle getRect() { return rect; } }
We have created a new Rectangle called rect and provided the import: import.com.badlogic.gdx.math.Rectangle. Notice that we do not use the Java Rectangle here as that is not available in some platforms.(the implementation for gdx.math.Rectangle uses the correct Rectangle based on the platform).
To make this private Rectangle accessible to anyone who has a reference to a GameWorld object, we create a getter method (great reasons on why we use these here).
We then added some code that will allow our rectangle to scroll to the right (and reset at the left)!
Now that our Rectangle is ready, let's go back to the GameRenderer.
Open up GameRenderer and make these changes to the RENDER method:
To make this private Rectangle accessible to anyone who has a reference to a GameWorld object, we create a getter method (great reasons on why we use these here).
We then added some code that will allow our rectangle to scroll to the right (and reset at the left)!
Now that our Rectangle is ready, let's go back to the GameRenderer.
Open up GameRenderer and make these changes to the RENDER method:
I have separated the method into three main sections. Please read the comments to see what is happening.
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; public class GameRenderer { private GameWorld myWorld; private OrthographicCamera cam; private ShapeRenderer shapeRenderer; public GameRenderer(GameWorld world) { myWorld = world; cam = new OrthographicCamera(); cam.setToOrtho(true, 136, 204); shapeRenderer = new ShapeRenderer(); shapeRenderer.setProjectionMatrix(cam.combined); } public void render() { Gdx.app.log("GameRenderer", "render"); /* * 1. We draw a black background. This prevents flickering. */ Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); /* * 2. We draw the Filled rectangle */ // Tells shapeRenderer to begin drawing filled shapes shapeRenderer.begin(ShapeType.Filled); // Chooses RGB Color of 87, 109, 120 at full opacity shapeRenderer.setColor(87 / 255.0f, 109 / 255.0f, 120 / 255.0f, 1); // Draws the rectangle from myWorld (Using ShapeType.Filled) shapeRenderer.rect(myWorld.getRect().x, myWorld.getRect().y, myWorld.getRect().width, myWorld.getRect().height); // Tells the shapeRenderer to finish rendering // We MUST do this every time. shapeRenderer.end(); /* * 3. We draw the rectangle's outline */ // Tells shapeRenderer to draw an outline of the following shapes shapeRenderer.begin(ShapeType.Line); // Chooses RGB Color of 255, 109, 120 at full opacity shapeRenderer.setColor(255 / 255.0f, 109 / 255.0f, 120 / 255.0f, 1); // Draws the rectangle from myWorld (Using ShapeType.Line) shapeRenderer.rect(myWorld.getRect().x, myWorld.getRect().y, myWorld.getRect().width, myWorld.getRect().height); shapeRenderer.end(); } }
No errors? Great! You should see something like this!
Spend some time experimenting with this code. If you can draw shapes, you can draw images, which we will do very soon.
I know progress has been slow so far, but things should start picking up now that we have our basic GameScreen structure ready!
Join me in Day 5, and please like us on Facebook using the button below!
I know progress has been slow so far, but things should start picking up now that we have our basic GameScreen structure ready!
Join me in Day 5, and please like us on Facebook using the button below!
Source Code for the day.
If you didn't feel like writing that code yourself, download the completed code here:
Download, extract and import into eclipse:
Download, extract and import into eclipse:

zombiebird_day_4.zip | |
File Size: | 10358 kb |
File Type: | zip |
Like us on Facebook to be informed as soon as the next lesson is available.
|
|
comments powered by Disqus