Fala galera, bem-vindos a mais um devlog do meu jogo Super Ollie, e depois de quase um ano inteiro de desenvolvimento, estou finalmente tentando resolver algo que todo jogo de plataforma precisa ter: Plataformas.
Um jogo de plataforma precisa de plataformas.
— Sherlock Holmes, 600 BC
Definindo tipos de plataformas#
Existem vários tipos de plataformas, mas para o meu jogo, decidi ficar com 6 tipos diferentes por enquanto. Espero que você esteja pronto porque este será um grande devlog 😬.
Plataformas que seguem um caminho#

Plataformas que seguirão um determinado caminho desenhado no Tiled.
Plataformas que se movem em um caminho circular#

Plataformas que seguem um movimento circular baseado em um círculo desenhado em Tiled.
Plataformas que desaparecem#

Plataformas que desaparecem alguns segundos após o herói subir nelas.
Plataformas que caem#

Plataformas que começarão a cair lentamente quando o herói estiver em cima delas.
Plataformas que funcionam como um elevador#

Plataformas que se movem para cima e para baixo como um elevador e só acionam o movimento quando o herói está em cima delas.
Plataformas em loop#

Plataformas que se movem constantemente na vertical até atingir o final do estágio e então voltam ao topo.
O código#
Primeiro eu vou adicionar uma nova condição no meu loop do dataLayer para lidar com o código de novas plataformas, e no Tiled vou um novo atributo chamado platformAI nas plataformas para que eu possa criar AIs / Tipos de plataformas diferentes.
// 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
}
}
}
});Com esse código base pronto, eu posso começar a codificar cada um dos diferentes comportamentos para as plataformas.
Seguidora de caminho#
Para esta plataforma, vou precisar desenhar um caminho no Tiled e, em seguida, fazer a plataforma segui-lo, e quando eu faço, o JSON do Tiled vem como uma array de posições no formato [{x: 1, y: 2}, {x: 3, y: 6}] , e a ideia é pegar cada uma dessas posições e criar um custom collider que, quando em contato com a plataforma, mudará sua velocidade e direção.
Para isso, vou precisar da função createInteractiveGameObject, que mostrei no 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;
}
}Caminho circular#
Essa foi a mais difícil de criar, porque eu precisava de algum conhecimento em física de movimento circular, no qual não tinha nenhum 😅 e, além disso, eu queria poder escolher a velocidade e a direção da rotação no Tiled.
Então eu pedi ajuda à minha namorada porque ela é formada em engenharia, mas ela sabia tanto, e eu sabia tão pouco, que foi uma conversa difícil, mas no final ela conseguiu me ajudar. Yay!
As fórmulas de rotação que vou usar são as seguintes:
raio = diametroDoCirculo / 2
rotacaoPorSegundo = gravidade / velocidade
velocidadeAngular = 360 / rotacoesPorSegundo
velocidade = (raio * (π * 2)) / rotacoesPorSegundo
O truque é criar um objeto que sera usado apenas para referência e definir uma rotação nele com a função setAngularVelocity e, em seguida, copiar a velocidade desse objeto para a plataforma usando a função velocityFromRotation.
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;
}
}Desaparecendo#
Esta é simples, vou apenas criar um custom collider que quando o herói tocar a parte de cima da plataforma, irei definir um timer para que a plataforma desapareça e um timer para que a plataforma apareça novamente.
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;
}
}Caindo#
Para este, vou precisar de uma função update para a minha plataforma para fazê-la voltar ao lugar lentamente depois que o herói não estiver mais em cima dela. Quanto ao resto, é praticamente o mesmo que as plataformas que desaparecem.
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;
}
}Elevador#
A plataforma elevador precisa de muito código para criar todos os recursos que eu queria. Esses são:
- Quando o herói pisa na plataforma, ele se move para cima / para baixo e depois para.
- Se o herói pular e voltar para a plataforma, o movimento será acionado novamente.
- Se a plataforma não estiver no mesmo nível do herói quando o herói se aproximar da plataforma, ela deve se mover até o herói.
Para isso, eu preciso de um custom collider no chão, que fará com que a plataforma desça quando estiver em cima, e um custom collider no nível superior para ativar a plataforma para subir quando estiver abaixada, e uma função update para determinar se a plataforma deve ser acionada ou não quando o herói está em cima dela.
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#
Outra fácil, irei apenas definir a velocidade y para ela, e quando estiver fora do mapa, mudar sua posição para o topo do mapa novamente.
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;
}
}E é isso… ou será que é…?
O problema#
Se você apenas copiou todo esse código para o seu próprio jogo e foi testá-lo, notará que o herói está meio bugado quando a plataforma está se movendo para baixo.

Isso acontece porque a aceleração do herói é mais lenta que a da plataforma. Para consertar isso tive que fazer uma gambiarra: Sempre que o herói estiver em cima de uma plataforma, aumentar a gravidade do herói em cerca de 10 vezes 😬.
A solução#
Primeiro, adicionarei uma nova propriedade às plataformas chamada changeHeroPhysics:
const platform = new Platform({
changeHeroPhysics: true,
scene: this,
velocity,
x,
y,
});Então, na função update do herói, adicionarei uma verificação para ver se o herói está tocando um objeto com a propriedade changeHeroPhysics definida como true e, se estiver, aumentarei a gravidade do herói.
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);
}
}Conclusão#
Ai esta, uma enorme lista de plataformas super úteis para o seu platformer no Phaser. Por favor, deixe um comentário se eu perdi algum tipo de plataforma e talvez eu possa adicioná-los à lista no futuro.
Por hoje é só, até o próximo devlog!