Monday 30 July 2012

Getting Started with LibGDX


Exciting times… By the end of this post we’ll have something that loosely resembles the beginnings of a game! Not only that, we’ll be able to run it on the desktop or if we like on android (either a real phone or the emulator).
It really will be just the beginning though… Today’s focus is on creating a game world comprised of block objects, adding a player controlled object (Bobo) to the world, and locking the camera onto Bobo such that the level ‘scrolls’ and Bobo always remains within sight. Later we’ll deal with collision detection, game winning states, scoring, etc, but for now let’s just take it one step at a time!

Getting Started with LibGDX

I’ll assume you already have your environment set-up. If not, follow the instructions here, and come back when you’re done :)
Ok, now generate a new shell libgdx project using the gdx-setup-ui.jar. These are the values I used:


Next, fire up eclipse, file > import, general > existing projects into workspace, set root dir to wherever you specified when creating the shell project (in my case D:\Workspace), make sure all three projects are ticked, then click finish. If it all went well you should see the three projects in project explorer. Right click the desktop project, opt to run as java application, click on the Main item in the dialog, then ok. The default hello world app should open, it looks a bit like this:


Assets

I created a couple of sprites to get us started, bobo.png and block.png (both 16x32px per original spec). These should be placed in the assets/data dir of the android project. While you’re at it, delete libgdx.png, you won’t be needing it again.

Manifest

Open AndroidManifest.xml (it should be in the root of the android project). For now, all we need to do here is change the android:screenOrientation value from “landscape” to “portrait”.

Bootstrap

I renamed MainActivity.java (android project src) AndroidStarter.java, and Main.java (desktop project src) DesktopStarter.java. Just personal preference thing. Nothing needs changing in the android starter, but in the desktop starter we should set cfg.width to 240, and cfg.height should remain as 320, this just means the app we preview on the desktop will default to the target resolution of 240×320.

Getting down to business

With the initial setup out of the way, we can now get on with the business of making our game.
We’re only implementing the GameScreen right now, but we’re aware that there will be more screens added later on, so we should probably plan for that from the outset. There’s a nice little article here on the subject, makes a lot of sense to me so let’s do it!
Open RunBoboRun.java, delete all the contents, and replace with the following:

 // In anticipation of further screens being added, I followed this suggestion:  
 // http://code.google.com/p/libgdx-users/wiki/ScreenAndGameClasses  
 package com.mrdt.runboborun;  
 import com.badlogic.gdx.Game;  
 public class RunBoboRun extends Game {  
     GameScreen gameScreen;  
     @Override  
     public void create() {  
         gameScreen = new GameScreen(this);  
         setScreen(gameScreen);  
     }  
 }  


The libgdx-users wiki article does a great job of explaining so I won’t bother going into it any further here.

Game Objects

There are a couple of very obvious game object candidates – the world is made up of Blocks, and Bobo moves around the world, let’s create a class for each…

 // A simple class for Block objects  
 package com.mrdt.runboborun;  
 import com.badlogic.gdx.math.Vector2;  
 public class Block {  
     public static final int WIDTH = 16;  
     public static final int HEIGHT = 32;  
     private Vector2 position = new Vector2();  
     public Block(Vector2 position) {  
         this.position = position;  
     }  
     public Vector2 getPosition() {  
         return position;  
     }  
 }  

There’s not a whole lot to the Block class. A couple of constants define the size (measured in pixels), a Vector2 is used to record its position in the world (set during construction), and there’s a position getter/accessor method. That’s all there is too it.

 // A simple class for Bobo, all straightforward apart from maybe update method.  
 // From obviam.net:  
 // We simply add the distance travelled in delta seconds to Bob’s current position.  
 // We use velocity.tmp() because the tmp() creates a new object with the same value  
 // as velocity and we multiply that object’s value with the elapsed time delta.  
 package com.mrdt.runboborun;  
 import com.badlogic.gdx.math.Vector2;  
 public class Bobo {  
     public static final int WIDTH = 16;  
     public static final int HEIGHT = 32;  
     private Vector2 position = new Vector2();  
     private Vector2 velocity = new Vector2();  
     public Bobo(Vector2 position) {  
         this.position = position;  
     }  
     public void update(float delta) {  
         position.add(velocity.tmp().mul(delta));  
     }  
     public Vector2 getPosition() {  
         return position;  
     }  
     public void setVelocity(Vector2 velocity) {  
         this.velocity = velocity;  
     }  
 }  


