Como um jogo digital é criado?

Olá, Web Developers!

Hoje vou mostrar como um jogo é criado. Para podermos ver ele funcionando aqui no navegador, usaremos JavaScript, mas esse conceito é basicamente o mesmo na criação de jogos com outras linguagens, frameworks e ferramentas, então nem é preciso conhecer a linguagem para entender.

Depois, mostrarei como os conceitos apresentados aqui estão presentes em ferramentas profissionais.

Vamos lá!

A tela (canvas)

Um jogo precisa de um lugar que nos possibilite exibir imagens. Normalmente esse elemento é chamado de canvas, que significa “tela de pintura”. Esse é um elemento que nos permite desenhar e renderizar imagens nele.

No Java e no C# por exemplo, temos a classe Canvas. No HTML5 possuímos o elemento <canvas>, que é o que iremos usar. Será o nosso único HTML. Todo o resto será desenhado no canvas.

Também teremos um CSS simples só para alinhar o canvas no meio da tela.

<canvas id="myCanvas" width="400" height="200" ></canvas>
canvas{
    border: 1px solid black;
    margin: 0 auto;
    display: block;
}

Agora todo o resto será JavaScript!

Desenhando na tela

Vamos fazer um simples desenho no canvas.

// primeiro selecionamos o elemento canvas
const canvas = document.querySelector('#myCanvas');
/* depois pegamos o contexto, que irá nos fornecer as funções
para desenhar no canvas.
*/
const ctx = canvas.getContext('2d');

/* a função "fillRect()" nos permite desenhar um retângulo.
Passamos a posição x, posição y, largura e altura.
*/
ctx.fillRect(25, 25, 50, 50);

Quadrado no Canvas

O que é uma animação?

Uma animação é basicamente a apresentação de várias imagens ligeiramente diferentes. Isso nos passa a ilusão de movimento.

Kirby Walking Sprite

Kirby Walking Sprite

No exemplo acima temos várias imagens que, ao serem exibidas uma após a outra, nos fazem pensar que estamos vendo o personagem se mover.

Cada imagem é chamada de frame(quadro). Quanto mais frames por segundo (FPS), mais natural será a ilusão de movimento. Os jogos hoje em dia costumam trabalhar em 60 FPS.

Então, o que acontece se pegarmos o canvas, limpá-lo, desenhar, limpá-lo novamente e fazer um desenho levemente diferente? Isso mesmo! Teremos a ilusão do movimento.

Para isso, basta desenharmos o quadrado com uma posição diferente a cada momento. Então podemos armazenar a posição em uma variável e criar um looping.

O Looping

Como os jogos são basicamente uma tela de desenho com vários elementos sendo desenhados a todo instante, uma das partes principais de um jogo é o Looping.

Basicamente teremos um Looping Infinito que limpa a tela, desenha os elementos no canvas baseado nas variáveis que possuímos e repete esse processo sem parar.

Vamos mudar um pouquinho o nosso código para ver o que acontece.

const canvas = document.querySelector('#myCanvas');
const ctx = canvas.getContext('2d');
/* declaramos uma variável que armazenará a posição
x do nosso quadrado.
*/
let x = 0;
/* o "setInterval()" repete a execução de uma
função de acordo com um tempo que passamos.
Neste caso, a função se repeta a cada 50 milissegundos.
Isso significa que desenharemos no canvas 20 vezes por
segundo, ou seja, teremos 20 frames por segundo.
*/
setInterval(function(){
    // aqui estamos limpando o canvas inteiro
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // após limpar o canvas, desenhamos o quadrado
    // na posição x
    ctx.fillRect(x, 25, 50, 50);
    // após desenhar o quadrado, alteramos nossa
    // variável x
    x+= 1;
    /* só para não perdermos o quadrado de vista,
    fiz com que o x volte a 0 caso o quadrado
    saia do canvas
    */
    if(x > canvas.width){
        x = 0;
    }
}, 50);

Isso fará com que o quadrado ande pela tela, como você pode ver logo abaixo:

Observação: No JavaScript, ao fazer animações, é recomendado o uso da função requestAnimationFrame() ao invés de setTimeout() ou setInterval();

Carregando imagens

Jogos utilizam vários recursos como áudio e imagens. Vamos criar uma função que irá carregar imagens.

Se tentarmos desenhar a imagem antes dela ter sido carregada, ocorrerá um erro. Então vamos aproveitar as Promises do JavaScript para iniciar o desenho no canvas apenas quando a imagem já estiver carregada.

// ...
/* essa função receberá o endereço de uma imagem
*/
function loadImage(src){
    /* ela retorna uma promise, que permite a
    execução de uma função apenas quando ela
    for resolvida
    */
    return new Promise(resolve => {
        /* criamos um elemento de imagem para
        carregar a imagem.
        */
        var img = document.createElement('img');
        /* criamos um listener que será executado
        assim que a imagem for carregada. Quando a
        imagem terminar de ser carregada, resolvemos
        a promise, retornando a imagem.
        */
        img.addEventListener('load', () => { 
            resolve(img); 
        });
        /* pegamos o elemento de imagem e passamos
        o endereço da imagem. Isso iniciará o
        carregamento
        */
        img.src = src;
    });
}

