Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

9. December 2014

 

 

 

In the previous tutorial we looked at how to use a camera with LibGDX to abstract away resolution differences so you are no longer using pixel coordinates.  This however doesn’t really help you all that much if your aspect ratios are massively different.  Fortunately LibGDX implements the concepts of Viewports, which can be considered the coding equivalent of the Aspect button on your HDTV, controlling how non-native content is scaled to be displayed on your TV.

 

There are a number of different Viewports available:

 

Or of course you can inherit Viewport and create your own.

 

We are going to use the following image ( click for full resolution, no squished version ) taken from here.

Aspect

 

 

Here is the code for creating a FillViewport, which results in behavior very similar to using no Viewport at all:

 

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.viewport.FillViewport;
import com.badlogic.gdx.utils.viewport.Viewport;

public class ViewportDemo extends ApplicationAdapter {
   SpriteBatch batch;
   Sprite aspectRatios;
   OrthographicCamera camera;
   Viewport viewport;

   @Override
   public void create () {
      batch = new SpriteBatch();
      aspectRatios = new Sprite(new Texture(Gdx.files.internal("Aspect.jpg")));
      aspectRatios.setPosition(0,0);
      aspectRatios.setSize(100,100);

      camera = new OrthographicCamera();
      viewport = new FillViewport(100,100,camera);
      viewport.apply();

      camera.position.set(camera.viewportWidth/2,camera.viewportHeight/2,0);
   }

   @Override
   public void render () {

      camera.update();
      Gdx.gl.glClearColor(1, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      batch.setProjectionMatrix(camera.combined);
      batch.begin();
      aspectRatios.draw(batch);
      batch.end();
   }

   @Override
   public void dispose(){
      aspectRatios.getTexture().dispose();
   }

   @Override
   public void resize(int width, int height){
      viewport.update(width,height);
      camera.position.set(camera.viewportWidth/2,camera.viewportHeight/2,0);
   }
}

 

The code is pretty straight forward but has a few caveats to be aware of.  Most importantly, you absolutely need to update the viewport in the ApplicationAdapter’s resize method or the viewport will not work.  Standard process is to create your camera, then create the viewport passing in the viewport resolution as well as the camera to apply to.  For the technically minded, the Viewport ultimately manipulates the GL viewport behind the scenes.  Now let’s look at the various viewport options, we simply replace one line of code in each example:

 

Here are the results of various options running at 900x600 resolution:

 

No Camera or Viewport

image

 

 

viewport = new ExtendViewport(100,100,camera);

image

 

 

viewport = new FillViewport(100,100,camera);

image

 

viewport = new FitViewport(100,100,camera);

image

 

viewport = new StretchViewport(100,100,camera);

image

 

viewport = new ScreenViewport(camera);

image

 

 

The behavior of most of those viewports should be evident from the results but a few certainly require a bit of explanation.  The oddest result is most likely ScreenViewport.  When you use ScreenViewport you are simply saying “create a viewport the same size as the screen resolution”.  However, we also told our game that the sprite is 100x100 in size, but instead of being treated in our arbitrary camera units, this value is now relative to actual pixels, so as a result, our background is drawn as a 100 x 100 pixel rectangle.

 

You may also be wondering what the difference between Fill and Stretch is.  It isn’t obvious because our source image and our resolution are both widescreen.  When you run in a 4:3 aspect ratio, such as 1024x768 of the iPad, the results become more obvious:

 

FillViewport @ 1024x768

image

 

StetchViewport @ 1024x768

image

 

Fill will always fill the viewport, even if it means have to crop part of the image.  As you can see in the first image, the blue border at the top and bottom is not visible.  IMHO, this is the worst looking of all scaling options.

 

Stretch on the other hand, stretches the result to fit the screen width and height, however it also results in some distortion.  This mode is probably the easiest to implement, but the results depend heavily on your games art style.

 

 

Mapping To And From Camera to Screen Coordinates

 

One last quick thing to touch upon with Cameras and Viewports… how do you handle converting coordinates, such as touch or click events from our arbitrary camera coordinates to screen coordinates?  Fortunately it’s quite easy.

 

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.viewport.StretchViewport;
import com.badlogic.gdx.utils.viewport.Viewport;

public class mouseproject extends ApplicationAdapter implements InputProcessor {
   SpriteBatch batch;
   Sprite aspectRatios;
   OrthographicCamera camera;
   Viewport viewport;

