Phaser Como Mudar a Paleta de Cores de um Sprite

Phaser é um Framework para criação de Jogos com Javascript poderosíssimo. Com ele é possível realizar tarefas complexas em apenas poucos minutos. Hoje eu quero mostrar para você como eu fiz para criar os Sprites das unidades do meu jogo de RTS de forma dinâmica.

Qual o problema?

O problema é o seguinte, eu só tenho um Spritesheet para cada unidade. Todos tem a mesma cor base, verde bebê, ou verde qualquer coisa que você queira chamar.

Sprite Sheet do Arqueiro do Little War Game

Vamos utilizar este Sprite para criar um novo Sprite com as cores alteradas, tudo utilizando o Javascript, desta forma eu minimizo o esforço de criação de novas imagens e deixo o meu jogo mais modularizado. Qualquer unidade que eu criar à partir deste conceito poderá ter as suas cores modificadas. O resultado final será semelhante à este aqui.

Phaser Palette - Arqueiro com cores diferentes.
Phaser Palette – Arqueiros com Cores diferentes

Qual o suporte do Phaser para isso?

Bom, o Phaser cria um novo canvas para alterar pixel à pixel as cores do Sprite. Após alterar as cores o Phaser salva o canvas em uma nova imagem que pode ser acessada para criar animações e finalmente ser utilizada em nosso jogo. O código base para realizar esta operação está disponível nos exemplos do Phaser 3. No entanto vamos utilizar um código mais avançado criado por Colbydude que gentilmente disponibilizou o seu código no GitHub.

Configurações

Primeiro precisamos criar algumas configurações mínimas.

  // Carrega a peleta de Cores.
  this.load.image("palette", "src/assets/palette.png");

  // Carrega o Sprite do Arqueiro.
  this.load.spritesheet("archer", "src/assets/archer.png", {
    frameWidth: 32,
    frameHeight: 32
  });

O primeiro passo é carregar a Paleta e o Sprite do Arqueiro que iremos utilizar. Eu criei uma Paleta de Cores utilizando um Software gratuito chamado Designer.io, ele pode ser utilizado no próprio Navegador, também utilizei o Piskel para saber quais eram as cores base do meu Sprite. Note que em minha paleta eu coloquei apenas as cores que eu quero mudar no meu Sprite.

Paleta de cores
Paleta de cores

Após isso vamos preparar o arquivo de configurações que o Phaser vai usar pra criar as animações e os Sprites propriamente ditos.

  var animConfig = {
    paletteKey: "palette", // Nome da paletta de Cores.
    paletteNames: ["green", "blue"], // Nomes das cores que tenoh nas paletas.
    spriteSheet: {
      // Spritesheet que estamos manipulando.
      key: "archer",
      frameWidth: 32,
      frameHeight: 32
    },
    animations: [
      // Configuração de animação
      { key: "idle-down", frameRate: 1, startFrame: 0, endFrame: 1 },
      { key: "walk-down", frameRate: 10, startFrame: 2, endFrame: 9 },
      { key: "walk-left", frameRate: 10, startFrame: 32, endFrame: 39 },
      { key: "walk-up", frameRate: 10, startFrame: 92, endFrame: 99 }
    ]
  };

O arquivo é auto-explicativo. Criamos as configurações do Sprite sheet, como vamos chamar as cores no novo Sprite, e configuramos as animações que serão apresentadas no Showcase.

Criando os novos Sprites

Após realizar a configuração necessária, vamos criar uma função que irá fazer toda a rotina de criação dos sprites coloridos.

/**
 * Cria um novo Spritesheed e animações para a paleta de cores.
 * @param {object} config - Objeto de configurações.
 */