/* iniciamos o carregamento da imagem. Assim que
a imagem for carregada, a promise é resolvida e
a função seguinte é executada.
*/
loadImage('https://i.imgur.com/WyqxuaD.png')
    .then(function(sprite){
        setInterval(function(){
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // desenhamos a imagem ao invés do retângulo
            ctx.drawImage(sprite, x, 25, 50, 50);
            x+= 1;
            if(x > canvas.width){
                x = 0;
            }
        }, 50);
  })

Com isso teremos o Kirby passando pela tela.

Para não termos que tratar a criação de uma animação, usei aqui uma imagem estática mesmo.

Organizando o código: O objeto Game

Até agora só fizemos bagunça. Vamos organizar mais o nosso código, o que nos permitirá ter mais controle também.

const canvas = document.querySelector('#myCanvas');

/* alterei levemente a função de carregar
imagens. Agora ele recebe um objeto em
que armazenaremos a imagem carregada,
e também passamos o nome do campo
em que a imagem será armazenada.
*/
function loadImage(src, storage, field){
    return new Promise(resolve => {
        var img = document.createElement('img');
        img.addEventListener('load', () => {
            storage[field] = img;
            resolve(img);
        });
        img.src = src;
    });
}
/* Todo o código que controla o jogo
ficará dentro desse objeto "Game"
*/
const Game = {
/* guardei o contexto do canvas dentro de Game
*/
    ctx : canvas.getContext('2d'),
/* a função start() será responsável por iniciar
o jogo. Ela executa a função load() e, quando
tudo estiver carregado, inicia o looping.
*/
    start(){
        this.load().then(() =>{
        /* guardo o interval na variável timer.
        Assim seremos capazes de parar o looping do
        jogo caso seja necessário.
        */
            this.timer = setInterval(() => {
            /* o looping executa essas duas funções.
            Logo veremos o que elas fazem.
            */
                this.update();
                this.draw();
            }, 30);
        })
    },
    /* a função stop() limpa o nosso interval, fazendo
    o jogo parar de ser executado.
    */
    stop(){
        clearInterval(this.timer);
    },
    /* a função load() é onde nós iremos carregar nossas
    imagens. Quando tudo é carregado, ele retorna uma promise.
    Assim garantimos que o jogo só será inicado quando
    todos os recursos estiverem carregados.
    */
    load(){ 
        return loadImage('https://i.imgur.com/k6heF1k.png', Game, 'background');
    },
    /* criei a função clear() para facilitar a limpeza do canvas.
    */
    clear(){
        this.ctx.clearRect(0, 0, canvas.width, canvas.height);
    },
    /* a função draw() será responsável por executar
    tudo o que for relacionado em desenhar algo no canvas.
    Em sua primeira linha nós já executamos o clear().
    */
    draw(){
        this.clear();
        this.ctx.drawImage(this.background, 0, 0, canvas.width, canvas.height);
    },
    /* a função update() serve para atualizarmos algum valor.
    Imagine que você queira criar um "relógio". Como a 
    função update() fica sendo executada pelo looping,
    podemos ir atualizando o tempo nela. Com o valor
    alterado, a função draw() cuidará do resto.
    */
    update(){
    }
}

/* chamamos essa função para iniciar o jogo.
Por enquanto tiramos o Personagem. Temos só
o fundo. Criaremos o personagem a seguir.
*/
Game.start();

Criando um personagem: O objeto Character

Agora que temos o objeto Game cuidando de tudo, está mais fácil criarmos outros objetos que estarão presentes no nosso jogo.

Vamos criar um personagem que poderemos controlar.

Em um jogo real, para poder reutilizar o código, seria mais comum criarmos uma classe. Mas como a ideia aqui é simplificar só para fins didáticos, eu vou criar um objeto literal mesmo.

// criamos o objeto Character para o nosso personagem
const Character = {
/* declaramos algumas variáveis que nos permitirão
ter mais controle do personagem.
Ao criar o seu jogo, poderá criar as variáveis que quiser.
*/
    x: 50, // controla a posição horizontal
    y: 125, // controla a posição vertical
    speed: 5, // controla a velocidade do movimento
    /* a função start() é executada apenas uma vez
    quando o personagem é inserido na tela
    */
    start(){
    /* aqui aproveitamos a funçõa start() para
    começar a escutar eventos do teclado.
    Verificamos se determinada tecla está sendo
    pressionada, e assim alteramos o valor da
    variável x, que fará com que o personagem
    seja desenhado em uma posição diferente,
    dando a ilusão de movimento.
    */
        window.addEventListener('keypress', (event) => {
            const key = event.key;
            switch(key){
                /* note que alteramos o valor
                da variável de posição com o valor
                que indicamos na variável de velocidade.
                */
                case 'a': this.x -= this.speed; break;
                case 'd': this.x += this.speed; break;
            }
        });
    },
    /* a função draw recebe o contexto do canvas e
    desenha a imagem do personagem, armazenada na
    variável sprite.
    Veja que desenhamos com base nas variáveis
    que declaramos.
    */
    draw(ctx){
        ctx.drawImage(this.sprite, this.x, this.y, 32, 30);
    },
    /* e também declaramos uma função update() caso
    seja preciso.
    */
    update(){
    }
}

