9. December 2014

 

In this video tutorial we look at using the different types of Cameras available, using a Camera to position your world in a device independent way.  Next we discuss the various Viewport options available for making your render results look best across a number of devices.  Since this video was released at the same time as the text version of the tutorial, I will be linking to those tutorials for code examples.

 

You can see the full 1080p video directly on YouTube or embedded below.

 

 

For the text tutorials, or for the code or assets used in this tutorial, check this tutorial on Cameras and this tutorial on Viewports.

 

Additionally, the text only tutorial also covers converting coordinates too and from your world coordinates, something I forgot to do in the video tutorial.

Programming , , ,

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 , ,

27. November 2014

 

 

Now we are going to look at using the P2 Physics engine.  Truth of the matter is, the experience is very similar to what we just experienced with Arcade physics.  The biggest difference is P2 is more accurate, has greater options for simulation and critically, takes up more CPU power.

 

Let’s jump in straight away with a simple example.  We are going to load two identical sprites, anchor one and create a spring between them.  Code time!

/// <reference path="phaser.d.ts"/>
class SimpleGame {
    game: Phaser.Game;
    player1: Phaser.Sprite;
    player2: Phaser.Sprite;

    constructor() {
        this.game = new Phaser.Game(640, 480, Phaser.AUTO, 'content', {
            create: this.create, preload: this.preload
        });
    }
    preload() {
        this.game.load.image("decepticon", "decepticon.png");
    }
    create() {
        this.player1 = this.game.add.sprite(this.game.width/2, 0 + 50, "decepticon");
        this.player2 = this.game.add.sprite(this.game.width/2, this.game.height, "decepticon");

        this.game.physics.startSystem(Phaser.Physics.P2JS);

        // Enabled physics on our sprites
        this.game.physics.p2.enable([this.player1, this.player2]);

        // Make our one body motionless
        this.player1.body.static = true;
        
        // Now create a sprite between our two bodies, parameters are rest length, stiffness and damping
        // Rest length is the length of the spring at rest ( where it's not under pressure )
        // Stiffness is the resistance to movement of the spring
        // Damping determines how fast the spring loses it's "boing"  Our low damping keeps our spring "boinging"
        // Boing is a word I made up to describe the up and down motion of a spring doing it's spring thing
        this.game.physics.p2.createSpring(this.player1, this.player2, 200, 2, 0.3);

        // Lets loop a timed event every 10 seconds that moves the one end of our spring back to the start
        // Mostly just so people that didn't see it run the first time in the browser have something to see!
        this.game.time.events.loop(Phaser.Timer.SECOND * 10, () => {
            this.player2.body.x = this.game.width/2;
            this.player2.body.y = this.game.height;
            }, this);

    }
}

window.onload = () => {
    var game = new SimpleGame();
};

The code is fairly well commented, so I'll just go through the gist of what's happening here.  Just like before, you have to start the Physics subsystem, this time we pass in Phaser.Physics.P2JS.  You also have to register your objects with the physics system.  In this case we do it by passing an array of Sprite to p2.enable();  We set one of our physics bodies to static, which will cause it to not be moved or updated by the physics system.  We then create a spring between the two sprites.  This will cause the second sprite to “spring” toward the first, until the rest length is met ( plus any momentum ), then the spring will, well, spring.  Finally, just to make the demo run a bit better in the browser, we schedule a looping event every 10 seconds that resets the second sprites physics body back to the starting position to start the whole game all over again. 

 

Here is our code running in the browser:

 

One of the big advantages to P2 is it can use Polygon data for more precise collision calculates than body boxes.  Consider the following sprite:

 

megatron

 

When doing bounding box based collisions tests, this would be considered a “hit”:

 

BoundingBox

 

Ideally instead we should create a tighter bounding volume for our collision tests, like so:

BoundingPoly

 

Well, the good news is we can, but we need a third party editor to create the bounding volumes.  One such tool and the one I am going to use in this example is Physics Editor by CodeAndWeb.  It’s commercial software, but there is a free version with the following restrictions:

 

image

 

It can be purchased currently for about $20.

 

CURRENTLY, I COULD NOT GET THE CURRENT WINDOWS VERSION TO EXPORT!

There is a DLL missing, I’ve reported it to the developer, for now instead use the older version available here.

 

For this, I am just using the unregistered version.  Load it up, then click Add Sprites:

image

 

Navigate to the sprite you want to work with and click OK.  It should appear in the window like so:

image

 

I’m full of serious lazy, so I’m going to let the app do the work, simply click the Shape Tracer icon:

image

 

Now in the resulting dialog, play with the tolerance and alpha value until you have a bounding volume you like with a vertex count you can deal with:

image

 

Once you are happy with it, set the Exporter on the right to Lime + Corona ( JSON ).

image

 

Save the resulting file to your project directory.

 

Now time to actually use the results in Phaser:

/// <reference path="phaser.d.ts"/>
class SimpleGame {
    game: Phaser.Game;
    player1: Phaser.Sprite;
    player2: Phaser.Sprite;

    constructor() {
        this.game = new Phaser.Game(640, 480, Phaser.AUTO, 'content', {
            create: this.create, preload: this.preload
        });
    }
    preload() {
        this.game.load.image("megatron", "megatron.png");
        this.game.load.physics("physicsInfo", "physics.json");
    }
    create() {
        this.player1 = this.game.add.sprite(0, 0, "megatron");
        this.player2 = this.game.add.sprite(0, 0, "megatron");

        // Being lazy, positioning sprite after creation so we have a valid width/height
        this.player1.position.set(this.player1.width / 2, this.player1.height / 2);
        
        // Now another sprite on the right side of the screen, down slightly
        this.player2.position.set(this.game.width - (this.player2.width / 2),  this.player2.height / 2 + 85);

        

        this.game.physics.startSystem(Phaser.Physics.P2JS);
        
        // Passing in true while enabling physics on an object causes the debug renderer to draw the physics body
        this.game.physics.p2.enable([this.player1, this.player2], true);

        // You need to call clearShapes() to get rid of the existing bounding box
        this.player1.body.clearShapes();
        this.player2.body.clearShapes();

        // Now load the polygon bounding data we created externally
        this.player1.body.loadPolygon("physicsInfo", "megatron");
        this.player2.body.loadPolygon("physicsInfo", "megatron");

        // Now let's get this party started
        this.player2.body.moveLeft(80);

        // Finally, when the collision occurs, move back to the beginning and start over
        this.player2.body.onBeginContact.add((body, shapeA, shapeB, equation) => {
            this.player2.body.x = this.game.width - (this.player2.width / 2);
            this.player2.body.y = this.player2.height / 2 + 85;
            }, this);
    }
}

window.onload = () => {
    var game = new SimpleGame();
};

 

WARNING!  When using P2 physics, if your physics body starts even slightly outside the bounds of the screen, it will start with forces applied against it!

 

When you run this code, it creates two identical Megatrons, on each side of the screen, slightly offset, then sends one towards the other… ok, let’s just watch it:

 

As you can see, using Polygon volumes allow for much greater precision in your collision tests.  Using simple bounding boxes, the collision would occur a great deal earlier.

 

There is a lot more about P2 physics, but in all honesty, Polygon’s are probably the single biggest reason to use it.  Again it is important to remember that P2 is the slowest option, but the most precise.  Also, you have the option of mixing and matching physics engines within the same game.

 

Programming , ,

Month List

DisqusCommentsSummary