Roguelike - Parte 2 - Pixel Perfection

Aqui começarei a falar de aspectos mais técnicos e decisões de design do jogo. Caso não tenha visto, na parte 1 explico como nasceu a ideia do projeto:

(Roguelike - Part 1)

Pixel Perfect

Um problema que tive logo de cara quando comecei a fazer o projeto foi montar a cena de testes. Eu ia começar o desenvolvimento pela movimentação do personagem, mas logo de cara me deparei com um problema bem óbvio. A renderização estava zoadissima.

Por que isso acontece? Porque A minha cena tem um número de pixels (que como estão medidos em espaço de textura irei me referenciar a eles como “texels” daqui pra frente) que precisam ser desenhados em uma tela que tem um número diferente de pixels. Se meu número de pixels não for um múltiplo do meu número de texels nas duas direções (x e y), só duas coisas podem acontecer, dependendo da escolha da técnica de amostragem:

1) Point Filter: A cor do pixel será a cor do texel mais próximo de sua coordenada. Se minha cena tem 2 texels, e minha tela tem 3 pixels, o pixel do meio terá de ser colorido com a cor do pixel da esquerda ou o pixel da direita. O resultado disso é que alguns texels vão ser vistos com tamanhos diferentes na hora que forem renderizados.

2) Interpolação (Bilinear, Trilinear etc): Dada a mesma situação de 2 texels e 3 pixels, o algoritmo irá usar uma combinação das duas cores dos pixels da esquerda e da direita para gerar uma terceira cor que será usada para pintar o pixel do meio. De todos os casos, esse é o pior para o nosso jogo de pixel art, pois isso vai borrar toda a imagem!

Então a única saída é garantir que cada texel seja desenhado na tela com o mesmo número de pixels (1x1, 2x2, 3x3 etc).

Depois de quebrar muito a cabeça, e ver várias soluções feitas pelas pessoas nas internets, resolvi conversar com meu amigo Felipe Lira. Ele é um dos caras que eu conheço que manja mais de Unity e de Computação Gráfica em geral (Valeu Felipe!) e me ajudou a ter um insight de como resolver o problema.

Primeira Técnica

A ideia inicialmente era ter uma câmera que iria renderizar a cena em uma textura com tamanho fixo usando a técnica Render to Texture, onde em vez da câmera jogar a imagem direto no buffer que vai para o video, ele vai para uma região de memória separada (Uma Textura! :v).

Dessa forma, eu consegui mapear 1 pixel do meu jogo para 1 pixel dessa textura.

A segunda parte dessa técnica envolve usar o CommandBuffer. O CommandBuffer basicamente é uma estrutura que me permite passar uma sequência de comandos para uma câmera da Unity. O que meu CommandBuffer estava fazendo era renderizando um quad, que é uma malha poligonal contendo quatro vértices, formando um quadrado. E nesse quadrado, o que estava sendo desenhado era a textura que veio do passo anterior.

Então tudo que precisava ser feito era algumas contas que iriam determinar as posições dos vértices do retângulo contendo a cena, para que os texels ficassem alinhados com a câmera.

Para determinar a conta, eu tive de tomar algumas decisões:

  1. Meu jogo será jogável tanto em landscape mode como em portrait mode. Para que isso seja possível, minha textura tinha de ser quadrada.
  2. Como a tela do device normalmente não é quadrada, ou uma parte da janela ficaria com tarjas pretas ao longo da maior dimensão, ou uma parte da minha cena teria de ser cortada fora, ficando para fora da janela (bleeding). Eu optei pela segunda opção.
  3. Como uma parte da área de jogo seria cortada fora, eu teria que garantir que pelo menos uma porção mínima da minha área de jogo seria visível em qualquer aspect ratio. Spoiler: Foi isso que me fez desistir dessa técnica.