   @Override
   public void create () {
      batch = new SpriteBatch();
      aspectRatios = new Sprite(new Texture(Gdx.files.internal("Aspect.jpg")));
      aspectRatios.setPosition(0,0);
      aspectRatios.setSize(100,100);

      camera = new OrthographicCamera();
      viewport = new StretchViewport(100,100,camera);
      viewport.apply();

      camera.position.set(camera.viewportWidth/2,camera.viewportHeight/2,0);
      Gdx.input.setInputProcessor(this);
   }

   @Override
   public void render () {

      camera.update();
      Gdx.gl.glClearColor(1, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      batch.setProjectionMatrix(camera.combined);
      batch.begin();
      aspectRatios.draw(batch);
      batch.end();
   }

   @Override
   public void dispose(){
      aspectRatios.getTexture().dispose();
   }

   @Override
   public void resize(int width, int height){
      viewport.update(width, height);
      camera.position.set(camera.viewportWidth / 2, camera.viewportHeight / 2, 0);
   }

   @Override
   public boolean keyDown(int keycode) {
      return false;
   }

   @Override
   public boolean keyUp(int keycode) {
      return false;
   }

   @Override
   public boolean keyTyped(char character) {
      return false;
   }

   @Override
   public boolean touchDown(int screenX, int screenY, int pointer, int button) {

      Gdx.app.log("Mouse Event","Click at " + screenX + "," + screenY);
      Vector3 worldCoordinates = camera.unproject(new Vector3(screenX,screenY,0));
      Gdx.app.log("Mouse Event","Projected at " + worldCoordinates.x + "," + worldCoordinates.y);
      return false;
   }

   @Override
   public boolean touchUp(int screenX, int screenY, int pointer, int button) {
      return false;
   }

   @Override
   public boolean touchDragged(int screenX, int screenY, int pointer) {
      return false;
   }

   @Override
   public boolean mouseMoved(int screenX, int screenY) {
      return false;
   }

   @Override
   public boolean scrolled(int amount) {
      return false;
   }
}

 

It's simply a matter of using Camera.unproject to convert a screen coordinate to your world coordinate, and Camera.project to do the reverse.  There is more to Viewports and Cameras, but that covers enough to get you going in most cases.  Keep in mind, you don’t need to use either, but they both certainly make making a game that runs across devices a lot easier.

 

Programming , ,

8. December 2014

 

 

In this tutorial we are going to look at how to use Cameras ( and in the next, Viewports ) in LibGDX.  I will admit, I am a bit late in covering this topic, as I should have covered it much earlier in the series.  In fact, in some prior tutorials I actually made use of Cameras with little prior discussion.  Better late than never, no?

 

The first immediate question that comes to mind are probably “What’s a camera, what’s a viewport and how are they different?”.

 

Well, basically a camera is responsible for being the players “eye” into the game world.  It’s an analogy to the way video camera’s work in the real world.  A viewport represents how what the camera sees is displayed to the viewer.  Any easy way to think about this is to think about your HD cable or satellite box and your HD TV.  The video signal comes in to your box ( this is the camera ), this is the picture that is going to be displayed.  Then your TV devices how to display the signal that comes in.  For example, the box may send you a 480i image, or a 1080p image, and it’s your TV’s responsibility to decide how it’s displayed.  This is what a viewport does… takes an incoming image and adapts it to run best on the device it’s sent to it.  Sometime this means stretching the image, or displaying black bars or possibly doing nothing at all.

 

So, simple summary description…

  • Camera – eye in the scene, determines what the player can see, used by LibGDX to render the scene.
  • Viewport – controls how the render results from the camera are displayed to the user, be it with black bars, stretched or doing nothing at all.

 

In LibGDX there are two kinds of cameras, the PerspectiveCamera and the OrthographicCamera.  Both are very big words and somewhat scary, but neither needs to be.  First and foremost, if you are working on a 2D game, there is a 99.9% change you want an Orthographic camera, while if you are working in 3D, you most likely ( but not always ) want to use a Perspective camera.

 

Now, the difference between them.  A perspective camera tries to mimic the way the human eye sees the world ( instead of how the world actually works ).  To the human eye, as something gets further away the smaller it appears.  One of the easiest ways to illustrate the effect is to fire up Blender and view a Cube in both perspectives:

 

Perspective Rendered:

image

 

Orthographic Rendered:

image

 

When dealing with 3D, a Perspective camera looks much more like we expect in the real world.  However, when you are working in 2D, you are actually still in 3D but for the most part you are ignoring depth ( except for sprite ordering ).  In a 2D game, you don’t want objects to change size the further “into the screen” they are.

 

So, TL;DR version, if you are making a 2D game, you probably want Orthographic.  If you aren’t, you probably don’t.

 

Ok, enough talk, CODE time.

 

We are going to implement a simple Orthographic camera that pans around a single image that represents our game world.  For this demo I am going to use this 2048x1024 image (click it for the full resolution, non-squished version, or make your own):

TheWorld

 

Paint skills at their finest!  Now lets look at rendering this using a camera:

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class CameraDemo extends ApplicationAdapter {
   SpriteBatch batch;
   Texture img;
   OrthographicCamera camera;
   
   @Override
   public void create () {
      batch = new SpriteBatch();
      img = new Texture("TheWorld.png");
      camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight());
   }

