Criando plataformas para o meu jogo em Phaser JS - Game Devlog #22
Escrito em 28 de agosto de 2021 - 🕒 10 min. de leituraFala 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!
Tags:
- programação
- jogos
- javascript
- phaser
- phaser 3
- game devlog
- gamedev
- skate platformer
- super ollie vs pebble corp
- webpack
- tiled
- plataformas
Posts relacionados
Publicar um comentário
Comentários
Nenhum comentário.