2012-02-27

#3 - libgdx Tutorial: scene2d

Libgdx version used on this post: 0.9.2 (download)
All we have by now is a splash screen that displays an static image. Wouldn't it be nice to add some fading effect on this image? Things like these improve the user experience a lot, so soon we'll deal with that. As we build our graphical user interface, we need to handle things like selecting options, cliking on buttons, giving focus to an input widget and so on so forth. Can you imagine doing that with images inside image atlases? You're lucky today. There is a very cool feature of libgdx called scene2d that basically provides useful abstractions for rendering 2D components. Let's understand it better!
As of libgdx 0.9.6 some of the classes used on this post were modified or removed. You may want to read the post #13 after reading this post, which covers those changes.

About scene2d

Scene2d is a module of libgdx that eases the management and rendering of 2D components, which are called Actors. These actors live on a tree structure inside a container called Stage. They keep track of many rendering attributes, such as position relative to the parent, color, visibility, dimensions, scale and rotation factors and more. They are also responsible for hit detection.

Examples of actors would be: buttons, text fields, images, enemy targets, coins, the ship we'll fly, shots and so on. We'll use scene2d a lot in our game. Also, it's possible to apply actions on the actors, like translate, rotate, scale and fade actions. If needed, you can also write your own action, but we'll get to that later.

I'll try and summarize the main concepts of scene2d below:
  • Actor - A 2D component that knows how to draw itself.
  • Group - An actor that contains other actors.
  • Stage - An engine that requests the actors to be drawn and handles user interaction by distributing touch events to the actors.
  • Action - A function that modifies the actors' properties over time.

The following UML diagram shows it graphically:


Using scene2d

The first thing to do is setup a Stage where the actors will act. A nice place for it to live is within a screen (each screen with its own stage). These are the steps for managing the stage in our game:
  1. Create a Stage instance on the constructor of AbstractScreen class
  2. Resize its viewport when the screen is resized
  3. Call the Stage's act and draw methods within the screen's render method
  4. Dispose the stage within the screen's dispose method
For further details, please have a look at the source code of the AbstractScreen class. Now, everything we have to do is add actors to the stage. Our actors should override methods like act() and draw(), which are automatically invoked by the stage on the appropriate time.

Modifying the Splash screen

We want the splash image to be an actor so that we can do cool things with it. Instead of extending the Actor class, we can just use the Image (com.badlogic.gdx.scenes.scene2d.ui.Image) actor that comes with scene2d. We still have to load the texture and the texture region. We should also tell the image how to be drawn, and that it should take all the screen size. Have a look at the source code for the modified SplashScreen class to see how it's done.

Regarding that "actions" object inside SplashScreen.resize(), that's the coolest thing we can do with scene2d. Each actor can be animated by actions. What we do with our splash image is to add a set of the following actions:
  1. Fade-in in 0.75 seconds
  2. Wait for 1.75 sconds
  3. Fade-out in 0.75 seconds
We want also to move on to the next screen when the action is completed, so all we have to do is add a completion listener to the "actions" object. Note that we didn't need to override the screen's render method on the splash screen, because the splash image is an actor that was added to the stage, and the stage is the one in charge of rendering it.

More about actions

The three actions we used on the splash image ship with libgdx. That is:

I suggest you take some time to check all the available actions inside the package com.badlogic.gdx.scenes.scene2d.actions, but basically there are two types of them:
  • Concrete actions - Actions that modifies the actors directly (FadeIn, FadeOut, MoveBy, RotateBy, ScaleTo, etc).
  • Support actions - Actions that group or organize other actions in specific ways (Delay, Sequence, Parallel, Repeat, Forever, etc).
Every action comes with a factory method named "$" that takes the required parameters for it to work properly. If you want to create a FadeIn action that lasts 2 seconds, you can just write:
FadeIn fadeInAction = FadeIn.$(2f);
The delay action has a different factory method. The following code creates the FadeOut action 5 seconds delayed.
FadeOut fadeOutAction = FadeOut.$(2f);
Delay delayAction = Delay.$(fadeOutAction, 5f);
In order to have the complete effect we want, we need to join both actions:
Sequence sAction = Sequence.$(fadeInAction, delayAction);
And finally, add the action to the actor:
actor.action(sAction);
Piece of cake, isn't it? The last thing about actions I should not forget, is that you can also add interpolators to them. That means your action can start slow and them accelerate gradually, for instance. Interpolators define the rate of change for a given animation. In libgdx they also come with the "$" factory method, so you can easily create them.

Domain model

A domain model describes the entities, their attributes, roles and relationships for a given domain of interest. Most of our business logic will stay on the domain entities, because in software engineering it's a nice idea to isolate the business logic. It makes the code straightforward to other programmers, and even to ourselves later on. It also helps when switching technologies. Say we want to switch from 2D to 3D graphics. If the business logic is isolated, the impact of this change will be kept to a minimum.