   @Override
   public void render () {
      Gdx.gl.glClearColor(1, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      camera.update();
      batch.setProjectionMatrix(camera.combined);
      batch.begin();
      batch.draw(img, 0, 0);
      batch.end();
   }
}

The process is quite simple.  We simply create a camera, then in our render loop we call it’s update() method then set it as the projectionMatrix of our SpriteBatch.  If you are new to 3D ( fake 2D ), the ProjectionMatrix along with the View Matrix are matrix based multiplications responsible for transforming 3D data to 2D screen space.  Camera.combined returns the camera’s view and perspective matrixes multiplied together.  Essentially this process is what positions everything from the scene to your screen.

 

Now if we go ahead and run this code we see:

image

 

Hmmmm… that may not be what you were expecting.  So what exactly happened here?

 

Well, the camera is located at the position 0,0.  However, the camera’s lens is actually at it’s center.   The red you see in the above image are the portions of the scene that have nothing in it.  So in the above example if you want to start at the bottom left of your world you actually need to take the camera’s dimensions into account.  Like so:

      camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight());
      camera.translate(camera.viewportWidth/2,camera.viewportHeight/2);

Now when you run it, the results are probably more along the lines you expected:

image

 

In the previous examples I actually did something you really don’t want to do in real life:

camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight());

 

I am setting the camera viewport to use the devices resolution, then later I am translating using pixels.  If you are working across multiple devices, this almost certainly isn’t the approach you want to take as so many different devices have different resolutions.  Instead what you normally want to do is work in world units of some form.  This is especially true if you are working with a physics engine like Box2D.

 

So, what’s a world unit?  The short answer is, whatever you want it to be!  The nice part is, regardless to what units you choose, it will behave the same across all devices with the same aspect ratio!  Aspect ration is the more important factor here.  The aspect ratio is the ratio of horizontal to vertical pixels.

 

