Ponto V!

Home Arquitetura de Jogos Matemática e Física O uso de vetores nos jogos
Vinícius Godoy de Mendonça
O uso de vetores nos jogosPDFImprimir
Escrito por Vinícius Godoy de Mendonça

Muitos programadores ao se depararem com assuntos envolvendo matemática sentem profundos calafrios. Entretanto, se você quer ir além de jogos envolvendo “quadradinhos” na tela, deve começar aprendendo uma das mais úteis ferramentas da matemática: os vetores.

Este artigo introduz o conceito de vetores sem muito linguajar matemático e de maneira prática, mostrando como programá-lo no seu programa. Ao final, também disponibilizamos o download de classes de vetores 2D e 3D para as linguagens Java, C++ e C#.

O que são vetores?

Mas, afinal, o que são vetores? Algumas grandezas, tais como um número de sapato, a altura de um prédio ou mesmo os minutos que faltam para você acabar de ler essa matéria são representadas por um simples número, dentro de uma escala qualquer. Essas são as grandezas escalares.

Outras grandezas não são tão fáceis de descrever assim. Por exemplo, como descrever a direção que o seu corpo se move? Ou, como entender a força do vento? Além de uma intensidade, a força do vento também tem uma direção. Por isso, ela não é simplesmente uma grandeza escalar, mas sim, uma grandeza vetorial.

Vetores possuem uma orientação (ou seja, uma direção e um sentido) e um tamanho. Graficamente, representamos um vetor por uma seta, onde a ponta indica a orientação e o comprimento dá a noção de seu tamanho, por exemplo:

Representação gráfica do vetor

Vetores são muito versáteis. No exemplo acima, enquanto o primeiro poderia indicar a direção de uma bola sendo chutada por um zagueiro, o segundo poderia indicar a força gravidade. Vamos pegar o primeiro vetor exibido ali e colocar num eixo cartesiano. Assim, podemos dissecá-lo e entender suas propriedades:

Propriedades dos vetores

Um vetor bidimensional de tamanho t, no eixo cartesiano, pode ser expresso por duas grandezas, uma em x e outra em y. No C++ ou no Java, essas grandezas virariam duas variáveis float ou double:

#include 
class Vector2D
{
   public:
      float x;
      float y;

      Vector2D() : x(0.0f), y(0.0f) {}
      Vector2D(float _x, float _y) : x(_x), y(_y) {}
};

Um valor em x... outro em y... Isso lembra a coordenada de um ponto, não? Apesar de parecidos em sua estrutura, os vetores não são equivalentes a pontos. Eles representam um deslocamento em uma direção e não tem posição no espaço. Já os pontos, representam uma posição, e não tem direção ou sentido. Entretanto, você pode usar um vetor para representar um ponto: ele indicará então o deslocamento do ponto em relação à origem (0,0). Só tome cuidado pois algumas operações não fazem sentido entre pontos, como somar dois pontos.

Se você for um bom observador, já deve ter notado que o tamanho do vetor pode ser facilmente obtido pelo teorema de Pitágoras. Não notou ainda? Então, repare melhor na figura anterior. Os tamanhos em x e y são lados de um triângulo retângulo e o vetor é a hipotenusa. Assim, podemos acrescentar a nossa classe a seguinte função:

float Vector2D::size() 
{ 
   return std::sqrt(x*x + y*y); 
}

Até agora nada disso teve muita graça. Não desanime, estamos chegando na parte interessante. Podemos fazer a soma e a subtração entre dois vetores. Isso é moleza: basta somar o x do primeiro vetor com o x do segundo e repetir o processo para o y. A subtração também ocorre da mesma forma. Vamos demonstrar isso graficamente:

Soma e subtração

Que tal ver como isso ficaria no C++?

Vector2D Vector2D::operator +(const Vector2D& other) const
{
   return Vector2D(x + other.x, y + other.y);
}

Vector2D& Vector2D::operator +=(const Vector2D& other) 
{
   x += other.x;
   y += other.y;
   return *this;
}

Deixamos para você a tarefa de repetir o código para o sinal de menos.

