9.4 KiB
Crie um Jogo Espacial Parte 1: Introdução
Quiz Pré-Aula
Herança e Composição no desenvolvimento de jogos
Em aulas anteriores, não havia muita necessidade de se preocupar com a arquitetura dos aplicativos que você criou, já que os projetos eram muito pequenos em escopo. No entanto, à medida que suas aplicações crescem em tamanho e complexidade, as decisões arquiteturais se tornam uma preocupação maior. Existem duas abordagens principais para criar aplicações maiores em JavaScript: composição ou herança. Ambas têm prós e contras, mas vamos explicá-las no contexto de um jogo.
✅ Um dos livros de programação mais famosos já escritos trata de design patterns.
Em um jogo, você tem objetos do jogo
, que são objetos que existem na tela. Isso significa que eles têm uma localização em um sistema de coordenadas cartesianas, caracterizada por ter uma coordenada x
e y
. À medida que você desenvolve um jogo, perceberá que todos os seus objetos do jogo têm propriedades padrão, comuns a qualquer jogo que você criar, ou seja, elementos que são:
- baseados em localização: A maioria, se não todos, os elementos do jogo são baseados em localização. Isso significa que eles têm uma localização, um
x
e umy
. - móveis: São objetos que podem se mover para uma nova localização. Normalmente, isso inclui um herói, um monstro ou um NPC (personagem não jogável), mas não, por exemplo, um objeto estático como uma árvore.
- autodestrutíveis: Esses objetos existem apenas por um período de tempo definido antes de serem marcados para exclusão. Geralmente, isso é representado por um booleano
dead
oudestroyed
que sinaliza ao motor do jogo que esse objeto não deve mais ser renderizado. - com tempo de espera: 'Tempo de espera' é uma propriedade típica entre objetos de curta duração. Um exemplo típico é um pedaço de texto ou efeito gráfico, como uma explosão, que deve ser visto apenas por alguns milissegundos.
✅ Pense em um jogo como Pac-Man. Você consegue identificar os quatro tipos de objetos listados acima nesse jogo?
Expressando comportamento
Tudo o que descrevemos acima são comportamentos que os objetos do jogo podem ter. Então, como codificamos isso? Podemos expressar esse comportamento como métodos associados a classes ou objetos.
Classes
A ideia é usar classes
em conjunto com herança
para adicionar um determinado comportamento a uma classe.
✅ Herança é um conceito importante para entender. Saiba mais no artigo da MDN sobre herança.
Expressado em código, um objeto do jogo pode ser algo assim:
//set up the class GameObject
class GameObject {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
}
}
//this class will extend the GameObject's inherent class properties
class Movable extends GameObject {
constructor(x,y, type) {
super(x,y, type)
}
//this movable object can be moved on the screen
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//this is a specific class that extends the Movable class, so it can take advantage of all the properties that it inherits
class Hero extends Movable {
constructor(x,y) {
super(x,y, 'Hero')
}
}
//this class, on the other hand, only inherits the GameObject properties
class Tree extends GameObject {
constructor(x,y) {
super(x,y, 'Tree')
}
}
//a hero can move...
const hero = new Hero();
hero.moveTo(5,5);
//but a tree cannot
const tree = new Tree();
✅ Reserve alguns minutos para imaginar como seria um herói do Pac-Man (Inky, Pinky ou Blinky, por exemplo) escrito em JavaScript.
Composição
Uma maneira diferente de lidar com herança de objetos é usando Composição. Nesse caso, os objetos expressam seu comportamento assim:
//create a constant gameObject
const gameObject = {
x: 0,
y: 0,
type: ''
};
//...and a constant movable
const movable = {
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//then the constant movableObject is composed of the gameObject and movable constants
const movableObject = {...gameObject, ...movable};
//then create a function to create a new Hero who inherits the movableObject properties
function createHero(x, y) {
return {
...movableObject,
x,
y,
type: 'Hero'
}
}
//...and a static object that inherits only the gameObject properties
function createStatic(x, y, type) {
return {
...gameObject
x,
y,
type
}
}
//create the hero and move it
const hero = createHero(10,10);
hero.moveTo(5,5);
//and create a static tree which only stands around
const tree = createStatic(0,0, 'Tree');
Qual padrão devo usar?
A escolha do padrão depende de você. O JavaScript suporta ambos os paradigmas.
--
Outro padrão comum no desenvolvimento de jogos aborda o problema de gerenciar a experiência do usuário e o desempenho do jogo.
Padrão Pub/Sub
✅ Pub/Sub significa 'publicar-assinar'
Esse padrão aborda a ideia de que as partes distintas da sua aplicação não devem saber umas das outras. Por quê? Isso facilita muito a visualização geral do que está acontecendo se as várias partes estiverem separadas. Também facilita mudar o comportamento repentinamente, se necessário. Como fazemos isso? Estabelecendo alguns conceitos:
- mensagem: Uma mensagem geralmente é uma string de texto acompanhada de uma carga opcional (um dado que esclarece sobre o que é a mensagem). Uma mensagem típica em um jogo pode ser
KEY_PRESSED_ENTER
. - publicador: Este elemento publica uma mensagem e a envia para todos os assinantes.
- assinante: Este elemento ouve mensagens específicas e executa alguma tarefa como resultado de receber essa mensagem, como disparar um laser.
A implementação é bem pequena, mas é um padrão muito poderoso. Veja como pode ser implementado:
//set up an EventEmitter class that contains listeners
class EventEmitter {
constructor() {
this.listeners = {};
}
//when a message is received, let the listener to handle its payload
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
//when a message is sent, send it to a listener with some payload
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach(l => l(message, payload))
}
}
}
Para usar o código acima, podemos criar uma implementação muito simples:
//set up a message structure
const Messages = {
HERO_MOVE_LEFT: 'HERO_MOVE_LEFT'
};
//invoke the eventEmitter you set up above
const eventEmitter = new EventEmitter();
//set up a hero
const hero = createHero(0,0);
//let the eventEmitter know to watch for messages pertaining to the hero moving left, and act on it
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
//set up the window to listen for the keyup event, specifically if the left arrow is hit, emit a message to move the hero left
window.addEventListener('keyup', (evt) => {
if (evt.key === 'ArrowLeft') {
eventEmitter.emit(Messages.HERO_MOVE_LEFT)
}
});
No exemplo acima, conectamos um evento de teclado, ArrowLeft
, e enviamos a mensagem HERO_MOVE_LEFT
. Escutamos essa mensagem e movemos o herói
como resultado. A força desse padrão é que o listener de eventos e o herói não sabem um do outro. Você pode remapear o ArrowLeft
para a tecla A
. Além disso, seria possível fazer algo completamente diferente no ArrowLeft
fazendo algumas edições na função on
do eventEmitter:
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
À medida que as coisas ficam mais complicadas com o crescimento do seu jogo, esse padrão mantém a mesma complexidade e seu código permanece limpo. É altamente recomendado adotar esse padrão.
🚀 Desafio
Pense em como o padrão pub-sub pode melhorar um jogo. Quais partes deveriam emitir eventos e como o jogo deveria reagir a eles? Agora é sua chance de ser criativo, pensando em um novo jogo e como suas partes poderiam se comportar.
Quiz Pós-Aula
Revisão e Autoestudo
Saiba mais sobre Pub/Sub lendo sobre o assunto.
Tarefa
Aviso Legal:
Este documento foi traduzido utilizando o serviço de tradução por IA Co-op Translator. Embora nos esforcemos para garantir a precisão, esteja ciente de que traduções automatizadas podem conter erros ou imprecisões. O documento original em seu idioma nativo deve ser considerado a fonte autoritativa. Para informações críticas, recomenda-se a tradução profissional realizada por humanos. Não nos responsabilizamos por quaisquer mal-entendidos ou interpretações equivocadas decorrentes do uso desta tradução.