Fog of War Com Phaser JS e Render Texture

Fog of War, eis um dos elementos mais básicos de um bom jogo de RTS, estilo Age of Empires. É incrível como apenas uma mecânica ou desafio pode alterar a dinâmica de um jogo. Imagine que em um jogo como Age of Empires, você precisa caçar recursos como comida e madeira, para isso é essencial que você saiba a posição exata destes recursos para tomar decisões que poderão lhe custar o jogo, se não forem bem tomadas. Remova esta mecânica e o jogo ficará totalmente diferente, tenha certeza disto.

Fog Of War no Phaser

Como em qualquer ferramenta, o Phaser possui várias formas de implementar Fog of War, eu testei muuuitas formas, não foi à toa que levei 2 semanas para chegar à um resultado que me agradasse. Neste post vou mostrar à você como fazer uma Fog of War utilizando uma render texture.

Render Texture

Bom, como eu disse, vamos utilizar uma Render Texture para Criar o nevoeiro. Eu Criei uma Classe para Abstrair todo o código e manter o tudo mais organizado.

export class FogWar {
	constructor(scene) {
		this.scene = scene; // Scene.
		this.rt; // Render Texture
		this.gameService = new GameService(scene); // Game Service.
		this.delay = 0; // delay to call the update.
		this.team = this.scene.team; // Team to update the Texture.
	}
}

Dentro desta classe vamos criar um método para gerar nossa textura.

	createFog(tileWidth, tileHeight) {
		console.log(tileWidth, tileHeight);

		const shadowMap = this.scene.make.tilemap({
			width: tileWidth,
			height: tileHeight,
			tileWidth: CONFIG.tileSize,
			tileHeight: CONFIG.tileSize,
		});

		const shadowTiles = shadowMap.addTilesetImage("shadowSprite");

		// For of War Map
		this.shadow = shadowMap.createBlankDynamicLayer(0, shadowTiles);
		// this.shadow.fill(1);
		// this.shadow.putTileAt(0, 3, 3);
		this.shadow.depth = 9999999;
		this.shadow.alpha = 0.0;
		// this.shadow.removeInteractive();

		const width = this.scene.scale.width;
		const height = this.scene.scale.height;
		// make a RenderTexture that is the size of the screen
		this.rt = this.scene.make.renderTexture(
			{
				width,
				height,
			},
			true
		);

		// fill it with black
		this.rt.fill(0x000000, 0.5);

		// draw the floorLayer into it
		this.rt.draw(this.shadow);

		// set a dark blue tint
		this.rt.setTint(0x0a2948);

		this.rt.depth = 999999;
	}

O Código é bem simples, criamos um layer de mapa do Phaser e usamos este layer para renderizar a nossa textura. Para garantir que ela ficará sobre todos os layers de visão, colocamos o depth com um número bem alto. Isso fará com que o z-index do layer sobreponha à todos os demais.

Dentro da sua scene, não esqueça de adicionar a criação da classe que acabamos de codificar. Isso fica dentro do create().

		this.fogWar = new FogWar(this); // Creates the object to add the Fog of War.
		this.fogWar.createFog(
			this.collision.layer.width,
			this.collision.layer.height
		); // Creates the fog of War.

Eu utilizei o meu layer de colisões para pegar o tamanho do mapa. Lembre-se que não é em Pixels e sim o número de Tiles.

Alterações no Sprite

Precisamos fazer algumas alterações no sprite que você quer que tenha visão no mapa. No meu caso, estou criando um jogo de RTS e eu tenho o que eu chamo de Unidades (Dá uma olhada no processo de criação), estas Unidades possuem um container onde eu movo tudo o que eu preciso, inclusive a imagem que eu uso de Máscara, que é a seguinte.

Fog of War
Fog of War

Perceba que a imagem é como se fosse um Gradiente, isso é o que garante que a visão da unidade tenha um efeito de Blur, sem precisar aplicar nenhum filtro extra. E o Código da unidade fica assim.

		this.maskImg = this.scene.add.image(0, 0, "mask");
		this.maskImg.visible = false;

		this.container = new Phaser.GameObjects.Container(scene, x, y, [
			this,
			this.maskImg,
		]);

Eu não preciso que a imagem fique visível, por tanto eu uso o atributo como visible = false. Tenha em mente que você pode usar o que quiser como máscara, até mesmo uma foto sua 😛

Pondo tudo pra funcionar

Por fim, precisamos criar mais 2 funções para atualizar a nossa máscara.

	updateFog(time) {
		if (this.delay < time) {
			this.delay = time + 300;
			let go = this.gameService.getGameObjects();
			this.redraw(); // Recreates the Fog.
			go.forEach((unit) => {
				if (unit.spriteType != SpriteTypes.RESOURCE && unit.team == this.team) {
					this.rt.erase(unit.maskImg, unit.container.x, unit.container.y);
				}
			});
		}
	}

	redraw() {
		this.rt.clear();
		// fill it with black
		this.rt.fill(0x000000, 0.7);

		// set a dark blue tint
		this.rt.setTint(0x0a2948);
	}

Devido à limitação de que uma render texture pode ter apenas 1 mascara por vez, precisamos redesenhar esta Render Texture a cada loop do jogo e fazer buracos nela apagando apenas a visão da unidade (Caso você queira para apenas 1 Sprite, veja a primeira referência do Artigo). Este processo é um pouco custoso, mas é um trade off que tive que fazer para ter uma Fog of War bonita. Por isso criamos o método redraw() que recria a nossa textura e percorremos todas as nossas unidades fazendo Buracos na textura com a nossa mascara. Dentro do Loop update() da minha Scene eu preciso colocar o updateFog() que acabamos de criar. Não esqueça de passar o time para controlar a quantidade de vezes que o método é chamado, eu coloquei para à cada 300ms.

update(time, delta) {
    this.fogWar.updateFog(time);
}
Fog of War Completa

Conclusão

Não é porque o jogo é feito com Javascript que ele não pode ficar bonito!!! PhaserJS é um Framework extremamente completo, com uma comunidade super-ativa e eu não teria conseguido chegar à este resultado sem a ajuda dela. Não deixe de checar as referências, elas vão te dar muitos insights. Sério, leia as referências! 😀

Referências

Leave a comment

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Copyright © – Jonatan Pietroski