-
Notifications
You must be signed in to change notification settings - Fork 0
Building a game from the bottom up
This writeup assumes that you have already completed the Setup Git section and have a working development environment.
You'll be creating a doodlejump clone called Megajumper, and the starter code is provided for you. Clone the repository https://github.com/MissionBit/megajumper into your projects folder. Import the project into Android Studio, and run it on your phone. You should see the Mission Bit logo on the screen that doesn't interact with anything. Pretty boring.
Just as before, the majority of the code for this game is located in the core
directory. Open core/src/com.missionbit.megajumper/MegaJumper.java in Android Studio. Right now, there isn't much code in here, but all the basic functions have been created for you. The lifecycle of the game looks like this:
- when you first open the app, the
create()
function is called, which loads the image you need, determines the dimensions of the screen, and callsresetGame()
. - right now
resetGame()
is empty, but it will soon hold all the code you need to reset the state of the game (randomizing the platform locations, setting the score to 0, etc) - when the game is running, the
render()
function will be called repeatedly. The render function clears the screen, and then callsupdateGame()
anddrawGame()
. -
updateGame()
is also left empty (which is why this game is so boring right now), but it will hold all the code that keeps track of game rules (gravity, accumulating points, checking for wins/losses, etc). -
drawGame()
is where all the graphics code for your game will go. All of your graphics calls should lie between thebatch.begin();
andbatch.end();
lines - this ensures that all the drawing happens at once, which is important to make your game performant. Note that the coordinates thatbatch.draw
uses to position images may not be what you're used to in other graphics programming. The coordinate (0, 0) is located at the bottom left of your screen and values increase going up and right.
Now let's take this game and add some interactivity to it - when you tap the screen, the image (lets call it a player from now on) should jump up and then fall back down. In order to do this, we will need to 1) process touchscreen events, 2) keep track of the position and velocity of the player, and 3) use some physics to make the movement realistic. Since the position and velocity of the player are part of the state of the game, we need to perform these calculations in the updateGame()
function. Thankfully libGDX makes these tasks pretty painless!
First, for the sake of clarity, rename all occurrences of the img
variable to playerImage
so you know what it is you're dealing with.
Let's first start by keeping track of the position and velocity of the player. We can use the Vector2
object provided by libGDX to keep track of coordinate pairs (x and y position, and x and y velocity respectively). Add the following properties, or instance variables to your code above the create()
function definition:
private Vector2 playerPosition;
private Vector2 playerVelocity;
These 2 lines state that playerPosition and playerVelocity are both variables which can hold Vector2
objects (recall that Java is picky when it comes to types, so you have to specify exactly what type each variable is when you declare it). The private
keyword will come into play later. You'll also notice that the word Vector2
is in red in Android Studio. This is because you have to specify which Java packages you want to use in your code, and Android Studio couldn't find the Vector2
class in any of those packages. With your cursor over the red class name, hit option
+ enter
to automatically import that class.
You declared those 2 variables, but you still need to assign values to them. Add the following code above the call to resetGame()
in the create()
function, which will create the Vector2
objects.
playerPosition = new Vector2();
playerVelocity = new Vector2();
When the Vector2
objects are created, they assign the x and y values to 0 by default. This isn't what you want for the initial position and velocity of the player so you will have to change that. Since the initial position and velocity of the player must be set every time the game is reset, the following code should go inside the resetGame()
function:
playerPosition.set(width/2, 0);
playerVelocity.set(0, 0);
When you run the game, you should see the player start off at the bottom center of the screen with no velocity.
Now add a Vector2
object called gravity
in the same way that you added playerPosition
and playerVelocity
, and set it to (0, -20) in resetGame()
.
Its important that you have the following line inside your updateGame()
function at the top:
float deltaTime = Gdx.graphics.getDeltaTime();
This gives you the amount of time (in seconds) that has elapsed since the last call to render()
. This is useful because libGDX (and your phone) make no guarantees that the render()
function will be called in uniform time intervals. In order to make your game operate smoothly, you want to use the deltaTime
to determine how big of a change to make since the last frame.
Finally, in updateGame()
, you will have to take care of all the business logic that makes this all work.
The steps you need to take are as follows:
- If there is a tap on the screen, change the y component of
playerVelocity
(if not, then do nothing to the velocity) - Change the player's velocity based on the force of gravity
- Change the player's position based on the player's current velocity and the time elapsed since the last frame (this is why we have
deltaTime
)
You can use the following code to handle the touchscreen events:
if (Gdx.input.justTouched()) {
playerVelocity.y = 500; //you'll have to experiment with this number
}
and another chunk of code (underneath the touchscreen code) to handle gravity. If you remember your physics, you can give this a shot on your own but if you don't here's something to get you started:
playerVelocity.add(gravity); //the force of gravity will make the player decelerate
playerPosition.mulAdd(playerVelocity, deltaTime); //use the player's current velocity and the elapsed time to determine the player's new position
We're doing all the calculations for the player's position, but we still need to make sure that the game
draws the player at the correct location. We need to make some changes to drawGame()
so that we draw the player at the tracked position (playerPosition
) instead of just (0, 0). Replace those values with playerPosition.x
and playerPosition.y
.
If you did everything correctly, you should have a game that shows the player at the bottom of the screen for a split second before it falls due to the gravity that you just added. Repeated taps on the touch screen should bring the player back into view using the same kind of motions as in Flappy Bird. In fact, the code you have right now could also be used to build that too!
The next step is to get a single platform on the screen that you can jump on.
First, create a placeholder image for the platform using an image editor, or find one online. Something that is close to 200x20px is ideal for this. When you have the image, move it to the android/assets
folder in your project.
Now we need to load the image into the game. The first step is to define a new variable that will hold the image. Add a declaration for a platformImage
Texture to the top of the class (this goes in the same place as the private Texture playerImage
line):
private Texture platformImage;
and also load the image in the create()
method: (Hint: look at how we do this for playerImage
in the create()
method)
Since the platform doesn't move (yet), you don't need to add anything to updateGame()
. You will have to add some code to draw the platform in drawGame()
. (Hint: look at how we draw the 'playerImage' in 'drawGame()')
After these steps, your game should draw the platform. But since there's no code for jumping on platforms, the player will simply fall through the image.
In order to make the platform "jumpable", we need to know when the player is touching the platform. LibGDX provides a Rectangle
object which can compute intersections for you.
Declare 2 Rectangle
objects at the top of the class - one called playerBounds
and one called platformBounds
, and assign each to a new Rectangle
object in create()
:
private Rectangle playerBounds;
private Rectangle platformBounds;
and also initialize the bounds in create()
:
playerBounds = new Rectangle();
platformBounds = new Rectangle();
You will have to import the Rectangle
class from Android Studio, but make sure you import the correct one! It should say 'com.badlogic.gdx.math' next to it.
We will use these rectangle areas to represent the boundaries around the player and the platform, and then we can tell when those areas intersect. Add the following code to resetGame()
, which will set the location and dimensions of each Rectangle
to match the location and dimensions of the images they accompany:
playerBounds.set(width/2, 0, playerImage.getWidth(), playerImage.getHeight());
platformBounds.set(width/2, 0, platformImage.getWidth(), platformImage.getHeight());
Since the player will be moving position frequently, add the following code to the bottom of updateGame()
so that playerBounds
is always up to date:
playerBounds.setX(playerPosition.x);
playerBounds.setY(playerPosition.y);
Now you can use the built in functionality of the Rectangle
class to determine when the player and the platform are intersecting. playerBounds.overlaps(platformBounds)
will return a boolean value telling you whether the 2 areas overlap at all. Change the code in updateGame()
so that this overlapping condition will also make the player jump. (Hint: look at the if statement for when the screen is touched. We want a similar if statement, except the condition for this one should be playerBounds.overlaps(platformBounds)
)
If you did it correctly, you should have a solid platform that will cause the player to jump.
The next step is to add multiple platforms, but you might have noticed that doing so would require adding another platformImage
and platformBounds
variable for each additional platform you add.
Instead, you can turn Platform
into its own object with an image
and bounds
as properties. Then you can move the asset loading into the constructor of Platform
, and rewrite the code in create()
, resetGame()
, updateGame()
, and drawGame()
to use the object instead.
Let's create a new Java file in the same directory as your Megajumper
file and call it Platform
. Inside the file, we are going to write the Platform
class like so:
package com.missionbit.megajumper;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
public class Platform {
private Texture platformImage;
//FIXME move the platformBounds variable here
public Platform() {
platformImage = new Texture("my_platform_image.png")
//FIXME move the initialization of platformBounds from create() to here
}
}
Once you have moved the platform variables into the Platform
class, we can also update Megajumper
's methods.
Your class can have a single variable for the platform, and you can delete the other platform related variables:
private Platform platform;
and in create()
we can delete the platform variables and replace them with a single line:
platform = new Platform();
and finally in drawGame()
we can update the platform code:
batch.draw(platform.platformImage, platform.platformBounds.x, platform.platformBounds.y);
Repeat this process with the Player
object. You can start with this code:
package com.missionbit.megajumper;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
public class Player {
// move playerImage here
// move playerPosition here
// move playerVelocity here
// move playerBounds here
public Player() {
//set the values for the variables here
}
public void reset() {
//move the player related code from resetGame() here
}
}
After you have moved the proper code into the Player
class, we can update Megajumper
:
- Delete all the player variables from the class, and add
private Player player;
- In
create()
, add aplayer = new Player();
- In
resetGame()
, add aplayer.reset();
- Update
updateGame()
anddrawGame()
to work with the newPlayer
object. (Hint: look at how we do this forPlatform
indrawGame()
. You can access thePlayer
object's variables usingplayer.playerPosition
, etc..
Now these objects are a little more organized than before and more importantly, its a lot easier to add more platforms with objects!
Now that all the properties of a platform are inside the Platform
object, its really easy to create a bunch of Platform
objects. In Java, we an array
to keep track of multiple objects in order.
The syntax for the declaration is
private Platform[] platforms;
and the initialization in create()
is
platforms = new Platform[7];
where you can decide how many platforms to have. If you are familiar with Lists, you could also use an ArrayList to keep track of the platforms, which allows you to dynamically change the number of platforms.
Now you have an array of platforms, but you need to create the objects! In Java, creating an array of objects doesn't create each object for you. In fact, after creating the platforms
array, each element will be null
, which is a special Java value similar to None
in Python and nil
in Ruby. You can get or set the value of an array by its index
, which is the position in the array. Note that the index values start at 0, not 1! For example foo[2]
will get the 3rd item in the array called foo
and foo[0] = 1
will set the 1st value of the array to 1
.
Write a for loop in resetGame()
which goes through each position in the platforms
array and randomly generates a platform:
for (int i = 0; i < platforms.length; i++) {
platforms[i] = new Platform();
platforms[i].platformBounds.setX(REPLACEME: x value for platform);
platforms[i].platformBounds.setX(REPLACEME: y value for platform);
}
You will also find (float)(Math.random() * X)
useful (a random value from 0 to X) for generating random coordinates for your platform.s
Finally, you will have to change the code in updateGame()
to check for overlaps between each platform and the player. It will look something like this:
for (int i = 0; i < platforms.length; i++) {
// do something if platforms[i].platformBounds.overlaps(player.playerBounds)
}
And also update drawGame()
to draw all the platforms. (Hint: you can use a for loop for this, similar to updateGame()
)
Once that is complete, you should have a bunch of platforms show up in your game!
LibGDX also makes it very easy for you to use the accelerometer in your game. Gdx.input.getAccelerometerX()
will give you an accelerometer value that you can use to set the x velocity of the player. Multiply the accelerometer value by something around -200, and set that as the new x velocity of the player in updateGame()
.
An optional but probably nice addition would be to make the player wrap around if they jump too far in either direction. You can do that using the modulus operator (% in Java) on the player's x position. When this is working, you have a playable game! Its a little boring because the screen doesn't move up yet, but the game functionality is definitely there. At this point you can remove the tap to jump functionality because your platforms will be working. You might have to add a condition to allow the player to jump on nothing until you get onto the first platform, otherwise the player will immediately begin falling once the game starts.
In order to make the game interesting, you want to be able to place platforms out of view of the phone screen and scroll up to them once the player reaches them. LibGDX has camera functionality that makes this possible.
Add the definition for an OrthographicCamera
object called camera
at the top of the class, and then assign it in create like so:
camera = new OrthographicCamera(width, height);
In resetGame()
, you also want to reset the position of the camera. The camera is centered on the screen, so the initial position of the camera should look like this:
camera.position.set(width/2, height/2, 0);
In drawGame()
, you need to add 2 lines which tell libGDX that you want to draw from the perspective of the camera's location. Place the following lines at the beginning of the function:
camera.update();
batch.setProjectionMatrix(camera.combined);
Finally, you will need to add a means for the camera position to change as the player moves up platforms. Try and come up with a way of doing this that only happens after the player jumps on a new platform, and only allows the camera to move up. You can change the position of the camera by setting the value of camera.position.y
in updateGame()
.
If you make sure you have enough platforms that expend past y = height
, you should start to see them as the camera scrolls up. If you've gotten to here, you've completed the core functionality for the game!
Now in order to display your score, you will need a font. You can download font.fnt
and font.png
to your android/assets
folder from the original libGDX demo here https://github.com/libgdx/libgdx-demo-superjumper/tree/master/android/assets/data. After doing that, you will need to load the fonts just like you loaded the images in create()
. Declare a BitmapFont
object named font
, an int
named score
, and load the fonts line so in the create()
function:
font = new BitmapFont(Gdx.files.internal("font.fnt"), Gdx.files.internal("font.png"), false);
You'll also want to reset score
to 0 in resetGame()
.
You should get a point for every platform that you have passed, which will require incrementing score
somehow in updateGame()
. Then in drawGame()
, you can use something like
font.draw(batch, "" + score, width / 2, camera.position.y + height / 2 - font.getLineHeight());
to place the score at the top of the screen.
Your game starts as soon as you open it, which probably isn't what you want. In order to get around this, you want to add several states for the game. This game can either be showing the menu, playing the game, showing the winning screen, or showing the losing screen. These are the 4 states of the game, and you can simply use an integer to keep track of which state you are in.
In order to make this work, you'll need to set the state in resetGame()
, handle touches in updateGame()
depending on the state (tapping on the menu should start the game, and tapping on the win/lose screen should restart the game), and also drawing screens in drawGame()
(the menu and win/lose screens can just be displaying text, but the gameplay screen has to draw the player, the platforms, the score, etc)
Once this is in place, you should have a playable, albeit rough game!
At this point, you're free to do whatever you want with the game. Here are some suggestions:
- add sound effects. Look at libGDX example code to see how to load and play sound clips.
- use original artwork - come up with a player, platforms, and a background image. if you're feeling really ambitious, you could design your own animations
- add items which affect your play
- add special platforms which move, crumble, make you jump higher, etc
- anything else you want!
You can load a Sound (used for sound effects) like so:
Sound sound = Gdx.audio.newSound(Gdx.files.internal("data/mysound.mp3"));
and you can play the sound effect using
sound.play(1.0f);
You can load Music (for background tracks that always play) like so:
Music music = Gdx.audio.newMusic(Gdx.files.internal("data/mymusic.mp3"));
and you can start the music (in resetGame()
) using
music.play();
See https://github.com/libgdx/libgdx/wiki/Sound-effects and https://github.com/libgdx/libgdx/wiki/Streaming-music for pausing, volume control and looping audio.
- Home
- Game Design
- Tutorials & Walkthroughs
- Development Resources
- Game Deliverables
- Hack SF
- Games
- Blogging