top of page
Search
cedarcantab

Phaser Coding Tips 5 Revisited: How to move like Pacman


I started learnning about tilemaps by writing Pacman. I found a great tutorial called “How to move like Pacman Tutorial” by Richard Davey posted originally on 6 February 2015 (here), but found it was written in Phaser 2. I converted the example into Phaser 3 and learned a lot in the process. For those taking a similar path as mine, you might be interested to read about some of my learnings from the experience.



The first part of the tutorial explains code that moves a car around a maze, as below.



The maze has been created with Tiled, exported in JSON format.


To get a basic understanding of how to handle Tiled tilemaps in Phaser, I converted the example code to Phaser 3, utilising the tilemap file used by the original example.


this.map = this.make.tilemap({ key: 'map', tileWidth: 32, tileHeight: 32 });
const tileset = this.map.addTilesetImage('tiles','tiles');
this.layer = this.map.createLayer('Tile Layer 1', tileset, 0, 0);

The bits to be careful about are the blue highlighted sections - these are the names given to the tiles and layer in the Tiled editor, which may not be immediately apparent, particularly if you did not create the tilemap yourself. However, if you take a peek in the JSON file you can find it. An extract of the actual map data is shown below - the name of the layer under question is highlighted in blue (this is actually the default name given to the initial layer, by Tiled when you start a new project).


Creating the tilemap in Phaser 3


The maze consists of tiles which are individually 32x32 pixels, and the maze itself consists of 20x15 tiles.