Inserindo o personagem no jogo

Agora que temos um objeto Character pronto para ser usado, temos que inseri-lo no jogo. Vamos então fazer algumas alterações no objeto Game para inserir nosso personagem no jogo.

const Game = {
    // ...
    start(){
        // chamamos o start() do personagem.
        Character.start()
        this.load().then(() =>{
            this.timer = setInterval(() => {
                this.update();
                this.draw();
            }, 30);
        })
    },
    // ...
    load(){
    /* agora estamos fazendo com que o load()
    aguarde o carregamento do sprite do personagem
    e do background do jogo.
    */
        return Promise.all([
            /* carregamos o sprite do personagem
            e armazenamos a imagem na propriedade
            sprite dentro de Character
            */
            loadImage('https://i.imgur.com/WyqxuaD.png', Character, 'sprite'),
            loadImage('https://i.imgur.com/k6heF1k.png', Game, 'background'),
        ]);
    },
    // ...
    draw(){
        this.clear();
        this.ctx.drawImage(this.background, 0, 0, canvas.width, canvas.height);
        /* Adicionamos a chamada da função draw()
        de Character
        */
        Character.draw(this.ctx);
    },
    // ...
}

Agora sim temos a seguinte tela:

Kirby - Green Greens

E com as teclas “A” e “D” você pode controlar o Kirby para a direita e esquerda.

O que aprendemos sendo usado na prática

Vamos ver o que aprendemos sendo usado por ferramentas profissionais.

Phaser

Phaser

Uma ferramenta muito usada para criar jogos com JavaScript é o Phaser.

https://phaser.io/examples/v2/games/breakout

No link acima temos um exemplo de um jogo clássico. Se você olhar o código que está junto, verá que o funcionamento é bem parecido com o código que criamos manualmente:

var game = new Phaser.Game(800, 600, Phaser.AUTO, 'phaser-example', { preload: preload, create: create, update: update });
/*
Iniciamos um novo jogo com a função "Phaser.Game()".
Para essa função, passamos o tamanho do canvas
(800x600).
No final, temos um objeto que passa as funções
preload(), create() e update().
preload() serve para carregar os recursos do
jogo antes dele ser iniciado, e para isso chamamos
"game.load".
create() é a função executada uma única vez
quando o jogo é criado.
update() é a função que é executada várias
vezes enquanto o looping estiver ativo.
*/

Game Maker

GameMaker Studio

O GameMaker é muito conhecido por possibilitar que as pessoas criem jogos sem saber programar. Você pode simplesmente arrastar ícones para criar a lógica do seu jogo.

Apesar disso, o GameMaker possui uma linguagem de programação própria que é bem simples de se aprender, possibilitando fazer coisas bem mais avançadas.

GameMaker IDE

Note que na edição da lógica temos a parte “Events”. Nele temos o evento “Create” (igual ao que conhecemos) e o evento “Step” (igual ao update). Vários outros eventos podem ser adicionados.

Unity3D

Unity3D

Outra ferramenta bem conhecida é o Unity3D, uma ferramenta bem completa para a criação de jogos e que exporta código nativo para mais de 25 plataformas. A linguagem usada é o C#.

Adam

Vamos dar uma pequena olhada em uma classe criada com C# para o Unity:

using UnityEngine;
using System.Collections;

public class Character : MonoBehaviour {

    public float speed = 0.5f;

    // Use this for initialization
    public void Start () {
        // ...
    }

    // Update is called once per frame
    public void Update () {
        // ...
    }
}

Com o Unity temos uma IDE completa para o desenvolvimento de jogos. Note que ainda temos funções como Start() e Update().
Agora você já sabe o que essas funções fazem né?

Também temos outros métodos que são chamados em determinados momentos, que facilitam muito na hora de desenvolver as ações. Um exemplo é o Awake(), OnGUI(), OnDestroy(), etc.

Conclusão

Entender como um jogo funciona é uma ótima base para conseguirmos entender como as ferramentas para a criação de jogos funcionam, facilitando o aprendizado.

Vimos então que jogos possuem um looping que desenha em uma tela com base nas variáveis, as quais podem ser alteradas de acordo com nossas ações. E esse conceito está presente em todas as ferramentas de desenvolvimento de jogos, desde as mais simples até as mais profissionais.

As ferramentas nos ajudam a tratar o carregamento de arquivos, como executar áudio e vídeo, como renderizar imagens, simular física (colisões, gravidade, atrito, etc), construção de cenários e posicionamento de objetos, desenvolvimento de animações, gerenciamento dos controles, otimização de memória, etc.

Deixe seu comentário

Graduado em Análise e Desenvolvimento de Sistemas e Pós-graduado em Projetos e Desenvolvimento de Aplicações Web. Especializado em Front End e curte desenvolvimento de jogos. Estudante de Machine Learning.