Fun with Phaser

12/31/16

Categories: Development Tags: Game Development

~/contents

Fun With Phaser HTML5 Framework

I’ve been interested in working on an HTML5 game and incorporating game-like elements into my site. There are a lot of engines out there so I made a list of criteria to narrow it down.

Phaser looks promising, it uses pure Javascript and has a really active community and lots of tutorials, including PhotonStorm’s development blog. Phaser requires a server for development. I used XAMPP back when I had a Wordpress site (yeah, yeah, nobody’s perfect) but it’s been a while so I’ll have to reacquaint myself with the process.

Learning Phaser 2.6.2 and Associated Hiccups

I found I can use Python’s SimpleHTTPServer module for development, which save me a little trouble. Python, I’ll always be true to you. <3

python -m SimpleHTTPServer

There are many Phaser tutorials, however, one thing I quickly noticed is developers are using different methods to build their base games. Sometimes a tutorial will say, “After you build your base game…” and then provides code I’m not immediately sure how to incorporate, being unfamiliar with Javascript. (If I were doing this the right way, I would learn a bit of Javascript before attempting to use this framework, but I’m not going to do this the right way.) The official Phaser beginner tutorial uses a quick-and-dirty method (by their own admission) and most other developers are organizing their code differently, which means they have slightly different syntax. Also, it appears Phaser has changed a lot during development and some tutorials are outdated.

I am now working off of this base template:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>Phaser Project</title>
  <script type="text/javascript" src="js/phaser.js"></script>
  <style>
    body {
      padding:0px;
      margin:0px;
      background:black;
    }
  </style>
</head>
<body>
  <script src="js/main.js"></script>
</body>
</html>

{% endhighlight %}

{% highlight javascript %} //main.js var game = new Phaser.Game(640,360,Phaser.AUTO);

var GameState = { preload: function(){ }, create: function() { }, update:function() { } };

game.state.add('GameStateName', GameState); game.state.start('GameStateName');

I initially had problems developing with Firefox. The game is sluggish and I had issues with tilemaps rendering properly. Switching to a fresh profile on Chrome helped. You can debug using the developer console. I learned that if I should be seeing a change, but I’m not, I probably need to dump the cache/history.

My first goal is to build a basic tilemap with a character that can wander the map and trigger links to other pages or sites.

Phaser and Tiled: Maps, Objects, Collisions

It took me longer to sort out using Tiled tile and object maps than I’d care to admit, but most of that is user error. I spent hours puzzling over importing object layers before I realized I was confusing the object ID, which is listed in the Tiled editor, with the GID, which is apparently only listed in the JSON. Placing objects by hand isn’t a reasonable solution so I knew I had to figure that part out before I did anything else. I had issues with my character “jumping” and continuing on into outer space until I realized I’d left an old up.isDown check in there from when I was messing with a top-down layout.

The Phaser example library has been useful, as is the documentation. Tutorials scattered around the Internet have been hit and miss, in part because several are outdated or use shortcuts instead of the baked-in commands, which is confusing to a new user. IIRC in every instance I ended up going back to the example library for the cleanest example.

I now have a basic framework where a critter (who looks a little like the NeoCities’ mascot) can walk and jump on a wrapped map. If the player presses UP in a doorway the browser opens the assigned url, which is set in Tiled (the nice object scaling is thanks to this user kindly sharing their solution, but in my case I set the position and then scale up the objects). I tried to keep this as simple and readable as possible. The HTML is the same as the base I posted above.

You will notice a big issue–slope collision. I learned that this is the result of Phaser’s arcade physics, which treats everything as a square or rectangle. We need Ninja Physics, which is complex enough to reserve for the next version.

Gimp has a very handy sprite sheet plugin that allows you to easily create a sprite sheet from layers. Right now our critter only has a walking animation, but next time I’ll add pzUH’s jump and fall frames so it looks nicer.

Resources: pzUH’s Cat & Dog Sprites and Carl Olsson’s Simple NES-like Village Tiles. The music is Satie’s Gymnopédie No. 1.

// main.js
var game = new Phaser.Game(window.innerWidth,window.innerHeight,Phaser.AUTO);
var speed = 300;
var doorway = false;
var jumpTimer = 0;
var gameScale = 2;