Que tal algumas aplicações práticas disso tudo? Lembra-se que vetores podem representar pontos? Pois bem, a subtração entre dois vetores que representam pontos resulta num vetor que é a trajetória entre esses dois pontos e o tamanho desse vetor representa a distância que terá de ser percorrida. Observe:

image

E o que mais tem de interessante nisso tudo? Observe o que aconteceria se, a cada segundo (ou a cada loop do nosso jogo), somássemos o vetor que representava a trajetória de uma bola ao vetor da gravidade. Na figura abaixo, as linhas tracejadas representam o vetor trajetória do segundo anterior e o vetor gravidade, enquanto a linha forte a trajetória resultante da soma dos dois:

Soma de forças

Que chute, hein? O mais interessante é que se quiséssemos adicionar vento, bastaria simplesmente somar o vetor indicando a força e a direção do vento a cada segundo, como foi feito com a gravidade.

Agora, e se desejássemos um vetor que fosse duas vezes maior que outro? Ou que tal duas vezes menor? O fato é que vetores podem ser multiplicados ou divididos também por um escalar. O processo é simples, basta multiplicar (ou dividir) o x e y do vetor pelo número desejado, como nesse código em C++:

Vector2D Vector2D::operator *(float scalar) const
{
   return Vector2D(x * scalar, y * scalar);
}

Vector2D& Vector2D::operator *=(float scalar) 
{
   x *= scalar;
   y *= scalar;
   return *this;
}

E se quiséssemos impor um tamanho a um vetor? Um dos processos mais importantes envolvendo vetores chama-se normalização. Esse processo reduz o tamanho do vetor para 1. Um vetor normalizado pode então ser multiplicado por um escalar para obtermos tamanho desejado. Para normalizar um vetor, basta dividirmos x e y pelo tamanho do vetor:

Vector2D& Vector2D::normalize()
{
   return (*this /= size());
}

Você pode estar curioso a respeito do porque do nome normalização. Acontece que o tamanho do vetor também tem outros nomes como: magnitude, módulo, intensidade ou norma. O que prova que os matemáticos também sabem ser criativos com o português quando querem.

Vetores e ângulos

Além de definir um x e y fixos, seria muito bom que pudéssemos criar um vetor a partir de um ângulo em relação ao eixo x e um tamanho. Assim, poderíamos dizer que o atacante chutou a bola com um ângulo de 50º e uma força de 10. Como fazer isso? Lembre-se que todo vetor pode ser desenhado como um triângulo retângulo. Por conseqüência, podemos aplicar senos e co-senos para achar os valores de x e y. A fórmula é a seguinte:

X = co-seno(ângulo) * tamanho;
Y = seno(ângulo) * tamanho

Como já temos um construtor que recebe dois floats, vamos definir um método estático para essa função (lembre-se que o C++ trabalha com ângulos em radianos, não em graus e que 1 grau equivale à PI/180 radianos):

Vector2D Vector2D::bySizeAndAngle(float size, float angle)
{
   return Vector2D(
      cos(angle) * size,
      sin(angle) * size);
}

Da mesma forma, podemos definir qual é o ângulo de um vetor em relação ao eixo x, através do arco-tangente entre y e x:

float Vector2D::angle() const
{
   return atan2f(y,x);
}

Outra operação interessante é a rotação de um vetor. Com um pouco de trigonometria, a rotação pode ser descrita da seguinte forma:

Vector2D& Vector2D::rotate(float angle)
{
   float s = sin(angle);
   float c = cos(angle);

   float newX = x * c - y * s;
   float newY = x * s + y * c;

   x = newX;
   y = newY;

   return *this;
}

E que tal acharmos o ângulo entre um vetor A e B quaisquer? Para isso, usamos uma propriedade dos vetores conhecida como produto escalar. Afinal, o produto escalar é definido por duas formas:

Produto escalar

Assim, podemos dizer que:

Produto escalar

Onde |A| e |B| são respectivamente os tamanhos de A e B e α o ângulo que queremos calcular. Agora, note como o cálculo seria simplificado se A e B fossem vetores de tamanho 1:

Produto escalar: vetor unitário