Let’s take the above code and modify it slightly to no longer use pixel coordinates.  Instead we will define our world as 50 units wide by 25 tall.  We then are going to set our camera to be the worlds height and centered.  Finally we will hook it up so you can control the camera using arrow keys.  Let’s see the code:

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class CameraDemo2 extends ApplicationAdapter implements InputProcessor {
   SpriteBatch batch;
   Sprite theWorld;
   OrthographicCamera camera;

   final float WORLD_WIDTH = 50;
   final float WORLD_HEIGHT = 25;

   @Override
   public void create () {
      batch = new SpriteBatch();
      theWorld = new Sprite(new Texture(Gdx.files.internal("TheWorld.png")));
      theWorld.setPosition(0,0);
      theWorld.setSize(50,25);

      float aspectRatio = (float)Gdx.graphics.getHeight()/(float)Gdx.graphics.getWidth();

      camera = new OrthographicCamera(25 * aspectRatio ,25);
      camera.position.set(WORLD_WIDTH/2,WORLD_HEIGHT/2,0);

      Gdx.input.setInputProcessor(this);
   }

   @Override
   public void render () {
      Gdx.gl.glClearColor(1, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      camera.update();
      batch.setProjectionMatrix(camera.combined);
      batch.begin();
      theWorld.draw(batch);
      batch.end();
   }

   @Override
   public boolean keyUp(int keycode) {
      return false;
   }

   @Override
   public boolean keyDown(int keycode) {
      if(keycode == Input.Keys.RIGHT)
         camera.translate(1f,0f);
      if(keycode == Input.Keys.LEFT)
         camera.translate(-1f,0f);
      if(keycode == Input.Keys.UP)
         camera.translate(0f,1f);
      if(keycode == Input.Keys.DOWN)
         camera.translate(0f,-1f);

      return false;
   }

   @Override
   public boolean keyTyped(char character) {
      return false;
   }

   @Override
   public boolean touchDown(int screenX, int screenY, int pointer, int button) {
      return false;
   }

   @Override
   public boolean touchUp(int screenX, int screenY, int pointer, int button) {
      return false;
   }

   @Override
   public boolean touchDragged(int screenX, int screenY, int pointer) {
      return false;
   }

   @Override
   public boolean mouseMoved(int screenX, int screenY) {
      return false;
   }

   @Override
   public boolean scrolled(int amount) {
      return false;
   }
}

Now when you run this code:

image

 

Now let’s look at what happens when we change our application’s resolution.  Since we aren’t using pixels anymore, the results should be fairly smooth.

 

In case you forgot, you set the resolution in the platform specific Application class.  For iOS and Android, you cannot set the resolution.  For HTML and Desktop you can.  Setting the resolution on Desktop is a matter of editing DesktopLauncher, like so:

public class DesktopLauncher {
   public static void main (String[] arg) {
      LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
      config.width = 920;
      config.height = 480;
      new LwjglApplication(new CameraDemo2(), config);
   }
}

Here is the code running at 920x480

image

 

And here is 1280x400:

image

 

As you can see, the code updates so the results render in a pixel independent way.

 

However, and as you can see above, if your aspect ratios don’t stay the same, the results look massively different.  In the previous example you can see in the 1280x400 render, the results look squished and the world contains a great deal less content.  Obviously, the values I used to illustrate this point are pretty extreme.  Let’s instead use the two most common different aspect ratios, 16:9 ( most Android devices, many consoles running at 1080p ) and 4:3 ( the iPad and SD NTSC signals ):

 

16:9 results:

image

 

4:3 results:

image

 

While much closure, the results are still going to look rather awful.  There are two ways to deal with this, both have their merits.

 

First, is create a version for each major aspect ratio.  This actually makes a great deal of sense although it can be a pain in the butt.  The final results are the best, but you double your burden for art assets.  We will talk about managing multiple resolution art assets in a different tutorial at a later date.

 

Second, you pick a native aspect ratio for your game to run at, then use a viewport to manage the different aspect ratios, just like you use the aspect button on your TV.  Since this tutorial is getting pretty long, we will cover Viewports in the next section, so stay tuned!

 

Programming , , ,

3. December 2014

 

In this video tutorial we look at all aspects of audio programming in LibGDX.  We look at how to play back sound effects, stream music audio we even record audio from the microphone and play it back.  We also touch on a couple important concepts like cross platform file formats and creating transitions between music tracks.

 

You can see the video in full 1080p resolution here.  All of the code used in this video is presented below.  For more LibGDX video tutorials, go here.

 

 

 

Sound Effects

 

Playing a Sound Effect

ackage com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.utils.Timer;

public class SoundFXDemo extends ApplicationAdapter {

   Sound sound;
   @Override
   public void create () {
      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreak.wav"));
      sound.play();
   }

   @Override
   public void render () {
   }

   @Override
   public void dispose() {
      sound.dispose();
   }
}

 

Setting Volume

      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreak.wav"));
      long id = sound.play(1.0f);
      sound.setVolume(id,1.0f);

 

Setting Pitch

      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreak.wav"));
      long id = sound.play();
      sound.setPitch(id,0.2f);

 

Setting Pan

      // Track must be in mono format to support panning
      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreakMono.wav"));
      long id = sound.play();
      sound.setPan(id,1.0f,1f);

 

Setting Volume, Pitch and Pan all at once

      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreakMono.wav"));
      sound.play(1.0f,0.0f,0.8f);

 

Looping and Pausing

      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreak.wav"));
      sound.loop();

      // Wait 10 seconds, then pause playback
      Timer.schedule(new Timer.Task(){
         @Override
         public void run(){
            sound.pause();
         }
      }, 10);

 

Multiple Concurrent Sounds

      sound = Gdx.audio.newSound(Gdx.files.internal("glassBreakMono.wav"));

      // Play 2, one fast, one slow
      final long fastSound = sound.loop(1f,2.5f,0f);
      final long slowSound = sound.loop(1f,0.5f,0f);

      // Wait 5 seconds, then stop ALL
      Timer.schedule(new Timer.Task() {
         @Override
         public void run() {
            //sound.stop();
            // Or stop just one
            sound.stop(fastSound);
         }
      }, 5);

 

 

Music

 

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.utils.Timer;

public class MusicDemo extends ApplicationAdapter {

   Music song1,song2;

   @Override
   public void create () {
      song1 = Gdx.audio.newMusic(Gdx.files.internal("song1.mp3"));
      song2 = Gdx.audio.newMusic(Gdx.files.internal("song2.mp3"));

      song1.play();


      // Then set it so song 2 plays when song1 ends
      song1.setOnCompletionListener(new Music.OnCompletionListener() {
         @Override
         public void onCompletion(Music music) {
            song2.play();
         }
      });

      // You can pan and set volume like sound fx, but cant play multiple instances and cant set pitch
      // You can however get the position of the sound ( but not set it )
      // Let's do a timer that lower the volume over the last 5 seconds of the song
      // Built in options are pretty limited, so there is no way to poll for the length of a song
      // Without using the audio-extension
      Timer.schedule(new Timer.Task() {
         @Override
         public void run() {
            if(song1.isPlaying())
               if(song1.getPosition() >= 10)
                  song1.setVolume(song1.getVolume() - 0.2f);
         }
      },10,1,4);

   }

   @Override
   public void render () {
   }

   @Override
   public void dispose() {
      song1.dispose();
      song2.dispose();
   }
}

 

Audio Recording and Playback

 

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.AudioDevice;
import com.badlogic.gdx.audio.AudioRecorder;

public class AudioRecorderDemo extends ApplicationAdapter {

   @Override
   public void create () {

      // This code may not actually work during video recording due to Microphone being in use.
      AudioRecorder recorder = Gdx.audio.newAudioRecorder(44100,true);
      short[] audioBuffer = new short[44100 * 5];

      // read() will fail if device doesnt support given resolution or a mic is not attached
      recorder.read(audioBuffer,0,audioBuffer.length);

      AudioDevice audioDevice = Gdx.audio.newAudioDevice(44100,true);
      audioDevice.writeSamples(audioBuffer,0,audioBuffer.length);
      audioDevice.dispose();
      recorder.dispose();
      Gdx.app.exit();
   }

   @Override
   public void render () {
   }
}

Programming , ,

24. November 2014

 

In this video tutorial, we look at handling Keyboard, Mouse and Touch input in LibGDX.  We look at both handling input via polling as well as an event driven approach.

 

You can see the video in full 1080p definition here.  Once again, all the code included in the video is available below:

 

 

Polled Input Sample

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class PolledInputDemo extends ApplicationAdapter {
   SpriteBatch batch;
   Texture img;
   Sprite sprite;
   
   @Override
   public void create () {
      batch = new SpriteBatch();
      img = new Texture("badlogic.jpg");
      sprite = new Sprite(img);
      sprite.setPosition(Gdx.graphics.getWidth()/2 - sprite.getWidth()/2,
            Gdx.graphics.getHeight()/2 - sprite.getHeight()/2);
   }

   @Override
   public void render () {

      // Keyboard events
      if(Gdx.input.isKeyPressed(Input.Keys.LEFT))
         sprite.translateX(-1f);
      if(Gdx.input.isKeyPressed(Input.Keys.RIGHT))
         sprite.translateX(1f);
      if(Gdx.input.isKeyPressed(Input.Keys.SPACE))
         sprite.setPosition(Gdx.graphics.getWidth()/2 - sprite.getWidth()/2,
               Gdx.graphics.getHeight()/2 - sprite.getHeight()/2);

      if(Gdx.input.isButtonPressed(Input.Buttons.RIGHT))
         sprite.setPosition(Gdx.input.getX(),Gdx.graphics.getHeight() - Gdx.input.getY());


      Gdx.gl.glClearColor(0, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
      batch.begin();
      batch.draw(sprite, sprite.getX(), sprite.getY());
      batch.end();
   }

   @Override
   public void dispose(){
      img.dispose();
   }
}

Event Driven Input Sample

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class EventDrivenInputDemo extends ApplicationAdapter implements InputProcessor {
   SpriteBatch batch;
   Texture img;
   Sprite sprite;
   boolean movingRight = false;
   
   @Override
   public void create () {
      batch = new SpriteBatch();
      img = new Texture("badlogic.jpg");
      sprite = new Sprite(img);
      sprite.setPosition(Gdx.graphics.getWidth()/2-sprite.getWidth()/2,
            Gdx.graphics.getHeight()/2 - sprite.getHeight()/2);

      Gdx.input.setInputProcessor(this);
   }

   @Override
   public void render () {

      if(movingRight)
         sprite.translateX(1f);
      Gdx.gl.glClearColor(0, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
      batch.begin();
      batch.draw(sprite, sprite.getX(),sprite.getY());
      batch.end();
   }

   @Override
   public boolean keyDown(int keycode) {
      if(keycode == Input.Keys.RIGHT)
         movingRight = true;
      return true;
   }

   @Override
   public boolean keyUp(int keycode) {
      if(keycode == Input.Keys.LEFT)
         sprite.translateX(-1f);
      if(keycode == Input.Keys.RIGHT)
         movingRight = false;
      return true;
   }

   @Override
   public boolean keyTyped(char character) {
      return false;
   }

   @Override
   public boolean touchDown(int screenX, int screenY, int pointer, int button) {
      return false;
   }

   @Override
   public boolean touchUp(int screenX, int screenY, int pointer, int button) {
      return false;
   }

   @Override
   public boolean touchDragged(int screenX, int screenY, int pointer) {
      // If the user is holding down ( or was holding down, and hasnt released ) three fingers, move the sprite
      if(pointer ==2)
         sprite.setPosition(screenX,Gdx.graphics.getHeight()-screenY);
      return true;
   }

   @Override
   public boolean mouseMoved(int screenX, int screenY) {
      sprite.setPosition(screenX,Gdx.graphics.getHeight()-screenY);
      return true;
   }

   @Override
   public boolean scrolled(int amount) {
      if(amount > 0)
         sprite.translateY(1f);
      if(amount < 0)
         sprite.translateY(-1f);

      return true;
   }
}

Programming , , ,

21. November 2014

 

In the last video tutorial we used a graphic instead of text to create a Hello World.  This is because drawing text is actually a multi step process in LibGDX and not really appropriate for a first tutorial.  It is however perfect for a second tutorial, so here we are! ;)

 

In this video we explore the difference between TTF and Bitmap fonts, show how to run the Hiero font generation tool in both Eclipse and IntelliJ IDEA then create and save a bitmap font.  We then explore the code needed to show a bitmap font on screen, including how to measure the results, apply color and text justification.

 

The video is available in up to 1080P on YouTube by clicking here.

 

The source code:

Initial Example – Loading a font and drawing text:

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class gdxtext extends ApplicationAdapter {
   SpriteBatch batch;
    BitmapFont font;

   @Override
   public void create () {
      batch = new SpriteBatch();
        font = new BitmapFont(Gdx.files.internal("Papy.fnt"));
   }

   @Override
   public void render () {
      Gdx.gl.glClearColor(0, 0, 0, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
      batch.begin();

        //Example One -- Drawing Text
        font.draw(batch,"Hello World",Gdx.graphics.getWidth()/2,Gdx.graphics.getHeight()/2);

        batch.end();
   }
}

 

Example 2 – Measuring and centering text:

BitmapFont.TextBounds bounds = font.getBounds("Hello World");
font.draw(batch,"Hello World",
       Gdx.graphics.getWidth()/2 - bounds.width/2,
       Gdx.graphics.getHeight()/2 + bounds.height/2);

Example 3 – Multi-line Text

String debugString = "I took one, one cause you left me\n"
           + "Two, two for my family\n"
           + "Three, three for my heartache\n"
           + "Four, four for my headaches\n"
           + "Five, five for my sorrow\n";
   BitmapFont.TextBounds bounds = font.getMultiLineBounds(debugString);

 

Example 4 -- Center justified text in the colour purple

    font.setColor(Color.PURPLE);
    font.drawMultiLine(batch,
            debugString,
            0,
            Gdx.graphics.getHeight()/2 + bounds.height/2,
            Gdx.graphics.getWidth(),
            BitmapFont.HAlignment.CENTER
            );

Programming , , ,

Month List

Popular Comments

A closer look at the Loom Game Engine, Part Four: Graphics
Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


Home > >

19. March 2013

In part 3 we created a simple Loom application. 

In this section, we are going to look at how to draw graphics using the Loom game engine.  I am going to make use of a sprite sheet I obtained here.  Feel free of course to substitute whatever image you want.  Keep in mind, I have no rights to that image, so neither do you!  If you are the author and want it removed, let me know and I will.

 

Anyways, lets jump right in and draw a single sprite centred on screen.

 

package

{

    import cocos2d.Cocos2DGame;

    import cocos2d.CCSprite;

    import cocos2d.Cocos2D;

 

    public class HelloWorld extends Cocos2DGame

    {

        override public function run():void

        {

            super.run();

            var sprite = CCSprite.createFromFile("assets/robin.png");

            

            sprite.x = Cocos2D.getDisplayWidth()/2;

            sprite.y = Cocos2D.getDisplayHeight()/2;

            layer.addChild(sprite);

        }

    }

}

 

The code is pretty much identical to our Hello World code earlier.  As you can see, CCSprite elements ( CCNode derived more accurately ) objects are positioned relative to their centre by default, while the positioning is relative to the bottom left corner of the screen.

Run this code and you will see:

Loom Sprite Example

 

As you can see from the bottom corner, we are running at 60 FPS, but considering we aren't doing anything, thats not really all that impressive.

 

 

Let's see what happens when we dial it up to 1000.

package

{

    import cocos2d.Cocos2DGame;

    import cocos2d.CCSprite;

    import cocos2d.Cocos2D;

 

    public class HelloWorld extends Cocos2DGame

    {

        override public function run():void

        {

            //var sprites:Array = new Array();

            var sprites = new Vector.<CCSprite>(); 

            var i:int = 0;

 

            for(i = 0; i < 1000; i++){

                var sprite = CCSprite.createFromFile("assets/robin.png");    

                sprite.x = Math.random() * Cocos2D.getDisplayWidth();

                sprite.y = Math.random() * Cocos2D.getDisplayHeight();

 

                sprites.push(sprite);

            }

 

            for each( var spr in sprites){

                layer.addChild(spr);     

            }

 

            super.run();

        }

    }

}

 

This code shows one of the differences from ActionScript.  In comments you can see how the ActionScript array would work.  Since LoomScript is typed you can't do this, so instead your create a Vector of type CCSprite.  Once created though, its functionally identical to an array, at least on the surface.  We simply loop a thousand times, randomizing the position within the screen limits.  We then loop through all of the sprites ( yes, I realize I could have simply done this in the first for loop ) in the array and add them to our layer.  This is an area I found a bit odd, from my C++ warped programming mind, I couldn't re-use the name sprite in my foreach scope because it recognized the previous variable 'sprite' as being in local scope.  I don't know if this odd scoping is an ActionScript thing, or LoomScript thing.

Here is our application running:

A Flock of Robins

 

Down to 24FPS… hmm, 1000 sprites isn't an unrealistic amount, let's see if we can't speed that up a bit.  Generally there is a sprite batching system available, for when you are drawing similar images over and over, and Loom is no exception.

Here is the same application using CCSpriteBatchNode:

package

{

    import cocos2d.Cocos2DGame;

    import cocos2d.CCSprite;

    import cocos2d.Cocos2D;

    import cocos2d.CCSpriteBatchNode;

 

    public class HelloWorld extends Cocos2DGame

    {

        override public function run():void

        {

            //var sprites:Array = new Array();

            var sprites = new Vector.<CCSprite>(); 

            var i:int = 0;

 

            var spriteBatch = CCSpriteBatchNode.create("assets/robin.png");

            layer.addChild(spriteBatch);

            for(i = 0; i < 1000; i++){

                var sprite = CCSprite.createFromFile("assets/robin.png");    

                sprite.x = Math.random() * Cocos2D.getDisplayWidth();

                sprite.y = Math.random() * Cocos2D.getDisplayHeight();

 

                sprites.push(sprite);

            }

 

            for each( var spr in sprites){

                spriteBatch.addChild(spr);     

            }

 

            super.run();

        }

    }

}

 

 

 Now when we run this newly updated version:

 

Sprites using spritebatching

 

A net gain of 13FPS.  Of course, there are probably a few thousand optimizations I could perform, but its nice to see the most common ones are in there.

 

Up until this point, we've only used one sprite in our sprite sheet.  Let's take a look at performing an animation using them all.

Here again is our source image:

Robins

 

The image is 1200x1570 in dimension and contains a 5x5 grid of sprites ( except the last three that is ).

 

Let's take a look at the code behind animating a sprite sheet ( or texture atlas, pick your poison ):

package

{

    import cocos2d.Cocos2DGame;

    import cocos2d.CCSprite;

    import cocos2d.Cocos2D;

    import cocos2d.CCAnimation;

    import cocos2d.CCSpriteFrame;

    import cocos2d.CCRect;

    import cocos2d.CCPoint;

    import cocos2d.CCSize;

    import cocos2d.CCAnimationCache;

 

    public class HelloWorld extends Cocos2DGame

    {

 

        public var sprite:CCSprite;

        

        public var currentFrame:int;

 

        public var lastFrameTime:Number;

        public var elapsedTime:Number;

 

 

        override public function run():void

        {

            lastFrameTime = 0;

 

            var animation:CCAnimation;

 

            currentFrame = 0;

            sprite = CCSprite.create();

            sprite.x = Cocos2D.getDisplayWidth()/2;

            sprite.y = Cocos2D.getDisplayHeight()/2;

 

            animation = CCAnimation.animation(); // Why not createAnimation()?

 

            var frameRect:CCRect;

            var frameSize:CCSize;

            frameSize.width = 240;

            frameSize.height = 314;

 

            for(var i =0; i < 5; i++){

                for(var j =0; j < 5; j++){

                    if(i == 4 && j > 1)

                        break;

                    frameRect.setRect(j * frameSize.width, i * frameSize.height, frameSize.width, frameSize.height);

                    var frame = CCSpriteFrame.create("assets/robins.png",frameRect,false,new CCPoint(), frameSize);

                    animation.addSpriteFrame(frame);

                }

            }

            CCAnimationCache.sharedAnimationCache().addAnimation(animation, "fly");

 

            layer.addChild(sprite);

            super.run();

        }

 

        override public function onTick():void {

            var thisFrameTime:Number = Platform.getTime();

            var delta:Number = thisFrameTime - lastFrameTime;

            

            lastFrameTime = thisFrameTime;

            elapsedTime += delta;

 

            if(elapsedTime > 100){

                elapsedTime = 0;

                sprite.setDisplayFrameWithAnimationName("fly",currentFrame);

                

                if(currentFrame > 20){

                    currentFrame = 0;

                }

                else{

                    currentFrame++;

                }

            }

        }

    }

}

 

Now when you run the application, you should see:


RobinFlight

 

The heart of this code is a pair of loops that create a Rect representing the location of the frame within the parent image.  Each frame is created as a CCSpriteFrame, all using the same source image file as well as the newly created rect for the frames location within the sprite sheet.  We then add each CCSpriteFrame to the CCAnimation variable with the call addSpriteFrame().

 

Now that our CCAnimation is fully populated, we add it to a global animation cache by calling CCAnimationCache.sharedAnimationCache().addAnimation(), passing in our newly created CCAnimation, as well as a string that we will access it by later.

 

This is an area I found quite annoying to deal with Loom, or more specifically Cocos2D.  I don't really like the idea of using global managers if I don't have to, so I attempted to just keep a CCAnimation locally and use it to populate my CCSprite each frame.  You can't, or at least, you can't easily.  All (Cocos2D, not Loom) samples you will find either lead to a deprecated method in CCSprite, or down a wild goose chase of Animation related functionality in Cocos2D.  There seem to be a dozen ways to perform animations, little of which work with each other.  The Cocos2D library is definite need of streamlining!  I also ran into inconsistent naming conventions, like the above mentioned CCAnimation.animation() call.  The convention generally is CCAnimation.create() or CCAnimation.createAnimation(), and I believe both exist, so why break with the naming convention here?  This is one of those things I ran into with Cocos2d-html and it's frustrating and makes learning and working with the SDK harder.  This isn't a bash on Loom, it's functionality it inherited from Cocos2D, but one is thoroughly tied to the other.

 

Our remaining code is the onTick handler, which will be called every iteration of the game loop.  We want to advance to the next frame after 100 milliseconds have elapsed, so we figure out how much time has elapsed since the prior frame and add it to our running total.  Once 100ms is reached, we advance to the next frame in the animation we cached earlier, with the call sprite.setDisplayFrameWithAnimationName().


As you can see though, once you puzzle out the convoluted hierarchy of classes provided by Cocos2D, drawing a sprite, drawing a sprite from a sprite sheet, and animating between frames is a rather easy task in Loom.

 

I did run into another snag, specific to LoomScript, on using the TimeManager class in place of Platform.getTime(), but truth of the matter, it's probably me not understanding dependency injection.  It's the first time I haven't been able to puzzle something out myself, which is rather impressive for a new library.  It is also the first time I have personally used their support forum.  It's been less than an hour since I posted, and I've already received a useful reply, so I have to give them thumbs up for that!

 

In the next part we will look at controlling your application and maybe a bit more, as we near the end of this tour.

, ,

blog comments powered by Disqus

Month List

Popular Comments