var GameState = {
  preload: function(){
    game.load.spritesheet('dog_walk','assets/dog_walk.png',64,64);
    game.load.tilemap('MyTilemap','assets/townmap.json', null, Phaser.Tilemap.TILED_JSON);
    game.load.image('MyTiles', 'assets/village.png', 16,16);
    game.load.audio('gymnopedie', ['assets/gymnopedie_no_1.ogg']);
  },
  create: function() {
    cursors = game.input.keyboard.createCursorKeys();
    spacekey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

    // on UP we will check to see if they're in a doorway, if so, we open that url
    cursors.up.onDown.add(this.activateDoor, this);

    game.physics.startSystem(Phaser.Physics.ARCADE);

    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;

    map = game.add.tilemap('MyTilemap');
    map.addTilesetImage('village', 'MyTiles');
  	blocklayer = map.createLayer('BlockedLayer');
    layer = map.createLayer('GroundLayer');
    layer.setScale(gameScale); // scales up 16x16 tiles
    blocklayer.setScale(gameScale);

    map.setCollisionBetween(1, 100000, true, 'BlockedLayer');
    layer.resizeWorld();
    blocklayer.resizeWorld();
    layer.wrap = true;
    blocklayer.wrap = true;

    // we pull all doors from the tiled object layer and confirm the door was added
    doors = game.add.group();
    doors.enableBody = true;
    map.createFromObjects('ObjectLayer', 65, '', 1, true, false, doors);
    doors.forEach(function(door) {
      door.x *= gameScale; *
	    door.y *= gameScale;*
      door.scale.set(gameScale);
      console.log(door.url + ' doorway exists');
     });

    player = game.add.sprite(0,0,'dog_walk');
    player.anchor.setTo(.5,.5); // this lets us flip the sprite when walking left
    game.physics.arcade.enable(player);
    game.camera.follow(player);

    player.body.bounce.y = 0.1;
    player.body.gravity.y = 200;
  	player.animations.add('walk',null,10,true);

    music = game.add.audio('gymnopedie');
    music.play();
  },
  update:function() {
    var onGround = game.physics.arcade.collide(player,blocklayer);
    var inDoorway = game.physics.arcade.overlap(player, doors, this.enterDoor);

    // if no keypress player stops walking
    player.body.velocity.x = 0;

    // warps player to opposite side when edge is reached
    game.world.wrap(player, 0, true);

    if (cursors.left.isDown)
  	{
  		player.body.velocity.x = -speed;
      player.scale.setTo(-1, 1);
      player.animations.play('walk');
  	}
  	else if (cursors.right.isDown)
  	{
      player.scale.setTo(1, 1);
      player.body.velocity.x=speed;
  		player.animations.play('walk');
  	}
  	else if (cursors.down.isDown && onGround)
  	{
  		player.body.velocity.y=0;
  	}
  	else
  	{
  		player.animations.stop();
  		player.frame=1;
  	}

    // jump
    if (spacekey.isDown && player.body.onFloor() && game.time.now > jumpTimer)
  	{
  		player.body.velocity.y = -200;
      jumpTimer = game.time.now + 500;
  	}

    //reset doorway trigger
    if (doorway && (!inDoorway))
    {
      doorway = false;
    }

  },
  enterDoor: function(player, door) {
    //ensures this fucntion only triggers once if doorway is false
    if (!doorway)
    {
      console.log('entered doorway!');
      doorway = door;
    }
  },
  activateDoor: function() {
    if (doorway)
    {
      console.log('travelling to ' + doorway.url);
      window.open(doorway.url);
    }
  },
};

game.state.add('GameStateName', GameState);
game.state.start('GameStateName');

Brief aside, one of my favorite things in the world right now is to be working on a game while Bookworm is playing with toys in my office floor, the more underfoot the better. I just like having my little buddy around.

Version 2: Extended animation, slope physics, pixel-perfect scaling

Extending our critter’s animations is easy.

player.animations.add('walk',[0,1,2,3,4,5,6,7,8,9],10,true);
player.animations.add('jump',[10,11,12,13,14,15,16,17],5,false);
player.animations.add('fall',[18,19,20,21,22,23,24,25],10,false);

If moving on the ground, we walk. If the spacebar is pressed, we jump. If our y velocity is over 0 and we’re not on the ground, we’re falling. This is my second platformer-type-thingie so I have already sorted out some platformer logic.

