A programação orientada a objetos, frequentemente chamada de POO, é um paradigma de desenvolvimento que organiza o software em torno de objetos que representam elementos do mundo real ou conceitos abstratos. Esses objetos combinam dados e comportamentos, criando estruturas mais robustas, modulares e reutilizáveis. Esse paradigma mudou profundamente a forma de projetar sistemas por permitir um maior nível de organização e por aproximar a estrutura do código das situações reais que ele tenta resolver.
Esqueça os exemplos clássicos
Se você já pesquisou sobre Programação Orientada a Objetos (POO), é quase certeza que esbarrou em exemplos genéricos como: “Imagine uma classe Animal. O Cachorro e o Gato herdam dessa classe e fazem sons diferentes…”.
Apesar de clássicos, esses exemplos de livros didáticos escondem um problema grave: eles não mostram como a programação orientada a objetos resolve problemas reais de engenharia de software. Na vida real, você não programa cachorros virtuais. Você programa conexões de banco de dados, renderização de telas e componentes de interface.
Para descomplicar a programação orientada a objetos de verdade, vamos abandonar a teoria abstrata e colocar a mão na massa. Neste artigo, vou te mostrar como utilizei os 4 pilares da Orientação a Objetos para arquitetar a minha própria game engine, um motor gráfico feito em Java capaz de rodar tanto um clone clássico do jogo Space Invaders quanto um jogo de labirinto criado por mim que chamei de Freeze Monster.
Veja abaixo a execução da minha implementação.
Space Invaders
Freeze Monster
Uma game engine feita do zero
Quando pensamos em criar jogos, é comum imaginarmos logo ferramentas visuais gigantescas. Mas, para realmente entender como a magia acontece por debaixo dos panos, decidi construir uma game engine do zero utilizando Java. O objetivo aqui não é competir com grandes motores gráficos do mercado, mas sim aplicar na prática os conceitos fundamentais da engenharia de software e provar como uma boa arquitetura faz toda a diferença.
A arquitetura do projeto foi dividida em duas partes principais: o Framework e o Jogo.
No coração do nosso motor, criei um pacote Framework que dita as regras universais de qualquer jogo 2D. Temos uma classe AbstractBoard que gerencia o loop principal do jogo, atualiza os quadros e cuida da renderização na tela. Além disso, tudo o que se move ou interage na tela é considerado um “Sprite“. Para isso, construí uma classe Sprite que gerencia coordenadas, dimensões e caixas de colisão.
A grande sacada dessa arquitetura é que a engine não faz a menor ideia de qual jogo está rodando. Ela apenas sabe que existem jogadores e inimigos que precisam ser desenhados e atualizados a cada milissegundo. Quando quis criar o jogo Space Invaders, precisei apenas estender essas fundações no pacote spaceInvaders, definindo como a Nave se move e como os alienígenas atiram.
Essa separação clara de responsabilidades só foi possível graças aos 4 pilares da Programação Orientada a Objetos. Vamos destrinchar como cada um deles foi aplicado no código fonte.
Os quatro pilares da programação orientada a objetos
Os pilares fundamentais da programação orientada a objetos dão suporte ao paradigma e definem as regras estruturais que tornam o modelo eficiente.