function createPalettes(config) {
  // 01 - Create color lookup from palette image.
  var colorLookup = {};
  var x, y;
  var pixel, palette;
  var paletteWidth = game.textures.get(config.paletteKey).getSourceImage()
    .width;

  // 02 - Percorre todos os pixels da palete de cores e adiciona ao array de cores.
  for (y = 0; y < config.paletteNames.length; y++) {
    palette = config.paletteNames[y];
    colorLookup[palette] = [];
    // Percorre todas as cores pixel à pixel e adiciona ao lookpup.
    for (x = 0; x < paletteWidth; x++) {
      pixel = game.textures.getPixel(x, y, config.paletteKey);
      colorLookup[palette].push(pixel);
    }
  }

  // 03 - Cria o Sprites sheet e animações à aprtir do Sprite base.
  var sheet = game.textures.get(config.spriteSheet.key).getSourceImage();
  var atlasKey, anim, animKey;
  var canvasTexture, canvas, context, imageData, pixelArray;

  // 04 - Percorre cada palette.
  for (y = 0; y < config.paletteNames.length; y++) {
    palette = config.paletteNames[y];
    atlasKey = config.spriteSheet.key + "-" + palette;

    // 05 - Cria um canvas e adiciona um sprite temporário nele.
    canvasTexture = game.textures.createCanvas(
      config.spriteSheet.key + "-temp",
      sheet.width,
      sheet.height
    );
    canvas = canvasTexture.getSourceImage();
    context = canvas.getContext("2d");

    // Copia o sprite sheet.
    context.drawImage(sheet, 0, 0);

    // Pega os dados da imagem de base..
    imageData = context.getImageData(0, 0, sheet.width, sheet.height);
    pixelArray = imageData.data;

    // 06 - Percorre todos os pixels de cada imagem..
    for (var p = 0; p < pixelArray.length / 4; p++) {
      var index = 4 * p;

      var r = pixelArray[index];
      var g = pixelArray[++index];
      var b = pixelArray[++index];
      var alpha = pixelArray[++index];

      // Se for um pixel transparante, ignora.
      if (alpha === 0) {
        continue;
      }

      // 07 - Percorre as cores da Palette.
      for (var c = 0; c < paletteWidth; c++) {
        var oldColor = colorLookup[config.paletteNames[0]][c];
        var newColor = colorLookup[palette][c];

        // Se a cor comparada for a mesma do sprite base, muda a cor para a nova.
        if (
          r === oldColor.r &&
          g === oldColor.g &&
          b === oldColor.b &&
          alpha === 255
        ) {
          pixelArray[--index] = newColor.b;
          pixelArray[--index] = newColor.g;
          pixelArray[--index] = newColor.r;
        }
      }
    }

    // 08 - Coloca o pixel modificado novamente ao Contexto.
    context.putImageData(imageData, 0, 0);

    // 09 - Adiciona o novo Spritesheet modificado no jogo.
    game.textures.addSpriteSheet(atlasKey, canvasTexture.getSourceImage(), {
      frameWidth: config.spriteSheet.frameWidth,
      frameHeight: config.spriteSheet.frameHeight
    });

    // 10 - Percorre e cria as animações.
    for (var a = 0; a < config.animations.length; a++) {
      anim = config.animations[a];
      animKey = atlasKey + "-" + anim.key;

      // Adiciona a animação ao jogo.
      game.anims.create({
        key: animKey,
        frames: game.anims.generateFrameNumbers(atlasKey, {
          start: anim.startFrame,
          end: anim.endFrame
        }),
        frameRate: anim.frameRate,
        repeat: anim.repeat === undefined ? -1 : anim.repeat
      });
    }

    // Destroy o sprite temporário.
    game.textures.get(config.spriteSheet.key + "-temp").destroy();
  }

  // Destroy as texturas que não são mais necessárias.
  // NOTA: Isso não remove as texturas do TextureManager.list.
  //       No entanto, Isso destroy os dados da imagem.
  game.textures.get(config.spriteSheet.key).destroy();
  game.textures.get(config.paletteKey).destroy();
}