Evitar o cálculo do tamanho reduz muito processamento, por isso é bom usar vetores normalizados para representar direções. No C++, o método para o cálculo do produto escalar fica assim:

float Vector2D::dot(const Vector2D& other) const
{
   return x * other.x + y * other.y;
}

E o do ângulo entre os dois vetores unitários, assim:

float Vector2D::angleBetween(const Vector2D& other) const
{
   float dp = dot(other); 

   //Força que o valor esteja na faixa esperada pelo C++
   //Se os floats fossem totalmente precisos, 
   //não precisaríamos fazer esses testes

   if(dp >= 1.0) dp = 1.0f;
   else if(dp <=-1.0) dp = -1.0f;

   //Faz o cálculo do ângulo. O C++ sempre retorna positivo
   float angPi = (float)acos(dp);

   //teste de lado para definir o sinal
   return y*other.x > x*other.y ? –angPi : angPi;
}

Mais exemplos práticos

E qual a utilidade disso tudo? Como isso é usado nos jogos? Em primeiro lugar, é fácil representar qualquer objeto no espaço por dois vetores. O primeiro descreve a posição do personagem. O segundo, descreve a sua direção (sendo esse normalmente um vetor unitário).

Antes de continuar lendo a matéria, pense em como você faria para que o Igor, o troll, caminhasse na direção a sua comida sem se virar bruscamente (os vetores da direção estão em azul e os da posição em preto):

Igor e sua comida

O primeiro passo seria calcular a trajetória do troll em relação a princesa. Já vimos que isso é fácil: basta subtrair as duas posições. O segundo, seria calcular o ângulo da direção do troll em relação a essa trajetória, para isso, normalizamos o vetor e usamos a função angleBetween. Por fim, bastaria rodarmos Igor alguns graus a cada loop do jogo, até que esse ângulo fosse atingido. O código se resumiria a:

//Calculamos a trajetória entre A e B
Vector2D path = A.position – B.position;

//Vemos se precisamos rodar
path.normalize() //Angulos so com vetores normalizados
float angleToRotate = A.direction.angleBetween(path);
float oneDegree = 3.1415f / 180;

//Roda suavemente, no máximo um grau
a.direction.rotate(
   angleToRotate > oneDegree ? oneDegree : angleToRotate); 

//Anda naquela direção
a.position += a.direction * speed * time;

Outro exemplo? Imagine uma nave com dois canhões laterais. Você já viu que é possível rodar vetores, portanto, seria fácil girar a nave. Como fazer para rodar os canhões junto com a nave e, assim, saber a posição onde os tiros devem sair?

A resposta é simples: Para cada canhão, crie um vetor partindo do centro da nave até a posição desejada (indicados em branco na terceira imagem). Sempre que o vetor de direção (amarelo) da nave rodar, gire também esses vetores de posicionamento dos canhões usando o mesmo ângulo.

Posições relativas dos canhões

Agora, basta somar os vetores à posição da nave para obter a posição real dos canhões.

Também seria possível calcular um campo de visão. Como saber se um inimigo da IA de seu jogo consegue ver o herói do jogador? Lembre-se: a subtração da posição do personagem e da posição do inimigo resulta no vetor da trajetória entre os dois. Com isso, você poderia definir que o jogador só seria visto se o tamanho desse vetor fosse menor que, por exemplo, 10 unidades. E melhor, além disso, o ângulo da direção do inimigo com esse vetor não poderia ser maior do que 45º (ou menor do que -45º). Voilá! Um campo de visão perfeito:

Campo de visão

Ufa! Não foi dessa vez que a princesa virou comida nas mãos de Igor (vamos torcer para ele não ter um bom faro).

Com vetores, você também seria capaz de modelar o comportamento dos planetas do sistema solar, exércitos, cardumes, fazer um carro perseguir outro, ou mesmo, manter aviões em uma perfeita formação no céu. Esses e outros exemplos podem ser encontrados no site sobre uma técnica conhecida como Steering Behaviors (site em inglês): http://www.red3d.com/cwr/steer/

