Matematica para Jogos: Vetores sem Susto

A matematica para jogos que voce realmente usa comeca em vetores. Aprenda Vector2, normalizar, distancia, dot e lerp com exemplos em GDScript.
Matematica para Jogos: Vetores sem Susto
Muita gente adia o sonho de fazer jogos por causa de uma frase que ouviu na escola: "isso ai e cheio de matematica". A boa noticia e que a matematica para jogos do dia a dia nao e prova de calculo, e sim um punhado de operacoes com vetores que a engine ja resolve pra voce. Seu trabalho e entender o que cada uma faz e quando chamar. Neste artigo a gente comeca do zero, com Vector2 no Godot 4, e cada conceito vem com um exemplo de jogo que roda como esta.
Se voce sabe somar e multiplicar, voce sabe o suficiente pra comecar. O resto e vocabulario. Vamos construir esse vocabulario devagar, sem susto, na ordem em que ele aparece num jogo de verdade.
A matematica para jogos comeca no Vector2
Um Vector2 e so um par de numeros guardados juntos: um x e um y. Nada mais que isso. No Godot 2D esse par serve pra duas coisas bem diferentes, e confundir uma com a outra e a causa de metade dos bugs de movimento de iniciante.
var posicao: Vector2 = Vector2(100.0, 50.0) # um PONTO no mundo
var direcao: Vector2 = Vector2(1.0, 0.0) # uma SETA apontando pra direita
posicao diz onde uma coisa esta: 100 pixels pra direita, 50 pra baixo, a partir do canto superior esquerdo. direcao diz pra que lado uma coisa aponta: (1, 0) e "pra direita", (0, -1) e "pra cima" (no Godot 2D o y cresce pra baixo, entao cima e negativo). Os dois sao Vector2, mas o significado e outro.
Somar dois vetores e andar um em cima do outro. Multiplicar um vetor por um numero (um escalar) estica ou encolhe a seta sem mudar o lado pra onde ela aponta:
var a: Vector2 = Vector2(2.0, 3.0)
var b: Vector2 = Vector2(1.0, 1.0)
var soma: Vector2 = a + b # (3, 4)
var dobro: Vector2 = a * 2.0 # (4, 6), mesma direcao, comprimento dobrado
Guarde essa imagem: vetor e seta com comprimento e direcao. Quase tudo daqui pra frente e brincar com essas duas propriedades. Se voce ainda esta se acostumando a raciocinar em passos como esses, vale a pena fortalecer antes a logica de programacao para jogos, porque vetor sem base de logica vira decoreba.
Normalizar para mover em velocidade constante
Aqui aparece o primeiro problema classico. Voce le o input do teclado e monta uma direcao somando os eixos:
extends CharacterBody2D
const SPEED: float = 300.0
func _physics_process(delta: float) -> void:
var entrada: Vector2 = Vector2.ZERO
entrada.x = Input.get_axis("ui_left", "ui_right")
entrada.y = Input.get_axis("ui_up", "ui_down")
velocity = entrada * SPEED
move_and_slide()
Funciona, mas com um defeito: na diagonal o personagem anda mais rapido. Quando voce segura direita e baixo ao mesmo tempo, a entrada vira (1, 1). O comprimento desse vetor nao e 1, e aproximadamente 1,41. Multiplicado pela velocidade, isso da uns 41% a mais de velocidade so por andar torto. O jogador percebe na hora.
A correcao e normalized(). Normalizar um vetor mantem a direcao dele mas forca o comprimento pra exatamente 1. Ai todo vetor de input, reto ou diagonal, chega no mesmo tamanho antes de multiplicar pela velocidade:
func _physics_process(delta: float) -> void:
var entrada: Vector2 = Vector2.ZERO
entrada.x = Input.get_axis("ui_left", "ui_right")
entrada.y = Input.get_axis("ui_up", "ui_down")
var direcao: Vector2 = entrada.normalized()
velocity = direcao * SPEED
move_and_slide()
A regra de bolso e simples: quando um vetor representa "pra que lado", normalize antes de usar como direcao. O comprimento vira responsabilidade da velocidade, nao do acaso do input. So um cuidado: normalizar o vetor zero (parado, sem input) devolve zero mesmo, entao voce nao precisa tratar nada, o Godot ja lida com esse caso.
O SPEED ali esta em pixels por segundo, e por isso ele se combina com o delta do loop de fisica. Se a relacao entre velocidade, tempo e frame ainda nao esta clara, o artigo sobre delta time e movimento por frame fecha exatamente esse buraco e complementa o que estamos vendo aqui.
Distancia entre dois pontos: alcance e mira
Agora que temos posicao e direcao, a proxima pergunta de todo jogo e: quao longe uma coisa esta da outra? Isso e distancia, e o Godot te da pronta com distance_to().
Pense num inimigo que so ataca quando o player chega perto:
extends CharacterBody2D
@export var alcance_ataque: float = 120.0
var player: Node2D
func _physics_process(delta: float) -> void:
if player == null:
return
var distancia: float = global_position.distance_to(player.global_position)
if distancia <= alcance_ataque:
atacar()
func atacar() -> void:
pass
distance_to() devolve um unico numero, a distancia em pixels entre os dois pontos. Voce compara com o alcance e pronto, tem a logica de "esta no meu alcance?" sem escrever nenhuma raiz quadrada na mao.
Um detalhe de desempenho que vale conhecer: calcular distancia exata envolve uma raiz quadrada, que e um tiquinho mais cara. Quando voce so precisa comparar distancias (quem esta mais perto, esta dentro do raio), da pra usar distance_squared_to(), que pula a raiz. Compare com o alcance ao quadrado:
var alcance_quad: float = alcance_ataque * alcance_ataque
if global_position.distance_squared_to(player.global_position) <= alcance_quad:
atacar()
Pra um inimigo so, a diferenca e invisivel. Pra centenas de inimigos checando distancia todo frame, ajuda. Comece com distance_to(), que e mais legivel, e so troque se algum dia isso pesar.
E a mira? Pra saber a direcao de um ponto ate outro, existe direction_to(), que ja devolve o vetor normalizado apontando de um pra outro. Um projetil teleguiado simples nasce disso:
extends Area2D
const SPEED: float = 400.0
var alvo: Node2D
func _physics_process(delta: float) -> void:
if alvo == null:
return
var direcao: Vector2 = global_position.direction_to(alvo.global_position)
global_position += direcao * SPEED * delta
direction_to() e literalmente (alvo - origem).normalized(), mas com nome que explica a intencao. Sempre que voce for perseguir, mirar ou apontar, esse e o metodo.
Antes de seguir pros conceitos que assustam mais gente, um momento de honestidade.
Se voce chegou ate aqui achando que ia travar na primeira formula e nao travou, guarde essa sensacao. O medo de matematica e quase sempre maior que a matematica de verdade. O que trava o sonho de fazer jogos raramente e a conta em si: e a historia que a gente conta pra si mesmo de que "nao leva jeito". Vetor de jogo e visual, e testavel, e voce ve o resultado na tela na hora. Isso muda tudo, porque voce aprende corrigindo o que ve, nao decorando o que nao entende.
Produto escalar (dot): o inimigo esta de frente?
O dot(), produto escalar, e o que separa quem copia tutorial de quem entende vetor. Parece abstrato, mas resolve uma pergunta muito concreta: duas setas apontam mais ou menos pro mesmo lado?
O produto escalar entre dois vetores normalizados devolve um numero entre -1 e 1:
- perto de
1: apontam pra mesma direcao (mesmo sentido) - perto de
0: sao perpendiculares (90 graus entre eles) - perto de
-1: apontam pra lados opostos
Isso e ouro pra IA e stealth. Imagine um guarda que so enxerga o player se ele estiver dentro do cone de visao, na frente do guarda, nao atras:
extends Node2D
@export var angulo_visao_graus: float = 45.0
var player: Node2D
func player_no_campo_de_visao() -> bool:
if player == null:
return false
var frente: Vector2 = Vector2.RIGHT.rotated(rotation)
var para_player: Vector2 = global_position.direction_to(player.global_position)
var limite: float = cos(deg_to_rad(angulo_visao_graus))
return frente.dot(para_player) >= limite
Vamos ler devagar. frente e a direcao pra onde o guarda esta olhando (o eixo direito girado pela rotacao dele). para_player e a direcao normalizada do guarda ate o player. O dot() entre os dois diz o quao alinhados eles estao. Se o resultado for maior ou igual ao cosseno do angulo de visao, o player caiu dentro do cone. Meio cone de 45 graus vira cos(45), que e cerca de 0,7, e a comparacao pronta.
O mesmo dot() responde outras perguntas do jogo: "o inimigo esta atras de mim?" (dot negativo com a minha frente), "esse tiro veio pela frente ou pelas costas?", "o carro esta indo pra frente ou de re?". Uma operacao, varios usos. Voce nao precisa entender a formula por tras do dot pra comecar a usar; precisa lembrar da regua -1 a 1 e do que cada ponta significa.
Interpolacao (lerp) para movimento suave
O ultimo conceito e o que deixa o jogo gostoso de ver: lerp, de interpolacao linear. Ela pega dois valores e te devolve um ponto no meio do caminho, controlado por um fator entre 0 e 1.
Com 0 voce fica no comeco, com 1 no fim, com 0.5 bem no meio. Vale pra numeros (uma barra de vida que desce suave) e pra vetores (uma camera que segue o player sem grudar). A versao pra vetor e o lerp() do proprio Vector2:
extends Camera2D
@export var suavidade: float = 5.0
var alvo: Node2D
func _physics_process(delta: float) -> void:
if alvo == null:
return
global_position = global_position.lerp(alvo.global_position, suavidade * delta)
A cada frame a camera cobre uma fracao da distancia que falta ate o alvo. Perto do alvo os passos ficam pequenos, entao ela desacelera de forma natural, sem parada seca. Trocar o suavidade deixa a camera mais preguicosa ou mais grudada. Isso e o mesmo efeito que voce ve em quase todo jogo de plataforma bem-feito.
Existe um primo do lerp muito util pra HUD e barras: move_toward(). A diferenca e que ele anda um passo de tamanho fixo em direcao ao alvo, e nunca passa do ponto. Bom pra uma barra de vida que voce quer que ande a uma taxa constante:
var vida_exibida: float = 100.0
var vida_real: float = 100.0
func _process(delta: float) -> void:
vida_exibida = move_toward(vida_exibida, vida_real, 40.0 * delta)
Aqui a barra caminha 40 unidades por segundo ate bater na vida real e para exatamente ali. lerp desacelera perto do fim; move_toward mantem o passo e trava no alvo. Duas ferramentas pra dois tipos de sensacao. Repare que nos dois casos o delta aparece de novo: taxa por segundo sempre multiplica pelo tempo do frame.
Juntando tudo num inimigo de verdade
Repare como os quatro conceitos vivem juntos num unico script. Um inimigo que persegue o player, so ataca dentro do alcance e vira de frente pra ele:
extends CharacterBody2D
const SPEED: float = 180.0
@export var alcance: float = 100.0
var player: Node2D
func _physics_process(delta: float) -> void:
if player == null:
return
var distancia: float = global_position.distance_to(player.global_position)
var direcao: Vector2 = global_position.direction_to(player.global_position)
if distancia > alcance:
velocity = direcao * SPEED # direcao ja vem normalizada
else:
velocity = Vector2.ZERO
atacar()
move_and_slide()
func atacar() -> void:
pass
Distancia decide se persegue ou ataca. Direcao normalizada resolve o movimento. Se voce quisesse um cone de visao, o dot() entraria; se quisesse suavizar a virada do sprite, o lerp de angulo faria o trabalho. Nenhuma formula assustadora, so metodos com nomes que dizem o que fazem.
O que levar pra proxima cena
Vector2e um par x, y que serve tanto pra posicao (ponto) quanto pra direcao (seta). Saber qual e qual evita metade dos bugs.normalized()forca o comprimento pra 1 e mantem a direcao: use sempre que o vetor for "pra que lado", inclusive no input pra corrigir a diagonal rapida.distance_to()mede quao longe (alcance, mira), edirection_to()aponta de um ponto ao outro ja normalizado.dot()compara direcoes num numero de -1 a 1: perfeito pra saber se algo esta de frente, de lado ou de costas.lerp()emove_toward()deixam movimento e valores suaves; um desacelera perto do fim, o outro mantem passo fixo.
Vetor e a base de quase toda a matematica para jogos que vem depois: fisica, colisao, IA, camera. Uma vez que a ideia de seta com comprimento e direcao assenta, o resto para de parecer feitico. O melhor jeito de fixar e abrir o Godot e testar cada metodo numa cena boba, imprimindo o resultado no console e vendo o numero mudar. Se voce quer um caminho guiado do zero ate seu primeiro jogo publicado, sem pular fundamento, veja o curso de criacao de jogos para iniciantes e comece pela pratica, que e onde a matematica finalmente para de dar susto.
Perguntas frequentes
Preciso ser bom em matematica para fazer jogos?
Nao. A matematica para jogos do dia a dia e basicamente vetores, e a engine ja resolve as contas pesadas pra voce. Voce precisa entender o que cada operacao faz e quando usar, nao decorar formulas.
Qual a diferenca entre posicao e direcao se os dois sao Vector2?
A estrutura e a mesma, dois numeros x e y. O que muda e o significado: posicao e um ponto no mundo, direcao e uma seta que aponta pra algum lado. Direcao quase sempre precisa ser normalizada para ter comprimento 1.
Por que meu personagem anda mais rapido na diagonal?
Porque a soma de dois eixos cria um vetor mais longo que 1. Chame normalized() no vetor de input antes de multiplicar pela velocidade e a diagonal fica na mesma velocidade das direcoes retas.