{ "height":15,
 "layers":[
        {
         "data":[20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 1, 20, 1, 1, 1, 1, 1, 1, 20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20, 20, 1, 20, 1, 20, 20, 20, 20, 20, 20, 1, 20, 20, 20, 20, 20, 20, 20, 1, 20, 20, 1, 20, 1, 20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20, 1, 20, 1, 20, 20, 1, 20, 1, 20, 1, 20, 20, 20, 20, 20, 1, 20, 20, 1, 20, 1, 20, 1, 20, 20, 1, 1, 1, 20, 1, 20, 1, 1, 1, 20, 1, 1, 20, 1, 20, 1, 1, 1, 20, 20, 1, 20, 1, 1, 1, 20, 1, 20, 1, 20, 1, 20, 20, 20, 20, 20, 20, 1, 20, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 1, 1, 1, 1, 1, 20, 1, 20, 20, 1, 20, 20, 20, 1, 20, 1, 20, 1, 20, 1, 20, 20, 20, 20, 1, 20, 1, 20, 20, 1, 1, 1, 1, 1, 20, 1, 20, 1, 20, 1, 1, 1, 1, 20, 1, 20, 1, 20, 20, 1, 20, 20, 20, 20, 20, 1, 20, 20, 20, 1, 20, 20, 20, 20, 1, 20, 1, 20, 20, 1, 20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20, 1, 1, 1, 20, 1, 20, 20, 1, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 1, 20, 20, 20, 20, 20, 1, 20, 20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20],
         "height":15,
         "name":"Tile Layer 1",
         "opacity":1,
         "type":"tilelayer",
         "visible":false,
         "width":20,
         "x":0,
         "y":0
        }, 

Further down the same map file is the following, which includes information about the name given to the tileset.


"tileheight":32,
 "tilesets":[
        {
         "firstgid":1,
         "image":"tiles.png",
         "imageheight":320,
         "imagewidth":448,
         "margin":0,
         "name":"tiles",
         "properties":
            {

            },
         "spacing":0,
         "tileheight":32,
         "tilewidth":32
        }],
 "tilewidth":32,
 "version":1,
 "width":20
}

Using Arcade Physics Engine for collision detection between car and wall

The tutorial example clearly utilises arcade physics engine to perform collision detection between the car (which is created as a sprite with a physics body) and the tilemap walls.


this.map.setCollision(20, true);
    this.car = this.physics.add.sprite(48, 48, 'car');
    this.physics.add.collider(this.car, this.layer);

In particular, the cryptic red highlighted code is telling the flagging tile 20 as a tile that must be "watched" by the arcade physics engine for collisions.


According to the official documentation, setCollisions"sets collision on the given tile or tiles within a layer by index. You can pass in either a single numeric index or an array of indexes: [2, 3, 15, 20]. The collides parameter controls if collision will be enabled (true) or disabled (false)."


The "index" of the tile is not immediately apparent, until you "count" the tile within the tileset file, or more directly, look at the tilemap file created by Tiled. In this particular case, tile #20 is the wall.


Moving sprites around the tilemap


But the main topic of this tutorial is moving a sprite around a tilemap. The image of the car is 32x32 pixels, ie the same size as one tile, but the tricky part comes frmo the fact that the car can move pixel by pixel (in fact, sub-pixel level) and overlap moer than one tile at any one time. The original tutorial goes into some detail about the principles of making the car moving along the paths and not driving into walls. However, the key points are noted below, for my own benefit.


They points of moving an object around a tile map are:

  1. knowing which direction the player can turn (ie follow the path, as opposed to move into a wall), and

  2. knowing when the object can turn, since you dont want the object turning half of the body is not "centered"

In particular, you don't want to make it too difficult for the player to meet condition (2).


The logic used by the tutorial is as follows:


Check the tiles to the left, right, above and below the car

To determine condition (1), the tile equivalent coordinate of the car is calculated - blue highlighted code. Then in the red highlighted code, the actual tile to the left, right, above and below the car is shoved into the array this.directions.

this.marker.x = Phaser.Math.Snap.Floor(Math.floor(this.car.x), this.gridsize) / this.gridsize;
    this.marker.y = Phaser.Math.Snap.Floor(Math.floor(this.car.y), this.gridsize) / this.gridsize;
    this.directions[Game.Direction.LEFT] = this.map.getTileAt(this.marker.x - 1, this.marker.y);
    this.directions[Game.Direction.RIGHT] = this.map.getTileAt(this.marker.x + 1, this.marker.y);
    this.directions[Game.Direction.UP] = this.map.getTileAt(this.marker.x, this.marker.y - 1);
    this.directions[Game.Direction.DOWN] = this.map.getTileAt(this.marker.x, this.marker.y + 1);

Then the cursor keys are checked.


 checkKeys() {
    if (this.cursors.left.isDown && this.current !== Game.Direction.LEFT) {
      this.checkDirection(Game.Direction.LEFT);
    } else if (this.cursors.right.isDown && this.current !== Game.Direction.RIGHT) {
      this.checkDirection(Game.Direction.RIGHT);
    } else if (this.cursors.up.isDown && this.current !== Game.Direction.UP) {
      this.checkDirection(Game.Direction.UP); 
    } else if (this.cursors.down.isDown && this.current !== Game.Direction.DOWN) {
      this.checkDirection(Game.Direction.DOWN);
    } else { 
      //  This forces them to hold the key down to turn the corner
      this.turning = Game.Direction.NONE;
   }

  }

Following which - and this is the crucial and most difficult part of the code to understand - checkDirection method is called with the parameter set to whatever cursor key is being pressed.

The first thing to do is to check if the direction in which the user wishes the car to turn to is a path.


If the user is trying to reverse the car, then it is allowed to do so.


The important bit of the code is highlighted in red. If we have come to this point, it means that the direction in which the cursor has been pressed is safe to turn to.

  1. Set the flag - this.turning - to indicate in which direction the car should turn (at this point in time, the car cannot necessarily turn - it might not be properly aligned to turn along the path)

  2. Calculate in pixel coodinates, the centre of the tile in which the car is currently in, and stick it in this.turnPoint.x-y.


  checkDirection(turnTo) {
    if (this.turning === turnTo || this.directions[turnTo] === null || this.directions[turnTo].index !== this.safetile) {
      //  Invalid direction if they're already set to turn that way
      //  Or there is no tile there, or the tile isn't index 1 (a floor tile)
      return;
    }
    //  Check if they want to turn around and can
    if (this.current === this.opposites[turnTo]) {
      this.move(turnTo);
    } else {      
      this.turning = turnTo;
      this.turnPoint.x = (this.marker.x * this.gridsize) + (this.gridsize / 2);      
      this.turnPoint.y = (this.marker.y * this.gridsize) + (this.gridsize / 2);
   }
  }

Then, method turn is called to actually make the turn.


The red highlighted code checks whether the car is "lined-up", ie at the centre of the tile. If not, then we cannot make the car turn just yet.


If the car is in the right spot to turn, method this.move is called, which sets the velocity of the sprite in the correct direction.


  turn() {
    var cx = Math.floor(this.car.x);
    var cy = Math.floor(this.car.y);
    //  This needs a threshold, because at high speeds you can't turn because the coordinates skip past
    if (!Phaser.Math.Fuzzy.Equal(cx, this.turnPoint.x, this.threshold) || !Phaser.Math.Fuzzy.Equal(cy, this.turnPoint.y, this.threshold)) {
        return false;
    }
    //  Grid align before turning
    this.car.x = this.turnPoint.x;
    this.car.y = this.turnPoint.y;
    this.car.body.reset(this.turnPoint.x, this.turnPoint.y);
    this.move(this.turning);
    this.turning = Game.Direction.NONE;
    return true;
  }

As much as possible, I have tried to faithfully follow the structure of the original Phaser 2 example, so should make sense if the original tutorial is read in conjuction with the converted code, available here.




Moving Pacman around the maze


The tutorial then goes onto explain how to move Pacman around the familiar pacman maze. The basic logic is the same as the car example, except in one very important aspect - that is the pacman sprite is larger than a single tile whereas in the car example the car was the same size as one tile. For this reason you need to set the hitbox of pacman to the same size as the tile, otherwise it will quickly get "stuck" between "colliding" wall tiles.


Here is the codepen of the Pacman example converted to Phaser 3




19 views0 comments

Recent Posts

See All

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2...

sopiro motor constranit

import { Matrix2, Vector2 } from "./math.js"; import { RigidBody } from "./rigidbody.js"; import { Settings } from "./settings.js";...

Comments


記事: Blog2_Post
bottom of page