Agora que você compreendeu a importância dos vetores e um pouco sobre como e quando usa-los, ponha seu conhecimento em prática! Você pode tentar fazer algo similar ao jogo Worms, onde um jogador atira objetos no outro. Além da gravidade, tente adicionar também a força do vento. Depois de pronto, chame seus amigos e divirta-se!

Clicando nos links a seguir, você poderá baixar as classes de vetores 2D e 3D em três versões: C++, Java e C#.

Comentários (18)
  • Vitor Almeida da Silva  - Excelente artigo
    avatar

    Excelente artigo Vinícius.

    Este é um tema que eu queria escrever um artigo a muito tempo.

    Os exemplos estão bem didáticos e os temas estão na medida.

    Parabéns.

    PS: qual programa você utilizou para fazer os diagramas e gráficos? Photoshop mesmo?

  • Vinícius Godoy de Mendonça  - Programas
    avatar

    Por incrível que pareça foi o próprio MS-Word. Os desenhos foram tirados da MS-Clipart Gallery.

    Eu tenho esse artigo há alguns anos, mas estava em formato PDF. Ia ser publicado no PDJ-Zine, mas a publicação acabou nunca acontecendo.

    Decidi então passa-lo para o formato de webpage e o resultado está aí. Ainda falta falar um pouco mais de produto vetorial, escalar e talvez até quaternions, mas isso deixo para um artigo futuro.

  • Bruno Crivelari Sanches  - Aulas de GA
    avatar

    Lendo esse artigo lembrei da frase do meu professor de Ga (Geometria Analítica): "Vetor é que nem anjinho de árvore de natal, você coloca ele onde quiser".

    Falava isso tentando enfiar na cabeça do pessoal que um vetor era uma direção e magnitude, independente da posição :).

  • Eduardo  - Ótimo
    avatar

    Muito bom esse artigo, ficou ótimo toda a metodologia utilizada, por favor continuem esse trabalho, sempre to seguindo vc !!! parabéns !

  • julio brito
    avatar

    Parabéns Vinícius.
    Muito fácil de entender, boas ilustrações.
    Artigo excelente.

  • Mikhail  - Duvida C++
    avatar

    Ainda estou aprendendo C++ ...

    Oque significa esse formato de contrutor/função:

    Vector2D(float _x, float _y) : x(_x), y(_y) {}

    por que tem esses ": x(_x), y(_y)" ???

  • Vinícius Godoy de Mendonça  - Lista de inicialização
    avatar

    Depois do : está a lista de inicialização. Ali, existe uma chamada ao construtor de cada atributo da classe. A vantagem da lista de inicialização é que ela é executada no momento que o C++ cria cada variável.

    Se você inicializar os dados apenas dentro das chaves, o C++ rodará a inialização padrão, para só então rodar o que está dentro das chaves. Ou seja, você acaba fazendo 1 inicialização seguida de uma atribuição de valor, desperdiçando tempo e recursos.

    Note que para a lista, os tipos primitivos também tem uma espécie de construtor. Por exemplo, mesmo uma variável x criada como int poderá ser inicializada com x(20), onde 20 é seu valor.

  • Bruno BPS  - Parabens
    avatar

    Parabens tio miii! se é foda D:!!

    Quando eu me formar em desgne de web sites, pode conta comigo para coisas do tipo oks?

    Muito bom o tutorial, estou lendo todos =D!!

    Vou começar tecnico em programação para ter uma base de programação =p

    Até mais tio mii!

  • Marcos  - Mto Show
    avatar

    Galera parabens pelo site,sou tecnico em eletrônica e "arranho" um pouco de programação.O trabalho de vcs é coisa nova no Brasil e deve ser valorizado...um abraço e continuen postando novos artigos.

  • Gustavo Borba  - Dúvida em relação à vetores
    avatar

    Olá Vinícius! Estou usando um Thread para executar o update:

    Código:
    Runnable r = new Runnable() {
    public void run() {
    while(true) {
    cubeSpeed = cubeSpeed.add(gravity);
    cubePos = cubePos.add(cubeSpeed);
    repaint();
    }
    }
    };

    update = new Thread(r);
    update.start();

    Mas este código aparentemente não funciona na hora de atualizar os vetores ao meu ver ele está indo muito rápido! Pois quando dou o println ele meio que faz um "sleep" e a imagem não sai voando tão rápido!

    Ex.:

  • Vinícius Godoy de Mendonça
    avatar

    Desse jeito vai ficar rápido mesmo. Dá uma lida [url=http://www.pontov.com.br/site/index.php/arquitetura-de-jogos/51-p rogramacao/113-animacao-baseada-em-tempo]aqui[/url], aqui e aqui. :)

  • Gustavo Borba  - Opa! Valeu pela dica!
    avatar

    Opa! Valeu pela dica, consegui consertar o problema, a gravidade estava muito forte, ;) .

    Mas tenho dúvida em relação a uma coisa. Estou utilizando a classe Graphics para desenhar simples retângulos e crio objetos da classe 'Rectangle' para checar uma colisão entre eles (intersects()). Mas se fosse o caso da figura geométrica estiver girada/rotacionada em um ângulo diferente, ele detectaria?

    Outra dúvida, meu quadrado cai somente no eixo Y ele fica parecendo muito falso! pois seu eixo X não se mexe nem um pouquinho e sua rotação não muda! Como posso fazer para que ele se torne mais real?

  • Vinícius Godoy de Mendonça
    avatar

    Detectaria, mas ao rotacionar o bounding box fica enorme, e a detecção pode ficar bem feia.

    O ideal seria rotacionar o retângulo junto, mas a classe Rectangle 2D não implementa a matemática disso, nem a detecção de colisão de retângulos rotacionados.

    Para melhorar a física de agora em diante, só com um bocado de matemática. Ou então, use uma engine pronta com isso pronto, como essa aqui: http://www.cokeandcode.com/phys2d/

  • Gustavo Borba  - Pergunta
    avatar

    o problema de eu não poder usar essa engine é porque eu mesmo gostaria de saber como fazer, não é nenhum trabalho, mas sim por curiosidade e interesse. Não haveria outro jeito?

  • Vinícius Godoy de Mendonça  - Física
    avatar

    Ter tem. Basta comprar um livro de física e estudar. Um ótimo material é o Mathematics and Physics for Programmers, mas ele é numa linguagem diferente do Java.

    Tem também o Physics for Game Programmers, do Grant Palmer, que é em Java. Os dois livros são muito bons.

    Infelizmente, não tem material assim tão direcionado em português. Talvez seja bom dar um pulo numa livraria e verificar se há algum livro de física (normal, não pra programadores) que você goste.

    Mas certamente, não é em um post que vou conseguir te explicar. Talvez eu elabore mais artigos sobre o assunto, mas primeiro vou terminar as publicações de OpenGL.

    Agora, com a teoria de vetores já dá para simular muita coisa, aliás, boa parte do que os jogos 3D usam no dia a dia.

  • Fabricio Maciel  - Parabéns, ótimo artigo
    avatar

    Muito bom o artigo, ja peguei sua classe em JAVA e comecei a brincar, em desenhar uma linha na tragetória entre 2 pontos.
    Ai é que estou fazendo confução, esse vetor 2D dentro de um plano cartesiano não seria um linha certo? (de um ponto x,y á z,w) e sim um ponto (somente x,y).
    Não sei se eu estou fazendo confusão, eu pediria se possivel, um exemplo pratico (tanto faz em java ou C++) que demonstrasse a utilização dessa classe para mostrar a linha dos/entre (algo bem simples mesmo)
    Obrigado

  • Vinícius Godoy de Mendonça
    avatar

    Oi. Realmente, ele representa um ponto.

    Esse ponto, a partir da origem (0,0) está na exata distância entre os dois pontos que você fez a subtração.

    E o ângulo da reta formada pela origem e ele em relação à reta x é o mesmo ângulo formado pela origem a reta entre esses dois pontos.

    Você pode entender esse ponto como aquela reta da trajetória "transportada" até a origem.

    O vetor também tem uma direção. Se você transportar um segmento de reta até a origem, terá a opção de coloca-la em 1 entre 2 quadrantes. O primeiro seria colocando uma das pontas da reta no (0,0) e o outro colocando a outra ponta.

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2