With the help of scene2d we can achieve this objective rather easy, as each of the drawable domain entities can map to a scene2d actor. That's how we'll separate the business code from the presentation code. So the next task for us is to define our domain model. In order to make it simple, we're not going to implement a full clone of Tyrian, but just the main aspects of it. After analysing the game for some time, I came up with the following diagram:


You can check the complete source code for the domain model here.

Conclusion

This post was an introduction to scene2d. We saw how easy it is to use it, and how to extend it with custom functionality. We added a fade effect to the splash image and now it looks like a real splash image. Finally, we implemented the domain model for our game in an isolatted manner. The tag for this post on the Subversion is here. Thanks for reading!

35 comments:

  1. Thank you for this article! I have been trying to find source code so I could learn how to make UIs in libgdx. But this is better.

    ReplyDelete
  2. thank you for your tutorial I appreciate you going step by step through your thought process vs. alot of tutorials that are not as comprehensive.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Your tutorials are just awesome! Maybe we need some more explanation on the implementation of the Domain Model but in all the other things the explanations was just perfect.

    Good Job ! I look forward for your new posts :D

    ReplyDelete
    Replies
    1. Thanks for the feedback! I'll review the domain model section. :)

      Delete
  5. In your latest sources splashscreen,

    I cant find the "Drawable splashDrawable",
    I have no "import com.badlogic.gdx.scenes.scene2d.utils.Drawable;"

    ReplyDelete
    Replies
    1. Maybe you're using the lastest version of the code, when you should use the tag for this post instead. Please have a look at the conclusion section, I put a link to the tag there.

      Delete
  6. The latest version Drawble you write in Ship2D.java inside?

    If I follow the official API with create interface Drawable,its work?

    http://code.google.com/p/libgdx/source/browse/branches/scene2d-new/gdx/src/com/badlogic/gdx/scenes/scene2d/utils/Drawable.java?spec=svn4265&r=4265

    ReplyDelete
  7. I agree with Honorio - Larsen. The Domain Model should have its own tutorial entry, IMO. I was understanding everything just fine until that part, and it might throw me off for the whole tutorial. I hate having to download the source code just because i don't understand it.

    ReplyDelete
    Replies
    1. Thanks for the feedback! I'll review the domain model section. I didn't want to detail it very much when I wrote the post, because it's simple and pure java. Also, it's not a final implementation, but just a start.

      Delete
    2. Well, I guess what I mean to say is that some people are simply better at learning by looking at the code than others. Personally, I'm not so great at that, and I'm surely not the only one. I would be extremely grateful if you did a post describing the domain model code in more detail. I've also been having a heck of a time getting a handle on scene2d, but that seems to be because every tutorial has a different way of doing it and the code is always spread out. Of course, I know that the code needs to be organized and such, but sometimes it just makes it harder to analyze when looking for one specific aspect at a time.

      Anyway, that's just my two cents. Of course, thanks for making these helpful tutorials and for responding to us in the comments!

      Delete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Thank you soo much for your tutorials... :)

    ReplyDelete
  10. Hi Gustavo. Your tutorial is awesome, really!
    I'm having trouble with one line of code in particular in the SplashScreen.java class. The line number is 59 and my problem is that I'm having an error that says that "The constructor Image(TextureRegion, Scaling, int) is undefined". Currently I see that there is a similar constructor but instead of a TextureRegion as a parameter, it needs a Drawable one.
    My question is if it will works the same if I cast splashRegion to Drawable?
    All the rest of the code is pretty straightforward.

    Thank you very much!

    ReplyDelete
    Replies
    1. Hi MJ! You're propably using an updated version of Libgdx. Yeah, go ahead and try it out using Drawables! Have a look at the TextureRegionDrawable class.
      Thanks for reading!

      Delete
    2. I got the same error. It seems that in the latest version they deleted the constructor Image(TextureRegion, Scaling, int). Instead of casting the Texture to Drawable or using TextureRegionDrawable, you simply have to use the new Image constructor Image(Texture), it will stretch and center automatically the Texture, which is what we need here.

      Delete
  11. I am getting:
    java.lang.NoSuchMethodError: com.badlogic.gdx.InputProcessor.touchMoved(II)Z
    ^this error when i run your project from your svn!

    Help me please!

    ReplyDelete
    Replies
    1. I'm getting the same error!

      Anyone have any advice on this?

      Delete
    2. This comment has been removed by the author.

      Delete
    3. I'm getting the same error with libgdx 0.9.6 when my mouse moves on the splash screen or the menu screen.

      Did you find how to fix it ?

      regards

      Delete
    4. I found a solution, upgrade libgdx to the 0.9.7 version and replace :
      - ActorEvent by InputEvent
      - actorListener by InputListener

      Delete
  12. I like your tutorial so far, but the new version of libGDX is making it very difficult. I want to learn how to use this engine for making my own games.

    These are the first tutorials that I am trying because they were on the top of the list on the official libGDX website. I don't understand it well enough yet to be changing code from the example. If you have a version of this program that works with the new version of libGDX, can you share it with us?

    I also don't understand why the render() method was deleted and replaced by a resize() method.

    Any help would be appreciated.

    ReplyDelete
    Replies
    1. Hello Keldor, I'm a newbie too but I can help you with the resize() method.

      He changed the abstractScreen code, see https://code.google.com/p/steigert-libgdx/source/browse/tags/post-20120227/tyrian-game/src/com/blogspot/steigert/tyrian/screens/AbstractScreen.java

      The SplashScreen extends AbstractScreen, remember? Now, at each render() call, the stage.act(delta) and stage.draw() methods are called.

      Delete
  13. Great tutorial, but is there any chance of seeing an updated version? It looks like the actions have been completely rewritten in libGDX, and many of the classes and methods are depreciated.

    ReplyDelete
  14. This comment has been removed by the author.

    ReplyDelete
  15. Thanks for your great tutorial.

    ReplyDelete
  16. Cool tutorial, thanks a lot Gustavo. The actions model is slightly different now. I suggest anyone who finds it hard to make the code work can refer to http://code.google.com/p/libgdx/wiki/scene2d. Keep up the good work!

    ReplyDelete
  17. Very cool tutorial Gustavo. I'm having compilation issues with the static imports of actions:

    import static com.badlogic.gdx.scenes.scene2d.actions.Actions.delay;
    import static com.badlogic.gdx.scenes.scene2d.actions.Actions.fadeIn;
    import static com.badlogic.gdx.scenes.scene2d.actions.Actions.fadeOut;
    import static com.badlogic.gdx.scenes.scene2d.actions.Actions.sequence;

    Eclipse tells me that those imports cannot be resolved, what can I do??


    Cheers
    Diego

    ReplyDelete
    Replies
    1. The Actions class has changed a bit. Just import this:

      import com.badlogic.gdx.scenes.scene2d.actions.Actions;

      and do the sequencing like this:

      splashImage.addAction( Actions.sequence( Actions.fadeIn( 0.75f ), Actions.delay( 1.75f ), Actions.fadeOut( 0.75f ));


      For the fadeIn to work, you must set splashImage alpha to 0.

      splashImage.setColor(1f, 1f, 1f, 0f);

      There are many other things that have changed since this tutorial was posted. If you have problems with this part, PM me on G+.

      Delete
  18. Thank you for this tutorial Gustavo. I'm a beginner in Java gaming and this is really helpful.
    P.S. The game could not have been choosed better, Tyrian was my youth too!

    ReplyDelete
  19. Very nice tutorial, Gustavo. I have a question: Having all that code in the resize() method makes all the sequence start over everytime I change the window size.
    I tried moving most of the code to the constructor, and on resize I just do:
    img.setWidth(width);
    img.setHeight(height);
    but the image size does not change.
    Is there a way to overcome this? Can I stop actions before they are completed?

    ReplyDelete
  20. For anyone that is having problems with adding a callback to the Actions, You need to use a RunnableAction at the end of a sequence in order to determine if the sequence has finished. Something like this works

    splashImage.addAction(Actions.sequence(Actions.delay(1.75f), Actions.fadeIn(0.75f), Actions.delay(1.75f), Actions.fadeOut(0.75f), Actions.after(new RunnableAction(){
    @Override
    public void run() {
    Gdx.app.log("Actions: ", "Action sequence has finished, switch to new screen :D");
    }
    })
    ));

    ReplyDelete
  21. After the updates on Stage2d, that resize function on SplashScreen class will work (see: http://steigert.blogspot.com.br/2012/07/13-libgdx-tutorial-libgdx-refactoring.html):

    @Override
    public void resize(int width, int height) {
    super.resize(width, height);

    // let's make sure the stage is clear
    stage.clear();

    // in the image atlas, our splash image begins at (0,0) of the
    // upper-left corner and has a dimension of 512x301
    TextureRegion splashRegion = new TextureRegion(splashTexture, 0, 0,
    512, 301);

    // here we create the splash image actor
    Image splashImage = new Image(splashRegion);
    splashImage.setWidth(width);
    splashImage.setHeight(height);

    // this is needed for the fade-in effect to work correctly; we're just
    // making the image completely transparent
    splashImage.setColor(1f, 1f, 1f, 0f);

    // configure the fade-in/out effect on the splash image
    splashImage.addAction(Actions.sequence(Actions.fadeIn(0.75f),
    Actions.delay(1.75f), Actions.fadeOut(0.75f), new Action() {
    public boolean act(float delta) {
    // when the image is faded out, move on to the next
    // screen
    game.setScreen(new MenuScreen(game));
    return true; // returning true consumes the event
    }
    }));

    // and finally we add the actors to the stage, on the correct order
    stage.addActor(splashImage);
    }

    ReplyDelete