a very simple “game”, that touches each module provided by the framework, to get a feeling for things. We’ll introduce a few different concepts without going into unnecessary detail. In the following, we’ll look at:
- -
Project Setup
Generating a Project guide. In the following, we will use these settings: dropcom.badlogic.dropDropNow fill in the destination. If you are interested in Android development, be sure to check that option an provide the Android SDK folder. For the purpose of this tutorial, we will uncheck the iOS sub project (as you would need OS X to run it) and all extensions (extensions are a more advanced topic).drop, and the sub projectsandroid(ordrop-androidunder Eclipse),core/drop-core,desktop/drop-desktop, andhtml/drop-html. Importing & Running a Project.Couldn't load file: badlogic.jpg. Your Run Configuration has to be properly configured first: Select as working directoryPATH_TO_YOUR_PROJECT/drop/android/assets. If we run it now, we will get the default ‘game’ generated by the startup app: a Badlogic Games image on a red background. Not too exciting, but that’s about to change.The Game
The game idea is very simple:- -
The Assets
We need a few images and sound effects to make the game look somewhat pretty. For the graphics we need to define a target resolution of 800x480 pixels (landscape mode on Android). If the device the game is run on does not have that resolution, we simply scale everything to fit on the screen. Note: for high profile games you might want to consider using different assets for different screen densities. This is a big topic on its own and won’t be covered here. The raindrop and the bucket should take up a small(ish) portion of the screen vertically, so we’ll let them have a size of 64x64 pixels. The following sources provide some sample assets: - junggle, see here
- acclivity, see here
- mvdv, see here
- mvdv, see here
assetsfolder. I named the 4 files: drop.wav, rain.mp3, droplet.png and bucket.png and put them inandroid/assets/. We only need to store the assets once, as both the desktop and HTML5 projects are configured to ‘see’ this folder through different means. After that, depending on your IDE you may have to refresh the project tree to make the new files known (in Eclipse, right click -> Refresh), otherwise you may get a ‘file not found’ runtime exception.Configuring the Starter Classes
desktop project. Open theDesktopLauncher.javaclass indesktop/src/…(ordrop-desktopunder Eclipse). We want a 800x480 window and set the title to “Drop”. The code should look like this:
If you are only interested in desktop development, you can skip the rest of this section. Android project, we want the application to be run in landscape mode. For this we need to modifypackage com.badlogic.drop.desktop;import com.badlogic.gdx.backends.lwjgl.LwjglApplication;import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;import com.badlogic.drop.Drop;public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); config.title = "Drop"; config.width = 800; config.height = 480; new LwjglApplication(new Drop(), config); }}
AndroidManifest.xmlin theandroid(ordrop-android) root directory, which looks like this:<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.badlogic.drop.android" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="20" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/GdxTheme" > <activity android:name="com.badlogic.drop.android.AndroidLauncher" android:label="@string/app_name" android:screenOrientation="landscape" android:configChanges="keyboard|keyboardHidden|orientation|screenSize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
android:screenOrientationis set to “landscape”. If we wanted to run the game in portrait mode we would have set that attribute to “portrait”.AndroidLauncher.javafile inandroid/src/…(ordrop-android), which should look something like this:package com.badlogic.drop.android;import android.os.Bundle;import com.badlogic.gdx.backends.android.AndroidApplication;import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;import com.badlogic.drop.Drop;public class AndroidLauncher extends AndroidApplication { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); config.useAccelerometer = false; config.useCompass = false; initialize(new Drop(), config); }}
Activity, as it is set by the Android operating system. As we defined earlier, we’ll simply scale the 800x480 target resolution to whatever the resolution of the device is. HTML5 project also uses a 800x480 drawing area. For this we modify theHtmlLauncher.javafile inhtml/src/…(ordrop-html):
All our starter classes are now correctly configured, let’s move on to implementing our fabulous game.package com.badlogic.drop.client;import com.badlogic.gdx.ApplicationListener;import com.badlogic.gdx.backends.gwt.GwtApplication;import com.badlogic.gdx.backends.gwt.GwtApplicationConfiguration;import com.badlogic.drop.Drop;public class HtmlLauncher extends GwtApplication { @Override public GwtApplicationConfiguration getConfig () { return new GwtApplicationConfiguration(800, 480); } @Override public ApplicationListener createApplicationListener () { return new Drop(); }}
The Code
Drop.javafile of the Core project, located incore/src/…(ordrop-corein Eclipse).Loading the Assets
ApplicationAdapter.create()method, so let’s do that:package com.badlogic.drop;import com.badlogic.gdx.ApplicationAdapter;import com.badlogic.gdx.Gdx;import com.badlogic.gdx.audio.Music;import com.badlogic.gdx.audio.Sound;import com.badlogic.gdx.graphics.Texture;public class Drop extends ApplicationAdapter { private Texture dropImage; private Texture bucketImage; private Sound dropSound; private Music rainMusic; @Override public void create() { // load the images for the droplet and the bucket, 64x64 pixels each dropImage = new Texture(Gdx.files.internal("droplet.png")); bucketImage = new Texture(Gdx.files.internal("bucket.png")); // load the drop sound effect and the rain background "music" dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav")); rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3")); // start the playback of the background music immediately rainMusic.setLooping(true); rainMusic.play(); // ... more to come ... } // rest of class omitted for clarity
Dropclass so we can later refer to it. The first two lines in thecreate()method load the images for the raindrop and the bucket. ATexturerepresents a loaded image that is stored in video ram. One can usually not draw to a Texture. ATextureis loaded by passing aFileHandleto an asset file to its constructor. SuchFileHandleinstances are obtained through one of the methods provided byGdx.files. There are different types of files, we use the “internal” file type here to refer to our assets. Internal files are located in theassetsdirectory of the Android project. As seen before, the desktop and HTML5 projects reference the same directory.Soundinstance if your sample is shorter than 10 seconds, and aMusicinstance for longer audio pieces. Note: libGDX supports MP3, OGG and WAV files. Which format you should use, depends on you specific needs, as each format has its own advantages and disadvantages. For example, WAV files are quite large compared to other formats, OGG files don’t work on RoboVM (iOS) nor with Safari (GWT), and MP3 files have issues with seemless looping.SoundorMusicinstance is done viaGdx.audio.newSound()andGdx.audio.newMusic(). Both of these methods take aFileHandle, just like theTextureconstructor.create()method we also tell theMusicinstance to loop and start playback immediately. If you run the application you’ll see a nice pink background and hear the rain fall.A Camera and a SpriteBatch
SpriteBatch. We’ll use the former to ensure we can render using our target resolution of 800x480 pixels no matter what the actual screen resolution is. TheSpriteBatchis a special class that is used to draw 2D images, like the textures we loaded. We add two new fields to the class, let’s call them camera and batch:private OrthographicCamera camera; private SpriteBatch batch;
create()method we first create the camera like this:
This will make sure the camera always shows us an area of our game world that is 800x480 units wide. Think of it as a virtual window into our world. We currently interpret the units as pixels to make our life a little easier. There’s nothing preventing us from using other units though, e.g. meters or whatever you have. Cameras are very powerful and allow you to do a lot of things we won’t cover in this basic tutorial. Check out the rest of the developer guide for more information.camera = new OrthographicCamera(); camera.setToOrtho(false, 800, 480);
SpriteBatch(we are still in thecreate()method):
We are almost done with creating all the things we need to run this simple game.batch = new SpriteBatch();
Adding the Bucket
The last bits that are missing are representations of our bucket and the raindrop. Let’s think about what we need to represent those in code: Textureinstances we loaded.Rectangleclass which we can use for this purpose. Let’s start by creating aRectanglethat represents our bucket. We add a new field:// add this import and NOT the one in the standard library import com.badlogic.gdx.math.Rectangle; private Rectangle bucket;
create()method we instantiate the Rectangle and specify its initial values. We want the bucket to be 20 pixels above the bottom edge of the screen, and centered horizontally.bucket = new Rectangle(); bucket.x = 800 / 2 - 64 / 2; bucket.y = 20; bucket.width = 64; bucket.height = 64;
bucket.yset to 20, shouldn’t it be 480 - 20? By default, all rendering in libGDX (and OpenGL) is performed with the y-axis pointing upwards. The x/y coordinates of the bucket define the bottom left corner of the bucket, the origin for drawing is located in the bottom left corner of the screen. The width and height of the rectangle are set to 64x64, our small-ish portion of our target resolutions height. Note: it is possible to change this setup so the y-axis points down and the origin is in the upper left corner of the screen. OpenGL and the camera class are so flexible that you use have pretty much any kind of viewing angle you want, in 2D and 3D. However, this is not recommended.Rendering the Bucket
render()method to look like this:@Override public void render() { ScreenUtils.clear(0, 0, 0.2f, 1); ... more to come here ... }
ScreenUtils.clear(r, g, b, a)are the red, green, blue and alpha component of that color, each within the range [0, 1]. Next we need to tell our camera to make sure it is updated. Cameras use a mathematical entity called a matrix that is responsible for setting up the coordinate system for rendering. These matrices need to be recomputed every time we change a property of the camera, like its position. We don’t do this in our simple example, but it is generally a good practice to update the camera once per frame:
Now we can render our bucket:camera.update();
batch.setProjectionMatrix(camera.combined); batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); batch.end();
SpriteBatchto use the coordinate system specified by the camera. As stated earlier, this is done with something called a matrix, to be more specific, a projection matrix. Thecamera.combinedfield is such a matrix. From there on theSpriteBatchwill render everything in the coordinate system described earlier.SpriteBatchto start a new batch. Why do we need this and what is a batch? OpenGL hates nothing more than telling it about individual images. It wants to be told about as many images to render as possible at once.SpriteBatchclass helps make OpenGL happy. It will record all drawing commands in betweenSpriteBatch.begin()andSpriteBatch.end(). Once we callSpriteBatch.end()it will submit all drawing requests we made at once, speeding up rendering quite a bit. This all might look cumbersome in the beginning, but it is what makes the difference between rendering 500 sprites at 60 frames per second and rendering 100 sprites at 20 frames per second.Making the Bucket Move (Touch/Mouse)
render()method will do this:if(Gdx.input.isTouched()) { Vector3 touchPos = new Vector3(); touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0); camera.unproject(touchPos); bucket.x = touchPos.x - 64 / 2; }
Gdx.input.isTouched(). Next we want to transform the touch/mouse coordinates to our camera’s coordinate system. This is necessary because the coordinate system in which touch/mouse coordinates are reported might be different than the coordinate system we use to represent objects in our world.Gdx.input.getX()andGdx.input.getY()return the current touch/mouse position (libGDX also supports multi-touch, but that’s a topic for a different article). To transform these coordinates to our camera’s coordinate system, we need to call thecamera.unproject()method, which requests aVector3, a three dimensional vector. We create such a vector, set the current touch/mouse coordinates and call the method. The vector will now contain the touch/mouse coordinates in the coordinate system our bucket lives in. Finally we change the position of the bucket to be centered around the touch/mouse coordinates. Note: it is very, very bad to instantiate a lot of new objects, such as the Vector3 instance. The reason for this is the garbage collector has to kick in frequently to collect these short-lived objects. While on the desktop this not such a big deal (due to the resources available), on Android the GC can cause pauses of up to a few hundred milliseconds, which results in stuttering. In this particular case, if you want to solve this issue, simply maketouchPosa private final field of theDropclass instead of instantiating it all the time. Note:touchPosis a three dimensional vector. You might wonder why that is if we only operate in 2D.OrthographicCamerais actually a 3D camera which takes into account z-coordinates as well. Think of CAD applications, they use 3D orthographic cameras as well. We simply abuse it to draw 2D graphics.Making the Bucket Move (Keyboard)
On the desktop and in the browser we can also receive keyboard input. Let’s make the bucket move when the left or right cursor key is pressed. We want the bucket to move without acceleration, at two hundred pixels/units per second, either to the left or the right. To implement such time-based movement we need to know the time that passed in between the last and the current rendering frame. Here’s how we can do all this:if(Gdx.input.isKeyPressed(Input.Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime(); if(Gdx.input.isKeyPressed(Input.Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();
Gdx.input.isKeyPressed()tells us whether a specific key is pressed. TheKeysenumeration contains all the keycodes that libGDX supports. The methodGdx.graphics.getDeltaTime()returns the time passed between the last and the current frame in seconds. All we need to do is modify the bucket’s x-coordinate by adding/subtracting 200 units times the delta time in seconds. We also need to make sure our bucket stays within the screen limits:if(bucket.x < 0) bucket.x = 0; if(bucket.x > 800 - 64) bucket.x = 800 - 64;
Adding the Raindrops
Rectangleinstances, each keeping track of the position and size of a raindrop. Let’s add that list as a field:private Array<Rectangle> raindrops;
Arrayclass is a libGDX utility class to be used instead of standard Java collections likeArrayList. The problem with the latter is that they produce garbage in various ways. TheArrayclass tries to minimize garbage as much as possible. libGDX offers other garbage collector aware collections such as hash-maps or sets as well. We also need to keep track of the last time we spawned a raindrop, so we add another field:
We’ll store the time in nanoseconds, that’s why we use a long.private long lastDropTime;
spawnRaindrop()which instantiates a newRectangle, sets it to a random position at the top edge of the screen and adds it to theraindropsarray.private void spawnRaindrop() { Rectangle raindrop = new Rectangle(); raindrop.x = MathUtils.random(0, 800-64); raindrop.y = 480; raindrop.width = 64; raindrop.height = 64; raindrops.add(raindrop); lastDropTime = TimeUtils.nanoTime(); }
MathUtilsclass is a libGDX class offering various math related static methods. In this case it will return a random value between zero and 800 - 64. TheTimeUtilsis another libGDX class that provides some very basic time related static methods. In this case we record the current time in nano seconds based on which we’ll later decide whether to spawn a new drop or not.create()method we now instantiate the raindrops array and spawn our first raindrop:create()method:raindrops = new Array<Rectangle>(); spawnRaindrop();
render()method that will check how much time has passed since we spawned a new raindrop, and creates a new one if necessary:
We also need to make our raindrops move, let’s take the easy route and have them move at a constant speed of 200 pixels/units per second. If the raindrop is beneath the bottom edge of the screen, we remove it from the array.if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();
for (Iterator<Rectangle> iter = raindrops.iterator(); iter.hasNext(); ) { Rectangle raindrop = iter.next(); raindrop.y -= 200 * Gdx.graphics.getDeltaTime(); if(raindrop.y + 64 < 0) iter.remove(); }
SpriteBatchrendering code which looks like this now:
One final adjustment: if a raindrop hits the bucket, we want to playback our drop sound and remove the raindrop from the array. We simply add the following lines to the raindrop update loop:batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); for(Rectangle raindrop: raindrops) { batch.draw(dropImage, raindrop.x, raindrop.y); } batch.end();
if(raindrop.overlaps(bucket)) { dropSound.play(); iter.remove(); }
Rectangle.overlaps()method checks if this rectangle overlaps with another rectangle. In our case, we tell the drop sound effect to play itself and remove the raindrop from the array.Cleaning Up
A user can close the application at any time. For this simple example there’s nothing that needs to be done. However, it is in general a good idea to help out the operating system a little and clean up the mess we created.Disposableinterface and thus has adispose()method needs to be cleaned up manually once it is no longer used. In our example that’s true for the textures, the sound and music and theSpriteBatch. Being good citizens, we override theApplicationAdapter.dispose()method as follows:
Once you dispose of a resource, you should not access it in any way. Disposables are usually native resources which are not handled by the Java garbage collector. This is the reason why we need to manually dispose of them. libGDX provides various ways to help with asset management. Read the rest of the development guide to discover them.@Override public void dispose() { dropImage.dispose(); bucketImage.dispose(); dropSound.dispose(); rainMusic.dispose(); batch.dispose(); }
Handling Pausing/Resuming
Android has the notation of pausing and resuming your application every time the user gets a phone call or presses the home button. libGDX will do many things automatically for you in that case, e.g. reload images that might have gotten lost (OpenGL context loss, a terrible topic on its own), pause and resume music streams and so on.ApplicationAdapter.pause()andApplicationAdapter.resume()methods.The Full Source
Here’s the tiny source for our simple game:package com.badlogic.drop;import java.util.Iterator;import com.badlogic.gdx.ApplicationAdapter;import com.badlogic.gdx.Gdx;import com.badlogic.gdx.Input.Keys;import com.badlogic.gdx.audio.Music;import com.badlogic.gdx.audio.Sound;import com.badlogic.gdx.graphics.OrthographicCamera;import com.badlogic.gdx.graphics.Texture;import com.badlogic.gdx.graphics.g2d.SpriteBatch;import com.badlogic.gdx.math.MathUtils;import com.badlogic.gdx.math.Rectangle;import com.badlogic.gdx.math.Vector3;import com.badlogic.gdx.utils.Array;import com.badlogic.gdx.utils.ScreenUtils;import com.badlogic.gdx.utils.TimeUtils;public class Drop extends ApplicationAdapter { private Texture dropImage; private Texture bucketImage; private Sound dropSound; private Music rainMusic; private SpriteBatch batch; private OrthographicCamera camera; private Rectangle bucket; private Array<Rectangle> raindrops; private long lastDropTime; @Override public void create() { // load the images for the droplet and the bucket, 64x64 pixels each dropImage = new Texture(Gdx.files.internal("droplet.png")); bucketImage = new Texture(Gdx.files.internal("bucket.png")); // load the drop sound effect and the rain background "music" dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav")); rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3")); // start the playback of the background music immediately rainMusic.setLooping(true); rainMusic.play(); // create the camera and the SpriteBatch camera = new OrthographicCamera(); camera.setToOrtho(false, 800, 480); batch = new SpriteBatch(); // create a Rectangle to logically represent the bucket bucket = new Rectangle(); bucket.x = 800 / 2 - 64 / 2; // center the bucket horizontally bucket.y = 20; // bottom left corner of the bucket is 20 pixels above the bottom screen edge bucket.width = 64; bucket.height = 64; // create the raindrops array and spawn the first raindrop raindrops = new Array<Rectangle>(); spawnRaindrop(); } private void spawnRaindrop() { Rectangle raindrop = new Rectangle(); raindrop.x = MathUtils.random(0, 800-64); raindrop.y = 480; raindrop.width = 64; raindrop.height = 64; raindrops.add(raindrop); lastDropTime = TimeUtils.nanoTime(); } @Override public void render() { // clear the screen with a dark blue color. The // arguments to clear are the red, green // blue and alpha component in the range [0,1] // of the color to be used to clear the screen. ScreenUtils.clear(0, 0, 0.2f, 1); // tell the camera to update its matrices. camera.update(); // tell the SpriteBatch to render in the // coordinate system specified by the camera. batch.setProjectionMatrix(camera.combined); // begin a new batch and draw the bucket and // all drops batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); for(Rectangle raindrop: raindrops) { batch.draw(dropImage, raindrop.x, raindrop.y); } batch.end(); // process user input if(Gdx.input.isTouched()) { Vector3 touchPos = new Vector3(); touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0); camera.unproject(touchPos); bucket.x = touchPos.x - 64 / 2; } if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime(); if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime(); // make sure the bucket stays within the screen bounds if(bucket.x < 0) bucket.x = 0; if(bucket.x > 800 - 64) bucket.x = 800 - 64; // check if we need to create a new raindrop if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop(); // move the raindrops, remove any that are beneath the bottom edge of // the screen or that hit the bucket. In the latter case we play back // a sound effect as well. for (Iterator<Rectangle> iter = raindrops.iterator(); iter.hasNext(); ) { Rectangle raindrop = iter.next(); raindrop.y -= 200 * Gdx.graphics.getDeltaTime(); if(raindrop.y + 64 < 0) iter.remove(); if(raindrop.overlaps(bucket)) { dropSound.play(); iter.remove(); } } } @Override public void dispose() { // dispose of all the native resources dropImage.dispose(); bucketImage.dispose(); dropSound.dispose(); rainMusic.dispose(); batch.dispose(); }}
Where to go from here
Screens andGames. To learn about these, there is a ** following on from this one.
