2012-02-15

#1 - libgdx Tutorial: Introduction

Libgdx version used on this post: 0.9.2 (download)
Developing games is a very difficult task, even if you want something simple. There are many technical concepts to grasp and then you must learn how to implement them correctly and efficiently on your game's target platform. This is the first post of a tutorial that I'll write about libgdx - an open source high-performance Java framework for developping games. A very cool feature of libgdx is that your game can run on your desktop computer (and that's not using the emulator), which eases the testing and debugging process. Then you can make it run on an Android device just by writing one line of code. You can even distribute your games as an applet or via Webstart if you want so. Some features of libgdx are implemented using natice code (like physics and audio processing functions), which optimizes its execution at runtime.

So libgdx seams to be a nice gaming framework, but what does it provide? I'll highlight its main features:
  • An API for OpenGL ES (texture, shaders, vertex arrays and so on)
  • An API for 2D graphics (bitmap fonts, sprites, UI widgets, animations and so on)
  • An API for 3D graphics (OBJ and MD5 loaders, camera manipulation and so on)
  • An API for I/O (audio, files, graphics, key/touch/click events and so on)
  • An API for physics (box2d) and math
  • Many utilities (JSON, bitmap font generator, texture packer and so on)

OpenGL ES is a royalty-free, cross-platform API for full-function 2D and 3D graphics on embedded systems - including consoles, phones, appliances and vehicles. It consists of well-defined subsets of desktop OpenGL, creating a flexible and powerful low-level interface between software and graphics acceleration. OpenGL ES includes profiles for floating-point and fixed-point systems and the EGL specification for portably binding to native windowing systems. OpenGL ES 1.X is for fixed function hardware and offers acceleration, image quality and performance. OpenGL ES 2.X enables full programmable 3D graphics.

The OpenGL ES 1.0 and 1.1 API specifications have been supported since Android 1.0. Beginning with Android 2.2 (API Level 8), the framework supports the OpenGL ES 2.0 API specification.

Installing libgdx

I'm considering you've already installed Java, Eclipse and the Android SDK, and everything is working perfectly. Now is the time to download libgdx. I'm using the latest version available for now: 0.9.2. Now, instead of going through all the installation steps involved, I want you to watch this video on YouTube. The guy on the video is Mario Zechner, writer of the Beginning Android Games book and the main programmer of libgdx. He'll guide you through the basic steps to setup some projects on Eclipse that use the libgdx framework.

Setting up the projects for our game

We'll implement a game together like Tyrian. I used to play this game a lot when I was a kid, so I'm really looling forward to what we'll achieve. Of course that our final result will be much simpler than the actual game, but that will do to learn how a game is created using libgdx. The game will improve as I write new posts for the tutorial. I'll put all the source code on Google Code, so you can easily check it out using a Subversion client, or just browse it with your browser.
Okay, so let's start off creating our projects. I followed Zechner's instructions on the video and came up with the following project structure on Eclipse:


The tyrian-android project depends on the tyrian-game project. This dependency is configured in the "Java Build Path > Projects" section of the tyrian-android project's settings. But it isn't enough. On the tab "Libraries" you should manually add the "gdx.jar" of tyrian-game. Should you skip this step, the game won't run on an Android device (although it will compile without errors). With this project structure created, you can run the tyrian-game on your desktop computer to develop it, and sometimes run the tyrian-android project on your Android smartphone/tablet to check if everything is going well.

Where to put resource files?

Eventually we'll need to add audio and image files to our game. These resources should be accessible by both our projects. Instead of duplicating the resources, there is a cool trick we can do in Eclipse. Note that our Android project has an "assets" folder. By the time we have some resources, we should put them in this folder. In order to share these resources with the game project, we create a "linked" source folder. Open the game project's settings and click the "Link Source..." button as follows:


The game's main class

At the simplest form, a game's main class is an instace of com.badlogic.gdx.ApplicationListener in libgdx. You should get used to read the Javadocs of libgdx's classes, because many details won't be covered here. Tip: the source code for the libgdx's JARs can be attached to our projects.

Your game's main class is responsible for handling specific application events, like "create", "render", "dispose" and so on. The events fired on the desktop don't have exactly the same behaviour when the game is executed on Android, but they're quite similar. The following list shows the application lifecycle events that can be fired, and when it happens:
  • create - Fired once when the application is created.
  • resize - Fired every time the game screen is re-sized and the game is not in the paused state. It is also fired once just after the create event.
  • render - Fired by the game loop from the application every time the rendering happens. The game update should take place before the actual rendering.
  • pause - Fired just before the application is destroyed. On Android it happens when the home button is pressed or an incoming call is happening. On desktop it's fired just before disposal when exiting the application. This a good time to save the game state on Android as it is not guaranteed to be resumed.
  • resume - Fired ONLY on Android, when the application receives the focus.
  • dispose - Fired when the application is being destroyed. It is preceded by the pause event.

In order to capture and handle these events, you implement the methods of the com.badlogic.gdx.ApplicationListener interface. You should also know that when libgdx creates our game, a separate thread called Main loop thread is created and an OpenGL context is attached to it. All event processing executes within this separate thread, and not within the UI thread.

Enough reading. Let's create our game's main class. Please have a look at the following code and read the inline comments.
package com.blogspot.steigert.tyrian;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.FPSLogger;
import com.badlogic.gdx.graphics.GL20;

/**
 * The game's main class, called as application events are fired.
 */
public class Tyrian
    implements
        ApplicationListener
{
    // constant useful for logging
    public static final String LOG = Tyrian.class.getSimpleName();

    // a libgdx helper class that logs the current FPS each second
    private FPSLogger fpsLogger;

    @Override
    public void create()
    {
        Gdx.app.log( Tyrian.LOG, "Creating game" );
        fpsLogger = new FPSLogger();
    }

    @Override
    public void resize(
        int width,
        int height )
    {
        Gdx.app.log( Tyrian.LOG, "Resizing game to: " + width + " x " + height );
    }

    @Override
    public void render()
    {
        // the following code clears the screen with the given RGB color (green)
        Gdx.gl.glClearColor( 0f, 1f, 0f, 1f );
        Gdx.gl.glClear( GL20.GL_COLOR_BUFFER_BIT );

        // output the current FPS
        fpsLogger.log();
    }

    @Override
    public void pause()
    {
        Gdx.app.log( Tyrian.LOG, "Pausing game" );
    }

    @Override
    public void resume()
    {
        Gdx.app.log( Tyrian.LOG, "Resuming game" );
    }

    @Override
    public void dispose()
    {
        Gdx.app.log( Tyrian.LOG, "Disposing game" );
    }
}
Notes on the above code:
  • As stated previously, our game's main class is just an instance of com.badlogic.gdx.ApplicationListener.
  • With the FPSLogger class it gets easy to output the current FPS each second, so we have a feel about our game's performance.
  • The Gdx class holds singleton instances of many modules: graphics, audio, files and so on.
  • By calling the Gdx.app.log() method you can log messages in a cross-platform manner.
  • The render() method just clears the screen with the color green.

Running our game

If we're able to run our game on different platforms, we need one specific launcher for each one. Let's start with the desktop launcher. Please have a look at the following code and read the inline comments.
package com.blogspot.steigert.tyrian;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;

/**
 * This class simply creates a desktop LWJGL application.
 */
public class TyrianDesktopLauncher
{
    public static void main(
        String[] args )
    {
        // create the listener that will receive the application events
        ApplicationListener listener = new Tyrian();

        // define the window's title
        String title = "Tyrian";

        // define the window's size
        int width = 800, height = 480;

        // whether to use OpenGL ES 2.0
        boolean useOpenGLES2 = false;

        // create the game
        new LwjglApplication( listener, title, width, height, useOpenGLES2 );
    }
}
That's all. If you run this class, you should be able to see the following result:


And for the Android devices, you can create an activity like this:
package com.blogsport.steigert.tyrian;

import android.os.Bundle;

import com.badlogic.gdx.backends.android.AndroidApplication;
import com.blogspot.steigert.tyrian.Tyrian;

/**
 * This class simply defines an Android activity for our game.
 */
public class TyrianAndroidLauncher
    extends
        AndroidApplication
{
    @Override
    public void onCreate(
        Bundle savedInstanceState )
    {
        super.onCreate( savedInstanceState );

        // whether to use OpenGL ES 2.0
        boolean useOpenGLES2 = false;

        // create the game
        initialize( new Tyrian(), useOpenGLES2 );
    }
}
And when you execute it on an Android device you'll get:



Conclusion

We created the project structure for our game and tested it both on the desktop and on the Android device. The next posts will focus on libgdx internals and how to use it to implement the features of our cool game. A project was created on Google Code so that you can easily browse the source code. I created a tag on the Subversion repository for this post's project state. Let me know if I can improve/correct something, and I see you soon!

26 comments:

  1. This is really good information I have visited this blog to read something fresh and I really admire you efforts in doing so.

    ReplyDelete
  2. Thank you for the new tutorial series. Very interesting stuff and easy to understand. I'm looking for new episodes!!!

    ReplyDelete
  3. Thanks a lot for these tutorials!

    ReplyDelete
  4. I appreciate you feedback! Thanks for reading! :)

    ReplyDelete
  5. I Just didn't get why you use the graphics.gl20 class while disabling Opengl 2.0 on the desktop and android code. Why is that? shouldn't you use graphics.gl10 then?

    ReplyDelete
    Replies
    1. You're correct and I'll modify the source code, but notice that:
      - I just use a constant from the GL20 class, that is: Gdx.gl.glClear( GL20.GL_COLOR_BUFFER_BIT );
      - This constant would also work on devices running older OpenGL ES versions; see: http://www.khronos.org/opengles/sdk/1.1/docs/man/glClear.xml
      - The "boolean useOpenGLES2 = false;" is just a hint, and would fallback to an older OpenGL ES implementation if needed. See: http://code.google.com/p/libgdx/source/browse/trunk/backends/gdx-backend-lwjgl/src/com/badlogic/gdx/backends/lwjgl/LwjglGraphics.java?r=3438#204

      Delete
  6. i ran your game on eclipse and it shows the following error,


    Tyrian: Creating game on Desktop
    Tyrian: Adjusting music volume to: 0.5
    Tyrian: Adjusting sound volume to: 0.5
    Tyrian: Retrieving profile from: data/profile-v1.json
    Tyrian: Installing item: USP Talon ($6,000) - Firing: 1
    Tyrian: Installing item: Pulse-Cannon ($500) - Damage: 1
    Tyrian: Installing item: Structural Field ($100) - Armor: 1
    Tyrian: Resizing game to: 800 x 480
    Tyrian: Retrieving profile from: data/profile-v1.json
    Tyrian: Showing screen: LevelScreen
    Tyrian: Playing music: LEVEL
    Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: Error creating music com.badlogic.gdx.backends.openal.Ogg$Music for file: music/level.ogg
    at com.badlogic.gdx.backends.openal.OpenALAudio.newMusic(OpenALAudio.java:141)
    at com.badlogic.gdx.backends.openal.OpenALAudio.newMusic(OpenALAudio.java:56)
    at com.blogspot.steigert.tyrian.services.MusicManager.play(MusicManager.java:96)
    at com.blogspot.steigert.tyrian.screens.LevelScreen.show(LevelScreen.java:42)
    at com.badlogic.gdx.Game.setScreen(Game.java:59)
    at com.blogspot.steigert.tyrian.Tyrian.setScreen(Tyrian.java:150)
    at com.blogspot.steigert.tyrian.Tyrian.resize(Tyrian.java:112)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication.mainLoop(LwjglApplication.java:145)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication$1.run(LwjglApplication.java:131)
    Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at com.badlogic.gdx.backends.openal.OpenALAudio.newMusic(OpenALAudio.java:139)
    ... 8 more
    Caused by: com.badlogic.gdx.utils.GdxRuntimeException: File not found: music\level.ogg (Internal)
    at com.badlogic.gdx.files.FileHandle.read(FileHandle.java:133)
    at com.badlogic.gdx.backends.openal.Ogg$Music.(Ogg.java:31)
    ... 13 more
    AL lib: ReleaseALC: 1 device not closed


    Please help me with it!

    ReplyDelete
    Replies
    1. Hi jeet! The exception complains about the "music\level.ogg" file. Is it there? Can you open it with some audio player? Did you just check-out the project and ran it, or did you code it by yourself, following the posts?

      Delete
    2. i checked the file and it runs on vlc media player,and i checked out the whole project so i can know the structure of the game...do u think its necessary to change the format of the music?

      Delete
    3. I suggest you place a breakpoint at MusicManager, line 96, and execute the following in the Display tab (select the following command, right click, execute):
      musicFile.file().getAbsolutePath()

      This will give you the absolute path of the music file. Based on the stacktrace you posted I bet this path points to an inexistent file. But if it does point to a valid file, we're in trouble.

      Delete
    4. Hi jeet, Gustavo,

      I had the same issue & here's how I solved it, in the .classpath file under the tyrian-game project replace this line:
      <classpathentry including="*.*" kind="src" path="assets"/> by this:
      <classpathentry kind="src" path="assets"/>
      it worked for me after this

      Regards,
      Sam

      Delete
  7. Hello GS,

    First thanks a lot for this tutorial.

    I am faacing an issue.
    In my case, resources location is as:assets/data

    So in SplashScreen I have coded as:
    splashTexture = new Texture( "data/splash.png" );

    I have copied splash.png from your repository to this location in my workspace.

    Now when I run the desktop launcher, I get following error:

    MyTyrianGdxGame: Creating game
    Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: Couldn't load file: data/splash.png
    at com.badlogic.gdx.graphics.Pixmap.(Pixmap.java:140)
    at com.badlogic.gdx.graphics.glutils.FileTextureData.prepare(FileTextureData.java:64)
    at com.badlogic.gdx.graphics.Texture.load(Texture.java:175)
    at com.badlogic.gdx.graphics.Texture.create(Texture.java:159)
    at com.badlogic.gdx.graphics.Texture.(Texture.java:133)
    at com.badlogic.gdx.graphics.Texture.(Texture.java:122)
    at com.badlogic.gdx.graphics.Texture.(Texture.java:118)
    at com.my.tyrian.SplashScreen.show(SplashScreen.java:23)
    at com.badlogic.gdx.Game.setScreen(Game.java:59)
    at com.my.tyrian.MyTyrianGdxGame.create(MyTyrianGdxGame.java:25)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication.mainLoop(LwjglApplication.java:144)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication$1.run(LwjglApplication.java:131)
    Caused by: java.io.IOException: couldn't load pixmap
    at com.badlogic.gdx.graphics.g2d.Gdx2DPixmap.(Gdx2DPixmap.java:57)
    at com.badlogic.gdx.graphics.Pixmap.(Pixmap.java:138)
    ... 11 more
    AL lib: ReleaseALC: 1 device not closed

    Please advise.

    Manish

    ReplyDelete
    Replies
    1. Maybe a configuration problem? You could try to check if the file exists before creating the Texture by analysing:
      Gdx.files.internal( "data/splash.png" );

      You're better of debugging your code and even libgdx code. Attach the libgdx source code to do so.

      Sorry I don't have much time to analyse this. You can always try the libgdx forum at: http://badlogicgames.com/forum/

      Delete
    2. Cleaning the project on Eclipse did trick for me.

      Delete
  8. I have this problem

    [2012-07-27 18:01:10 - Dex Loader] Unable to execute dex: GC overhead limit exceeded
    [2012-07-27 18:01:11 - FirstGame] Conversion to Dalvik format failed: Unable to execute dex: GC overhead limit exceeded

    ReplyDelete
  9. I liked a lot how you explain everything. You inspire me, i will be following your next post ;D

    ReplyDelete
  10. Hi Gustavo, I don't have to say that your entire tutorial is just awesome. I followed it 6 months ago to make my own project that is nearly complete, but I found a very important mistake you made in this part of the tutorial, but I successfully fixed after hours of deep research.
    See, you say we have to add the linked source folder in the tyrian-game project but this is wrong, because the android project is linked with the core game project so the asset folder will be duplicated inside the apk generating an unnecessary asset folder inside. The solution is, instead of linking the asset folder in the core project, link it in the desktop project (and the others too).
    Again thank you very much for the tutorial and I look forward to your response.

    ReplyDelete
    Replies
    1. Thanks Jairo, I'll review this first part! And good luck with your game! :)

      Delete
  11. Thank you Sir !!! It worked ... let's continue ...

    ReplyDelete
  12. Gustavo Sir, would you give me a hand?

    run:
    org.lwjgl.LWJGLException: Could not locate OpenAL library.
    at org.lwjgl.openal.AL.create(AL.java:151)
    at org.lwjgl.openal.AL.create(AL.java:102)
    at org.lwjgl.openal.AL.create(AL.java:201)
    at com.badlogic.gdx.backends.openal.OpenALAudio.(OpenALAudio.java:70)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication.(LwjglApplication.java:80)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication.(LwjglApplication.java:64)
    at com.badlogic.gdx.backends.lwjgl.LwjglApplication.(LwjglApplication.java:56)
    at Main.Main.main(Main.java:29)
    Tyrian: Creating game
    Tyrian: Resizing game to: 800 x 480
    FPSLogger: fps: 61
    Tyrian: Pausing game
    Tyrian: Disposing game
    Java Result: -1
    CONSTRUÍDO COM SUCESSO (tempo total: 3 segundos)

    http://badlogicgames.com/forum/viewtopic.php?f=11&t=8880&p=40197#p40197

    Please, idk what to do anymore.

    ReplyDelete
  13. import com.badlogic.gdx.backends.lwjgl.LwjglApplication;

    this import says it cannot be resolved to a type? Help!

    ReplyDelete
  14. for those who want a working netbeans project version, https://code.google.com/p/jtyrian/
    all libraries are already included. Btw, it's desktop only

    ReplyDelete
  15. Thank you very much it's worked for me..please keep sharing your tutorials Thumbs Up..

    ReplyDelete