As imagens a seguir são para ilustrar o que estou falando sobre as margens. É importante notar que dependendo do dispositivo que você estiver olhando meu blog, a imagem pode ser redimensionada, e vai impossibilitar de ver o pixel perfect funcionando (Provavelmente vão formar padrões. https://pt.wikipedia.org/wiki/Padr%C3%A3o_moir%C3%A9).

Uma margem pode ser adicionada nas duas direções, mas toda a textura é visível.

A textura original está cortada, perdendo informação nas duas direções.

Esse era o trecho do protótipo do algoritmo que era executado sempre que a resolução ou orientação da tela mudasse:

// Start()
mesh = new Mesh();
mesh.SetVertices(vertex);
mesh.SetIndices(index, MeshTopology.Quads, 0);
mesh.SetUVs(0, uvs);

var commands = new CommandBuffer();
commands.DrawMesh(mesh, Matrix4x4.identity, PixelMaterial, 0);

camera.AddCommandBuffer(CameraEvent.AfterEverything, commands);
// When resolution changes
camera.orthographicSize = Mathf.Ceil(screenHeight / 2.0f);

int scaleFactor = Mathf.Ceil(Mathf.Max(screenWidth, screenHeight) / (float)BaseResolution);
int scale = BaseResolution * scaleFactor;
Vector3 offset = Vector2.one * (BaseResolution / 2.0f * scaleFactor);

vertex[0] = new Vector3(0, 0, 0) * scale - offset;
vertex[1] = new Vector3(1, 0, 0) * scale - offset;
vertex[2] = new Vector3(1, 1, 0) * scale - offset;
vertex[3] = new Vector3(0, 1, 0) * scale - offset;

mesh.SetVertices(vertex);

E pronto! Isso simplesmente funcionava. Mas como eu citei no ponto #3, isso limitava minha área de jogo, já que eu tinha um tamanho máximo, e boa parte disso seria cortado.

Eu precisava garantir um mínimo de visualização, e não queria ter de me preocupar com o máximo (estou de olho em vocês, devices com aspect ratios bizarros.). Ficar redimensionando a textura me parecia uma péssima ideia.

Segunda Técnica

Olhando para a implementação anterior, dá para ver um trecho interessante:

camera.orthographicSize = Mathf.Ceil(screenHeight / 2.0f);

Isso foi feito para que eu pudesse setar a posição dos vértices em coordenadas de pixels da câmera, e simplificar minhas contas (em vez de ter de trabalhar com floats quebradissimos, poderia trabalhar no domínio dos inteiros).

Mas aí eu percebi que era exatamente isso que eu precisava. Se eu conseguia mudar a noção de Pixels Per Unit da câmera, já seria uma solução relativamente boa para o meu problema, e a implementação ficaria bem mais simples!

Então, depois de alguns testes, cheguei ao seguinte resultado:

var scale = Mathf.Floor(Mathf.Min(Screen.width, Screen.height) / Size);
cam.orthographicSize = Screen.height / (2.0f*scale);

E pronto! Veja que eu optei por usar Mathf.Min, o que me fez usar o menor lado da tela como base para meus cálculos, enquanto eu vou ignorar o maior lado da tela, e deixar caber o que couber.

Como o jogo terá fog, mostrar mais áreas do jogo não trará muita vantagem ao jogador, e não prejudicará jogadores que com a técnica anterior teriam seu campo de visão reduzido.

Trade off

O que eu ganhei e o que eu perdi ao trocar uma técnica pela outra?

Vantagens:

  • Agora posso extender a câmera em uma direção infinitamente, já que não tenho mais uma textura de tamanho fixo me limitando.
  • Só preciso de uma câmera para essa técnica! Com a primeira técnica eu não conseguia reaproveitar a câmera usada para o Render to Texture para jogar imagens no display também.
  • Mais simples de implementar. O Código foi de várias linhas, e várias estruturas internas sendo criadas para poucas linhas!
  • Não preciso criar assets de textura, shader e material por fora
  • Suporta bem devices quadrados!

Desvantagens:

  • Preciso movimentar o objeto em posições inteiras para não perder o pixel perfect.
  • Rotacionar um objeto não causará aquele efeito retrô de rotação.
  • Em resoluções ímpares, um artefato estranho aparece, já que não é possível dividir a resolução por 2. Uma saída para isso sera modificar o viewport pra ignorar o pixel ímpar, mas isso é um corner case que provavelmente nunca vai acontecer.

Resultados

E o resultado disso tudo é esse:

Com a primeira técnica eu garanto um tamanho ao longo da maior dimensão da tela, e perco área de jogo na menor dimensão da tela.

Com a segunda técnica, eu garanto uma área mínima de jogo na menor dimensão, e deixando o jogo livre para expandir o quanto for necessário na maior dimensão.

No video acima vemos a “rotação retrô” com a primeira técnica. Fica bem maneiro, né?

E aqui vemos a rotação esquisitona. Isso tá acontecendo porque os texels são compostos por vários pixels, e quando você rotaciona, cada pixel que compõe ele se move independentemente. :(

É importante notar que essa solução é ideal para o tipo de jogo que eu desejo projetar, considerando todas as decisões de design que eu devo ter citado no texto acima.

Conclusão

Após testar tudo, eu percebi que minha implementação de pixel perfect acabou ficando melhor que a do Cardinal Quest 2, que eu estava usando como parâmetro de qualidade! XD

Eu peguei um emulador para executar o jogo simulando devices arbitrários, e percebi que o CQ2 não se comporta muito bem com devices quadrados e com algumas resoluções específicas, comendo quase toda a área de jogo e fazendo a interface ficar maluca.

Sei que é um corner case extremo que nunca vai acontecer, mas no CQ2 você perde quase toda sua área de jogo quando está na resolução 800x800. XD

Bom, é isso! Espero ter ajudado alguém que tenha passado pelo mesmo problema que eu. Se alguém tiver percebido algum equivoco ou tiver uma sugestão melhor de como contornar esse problema, pode entrar em contato comigo! :D

EDIT (21:54):

Tive uma ideia pra resolver o problema de telas com tamanho impar (isso tava me incomodando no editor):

var rect = camera.pixelRect;

rect.width = GetEvenPart(screenWidth);
rect.height = GetEvenPart(screenHeight);

camera.pixelRect = rect;

var scale = Mathf.Floor(Mathf.Min(rect.width, rect.height) / Size);
camera.orthographicSize = rect.height / (2.0f*scale);
private static int GetEvenPart(int value) {
  if (value % 2 == 0) {
    return value;
  }

  return value - 1;
}

O unico side effect é que vai ficar uma linha preta se a resolução for impar. *shrug*

Tags:

Compartilhar "Roguelike - Parte 2 - Pixel Perfection"

Compartilhe: FacebookTwitter

Roguelike - Parte 1 - The Beginning

Hoje resolvi escrever um post no blog sobre desenvolvimento. Faz bastante tempo que não faço isso e acho que é uma das coisas que me motiva pra continuar fazendo meus projetos pessoais.

Como esse será o primeiro post dessa série sobre esse jogo específico, vou antes falar um pouco de como decidi fazer esse jogo.

Em 2015 comecei a desenvolver jogos em Unity pra valer, graças a um projeto da Supernova Games chamado Rogue Hearts (Que acabou sendo postergado/cancelado): Rogue Hearts Alpha (Facebook)

E por conta disso, acabei conhecendo um joguinho chamado Cardinal Quest 2, que era usado como uma das inspirações para o projeto:

http://cardinalquest2.com/ (Sério, se nunca jogaram, baixem agora! Tem pra iOS e Android. Então, sem desculpas!)

Roguelikes em geral são um dos meus gêneros favoritos de jogos, e por ser um jogo com uma boa usabilidade mobile, Cardinal Quest 2 está no topo da minha lista de jogos da sua categoria para serem jogados em celular!

Para conseguir se tornar um jogo mobile, ele acabou simplificando diversos aspectos que tornam um Roguelike um Roguelike. Por conta disso, começaram a chamá-lo de “Roguelite”. É um jogo que não tem muito conteúdo, a aleatoriedade é bastante controlada (algumas áreas chegando a mudar quase nada), e tem estratégias de jogo bem limitadas.

Voltado ao Rogue Hearts… Naquele mesmo ano de 2015, eu fui para um evento grande da Unity chamado Unite Brasil, que aconteceu junto com o SBGames, onde fomos apresentar o jogo que estávamos desenvolvendo. Fiz uma postagem no meu blog sobre isso, que pode ser vista aqui:

http://busta.com.br/post/2015/11/14/fim-viagem-unite-sbgames/

Nesse evento foram apresentadas várias features da Unity que estariam disponíveis em early access, como o editor de Tiles e várias outras melhorias para o desenvolvimento de jogos 2D em geral!

https://blogs.unity3d.com/pt/2015/04/24/early-access-to-new-2d-tools/

Isso reacendeu minha vontade de criar meu próprio Roguelike. E como o Cardinal Quest 2 foi o jogo com a melhor usabilidade (Em termos de controle, coisas que você consegue ver na tela, tamanho dos elementos e interface), eu resolvi utilizá-lo como parâmetro para saber se meu jogo está indo no rumo certo.

De lá pra cá vim fazendo anotações na minha pasta de GDDs do Google Drive para não perder as ideias que iam surgindo de como esse jogo poderia ser. E agora, com a Unity 2017.4 tendo ~quase~ todas as features necessárias para o andamento desse projeto, resolvi dar continuidade.

E como eu disse ~quase~, uma das features que me faltou foi a de Pixel perfection. Isso eu vou explicar em mais detalhes no próximo post do blog (Roguelike Parte 2)!

PS: O Dev Blog do desenvolvedor do Cardinal Quest 2 é excelente também! Recomendo quem curtir acompanhar o andamento das coisas a dar uma olhada: http://wootfu.com/category/cardinal-quest/

Tags:

Compartilhar "Roguelike - Parte 1 - The Beginning"

Compartilhe: FacebookTwitter

Configurando o Jekyll

Como resolvi voltar a mexer no meu blog depois de muito tempo, resolvi começar pelo problema que estou tendo agora: Como rodar o Jekyll localmente para testar as mudanças no blog?

Primeiro, tem que ter o ruby + devkit instalado (peguei a versão 2.5.1-1_x64) https://rubyinstaller.org/downloads/

Depois, precisa instalar o bundler. Se o ruby estiver setado no path, basta executar o comando a seguir:

gem install bundler

Depois, precisa instalar as dependencias do seu projeto (Que estão no arquivo GemFile) usando o comando:

bundle install

Caso seu projeto já tenha instalado as dependencias, você pode atualizar elas com

bundle update

E por fim você executa um comando para gerar e hostear sua página:

bundle exec jekyll serve

Vale lembrar que a página ficará no endereço localhost:4000

Minha postagem foi só um TL;DR do que eu tive que fazer. Para mais detalhes, ver a página do github que tem um tutorial mais detalhado: https://help.github.com/articles/setting-up-your-github-pages-site-locally-with-jekyll/

Espero ter ajudado alguém!

Tags:

Compartilhar "Configurando o Jekyll"

Compartilhe: FacebookTwitter

Voltando ao Blog!

Olá! Estou voltando com meu blog em 2018, depois de alguns meses sem postar.

Muitas coisas aconteceram desde a ultima postagem, e algumas eu gostaria de falar sobre:

  • BIG 2017
  • Minha saida do doutorado
  • Minha vinda pra SP
  • Meu novo emprego na indústria de jogos!
  • Unity Developer’s Day
  • Como não passar um natal em SP
  • Lugares interessantes que já visitei
  • Ludum Dares 43 e 44
  • Global Game Jam 2018
  • Avanços no Le Petit Shooter

Vamos ver se eu consigo escrever tanta coisa!

Tags:

Compartilhar "Voltando ao Blog!"

Compartilhe: FacebookTwitter

BIG 2017 São Paulo

Pessoal, estaremos amanhã (23/06) viajando para São Paulo SP para participar do BIG Festival, que acontecerá na semana que vem! Iremos ver os jogos independentes sendo expostos por lá, e tentaremos mostrar o nosso “Le Petit Shooter” também. Seria um favor enorme quem puder curtir as nossas redes sociais, e dar feedback sobre nosso site! Links:

Facebook: https://www.facebook.com/yesdev.games/

Twitter: https://twitter.com/yesdevgames

Instagram: https://www.instagram.com/yesdev.games/

Telegram: https://t.me/yesdev

Youtube: https://www.youtube.com/channel/UCWWHrVRbkTaTK-KPl2s0_AQ

Site: http://yesdev.games

Precisamos de 100 inscritos no YouTube para poder criar uma URL personalizada para o nosso canal! Ajude-nos inscrevendo-se você também! Mandaremos trailers de nossos jogos e outras coisas interessantes!

Tags:

Compartilhar "BIG 2017 São Paulo"

Compartilhe: FacebookTwitter