top of page
Search
cedarcantab

Phaser Coding Tips 3 Revisited, Part 1: "Ride on" Platforms


Platform Game Tutorial: "Ride on" Platforms


Continuing the journey through the fantastic tutorial series (for Phaser 2) by the great Richard Davey, this one is about making player characters "ride" on horizontally moving platforms.



In practice, making characters ride on horizontal moving platforms is very easy in Phaser 3 and (as I found out after I worked through the tutorial) there is an example in the official examples site here; so much so that it probably does not warrant much in the way of exploration. However, I personally did learn a few things as I worked through the original tutorial that other people starting out in Phaser 3 might also find useful, hence this post.


In anycase, things suddenly become more complicated once you start moving platforms vertically. And things really become challenging (at least for me) when you start moving platforms along more complicated paths (specifically, using tweens - which is the main topic of Phaser Coding Tips 4 tutorial).


This post starts with recreating the horizontal moving platform in Phaser 3 (very easy). I then experiment with vertically moving platforms before arriving at moving platforms along curved paths.


But first...


Creating a group fo physics enabled objects


The Phaser 2 tutorial example includes the following to create 4 platforms of 2 different types:


this.platforms = this.add.physicsGroup();

this.platforms.create(0, 64, 'ice-platform');
this.platforms.create(200, 180, 'platform');
this.platforms.create(400, 296, 'ice-platform');
this.platforms.create(600, 412, 'platform');

Seems simple enough. However, when I create groups, I never quite seem to get everything right the first time, so just as a reminder to myself:


The primary use of a Physics Group is a way to collect together physics enable objects that share the same intrinsic structure into a single pool. [...] All Game Objects created by, or added to this Group will automatically be given dynamic Arcade Physics bodies (if they have no body already) and the bodies will receive the Groups default values.


In Phaser 2, there seems to be an object called physicsGroup. As far as I can tell, the equivalent in Phaser 3 is as follows. The red highlighted line will create a Phaser.Physics.Arcade. Group, which is described as


All Game Objects created by, or added to this Group will automatically be given dynamic Arcade Physics bodies (if they have no body already) and the bodies will receive the Groups default values.


    this.platforms = this.physics.add.group();
    this.platforms.create(0, 64, 'ice-platform');
    this.platforms.create(200, 180, 'platform');
    this.platforms.create(400, 296, 'ice-platform');
    this.platforms.create(600, 412, 'platform');

**********************************************

As a reminder very much to myself,

this.platforms.create(200, 180, 'ice-platform');

is a short-cut for:


var sprite = this.add.sprite(200, 180, 'phaser');

this.platforms.add(sprite);

**********************************************


Creating a group and adding physics body afterwards

Whereas the above method will create a group that will "automatically" add a physics body upon creation, you could do this in two steps:


    this.platforms = this.add.group(
      [
        {key: 'ice-platform',setXY: {x: 0, y: 64},setOrigin: {x:0, y:0}},
        {key: 'platform',setXY: {x:200,y: 180},setOrigin: {x:0, y:0}},
        {key: 'ice-platform',setXY: {x:400,y: 296},setOrigin: {x:0, y:0}},     
        {key: 'platform',setXY: {x:600,y: 412},setOrigin: {x:0, y:0}},  
      ]
    );


Then add the physics body to all the elements


   this.platforms.getChildren().forEach(platform =>{
      this.physics.add.existing(platform);
      platform.body.setAllowGravity(false);
      platform.body.setImmovable(true);
      platform.body.setVelocityX(100)
    })

Curiously, you cannot do something like the following, and hence in this particular example, it would make no sense to create the group in this manner.


this.platforms = this.physics.add.group(
      [
        {key: 'ice-platform',setXY: {x: 0, y: 64}, setOrigin: {x:0, y:0}},
        {key: 'platform',setXY: {x:200,y: 180}, setOrigin: {x:0, y:0}},
        {key: 'ice-platform',setXY: {x:400,y: 296}, setOrigin: {x:0, y:0}},     
        {key: 'platform',setXY: {x:600,y: 412}, setOrigin: {x:0, y:0}},  
      ]
    );