Encapsulamento
O encapsulamento trata de proteger o estado interno de um objeto e expor apenas o que é estritamente necessário. Na nossa engine, a classe Sprite é um exemplo clássico.
public class Sprite {
private boolean visible;
protected Image image;
protected int x;
protected int y;
// ...
public boolean isVisible() {
return visible;
}
protected void setVisible(boolean visible) {
this.visible = visible;
}
// ...
}
A variável visible, que determina se o elemento deve ser desenhado na tela, é marcada como private. Isso significa que nenhuma outra classe do jogo pode simplesmente alterar essa variável diretamente e causar um bug visual. A única forma de ler ou alterar essa propriedade é através dos métodos isVisible() (método também conhecido como getVisible()) e setVisible(), garantindo o controle total sobre como a informação é manipulada.
Abstração
A abstração é outro pilar essencial e consiste em ocultar detalhes internos, expondo apenas o necessário para uso externo. Em outras palavras é focar no que deve ser feito, escondendo os detalhes complexos de como é feito.
public abstract class Player extends Sprite {
// ...
protected abstract void loadImage();
public abstract void act();
public abstract void keyPressed(KeyEvent e);
// ...
}
A classe Player é declarada como abstract. O motor do jogo estabelece um contrato: qualquer entidade que queira ser um jogador no nosso universo precisa saber agir (act), responder ao teclado (keyPressed) e ter uma imagem para ser representado(loadImage). Porém, a classe abstrata em si não implementa esse código. Ela abstrai a ideia genérica de um “jogador” e obriga que as classes filhas resolvam os detalhes de acordo com o jogo em questão.
Herança
A herança possibilita a criação de novas classes baseadas em classes já existentes. Esse mecanismo permite que subclasses herdem atributos e métodos das classes superiores, favorecendo a reutilização e a especialização de comportamentos. Veja a implementação da classe Sprite abaixo.
import java.awt.Image;
import java.awt.Rectangle;
public class Sprite {
private boolean visible;
protected Image image;
private boolean dying;
protected int x;
protected int y;
protected int imageWidth;
protected int imageHeight;
protected int dx;
protected int dy;
//...
}
Note agora que a classe Player que está logo abaixo herda as características de Sprite.
import java.awt.event.KeyEvent;
public abstract class Player extends Sprite {
public String posicao;
public String getPosicao() {
return posicao;
}
public int width;
public Player() {
loadImage();
getImageDimensions();
resetState();
}
protected abstract void loadImage();
public abstract void act();
public abstract void keyPressed(KeyEvent e);
public abstract void keyReleased(KeyEvent e);
public abstract void resetState();
}
Por fim, ao declarar public class Nave extends Player, a classe Nave automaticamente “herda” todas as características (coordenadas X e Y, imagem, largura, etc.) da classe Player (e, consequentemente, da classe Sprite que está acima dela). Não foi preciso reescrever o código de movimentação básica ou gerenciamento de dimensões da imagem. A Nave apenas reaproveita essa estrutura e foca no que a torna única: os limites específicos do mapa do Space Invaders. Além disso, note que a classe Nave tem alguns método com a notação @Override isso significa que esse método foi herdado de uma classe acima, no caso, Player e foi implementado respeitando as características da Nave.
import Framework.Commons;
import Framework.sprite.Player;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
public class Nave extends Player{
private int width;
@Override
protected void loadImage(){
ImageIcon ii = new ImageIcon(this.getClass().getResource("/images/player.png"));
width = ii.getImage().getWidth(null);
setImage(ii.getImage());
}
@Override
public void keyPressed(KeyEvent e){
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = -2;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 2;
}
}
@Override
public void keyReleased(KeyEvent e){
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = 0;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 0;
}
}
@Override
public void act() {
x += dx;
y +=dy;
if (x <= 0) {
x = 0;
}
if (x >= Commons.BOARD_WIDTH-30) {
x = Commons.BOARD_WIDTH-30;
}
if (y <= 0) {
y = 0;
}
if (y >= Commons.BOARD_HEIGHT-80) {
y = Commons.BOARD_HEIGHT-80;
}
}
@Override
public void resetState() {
setX(Commons.INIT_PLAYER_X-90);
setY(Commons.INIT_PLAYER_Y);
}
}
Polimorfismo
O polimorfismo permite que um mesmo método execute comportamentos diferentes conforme o objeto que o utiliza. Isso ocorre porque subclasses podem sobrescrever métodos herdados da classe base. Na nossa game engine, a classe abstrata Player estabelece o contrato de que todo jogador deve responder ao teclado através do método keyPressed(KeyEvent e). O motor do jogo simplesmente dispara esse evento, sem saber qual personagem está na tela.
No jogo Space Invaders, a classe Nave sobrescreve esse método focando apenas no movimento horizontal, alterando a variável dx, já que uma nave não sobe nem desce.
//...
@Override
public void keyPressed(KeyEvent e){
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = -2;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 2;
}
}
Por outro lado, no jogo Freeze Monster, o personagem principal tem liberdade total de exploração. A classe Cowboy sobrescreve o mesmíssimo método keyPressed, mas além de calcular o movimento horizontal e vertical (dx e dy), ela também altera uma variável interna posicao que vira a imagem do boneco para a direção correta.
//...
@Override
public void keyPressed(KeyEvent e){
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = -2;
posicao = "left";
}
if (key == KeyEvent.VK_RIGHT) {
dx = 2;
posicao = "right";
}
if(key == KeyEvent.VK_UP){
dy = -2;
posicao = "up";
}
if(key == KeyEvent.VK_DOWN){
dy = 2;
posicao = "down";
}
}
Isso é polimorfismo na prática: o sistema dá a ordem universal, mas cada objeto reage da sua própria maneira.
Por que não mostrei todo o código da game engine aqui ?
Construir uma game engine funcional do zero exige centenas de linhas de código. O motor gráfico possui lógicas complexas de renderização de quadros, loops de tempo, física de colisões e cálculos matemáticos. Inserir todo esse escopo aqui transformaria este artigo em um livro interminável e tiraria o nosso foco principal: entender como a arquitetura da orientação a objetos funciona no mundo real.
No entanto, este projeto não é apenas teórico! Todo o código-fonte da engine, com os jogos Space Invaders e Freeze Monster totalmente funcionais, está no meu GitHub. Você pode explorar cada pasta, ver como as classes se comunicam, baixar e testar na sua própria máquina.
Acesse o código completo da Game Engine no meu GitHub clicando aqui.
Desafio: A batalha com o Boss
A partir desse post, vimos como os quatro pilares da Programação Orientada a Objetos sustentam a arquitetura de um projeto escalável. Agora é a sua vez de sujar as mãos no código!
Se você acessar o nosso repositório no link acima, verá que os inimigos clássicos (como os alienígenas) morrem com apenas um único impacto. O seu desafio é expandir a nossa game engine criando uma batalha contra um Chefão (Boss).
A sua missão é:
- Criar uma nova classe chamada Boss.
- Utilizar a Herança para que essa classe estenda a classe base de inimigos do motor gráfico.
- Aplicar o Encapsulamento para criar uma variável de “pontos de vida” protegida de acessos indevidos externos.
- Usar o Polimorfismo para sobrescrever o comportamento de colisão. O Boss deve perder 1 ponto de HP a cada tiro recebido, e o método de morte só deve ser ativado quando essa vida chegar a zero.
Ficou curioso para saber como resolver esse problema de arquitetura de software na prática? Acesse o repositório, tente fazer a sua implementação e, se precisar de ajuda, deixe um comentário com sua dúvida ou coloque nos comentários se você quer que eu grave um vídeo para o canal Descomplicando Algoritmos propondo a solução!
Se você curtiu entender os fundamentos que sustentam a programação, também vai curtir saber o que é uma Máquina de Turing e porque ela é tão importante.
