2012-03-22

#9 - libgdx Tutorial: Viewport

Libgdx version used on this post: 0.9.2 (download)
In this post we will start implementing the Levels screen by creating an actor for our ship.

Important changes

  • Refactorings:
    • Removed the preview image's attribute from the enumerated items ShipModel, FrontGun and Shield in favour of a method Item#getSimpleName(). With this method the advantage is that the ship's items don't contain any specific presentation information anymore. That allows us to create a convention to name our image files.
    • Created the LevelManager in order to make the Level domain class dumber.
  • Configurations:
    • Changed the tyrian-android project so that the activity prevents the screen from dimming. This is as simple as adding a flag to the window (see TyrianAndroidLauncher):
      getWindow().addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON );

Creating the ship's actor

With the help of scene2d we'll create the presentation object for our ship, namely the Ship2D actor. Instead of extending com.badlogic.gdx.scenes.scene2d.Actor we can rather extend com.badlogic.gdx.scenes.scene2d.ui.Image, saving us many hours of work. Its dimensions are automatically set based on the texture region's dimensions. These are the requirements for the Ship2D actor:
  • The actor's image should be chosen based on the ShipModel of our ship.
  • On the desktop the keyboard should be used to control the ship. On Android, the accelerometer should be used instead.
  • The ship should not be allowed to go off the screen.
  • When the ship moves horizontally it should tilt to the side it's moving.
  • The ship's acceleration/deceleration speed should start slow and increase gradually.

Presenting the ship

The first thing to do is to create a new group of images containing each of our ship models, which will be packed into an image atlas by the texture packer (for more details about this tool please have a look at the post #7). Browsing the web I found this cool image atlas, which I'll use to crop the images I want (highlighted in red):


Note that I just need the ship images tilting to the one side, because we can reuse these images just by flipping them horizontally (e.g., by using a negative width). We'll have to work with animations to tilt the ship, so I'll add an index to the image's name and later I'll be able to retrieve an ordered collection of all the regions of a given image. Finally, I'll add these images to tyrian-game\etc\images\level-screen, run the texture packer and refresh my projects.

Now we can start implementing the Ship2D actor. I'll create it under the package com.blogspot.steigert.tyrian.screens.scene2d. In order to ease its instantiation, I'll add the following factory method:
    /**
     * Factory method to create a {@link Ship2D}.
     */
    public static Ship2D create(
        Ship ship,
        TextureAtlas textureAtlas )
    {
        List<AtlasRegion> regions = textureAtlas.findRegions( ship.getShipModel().getSimpleName() );
        Ship2D ship2d = new Ship2D( ship, regions );
        return ship2d;
    }
Respecting our MVC architecture, Ship2D may know Ship but Ship may not know Ship2D. So far, so good. In the Ship2D's constructor, all we need to do is the following:
    /**
     * Creates a new {@link Ship2D}.
     */
    private Ship2D(
        Ship ship,
        List<AtlasRegion> regions )
    {
        // the super constructor does a lot of work
        super( regions.get( 0 ) );

        // set some basic attributes
        this.touchable = false;
    }
Finally we have to modify the Level screen to add our actor to the stage and center it horizontally, similar to what we did on the other screens. You can view the complete source code for the Level screen here. I also added a fade-in action to the whole stage (by referencing the stage's root), because the real Tyrian also does it. When starting the "Episode 1" level, we get the following screen:


Everything is working but the ship is too small, isn't it? We could create bigger images for this screen, but I want to show you another approach.

Working with the stage's viewport

We've been using a fixed resolution for the game screen and we also resize the stage to this resolution later on. But we know that Android devices may have different screen resolutions, and sooner or later we'll have to deal with that, so let's go with sooner. What we want to achieve is called resolution independence, and for that we have two options:
  1. Let the stage's dimension match the current resolution and show more or less game objects as needed.
  2. Use a fixed resolution and stretch it to fit the current resolution.
In order to pick an option you have to analyze your game. Is it a problem to show more of the game for devices with greater resolutions? Will that give an unfair advantadge to some players? Some games use an hybrid approach. They stretch the game without losing the aspect ratio and then they fill in the blanks with something (generaly by showing more of the game).

We're going with the second option, as it is way easier than the first. But how can that increase the size of our ship? Simply put, we could define a fixed dimension for our stage's viewport in that the ship is not too small, and then we stretch the viewport to fill the current screen resolution whatever it is. So far the viewport size matched the game window's size (800x480), so if we want the ship to be two times bigger, we can set the viewport's dimensions to 400x240 without losing the aspect ratio of 1.6 we had before.

Implementing the changes

In the AbstractScreen class we need to:
  • Define two constants for the fixed viewport dimensions:
       protected static final int VIEWPORT_WIDTH = 400, VIEWPORT_HEIGHT = 240;
       
  • Modify the instantiation of the stage object to:
       this.stage = new Stage( VIEWPORT_WIDTH, VIEWPORT_HEIGHT, true );
       
  • Remove the resizing of the stage from the resize method, as we don't care about the current screen resolution anymore.
And finally we should:
  • Remove all the overrides of the AbstractScreen#resize method.
  • Set the dimensions of the actors that should fill the viewport within the show method of each screen. For instance, in SplashScreen#show:
       splashImage = new Image( splashRegion, Scaling.stretch );
       splashImage.width = stage.width();
       splashImage.height = stage.height();
       
  • Update the layout descriptors to better fit the smaller viewport. I'll use relative dimensions on the layout descriptor, so if we change the fixed dimensions of the viewport again there is a chance the layouts won't break. You can view the modified layout descriptors here.
When I run the Desktop launcher on a 800x480 window I get the following screens:




The ship is now bigger, but the start game screen is now broken. We should refactor this screen, creating other screens for each customizable item, but as we're playing with the viewport, we could increase its dimensions just for the menu screens. So this is the plan:
  • All the menu screens should have a viewport of 800x480.
  • The level screen should have a viewport of 400x240.
You can browse the source code for the modified screens here. Now I suggest you play with the game window's size, either by resizing it manually or using specific values on TyrianDesktopLauncher. As you do that don't worry if the screen's contents are ugly. On Android they look just nice.

Conclusion

We've finally started implementing the game by creating the Ship2D actor. We played with the stage's viewport to increase the ship's size, and ended up discussing about how to achieve resolution independence. In the next post we'll handle the ship's requirements not yet implemented. Thanks for reading!

5 comments:

  1. Hi Gustavo,

    Really useful blog for using scene2d, respect!
    I am very interessting on your solution for the animation cycle within scene2d, cause there are no default routines implemented at the moment.

    Greets,
    Peter

    ReplyDelete
    Replies
    1. Hi Peter!
      We're definitely going to see some interesting solutions as the level screen gets improved.
      Thanks for reading, and for the feedback!
      Cheers

      Delete
    2. Hi,

      Your blog is already added to my link list :)
      I am curious about reading new blog entry for this essential feature!

      Keep it up!
      Greets

      Delete
  2. Hi Gustavo,

    I found that adding "getScreen().resize(width, height);" to the resize method of the game class will keep the aspect ratio in the desktop version.

    ReplyDelete
    Replies
    1. Ohh I see now, I was missing the super.resize call. My fault :)

      Delete