The other important difference here, vs the original Phaser 2 code is that the default origin of a game object in Phaser 3 is the centre of the texture, as opposed to the top-left hand corner, so we will need to set the origin of all the newly created platforms as follows:


 this.platforms.setOrigin(0);

Setting the same value to particular set of properties of all group objects

The original Phaser 2 example includes the following lines of code, after the creation of the group to make all the platforms free of gravity, make them immovable, and velocity to 100.


this.platforms.setAll('body.allowGravity', false);
this.platforms.setAll('body.immovable', true);
this.platforms.setAll('body.velocity.x', 100);

As with the setOrigin method, the group class provides a number of methods to set a particular property of all the group members to the same value. For example, the red line above can be replaced by the following code:


    this.platforms.setVelocityX(100)

As for the blue highlighted code, there appears to be a Phaser 3 equivalent of the Phaser 2 "setAll" method called propertyValueSet, which is described as:


Sets the property as defined in key of each group member to the given value.


So you would think that below would do the trick.

    this.platforms.propertyValueSet('body.allowGravity', false);
    this.platforms.propertyValueSet('body.immovable', true);

However, I could not get it to work...and I was not able to figure out why.


Anyway, not to dwell on this matter...as far as I can tell there are a number of other ways of achieving the required result:


Option 1:

Phaser.Actions.Call(this.platforms.getChildren(), function(platform) {
    platform.body.setImmovable(true);
    platform.body.setAllowGravity(false);
  }, this);

Option 2:

  this.platforms.children.each(function(platform) {
    platform.body.setImmovable(true);
    platform.body.setAllowGravity(false);
  }, this);

Option 3:

this.platforms.getChildren().forEach((platform, index) => {
      platform.body.setImmovable(true);
      platform.body.setAllowGravity(false);  
}, this);

I have used option 3 (ie the native Javascript forEach method) simply because I am most familiar with this - I think all the methods are pretty much the same. In fact, you can make it a bit more compact than the above, like so:


   this.platforms.getChildren().forEach(platform => {
      platform.body.setImmovable(true);
      platform.body.setAllowGravity(false);
    });

or, in this particular case perhaps the simplest way is to set the default values when the group is first created, as follows:


this.platforms = this.physics.add.group({
      immovable: true,
      allowGravity: false
    });

What took me a long time to realise (and I am still not sure if I have understood it correctly) is that the default properties that can be set in this way are limited to those defined here, as

PhysicsGroupConfig and the list does not include properties such as scaleXY or even originXY.



Immovable physics bodies

You will have noticed the use of setImmovable method above, which sets the "immovable" property of the physics body to true (or false, by default). Simply put, this tells the physics engine that the game object is...immovable. More specifically, it means that if set to true, when this body collides with another body, and the physics engine executes the collision resolution code, this particular body will not be given any impulse; rather the other body will receive all change in momentum.


Difference between Static Body and Dynamic Body

Although I have set the immovable property to true, this is still what Phaser 3 categorises as a dynamic body. This is different from static bodies (this will become important in understanding Phaser 3 Coding Tips 4, which deals with platforms that move vertically), which are bodies that...as the name suggest, are static. More specifically, static bodies have none of the "movement" related properties, like velocity, acceleration, angular velocity etc, that dynamic bodies have, although the physics engine will treat them as having bodies so will collide with other objects. If you have objects that definitely does not move (like the ground) then it is preferable to use static bodies, as there is less "maintenance" that the physics engine will need to deal with in refreshing such bodies.


"Slippery" Platforms

The tutorial example has two types of platforms: ice-platform, and normal platforms. The player "sticks" to the normal platform but if lands on an ice platform, the player will "slide" and fall off it unless you move the player in the direction of the platform.


To achieve the desired effect, the original Phaser 2 has some complicated code to make the player move at the same speed as the platform, when the player is on top of a normal platform, in effect, creating "friction" between the player and the platform. In Phaser 3, there is a "friction" property "built-in" to physics bod, which is described as follows:


