Criando uma AI mais inteligente e performática no Phaser JS - Game Devlog #15
Escrito em 15 de dezembro de 2020 - 🕒 5 min. de leituraVamos lá, se prepare pois o devlog de hoje é longo, mas vale a pena. Eu estou muito feliz que eu consegui fazer esse devlog, afinal Cyberpunk 2077 saiu essa semana e eu comecei a jogar no Stadia! O devlog de hoje está longo porque quero explicar o motivo pelo qual estou fazendo certas coisas.
Hoje vou mostrar como melhorei a AI dos inimigos do jogo e por que lido com tantos edge cases no meu código. Bora lá!
Minha primeira tentativa no algoritmo de AI foi calculá-lo em tempo de execução, então onde quer que eu colocasse um inimigo, ele se moveria e calcularia se deveria girar ou não em cada frame, isso era muito pesado para o processador, especialmente quando eu tinha muitos inimigos na tela ao mesmo tempo.
Havia duas maneiras de abordar esse problema, uma seria criar manualmente um path estático no Tiled e fazer o inimigo seguir esse path, mas sempre que adicionar um novo inimigo, eu teria que adicionar um path para ele também, e isso pode acaba sendo um trabalho super tedioso, sem contar às vezes que eu com certeza esqueceria de adicionar o path. Complicado.
A segunda opção é ao invés de calcular o comportamento do inimigo em cada frame, eu poderia pré-calcular todos eles assim que a fase fosse carregada, e então definir esse path estaticamente, desta forma eu teria a liberdade de adicionar inimigos onde eu quisesse no Tiled, sem ter que adicionar paths extras para eles seguirem e também manter a taxa de frames estável por não processar a lógica do inimigo em cada frame.
Então, como eu fiz isso? Primeiro, há alguns edge cases que eu preciso lidar, tipo e se o inimigo não estiver em uma plataforma ou chão? Ou e se a plataforma não tiver bordas? Eu sei que eu mesmo estarei fazendo as fases no Tiled, mas é bom lidar com esses problemas de qualquer forma, porque nunca sabemos quais bugs o futuro nos reserva.
Já que eu não tenho como saber se o inimigo já está em uma plataforma ou não, eu preciso esperar a física do jogo entrar em ação e calcular a posição do inimigo, como à gravidade e outras coisas, então ao invés de calcular a AI estática no construtor
, estou fazendo isso na função update
e, depois de calculado, simplesmente “removo” a função update
substituindo-a por uma função no-op
(no operation).
class Enemy extends GameObjects.Sprite {
constructor({ scene, x, y, asset }) {
super(scene, x, y, asset);
this.staticAi = [PLATFORM_WALKING];
this.aiColliders = [];
}
update(time, delta) {
if (this.aiColliders.length) {
// we don't need the update function anymore
this.update = () => {};
return;
}
this.staticAi.forEach((aiStrategy) => {
if (aiStrategy === PLATFORM_WALKING) {
// first check if the enemy is on a platform
if (this.touchingDownObject?.layer?.tilemapLayer) {
// then calculate the colliders that will make the enemy change direction
const colliders = staticPlatformWalkingBehavior(
this,
this.touchingDownObject.layer.tilemapLayer
);
// TODO concat array
colliders.forEach((collider) => this.aiColliders.push(collider));
}
}
});
}
}
export default Enemy;
E então toda a mágica acontece dentro do staticPlatformWalkingBehavior
, que honestamente é super simples e zero mágica, eu só preciso pegar a layer de colisão do inimigo e verificar se há uma borda na plataforma em ambas as direções. O único problema dessa função é que não quero que ela rode para sempre até encontrar uma borda, porque talvez a plataforma não tenha uma borda, quem sabe? Então, em vez disso, executarei esse loop para cada tile que eu tenho dentro da largura do mapa.
export function staticPlatformWalkingBehavior(
gameObject,
dynamicLayer
) {
const { scene } = gameObject;
const divider = TILE_WIDTH;
const loopSize = scene.mapData.tileSize.width * divider;
const result = [];
const layers = dynamicLayer?.getChildren?.() || [dynamicLayer];
// for each collision layer
layers.forEach((layer) => {
// for each direction
[FACING_RIGHT, FACING_LEFT].forEach((direction) => {
// for each tile of the map * divider
let lastGroundTile;
new Array(loopSize).fill(null).some(
(num, index) => {
const multiplier = direction === FACING_RIGHT ? 1 : -1;
const posX = gameObject.x + (((index + 1) - 0.5) * multiplier);
const groundTile = layer.getTileAtWorldXY(
posX,
gameObject.y + 0.5
);
const tile = layer.getTileAtWorldXY(
posX,
gameObject.y - 0.5
);
// check for collisions
if (
!groundTile?.properties?.collideUp
|| (direction === FACING_RIGHT && tile?.properties?.collideLeft)
|| (direction === FACING_LEFT && tile?.properties?.collideRight)
) {
let newPosX =
lastGroundTile?.pixelX
|| tile?.pixelX
|| Math.round(posX / TILE_WIDTH) * TILE_WIDTH;
newPosX += (TILE_WIDTH * multiplier);
// create a custom collider object
const platformLimitCollider = createInteractiveGameObject(
gameObject.scene,
newPosX,
gameObject.y - TILE_HEIGHT,
TILE_WIDTH,
TILE_HEIGHT,
'something'
);
// create a collision between the gameobject and the custom collider
scene.physics.add.collider(gameObject, platformLimitCollider, () => {
gameObject.body.setVelocityX(
-gameObject.body.velocity.x
);
gameObject.setFlipX(!gameObject.flipX);
});
result.push(platformLimitCollider);
// return true so the 'some' doesn't run anymore
return true;
}
lastGroundTile = groundTile;
return false;
}
);
});
});
return result;
}
E com esse código, temos o resultado abaixo.
O meu principal objetivo é criar todas as regras de jogo baseadas em elementos e interações para que depois eu possa simplesmente criar uma fase no Tiled e tudo funcionar sem eu precisar ficar mexendo muito nela, criando colliders e outras coisas manualmente, assim como fizeram no Breath of The Wild. Eu prefiro que o jogo calcule tudo isso pra mim para que eu possa focar em criar novas fases sem dificuldade. E no final eu vou ter tipo um Mario Maker 😃.
E por hoje ficamos por aqui, obrigado por ler e assistir o vídeo e não se esqueça de deixar o seu comentário sobre o que você está achando dessa série de posts / vídeos. Até semana que vem!
Tags:
- programação
- jogos
- javascript
- phaser
- phaser 3
- game devlog
- gamedev
- skate platformer
- super ollie vs pebble corp
- webpack
- tiled
- cyberpunk 2077
- stadia
- ai
Posts relacionados
Publicar um comentário
Comentários
Nenhum comentário.