Como programar um jogo isométrico (2.5D)

Table of Contents

TL;DR

Aqui está a função JavaScript para converter um ponto tridimensional em um bidimensional:

function to_isometric(point) {
  const angle = Math.PI / 6; // 30°
  const cos = Math.cos(angle); // ≈ 0.866
  const sin = Math.sin(angle); // = 0.5

  return {
    x: (point.x - point.y) * cos,
    y: (point.x + point.y) * sin - point.z
  };
}

Se você não consegue converter isso para a linguagem de programação que está usando, você não merece ter um design isométrico em seu jogo.

Explicação mais detalhada

Gráficos isométricos eram e são comuns em jogos. Esse estilo oferece uma espécie de mistura entre 2D e 3D que forma um estilo único.

O jogo Sims City é um exemplo de um que usa o estilo de arte isométrica.

O jogo Sims City é um exemplo de um que usa o estilo de arte isométrica.

O funcionamento do desenho isométrico é por meio de três retas separadas por 60º entre si, onde cada uma representa uma dimensão. Essas retas permitem converter um ponto tridimensional em um bidimensional usando a projeção ortográfica:

As três retas do desenho isométrico

A matemática por trás

As três retas

No desenho isométrico, como dito, temos três retas separadas por entre si 60º. Entretanto, como o eixo Z é totalmente vertical, vamos focar, por enquanto, apenas na X e na Y:

Eixos X e Y

Como é possível observar, os eixos X e Y estão em um ângulo de 30º em relação ao eixo X real (no caso anterior estavam em ângulos de 60º entre si), então a primeira coisa que será feita é encontrar os pontos de cada reta de acordo com a distância da origem – o que pode ser feito com seno e cosseno. Para isso, vamos supor um ponto tridimensional T = (3, 5, 0) (a dimensão Z não será considerada no momento), e queremos encontrar um ponto bidimensional A (que está na reta X) e um B (que está na reta Y):

T=(350) \vec{T} = \begin{pmatrix} 3 \\ 5 \\ 0 \end{pmatrix}

Calculando A e B:

A=T1(cos30sin30)A=3(cos30sin30)3(0.870.5)=(2.611.5) \vec{A} = T_1 \begin{pmatrix} \cos 30 ^ \circ \\ \sin 30 ^ \circ \end{pmatrix} \therefore \vec{A} = 3 \cdot \begin{pmatrix} \cos 30 ^ \circ \\ \sin 30 ^ \circ \end{pmatrix} \approx 3 \cdot \begin{pmatrix} 0.87 \\ 0.5 \end{pmatrix} = \boxed{ \begin{pmatrix} 2.61 \\ 1.5 \end{pmatrix} } B=T2(cos30sin30)B=5(cos30sin30)5(0.870.5)=(4.352.5) \vec{B} = T_2 \begin{pmatrix} -\cos 30 ^ \circ \\ \sin 30 ^ \circ \end{pmatrix} \therefore \vec{B} = 5 \cdot \begin{pmatrix} -\cos 30 ^ \circ \\ \sin 30 ^ \circ \end{pmatrix} \approx 5 \cdot \begin{pmatrix} -0.87 \\ 0.5 \end{pmatrix} = \boxed{ \begin{pmatrix} -4.35 \\ 2.5 \end{pmatrix} }

Plotando os pontos no gráfico:

Pontos A e B no gráfico

Agora, para sabermos o ponto real R – o que será desenhado na tela – temos que somar os dois vetores:

R=A+BR=(2.611.5)+(4.352.5)=(1.744) \vec{R} = \vec{A} + \vec{B} \therefore \vec{R} = \begin{pmatrix} 2.61 \\ 1.5 \end{pmatrix} + \begin{pmatrix} -4.35 \\ 2.5 \end{pmatrix} = \boxed{ \begin{pmatrix} -1.74 \\ 4 \end{pmatrix} }

Plotando no gráfico:

Ponto R no gráfico

Para adicionar o eixo Z é só somar esse valor ao Y real plotado – já que ele é totalmente vertical –, então se o valor de Z do ponto T fosse 3, o vetor de posição que será desenhado será (1.747)\begin{pmatrix} -1.74 \\ 7 \end{pmatrix}.

Função genérica