friction :Phaser.Math.Vector2

If this Body is immovable and in motion, friction is the proportion of this Body's motion received by the riding Body on each axis, relative to 1. The horizontal component (x) is applied only when two colliding Bodies are separated vertically. The vertical component (y) is applied only when two colliding Bodies are separated horizontally. The default value (1, 0) moves the riding Body horizontally in equal proportion to this Body and vertically not at all.


And there are a number of methods to set this property:


  • setFriction(x,y)

  • setFrictionX(value)

  • setFrictionY(value)


The explanation given for the friction property is a bit complicated for someone like me not too familiar with the workings of a physics engine, but I believe what it is saying is that when the physics engine detects two bodies colliding, and executes the collision separation process, it takes into account the friction property. The red line is suggesting that if, for example, the x-component of the friction property is set to 1, then when the collision separation routine separates the colliding bodies in the y-direction, the movable body is moved by the same amount as the "immovable" body (which does not get "budged") that is in motion in the horizontal direction. In simple terms, if you have the character standing on a horizontally moving platform the character is constantly colliding with the platform (due to gravity) and hence its x-position is adjusted in line with the movement of the platform when the platform and the character is separately vertically.


Confirming the underlying code

The above interpretation is confirmed more easily by looking at the underlying code. Specifically, Phaser.Physics.Arcade.SeparateY is the method that handles the separation of two bodies as part of the collision handling process. Among other things, this method checks whether the bodies are immovable (the friction property only comes into action if one of the bodies is immovable).



else if (body1Immovable)
    {
        ProcessY.RunImmovableBody1(blockedState);
    }
    else if (body2Immovable)
    {
        ProcessY.RunImmovableBody2(blockedState);
    }