Bem, aqui você vai precisar de um conhecimento mais profundo no Phaser, do contrário você vai ficar perdido. Acho que esta é uma boa hora para verificar o Exemplo do próprio Phaser não é mesmo? Eu coloquei ele em um Sandbox com meus comentários pra que fique mais fácil de você visualizar, ou se preferir pode ver o código aqui mesmo.

Exemplo do Phaser

import Phaser from "phaser";

var config = {
  type: Phaser.AUTO,
  parent: "phaser-example",
  width: 300,
  height: 150,
  scene: {
    preload: preload,
    create: create
  }
};

var originalTexture;
var newTexture;
var context;

var game = new Phaser.Game(config);

function preload() {
  // Carrega a imagem do DUDE
  this.load.image("dude", "src/phaser-dude.png");
}

function create() {
  // 01 - Armazena os dados do Dude.
  originalTexture = this.textures.get("dude").getSourceImage();

  // 02 - Cria uma nova Textura vazia à partir dos dados da original
  newTexture = this.textures.createCanvas(
    "dudeNew",
    originalTexture.width,
    originalTexture.height
  );

  // Pega o contexto da imagem.
  context = newTexture.getSourceImage().getContext("2d");

  // Desenha o Sprite sheet original
  context.drawImage(originalTexture, 0, 0);

  // Adiciona as duas Textures dos Dudes
  this.add.image(100, 100, "dude");
  this.add.image(200, 100, "dudeNew");

  // Times para mudar a cor do Sprite a cada meio segundo.
  this.time.addEvent({ delay: 500, callback: hueShift, loop: true });
}

/**
 * @description Muda a cor da textura.
 **/
function hueShift() {
  // Pega os Pixels da imagem original
  var pixels = context.getImageData(
    0,
    0,
    originalTexture.width,
    originalTexture.height
  );

  // Percorre cada pixel e muda por um pequeno Delta a cor.
  for (let i = 0; i < pixels.data.length / 4; i++) {
    processPixel(pixels.data, i * 4, 0.1);
  }
  // Coloca os pixels novamente na imagem.
  context.putImageData(pixels, 0, 0);

  // Atualiza somente a textura nova.
  newTexture.refresh();
}

/**
 * @description Processa a mudança de pixels.
 * @params {data} pixels que serao alterados. R, G, B.
 * @params {index} index do array qye esta sendo alterado.
 * @params {index} Valor da diferença da cor antiga para a nova.
 **/
function processPixel(data, index, deltahue) {
  var r = data[index];
  var g = data[index + 1];
  var b = data[index + 2];

  var hsv = Phaser.Display.Color.RGBToHSV(r, g, b);

  var h = hsv.h + deltahue;

  var rgb = Phaser.Display.Color.HSVToRGB(h, hsv.s, hsv.v);

  data[index] = rgb.r;
  data[index + 1] = rgb.g;
  data[index + 2] = rgb.b;
}

Agora que você viu este exemplo mais fácil, volte ao mais complexo e as coisas devem começar a fazer sentido pra você. Se não, aconselho que você estude um pouco mais Phaser, Texturas e Sprites.

Resultado

Use as setas para visualizar as animações e Pressione X para mudar a cor do Arqueiro.

Conclusão

Phaser é um framework realmente poderoso, é um dos frameworks mais bem documentados que já utilizei. Sem falar nos exemplos que são milhares. É muito fácil de criar novos Sprites de forma dinâmica, este exemplo serve pra mim para a separação de Times, mas não há limites para as suas aplicações, talvez você tenha alguma outra ideia mais mirabolante. Quem sabe nem é pra usar em jogos. Espero que este guia tenha sido útil para você, assim como foi pra mim. Agradecimentos especiais à comunidade de jogos HTML5 e ao Colbydude que disponibilizou seu código no GitHub.

Referências

Edit Texture Hue Shift – Exemplo Phaser
Phaser 3 palette swapping example
Palette Swapping
Phaser 3 Texture PI Reference

Leave a comment

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

Copyright © – Jonatan Pietroski