Uma função genérica seria dessa forma:

f(x)=x1(cos30sin30)+x2(cos30sin30)+(0x3) \vec{f}(\vec{x}) = x_1 \begin{pmatrix} \cos 30 ^ \circ \\ \sin 30 ^ \circ \end{pmatrix} + x_2 \begin{pmatrix} -\cos 30 ^ \circ \\ \sin 30 ^ \circ \end{pmatrix} + \begin{pmatrix} 0 \\ x_3 \end{pmatrix}
Por que na função JavaScript o Z é subtraído e não somado?

Se você for bem observador, você deve ter percebido isso. O motivo disso é porque na maioria das bibliotecas gráficas (SDL, HTML Canvas, Raylib, p5.js, etc.), quanto menor o valor de Y mais alto fica o ponto – o que é o inverso da maioria dos planos cartesianos, como o do Geogebra, que eu usei para demonstrar o funcionamento. Entretanto, se você estiver usando uma Game Engine como a Unity ou Godot, você deve mudar isso para somar ao invés de subtrair.

Implementando em p5.js

Abrindo editor.p5js.org, tem um editor on-line onde é possível programar em JavaScript usando a biblioteca gráfica p5.js – ela não é uma das melhores pra criar jogos, mas é muito fácil e muito usada para aprender programação.

Inicialmente, vai aparecer um código assim:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}

A função setup roda apenas uma vez, no início do jogo, enquanto a draw roda todo loop. Rodando, vemos apenas isso:

Tela em branco

O que queremos fazer é criar uma grade que se movimente como ondas, para isso, vamos usar a função do TL;DR. Depois, vamos desenhar pontos separados igualmente entre si por meio dessa função:

[...]
function draw() {
  background(220);
  
  // Coloca a câmera no centro
  translate(width/2, height/2)
  
  const grid_size=5
  const grid_distance=30
  for (let y=-grid_size; y<grid_size; y++) {
    for (let x=-grid_size; x<grid_size; x++) {
      // Transforma ponto 3D em um 2D
      let drawn_point = to_isometric(
        {x: x*grid_distance, y: y*grid_distance, z: 0}
      )
      
      // Desenha o ponto
      stroke(255, 0, 0)
      strokeWeight(3)
      point(drawn_point.x, drawn_point.y)
    }
  }
}

Assim, o resultado é esse:

Grid de pontos

Animando os pontos

Para isso, vamos usar a função seno, que, em um gráfico, se parece assim:

Sine graph

Passando o tempo desde o início da execução do programa para a função, o que pode ser feito pela função millis() do p5.js – que retorna quantos milissegundos desde o início da execução–, podemos animá-los:

let drawn_point = to_isometric(
  {x: x*grid_distance, y: y*grid_distance, z: Math.sin(millis()/1000)*20}
)

Resultado:

Animação inicial

Entretanto, como podemos ver, todos os pontos se movem igualmente. Para mudarmos isso, vamos adicionar um valor de offset para cada ponto, definido pela posição Y deles:

let drawn_point = to_isometric(
  {x: x*grid_distance, y: y*grid_distance, z: Math.sin(millis()/1000+y)*20}
)

Resultado:

Animação inicial

Adicionando sprites

Para isso, eu vou usar esses sprites no opengameart.org. Daí, eu cortei e diminuí a resolução no GIMP de forma que fique apenas um cubo:

Cubo isométrico pixel-art

Baixe essa imagem e coloque no editor. Para isso, crie uma conta e depois clique na seta no lado esquerdo do editor, daí clique no símbolo de “mais” e em “Upload file”, e selecione o arquivo do cubo.

Para desenhar o cubo, defina uma variável global chamada cube:

let cube

Depois, dentro da função setup(), declare a variável como sendo a textura do cubo:

cube = loadImage('cube.png')

E, ao invés de desenhar o ponto, desenhe a textura, lembrando de manter o tamanho delas como sendo grid_distance vezes dois:

/*stroke(255, 0, 0)
strokeWeight(3)
point(drawn_point.x, drawn_point.y)*/
image(cube, drawn_point.x, drawn_point.y, grid_distance*2, grid_distance*2)

Resultado:

Animação com texturas

#Tutorial   #Programacao   #Gamedev   #Matematica