Creating moving platforms for my Phaser JS game - Game Devlog #22
Written in August 28, 2021 - 🕒 10 min. readHello everyone, welcome to another game devlog for my game Super Ollie, and after almost a whole year of development I’m finally tackling something that every platformer must have: Moving platforms.
A platformer needs platforms.
— Sherlock Holmes, 600 BC
Defining platforms types
There are many types of moving platforms, but for my game, I decided to stick to 6 different types for now. I hope you’re ready because this is going to be a big devlog 😬.
Platforms that follow a path
Platforms that will follow a certain path drawn on Tiled.
Platforms that move in a circular path
Platforms that follow a circular movement based on a circle drawn on Tiled.
Platforms that vanish
Platforms that will fall/vanish after a couple of seconds of the hero getting on top of them.
Platforms that fall
Platforms that will start to slowly fall when the hero is on top of them.
Elevator platforms
Platforms that will move up and down like an elevator, and will only trigger the movement once the hero is on top of them.
Looping platforms
Platforms that will constantly move vertically until it reaches the end of the stage and then loops back to the top.
The code
I will start by adding a new if condition to my data layer objects loop, and an attribute called platformAI
on the platform object on Tiled, so I can create different AIs/Types of platforms.
// in the scene create function
const dataLayer = map.getObjectLayer('data');
dataLayer.objects.forEach((data) => {
const { x, y, name, height, width } = data;
if (name === 'platform') {
const platformAI = properties?.find(
(property) => property?.name === 'platformAI'
)?.value;
const velocity = properties?.find(
(property) => property?.name === 'velocity'
)?.value || DEFAULT_PLATFORM_VELOCITY;
const platform = new Platform({
scene: this,
velocity,
x,
y,
});
this.platforms.add(platform);
switch (platformAI) {
case LOOP_PLATFORM: {
// TODO
}
case FALLING_PLATFORM: {
// TODO
}
case ELEVATOR_PLATFORM: {
// TODO
}
case VANISHING_PLATFORM: {
// TODO
}
case CIRCULAR_PATH_PLATFORM: {
// TODO
}
case PATH_PLATFORM:
default: {
// TODO
}
}
}
});
With this base code in place, I can now start coding each of the different behaviors for the platforms.
Path follower
For this platform I will need to draw a path on Tiled and then make the platform follow it, and when I do that it comes as an array of positions like [{ x: 1, y: 2 }, { x: 3, y: 6 }]
, and the idea is to get each of these positions and create a custom collider that when in contact with the platform will set its speed and direction.
For that I’m going to need the createInteractiveGameObject
function, that I showed on the devlog 15.
switch (platformAI) {
case PATH_PLATFORM:
default: {
// Get path from Tiled
const platformPath = dataLayer.find(
(object) => {
const pathType = object.properties?.find(
(property) => property?.name === 'type'
)?.value;
return object.polyline?.length
&& (object.name === (pathName || objectId))
&& pathType === PATH_TYPE_PLATFORM;
}
);
platformPath.polyline.forEach((path, index, polylines) => {
// create a custom interactive object for each of the polyline points
const platformCollider = createInteractiveGameObject(
this,
platformPath.x + path.x,
platformPath.y + path.y,
4,
4,
'platform_collider'
);
});
// set custom properties for the collider
platformCollider.setOrigin(0.5);
platformCollider.nextPoint = polylines[index + 1];
platformCollider.currentPoint = polylines[index];
platformCollider.previousPoint = polylines[index - 1];
platform.direction = 'forward';
// now create a collider for it
this.physics.add.collider(platform, platformCollider, () => {
if (!platformCollider.nextPoint) {
platform.direction = 'backward';
} else if (!platformCollider.previousPoint) {
platform.direction = 'forward';
}
const {
nextPoint,
currentPoint,
previousPoint,
} = platformCollider;
const { direction } = platform;
const nextPos = direction === 'forward' ? nextPoint : previousPoint;
const newVelocity = {
x: nextPos.x !== currentPoint.x ? (velocity * (nextPos.x > currentPoint.x ? 1 : -1)) : 0,
y: nextPos.y !== currentPoint.y ? (velocity * (nextPos.y > currentPoint.y ? 1 : -1)) : 0,
};
platform.body.setVelocity(
newVelocity.x,
newVelocity.y
);
});
// Create collider for player and platform
this.physics.add.collider(platform, this.player);
break;
}
}
Circular path
This one was the toughest to create, because I needed some knowledge on basic circular motion physics, in which I had none 😅 and I wanted to be able to choose the speed and direction of the spin on Tiled.
So I asked my girlfriend for help since she has a degree in engineer, but she knew so much, and I knew so little, that it was a hard conversation but in the end, I managed to do it with her help.
The base rotation formulas are the following:
radius = circleDiameter / 2
secondsPerRotation = gravity / velocity
angularSpeed = 360 / secondsPerRotation
speed = (radius * (π * 2)) / secondsPerRotation
The trick is to create a dummy object and set a spin on it with the setAngularVelocity
function, then copy the velocity from the dummy object to the platform using the velocityFromRotation
function.
switch (platformAI) {
case CIRCULAR_PATH_PLATFORM: {
const direction = properties?.find(
(property) => property?.name === 'direction'
)?.value || DIRECTION_RIGHT;
const platformCircularPath = dataLayers.find(
(object) => {
const pathType = object.properties?.find(
(property) => property?.name === 'type'
)?.value;
return object.ellipse
&& (object.name === (pathName || objectId))
&& pathType === PATH_TYPE_PLATFORM;
}
);
const radius = platformCircularPath.width / 2;
// set platform position to the top of the circle to start the rotation
platform.setPosition(
platformCircularPath.x + radius - (platform.width / 2),
platformCircularPath.y + (platform.height / 2)
);
// This is where the magic happens
// I honestly have no idea why this works, but it works
const secondsPerRotation = this.physics.world.gravity.y / velocity;
// increase angular speed, decreases radius
const angularSpeed = 360 / secondsPerRotation;
// increase speed, increase radius
const speed = (radius * (Math.PI * 2)) / secondsPerRotation;
// create a dummy object to create the base rotation physics
const dummyRotation = new GameObjects.Rectangle(
this,
platformCircularPath.x + radius,
platformCircularPath.y + radius,
1,
1
);
this.physics.add.existing(dummyRotation);
dummyRotation.body.setAllowGravity(false);
dummyRotation.body.setImmovable(true);
dummyRotation.body.setMaxVelocity(speed);
if (direction === DIRECTION_LEFT) {
platform.setY(platform.y + (radius * 2));
dummyRotation.body.setAngularVelocity(-angularSpeed);
} else {
dummyRotation.body.setAngularVelocity(angularSpeed);
}
// set the platform velocity to be equal to the one from the dummy object
platform.update = (time, delta) => {
this.physics.velocityFromRotation(
PhaserMath.DegToRad(dummyRotation.body.rotation),
speed,
platform.body.velocity
);
};
// Create collider for player and platform
this.physics.add.collider(platform, this.player);
break;
}
}
Vanishing
This one is simple, I will just create a collider that when the player touches the upper part of the platform, I will set a timer for the platform to vanish and a timer for the platform to show up again.
switch (platformAI) {
case VANISHING_PLATFORM: {
platform.isTriggered = false;
const originalCheckCollision = {
...platform.body.checkCollision,
};
const platformReapearingCallback = () => {
platform.isTriggered = false;
platform.body.setVelocityY(0);
platform.body.checkCollision = originalCheckCollision;
platform.setPosition(
platformX,
platformY
);
};
const platformVanishingCallback = () => {
platform.body.setVelocityY(velocity);
this.time.delayedCall(
2000,
platformReapearingCallback
);
};
const colliderCallback = () => {
if (platform.isTriggered) {
return;
}
platform.isTriggered = true;
const {
x: platformX,
y: platformY,
} = platform;
this.time.delayedCall(
1000,
platformVanishingCallback
);
};
this.physics.add.collider(
platform,
this.player,
colliderCallback
);
break;
}
}
Falling
For this one, I will need an update
function for my platform to make it slowly come back up after the player is no longer on top of it. As for the rest, it is pretty much the same as the vanishing platforms.
switch (platformAI) {
case FALLING_PLATFORM: {
// get initial position of the platform
// to reset it to
const {
y: platformY,
} = platform;
platform.update = (time, delta) => {
if (!platform.touchingUpObject) {
if (platform.y > platformY) {
platform.body.setVelocityY(
-platform.velocity
);
} else {
// reset platform to original position
platform.body.setVelocityY(0);
platform.setY(platformY);
}
}
};
this.physics.add.collider(
platform,
this.player,
() => {
if (platform.touchingUpObject) {
platform.body.setVelocityY(velocity);
}
}
);
break;
}
}
Elevator
The elevator platform needs a lot of code to create all the features that I wanted. Those are:
- When the player steps on the platform, it will move up/down and then stop.
- If the player jumps and steps back into the platform, it will trigger the move again.
- If the platform is not at the same level as the player when the player approaches the platform, it should move to the player.
For that, I need one custom collider in the ground, that will trigger the platform to come down when it is up, a custom collider in the upper level to trigger the platform to go up when it is down, and an update
function to determine if the platform should be triggered or not when the player is on top of it.
switch (platformAI) {
case ELEVATOR_PLATFORM: {
// get path from Tiled
const platformPath = combinedDataLayers.find(
(object) => {
const pathType = object.properties?.find(
(property) => property?.name === 'type'
)?.value;
return object.polyline?.length
&& (object.name === (pathName || objectId))
&& pathType === PATH_TYPE_PLATFORM;
}
);
const {
y: platformY,
} = platform;
// get base positions
const groundFloorPosY = platform.y - (platform.height - platform.body.height);
const secondFloorPosY = platformY + platformPath.polyline[1].y;
// create ground object custom collider
const elevatorGroundCollider = createInteractiveGameObject(
this,
platformPath.x - platform.width,
platformPath.y - platform.height,
platform.width + (TILE_WIDTH * 2),
platform.height + TILE_HEIGHT,
'ground-collider'
);
const elevatorGroundColliderCallback = () => {
if (platform.body.velocity.y === 0) {
if (platform.y <= secondFloorPosY) {
platform.body.setVelocityY(velocity);
}
}
};
this.physics.add.overlap(
elevatorGroundCollider,
this.player,
elevatorGroundColliderCallback
);
// create floor object custom collider
const elevatorFloorCollider = createInteractiveGameObject(
this,
platformPath.x - platform.width,
secondFloorPosY - (platform.height + TILE_HEIGHT),
platform.width + (TILE_WIDTH * 2),
platform.height + TILE_HEIGHT,
'floor-collider'
);
const elevatorFloorColliderCallback = () => {
if (platform.body.velocity.y === 0) {
if (platform.y >= groundFloorPosY) {
platform.body.setVelocityY(-velocity);
}
}
};
this.physics.add.overlap(
elevatorFloorCollider,
this.player,
elevatorFloorColliderCallback
);
// create the platform update function
platform.update = (time, delta) => {
if (!platform.touchingUpObject) {
platform.wasTriggered = false;
}
if (
platform.body.velocity.y === 0
&& platform.y < groundFloorPosY
&& platform.y > secondFloorPosY
) {
platform.body.setVelocityY(velocity);
}
if (platform.y >= groundFloorPosY) {
platform.setY(groundFloorPosY);
if (platform.body.velocity.y > 0) {
platform.body.setVelocityY(0);
}
}
if (platform.y <= secondFloorPosY) {
if (platform.body.velocity.y < 0) {
platform.body.setVelocityY(0);
}
if (!platform.touchingUpObject) {
platform.setY(secondFloorPosY);
}
}
};
const colliderCallback = () => {
if (!platform.body.touching.up) {
return;
}
if (platform.y < (this.player.y + platform.body.height)) {
return;
}
if (platform.wasTriggered) {
return;
}
if (platform.y >= groundFloorPosY) {
platform.body.setVelocityY(-velocity);
} else if (platform.y <= secondFloorPosY) {
platform.body.setVelocityY(velocity);
}
platform.wasTriggered = true;
};
this.physics.add.collider(
platform,
this.player,
colliderCallback
);
break;
}
}
Looping
Another easy one, I will just set the y
velocity for it, and when it’s off the map, I will reset it to the top of the map.
switch (platformAI) {
case LOOP_PLATFORM: {
platform.body.setVelocityY(platform.velocity);
platform.update = (time, delta) => {
if ((platform.y - platform.height) > mapSize.height) {
platform.setY(platform.height);
}
};
this.physics.add.collider(platform, this.player);
break;
}
}
And that’s it… or is it…?
The problem
If you just copied all this code into your own game and went ahead to test it, you will notice that the player is in a constant falling state when the platform is moving downwards.
This happens because the acceleration of the player is slower than the platform’s, to fix this I will make a very hacky solution, make the player’s gravity around 10 times stronger when they are on top of a platform 😬.
The solution
First I will add a new property to the platforms called changeHeroPhysics
:
const platform = new Platform({
changeHeroPhysics: true,
scene: this,
velocity,
x,
y,
});
Then in the player update
function, I will add a check to see if the player is touching an object with the property changeHeroPhysics
set to true and if so, increase the player’s gravity.
if (this.touchingDownObject?.changeHeroPhysics) {
const platformVelocityY = this.touchingDownObject?.body?.velocity?.y;
const { gravity } = this.scene.physics.world;
if (platformVelocityY > 0) {
this.body.setGravity(
0,
gravity.y * (platformVelocityY / 12)
);
} else {
this.body.setGravity(0, 0);
}
} else {
// eslint-disable-next-line no-lonely-if
if (this.body.gravity.y !== 0) {
// set back original gravity to the hero
this.body.setGravity(0, 0);
}
}
Wrap up
There you go, a huge list of super useful platforms for your Phaser platformer game. Please let me know if I missed any types of platforms and I might add them to the list in the future.
Have a good one and happy coding!
Tags:
- coding
- games
- javascript
- phaser
- phaser 3
- game devlog
- gamedev
- skate platformer
- super ollie vs pebble corp
- webpack
- tiled
- moving platforms
- platforms
Related posts
Post a comment
Comments
No comments yet.