Now, about Ninja physics… it turns out Ninja physics are currently depreciated because people didn’t use the feature. After reviewing the options, I feel that using the arcade slopes plugin is the best way to enable slope physics. I usually try not to rely heavily on other people’s code (that I don’t understand) in the learning phase, but this plugin fills a distinct gap and I would be surprised if it wasn’t incorporated into a later version of Phaser. For simplicity’s sake, I recommend using a ninja physics debug sheet on your collision layer. There is a bug workaround and slopes need to be enabled in a certain order–see my notes in the code. With arcade slopes enabled you’ll have to tweak gravity and any jump velocities, otherwise your critter will fly up into the air whenever it hits a slope.

It took a bit of fiddling to sort out pixel-perfect scaling, the solution that worked for me is thanks to thelucre, who kindly shared their solution. In Javascript:

Phaser.Canvas.setImageRenderingCrisp(game.canvas);
PIXI.scaleModes.DEFAULT = PIXI.scaleModes.NEAREST;
game.stage.smoothed = false
Phaser.Canvas.setSmoothingEnabled(game.context, false);

The game actually looks a bit better with 16px scaled up 3 times, but for now we’ll stick with 2.

Of note, I am not sure what sort of ancient dark magic is used to generate the GID for Object layers, but mine changed whenever I saved the map under a different name. If my objects all disappeared, it was because the GID was different.

With this version we have a basic platformer template.

//main.js
var game = new Phaser.Game(window.innerWidth,window.innerHeight,Phaser.CANVAS);
var speed = 250;
var doorway = false;
var jumpTimer = 0;
var gameScale = 2;