(from var SeparateY = function (body1, body2, overlapOnly, bias)


and looking at Process.Y.RunImmovableBody, one can confirm how the friction property is being handled.


var RunImmovableBody1 = function (blockedState)
{
    if (blockedState === 1)
    {
        //  But Body2 cannot go anywhere either, so we cancel out velocity
        //  Separation happened in the block check
        body2.velocity.y = 0;
    }
    else if (body1OnTop)
    {
        body2.processY(overlap, body2FullImpact, true);
    }
    else
    {
        body2.processY(-overlap, body2FullImpact, false, true);
    }

    //  This is special case code that handles things like horizontally moving platforms you can ride
    if (body1.moves)
    {
        body2.x += (body1.x - body1.prev.x) * body1.friction.x;
        body2._dx = body2.x - body2.prev.x;
    }
};

Anyway, the examples here and here illustrate the property behaviours much better than my words.


In this tutorial example, in the case of ice-platform, the player should not move horizontally when it is landing on top of the platform so should set the x-component of the friction property to zero, and the x-component of the friction for the normal platform should be 1. We can achieve the desired effect by amending the platforms creation code as follows.


this.platforms.create(0, 64, 'ice-platform').setFrictionX(0);
this.platforms.create(200, 180, 'platform').setFrictionX(1);
this.platforms.create(400, 296, 'ice-platform').setFrictionX(0);
this.platforms.create(600, 412, 'platform').setFrictionX(1);

In fact, frictionX appears to be set to zero when the group is first created (default value is zero, as documented here). Hence the setFriction(0) is not actually required.


Collision Detection

In the original tutorial code, the following code is included in the update method to execute collision detection.


Phaser 2 code

this.physics.arcade.collide(this.player, this.platforms, this.setFriction, null, this);

//  Do this AFTER the collide check, or we won't have blocked/touching set
var standing = this.player.body.blocked.down || this.player.body.touching.down;

The blue highlighted code can be used as is in Phaser 3, but is worth describing in more detail; in particular the two properties of the body object: blocked and touching


body.blocked

The explanation is as follows:


Whether this Body is colliding with a Static Body, a tile, or the world boundary. In a collision with a Static Body, if this Body has zero velocity then embedded will be set instead.


The notes here gives further details; in particular the blocked property has sub-properties as follows.


{
    none: true,
    up: false,
    down: false,
    left: false,
    right: false
}

body.touching


The explanation is as follows:


Whether this Body is colliding with a Body or Static Body and in which direction. In a collision where both bodies have zero velocity, embedded will be set instead.


Hmm...very similar to blocked except the comparator is different.


In the case of the tutorial example, you will find that:

  • on the ground (ie on world boundary): blocked.down = true, touching.down = false

  • on a platform: blocked.down = true, touching.down = true

  • when jumping in the air: blocked.down = false, touching.down = false


For those wanting to understand in more detail, the examples here and here speak a thousand words.


Order in which to carry out collision detection & checking player's status


Of course, as explained above, we no longer need to call the custom setFriction function like in the original Phaser 2 example as this is all handled by setting appropriately the Phaser 3 arcade physics friction property, and therefore you can achieve the same affect with the following code.


Phaser 3 version

this.physics.world.collide(this.player, this.platforms);
//  Do this AFTER the collide check, or we won't have blocked/touching set
var standing = this.player.body.blocked.down || this.player.body.touching.down;

As the notes in the example code mentions, the order is important. You need to carry out the collision detection before you check the blocked and touching properties, since these properties are updated by the collision detection routine. If you reverse the red and the blue lines, you will not be able to jump, as the blocked and touching properties would have been reset to false.


Creating a collider


Instead of calling the collision detection method in the update loop, you can create a "collider" in the create stage.


this.physics.add.collider(this.player, this.platforms);

As the documentation explains:


A Collider is a way to automatically perform collision checks between two objects, calling the collide and process callbacks if they occur.

Colliders are run as part of the World update, after all of the Bodies have updated.

By creating a Collider you don't need then call World.collide in your update loop, as it will be handled for you automatically.


What in the world is the the "World update"? Well, according to the documentation, the World is the Arcade Physics World, and


the World is responsible for creating, managing, colliding and updating all of the bodies within it. An instance of the World belongs to a Phaser.Scene and is accessed via the property physics.world.


Typically, I simply "include" the arcade physics engine with the config object when launching Phaser game, but there's actually a lot of fine tuning you can do with this, should you so wish, as described here, and most importantly for this tutorial example, the documentation confirms that the World update loop runs before the scene update loop; hence setting up the collider instead of calling the world.collide method works just fine.


Adjusting the hitbox of Mr Dude

this.player = this.physics.add.sprite(320+16, 432+24, 'dude'); 
this.player.body.setSize(20, 32).setOffset(6,16);

The hitbox of Mr Dude (original sprite image is 32x48) is shrunk to 20x32 and shifted by 6 px in x-direction and 16 px in the y-direction. Otherwise, he will hit his pointy head against the lowest platform.


Wrapping the platforms

In the original tutorial, there is the following code which makes the platform re-appear on the left hand side when after they disappear off the right hand side.


  wrapPlatform(platform) {
    if (platform.body.velocity.x < 0 && platform.x <= -160) {
      platform.x = 640;
    } else if (platform.body.velocity.x > 0 && platform.x >= 640) {
      platform.x = -160;
    }

which is called from the update method with the following one line.


this.platforms.forEach(this.wrapPlatform, this);

In Phaser 3, the wrapPlatform method can remain exactly the same, but the method to call it is as follows:


this.platforms.children.each(this.wrapPlatform, this);

Phaser 3 also provides a method to "wrap" sprites around the screen and you can achieve a similar effect to the wrapPlatform method with the following one line. Although similar, it is not quite the same as the "padding" of 160 pixels is applied on both sides of the edge, so the platform will not "wrap" until it is 160 pixels to the right of the right-hand edge; so platforms will take longer to reappear on the left hand side, if you use this method.


 this.physics.world.wrap(platform, 160);

The Phaser 3 conversion is in the CODEPEN below.



Jump Up Game!


The tutorial then goes onto create a mini game called Jump Up Game, based on the logic developed.


I have converted this mini-game also into Phaser 3, and can be accessed below.




81 views0 comments

Comments


記事: Blog2_Post
bottom of page