There isn’t much more to the Bobo class. In fact, looking at the two classes I think maybe there should be a parent Tile object that Block and Bobo extend, but let’s forget about that for now, we can always come back to this later if we like. So what is different about the Bobo class? Put simply, it’s capable of movement. Bobo has a velocity (a Vector2), the value of which is set via a setter/mutator method. On each update Bobo’s position is altered in accordance with his velocity. I pinched the update method from obviam.net, he explains it as follows – “We simply add the distance travelled in delta seconds to Bob’s current position. We use velocity.tmp() because the tmp() creates a new object with the same value as velocity and we multiply that object’s value with the elapsed time delta.”
So far so good. Time to implement the GameScreen!

GameScreen

I’ve said this many times already, but I’ll say it again… I’m learning as I go, and I may not be doing things the “right” way. This class should probably be split into smaller pieces, maybe I should have distinct renderer and controller classes, etc. For now I’m just working on making things work, I can worry about doing things the “right” way later on. When you’re making a game who cares if you did it the “right” way anyway, it just needs to work! With that out of the way, let’s have a look at the code:

 // This basic GameScreen class demonstrates:  
 //  How to create a game level comprised of multiple block objects  
 //  How to create a dynamic bobo object that moves around the level in response to user input  
 //  How to create a 2D camera that follows bobo on the y-axis  
 package com.mrdt.runboborun;  
 import com.badlogic.gdx.Gdx;  
 import com.badlogic.gdx.Input;  
 import com.badlogic.gdx.Screen;  
 import com.badlogic.gdx.graphics.GL10;  
 import com.badlogic.gdx.graphics.Texture;  
 import com.badlogic.gdx.graphics.g2d.SpriteBatch;  
 import com.badlogic.gdx.graphics.OrthographicCamera;  
 import com.badlogic.gdx.math.Vector2;  
 import com.badlogic.gdx.utils.Array;  
 public class GameScreen implements Screen {  
     private static final int WIDTH = 240;  
     private static final int HEIGHT = 320;  
     private RunBoboRun game;  
   private Bobo bobo;  
   private Array<Block> blocks;  
   private OrthographicCamera cam;  
   private Texture boboTexture;  
   private Texture blockTexture;  
     private SpriteBatch spriteBatch;  
     public GameScreen(RunBoboRun game) {  
         this.game = game;  
         boboTexture = new Texture(Gdx.files.internal("data/bobo.png"));  
         blockTexture = new Texture(Gdx.files.internal("data/block.png"));  
         createLevel();  
         // orthographic camera (2D camera) of fixed width and height, larger screens will scale to fit  
         cam = new OrthographicCamera(WIDTH, HEIGHT);  
         // camera focus: x-axis middle of level, y-axis 100px below bobo, z-axis ignored  
         cam.position.set(WIDTH/2, bobo.getPosition().y - 100, 0);  
       spriteBatch = new SpriteBatch();  
   }  
     private void createLevel() {  
         // create bobo and add him to the level  
         bobo = new Bobo(new Vector2((WIDTH/2)-(Bobo.WIDTH/2), 0));  
         // create an array to hold all the block objects  
         blocks = new Array<Block>();  
         // nasty temp cludge to blat out a bunch of blocks... will be replaced by level generation algorithm soon!  
         addBlock(new Vector2(0, 32));  
         addBlock(new Vector2(16, 32));  
         // -- add lots more blocks here if you like...  
         addBlock(new Vector2(224, -448));  
     }  
     private void addBlock(Vector2 position){  
         Block block = new Block(position);  
         blocks.add(block);  
     }  
   @Override  
     public void render(float delta) {  
         handleInput();  
         // user input has an impact on bobo, so update his state  
           bobo.update(delta);  
           // following bobo update, move camera accordingly (focus rule same as in constructor)  
         cam.position.set(WIDTH/2, bobo.getPosition().y - 100, 0);  
         // clear screen  
           Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);  
           // call cam.update() after changes to cam (http://code.google.com/p/libgdx/wiki/OrthographicCamera)  
           cam.update();  
           // set projection and model-view matrix after update (http://code.google.com/p/libgdx/wiki/OrthographicCamera)  
           cam.apply(Gdx.gl10);  
           // setProjectionMatrix before drawing sprites (http://code.google.com/p/libgdx-users/wiki/Sprites)  
           spriteBatch.setProjectionMatrix(cam.combined);  
     spriteBatch.begin();  
       for(Block block: blocks) {  
           spriteBatch.draw(blockTexture, block.getPosition().x, block.getPosition().y, Block.WIDTH, Block.HEIGHT);  
       }  
         spriteBatch.draw(boboTexture, bobo.getPosition().x, bobo.getPosition().y, Bobo.WIDTH, Bobo.HEIGHT);  
         spriteBatch.end();       
     }  
     private void handleInput() {  
         // if a valid keypress, respond accordingly (set bobo's velocity such that he moves in that direction)  
     if(Gdx.input.isKeyPressed(Input.Keys.DOWN)) {  
         bobo.setVelocity(new Vector2(0, -100));  
     }  
     if(Gdx.input.isKeyPressed(Input.Keys.UP)) {  
             bobo.setVelocity(new Vector2(0, 100));  
     }  
     if(Gdx.input.isKeyPressed(Input.Keys.LEFT)) {  
         bobo.setVelocity(new Vector2(-100, 0));  
     }  
     if(Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {  
             bobo.setVelocity(new Vector2(100, 0));  
     }  
         // if no valid keypress, respond accordingly (set bobo's velocity such that he does not move)  
     if   
     (!(Gdx.input.isKeyPressed(Input.Keys.UP))&&!(Gdx.input.isKeyPressed(Input.Keys.DOWN))&&!(Gdx.input.isKeyPressed(Input.Keys.LEFT))&&!(Gdx.input.isKeyPressed(Input.Keys.RIGHT)))  
     {  
         bobo.setVelocity(new Vector2(0, 0));  
     }  
   }  
   @Override  
   public void resize(int width, int height) {  
     }  
     @Override  
     public void show() {  
     }  
     @Override  
     public void hide() {  
     }  
     @Override  
     public void pause() {  
     }  
     @Override  
     public void resume() {  
     }  
     @Override  
     public void dispose() {  
     }  
 }  