var GameState = {
  preload: function(){
    Phaser.Canvas.setImageRenderingCrisp(game.canvas);
    PIXI.scaleModes.DEFAULT = PIXI.scaleModes.NEAREST;
    game.stage.smoothed = false
    Phaser.Canvas.setSmoothingEnabled(game.context, false);

    game.load.spritesheet('dog','assets/dog_animations.png',52,64);
    game.load.tilemap('MyTilemap','assets/townmap.json', null, Phaser.Tilemap.TILED_JSON);
    game.load.image('MyTiles', 'assets/village.png', 16,16);
    game.load.image('MyNinjaTiles', 'assets/ninja-tiles16.png',16,16);
  },
  create: function() {
    game.plugins.add(Phaser.Plugin.ArcadeSlopes);
    cursors = game.input.keyboard.createCursorKeys();
    spacekey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

    // on UP we will check to see if they're in a doorway, if so, we open that url
    cursors.up.onDown.add(this.activateDoor, this);
    game.physics.startSystem(Phaser.Physics.ARCADE);

    map = game.add.tilemap('MyTilemap');
    map.addTilesetImage('village', 'MyTiles');
    map.addTilesetImage('ninja-tiles16', 'MyNinjaTiles');
    layer = map.createLayer('GroundLayer');
  	blocklayer = map.createLayer('BlockedLayer');
    blocklayer.alpha = 0.0
    layer.setScale(gameScale); // scales up 16x16 tiles
    blocklayer.setScale(gameScale);
    layer.resizeWorld();
    blocklayer.resizeWorld();
    layer.wrap = true;
    blocklayer.wrap = true;

    game.slopes.convertTilemapLayer(blocklayer, {
      2:  'FULL',
      3:  'HALF_BOTTOM_LEFT',
      4:  'HALF_BOTTOM_RIGHT',
      6:  'HALF_TOP_LEFT',
      5:  'HALF_TOP_RIGHT',
      15: 'QUARTER_BOTTOM_LEFT_LOW',
      16: 'QUARTER_BOTTOM_RIGHT_LOW',
      17: 'QUARTER_TOP_RIGHT_LOW',
      18: 'QUARTER_TOP_LEFT_LOW',
      19: 'QUARTER_BOTTOM_LEFT_HIGH',
      20: 'QUARTER_BOTTOM_RIGHT_HIGH',
      21: 'QUARTER_TOP_RIGHT_HIGH',
      22: 'QUARTER_TOP_LEFT_HIGH',
      23: 'QUARTER_LEFT_BOTTOM_HIGH',
      24: 'QUARTER_RIGHT_BOTTOM_HIGH',
      25: 'QUARTER_RIGHT_TOP_LOW',
      26: 'QUARTER_LEFT_TOP_LOW',
      27: 'QUARTER_LEFT_BOTTOM_LOW',
      28: 'QUARTER_RIGHT_BOTTOM_LOW',
      29: 'QUARTER_RIGHT_TOP_HIGH',
      30: 'QUARTER_LEFT_TOP_HIGH',
      31: 'HALF_BOTTOM',
      32: 'HALF_RIGHT',
      33: 'HALF_TOP',
      34: 'HALF_LEFT' });
    map.setCollisionBetween(1, 34, true, 'BlockedLayer');

    // we pull all doors from the tiled object layer and confirm the door was added
    doors = game.add.group();
    doors.enableBody = true;
    map.createFromObjects('ObjectLayer', 113, "", 1, true, false, doors);
    doors.forEach(function(door) {
      door.x *= gameScale;
	    door.y *= gameScale;
      door.scale.set(gameScale);
      console.log(door.url + ' doorway exists');
     });

    player = game.add.sprite(0,0,'dog');
    player.anchor.setTo(.5,.5); // this lets us flip the sprite when walking left
    game.physics.arcade.enable(player);

    // Order is important, slopes did not work until I used this sequence
    player.body.slopes = {sat: {response: 0}}; // workaround for a phaser bug
    game.slopes.enable(player);
    player.body.slopes.preferY = true; // stops the player sliding down slopes

    player.body.bounce.y = 0.1;
    player.body.gravity.y = 2000;
  	player.animations.add('walk',[0,1,2,3,4,5,6,7,8,9],10,true);
    player.animations.add('jump',[10,11,12,13,14,15,16,17],5,false);
    player.animations.add('fall',[18,19,20,21,22,23,24,25],10,false);
    game.camera.follow(player);
  },
  update:function() {
    var onGround = game.physics.arcade.collide(player,blocklayer);
    var inDoorway = game.physics.arcade.overlap(player, doors, this.enterDoor);

    // if no keypress while on ground player stops walking
    player.body.velocity.x = 0;

    // warps player to opposite side when edge is reached
    game.world.wrap(player, 0, true);

    if (cursors.left.isDown)
  	{
  		player.body.velocity.x = -speed;
      player.scale.setTo(-1, 1);
      if (onGround)
      {
        player.animations.play('walk');
      }
  	}
  	else if (cursors.right.isDown)
  	{
      player.scale.setTo(1, 1);
      player.body.velocity.x=speed;
      if (onGround)
      {
  		    player.animations.play('walk');
      }
  	}
  	else if (cursors.down.isDown && onGround)
  	{
  		player.body.velocity.y=0;
  	}
  	else
  	{
      if (onGround)
      {
  		    player.animations.stop();
  		      player.frame=0;
      }
  	}

    // jump
    if (spacekey.isDown && player.body.onFloor() && game.time.now > jumpTimer)
  	{
      player.animations.play('jump');
      player.body.velocity.y = -750;
      jumpTimer = game.time.now + 500;
  	}

    if (player.body.velocity.y > 0 && player.animations.currentAnim.name != 'fall')
    {
      player.animations.play('fall');
    }

    //reset doorway trigger
    if (doorway && (!inDoorway))
    {
      doorway = false;
    }

  },
  enterDoor: function(player, door) {
    //ensures this function only triggers once if doorway is false
    if (!doorway)
    {
      console.log('entered doorway!');
      doorway = door;
    }
  },
  activateDoor: function() {
    if (doorway)
    {
      console.log('travelling to ' + doorway.url);
      window.open(doorway.url);
    }
  },
};

game.state.add('GameStateName', GameState);
game.state.start('GameStateName');

Version 3: Text & Tweens

screenshot

It would be nice if our critter could read signs and maybe even talk to other critters. For signs, I utilized a popup with an easing animation. These transitioning animations are called tweens. Refactoring leads to a popup box sprite with a child text object that is closed by a timer. I could use this for dialogue, too, but I’ll need to incorporate pausing the game and key to continue.

Phaser can use Google webfonts and bitmap fonts, which can be generated online. We at Bless Industries have selected the bitmap approach for the Sneak Attack font.

I am having difficulty with scope and the concept of ’this’ in Javascript. It will click eventually. I thought I was seeing some lag on the tilemap, which is not good considering the small size. I followed the suggestion to turn off tilemap rendering and replace it with a flat image, which in this particular instance is results in a much leaner file than the tileset. Once I started debugging, I saw my FPS went from low-end 35 to consistently 60, which is where Phaser should be. After .png compression, it’s definitely a reasonable solution.

I now have a basic working version of the game I wanted, which is an interactive sitemap/links page. It can and should be refactored. Beyond that, I might like to add neighbors eventually, but I doubt it will need to go much further.

Making tilesets is a lot of work and I’m not very good at it, so the finished product is dependent on the types of assets I can find. I like the little sprite critter I’m using but I would actually prefer something that better matches the tileset and the NeoCities cat (I could have sworn the cat was named somewhere on the main site but I can’t find it).

var game = new Phaser.Game(window.innerWidth,window.innerHeight,Phaser.CANVAS);
var speed = 250;
var doorway = false;
var jumpTimer = 0;
var gameScale = 2;
var timer;

var GameState = {
  preload: function(){
    game.time.advancedTiming = true; //debug
    Phaser.Canvas.setImageRenderingCrisp(game.canvas);
    PIXI.scaleModes.DEFAULT = PIXI.scaleModes.NEAREST;
    game.stage.smoothed = false
    Phaser.Canvas.setSmoothingEnabled(game.context, false);

    game.load.bitmapFont('MyFont', 'assets/font.png', 'assets/font.fnt');
    game.load.spritesheet('dog','assets/dog_animations.png',52,64);
    game.load.tilemap('MyTilemap','assets/townmap.json', null, Phaser.Tilemap.TILED_JSON);
    game.load.image('MyTiles', 'assets/village.png', 16,16);
    game.load.image('MyNinjaTiles', 'assets/ninja-tiles16.png',16,16);
    game.load.audio('gymnopedie', ['assets/gymnopedie_no_1.ogg']);
    game.load.image('box','assets/box.png');
    // if turning off tilemap rendering
    game.load.image('levelflat', 'assets/levelflat.png');
  },
  create: function() {
    game.plugins.add(Phaser.Plugin.ArcadeSlopes);
    cursors = game.input.keyboard.createCursorKeys();
    spacekey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

    cursors.up.onDown.add(this.activateDoor, this);
    game.physics.startSystem(Phaser.Physics.ARCADE);

    map = game.add.tilemap('MyTilemap');
    map.addTilesetImage('village', 'MyTiles');
    map.addTilesetImage('ninja-tiles16', 'MyNinjaTiles');
    console.log(map.layers);
    layer = map.createLayer('GroundLayer');
  	blocklayer = map.createLayer('BlockedLayer');
    blocklayer.alpha = 0.0
    layer.setScale(gameScale); // scales up 16x16 tiles
    blocklayer.setScale(gameScale);
    layer.resizeWorld();
    blocklayer.resizeWorld();
    layer.wrap = true;
    blocklayer.wrap = true;

    // if turning off tilemap rendering
    layer.visible = false;
    blocklayer.visible=false;
    var levelbg = game.add.image(0, 0, 'levelflat');
    levelbg.scale.setTo(gameScale,gameScale);

    game.slopes.convertTilemapLayer(blocklayer, {
      2:  'FULL', 3:  'HALF_BOTTOM_LEFT', 4:  'HALF_BOTTOM_RIGHT', 6:  'HALF_TOP_LEFT', 5:  'HALF_TOP_RIGHT',15: 'QUARTER_BOTTOM_LEFT_LOW',16: 'QUARTER_BOTTOM_RIGHT_LOW',17: 'QUARTER_TOP_RIGHT_LOW',18: 'QUARTER_TOP_LEFT_LOW',19: 'QUARTER_BOTTOM_LEFT_HIGH',20: 'QUARTER_BOTTOM_RIGHT_HIGH',21: 'QUARTER_TOP_RIGHT_HIGH', 22: 'QUARTER_TOP_LEFT_HIGH', 23: 'QUARTER_LEFT_BOTTOM_HIGH',24: 'QUARTER_RIGHT_BOTTOM_HIGH',25: 'QUARTER_RIGHT_TOP_LOW',26: 'QUARTER_LEFT_TOP_LOW',27: 'QUARTER_LEFT_BOTTOM_LOW',28: 'QUARTER_RIGHT_BOTTOM_LOW',
      29: 'QUARTER_RIGHT_TOP_HIGH',30: 'QUARTER_LEFT_TOP_HIGH',31: 'HALF_BOTTOM',32: 'HALF_RIGHT',33: 'HALF_TOP',34: 'HALF_LEFT' });
    map.setCollisionBetween(1, 34, true, 'BlockedLayer');

    // we pull all doors from the tiled object layer and confirm the door was added
    doors = game.add.group();
    doors.enableBody = true;
    map.createFromObjects('ObjectLayer', 113, "", 1, true, false, doors);
    doors.forEach(function(door) {
      door.x *= gameScale;
	    door.y *= gameScale;
      door.scale.set(gameScale);
      console.log(door.url + ' doorway exists...');
     });

    player = game.add.sprite(0,0,'dog');
    player.anchor.setTo(.5,.5); // this lets us flip the sprite when walking left
    game.physics.arcade.enable(player);

    cake = game.add.sprite(500,0,'cake');
    game.physics.arcade.enable(cake);
    cake.body.gravity.y = 500;
    cake.body.bounce.y = 0.1;
    game.slopes.enable(cake);

    // not needed for wrapped map
    //player.body.collideWorldBounds=true;

    // Order is important, slopes did not work until I used this sequence
    player.body.slopes = {sat: {response: 0}}; // workaround for a phaser bug
    game.slopes.enable(player);
    player.body.slopes.preferY = true; // stops the player sliding down slopes

    player.body.bounce.y = 0.1;
    player.body.gravity.y = 2000;
  	player.animations.add('walk',[0,1,2,3,4,5,6,7,8,9],10,true);
    player.animations.add('jump',[10,11,12,13,14,15,16,17],5,false);
    player.animations.add('fall',[18,19,20,21,22,23,24,25],10,false);
    game.camera.follow(player);

    popup = game.add.sprite(window.innerWidth/2, 200, 'box');
    popup_text = game.add.bitmapText(0,0, "MyFont", "", 20);
    popup.fixedToCamera = true;
    popup_text.maxWidth=450;
    popup_text.align="center";
    popup.anchor.setTo(0.5,0.5);
    popup.addChild(popup_text);
    popup_text.anchor.setTo(0.5,0.5);
    popup.scale.set(0.0);

    music = game.add.audio('gymnopedie');
    music.play();

  },
  update:function() {
    var onGround = game.physics.arcade.collide(player,blocklayer);
    var inDoorway = game.physics.arcade.overlap(player, doors, this.enterDoor.bind(this));

    // if no keypress while on ground player stops walking
    player.body.velocity.x = 0;
    // warps player to opposite side when edge is reached
    game.world.wrap(player, 0, true);

    if (cursors.left.isDown)
  	{
  		player.body.velocity.x = -speed;
      player.scale.setTo(-1, 1);
      if (onGround)
      {
        player.animations.play('walk');
      }
  	}
  	else if (cursors.right.isDown)
  	{
      player.scale.setTo(1, 1);
      player.body.velocity.x=speed;
      if (onGround)
      {
  		    player.animations.play('walk');
      }
  	}
  	else if (cursors.down.isDown && onGround)
  	{
  		player.body.velocity.y=0;
  	}
  	else
  	{
      if (onGround)
      {
  		    player.animations.stop();
  		      player.frame=0;
      }
  	}

    // jump
    if (spacekey.isDown && player.body.onFloor() && game.time.now > jumpTimer)
  	{
      player.animations.play('jump');
      player.body.velocity.y = -750;
      jumpTimer = game.time.now + 500;
  	}

    if (player.body.velocity.y > 0 && player.animations.currentAnim.name != 'fall')
    {
      player.animations.play('fall');
    }

    //reset doorway trigger
    if (doorway && (!inDoorway))
    {
      doorway = false;
    }
  },
  render: function()
    {
        game.debug.text(game.time.fps || '--', 2, 14, "#00ff00");
    },
  showPopup: function(t,s=1000){
    popup_timer = game.time.create(false);
    popup_timer.add(s, this.hidePopup, this);
    popup_text.setText(t);
    tween = game.add.tween(popup.scale).to({x:1,y:1},500, Phaser.Easing.Elastic.Out, true);
    popup_timer.start();
  },
  hidePopup: function(){
    tween = game.add.tween(popup.scale).to( { x: 0.0, y: 0.0 }, 500, Phaser.Easing.Elastic.In, true);
  },
  enterDoor: function(player, door) {
    //ensures this fucntion only triggers once if doorway is false
    if (!doorway)
    {
      console.log('entered doorway!');
      doorway = door;
      this.showPopup(door.page);
    }
  },
  activateDoor: function() {
    if (doorway)
    {
      console.log('travelling to ' + doorway.url);
      window.open(doorway.url);
    }
  },
};

game.state.add('GameStateName', GameState);
game.state.start('GameStateName');