I think the code and comments (don’t miss the links in the comments!) should make sense without too much further explanation.
On entry we:
  1. Create our assets (in this case our two textures)
  2. Create a level comprised of an array of Blocks and a Bobo
  3. Create a camera focused on Bobo
  4. Create a spritebatch to allow drawing of sprites to screen
The render method can be thought of as the main game loop, it’s called repeatedly while the GameScreen is active. Here’s what we need to do every time render is called:
  1. Handle user input – if a valid key is pressed (or not), set Bobo’s velocity accordingly
  2. Update Bobo
  3. Update camera position (lock on to Bobo)
  4. Draw the sprites to screen
That’s basically it :)
The controls are more than a little bit dodgy (no diagonal movement, and you’re stuffed if your phone has no cursor keys), the ‘level’ is hardcoded, the assets aren’t disposed cleanly on close, and there’s no doubt a lot more that needs tidying up, but this post is getting a bit long so I’m going to leave it there for now. When all is said and done, we have an application that will run on the desktop and on android, and the camera follows Bobo around the world, that’s exactly what I hoped to achieve today, so this seems like a good place to stop.
Here’s a screenshot of the ‘game’ as it stands:


Next post I’ll probably look into pulling in the level generation algorithm, and maybe do some collision detection, or maybe investigate accelerometer controls, I don’t really know yet!


          

1 comment:

  1. I seriusly apreciate your work creating these tutorials , it is going to help me a lot

    ReplyDelete