Objetivo
O objetivo deste livro é familiarizar o programador com os procedimentos básicos de programação de jogos dentro da engine Oficina Framework. Este livro assume que você esteja usando Linux e tenha a Oficina instalada em seu Path, como especificado no README e na documentação da Oficina. Suporte a IronScheme e IronLua é desejável, porém não será um pré-requisito por completo; dado que este tutorial tem o objetivo de ser fluido no sentido de ser atualizado à medida que as versões da Oficina assim demandarem, atualmente, a Oficina Framework traz o suporte a essas linguagens de script como OPCIONAL no momento da compilação.
É importante ressaltar que ESTE LIVRO AINDA ESTÁ EM FASE DE ESCRITA, bem como a engine em si, de forma que tanto o livro quanto a engine podem ter alterações aqui ainda não documentadas.
Sobre o livro
Este livro surgiu da minha necessidade de explicar e mostrar a forma como a Oficina foi programada, seu foco e sua maleabilidade para desenvolvimento de jogos C++. Enquanto possui diversas alternativas no mercado, a Oficina foi criada com foco no código livre e aberto e em manter a simplicidade, mas ainda assim trazer certa portabilidade e poderosos recursos controláveis através de scripting, bem como um tom voltado para programação ao vivo de jogos.
Trocando em miúdos, isso significa que seus jogos serão majoritariamente programados em C++, mas você possuirá ferramentas para alterar o comportamento do jogo ao vivo sem precisar recompilá-lo, e poderá também utilizar a sua própria linguagem de script escolhida para alterar o comportamento de objetos e variáveis enquanto o seu jogo está sendo executado. Poderá, também, debugar erros nestes scripts enquanto o jogo roda.
Pré-requisitos
Para criar um jogo com a Oficina, recomendo que você tenha conhecimentos básicos de C/C++, e alguma noção a respeito de programação orientada a objetos e gerenciamento manual de memória nestas linguagens. Caso vá se aventurar com scripting, recomendo conhecimentos nas linguagens Scheme (um dialeto de Lisp; mais especificamente a implementação GNU Guile) e Lua (amplamente usada na indústria de jogos como uma linguagem de script rápida, fácil e simples); você terá a opção de escolher qual linguagem usar mais à frente, nos capítulos sobre Scripting. Porém, recomendo a linguagem Scheme por uma gama de razões, e também por crer que uma linguagem mais voltada para um paradigma declarativo ou funcional possa melhorar a forma como você programa. Além disso, apesar da aparência intimidadora dos dialetos de Lisp, são linguagens que, ao escrever seu código, ou quando se tem experiência na linguagem, tornam-se muito mais sucintas e fáceis de acompanhar um raciocínio. Pretendo exemplificar mais à frente.
Na verdade, o design da Oficina foi feito de forma a possibilitar que o programador não precise de conhecimentos tão profundos nestes conceitos; a maior parte das operações são generalizadas a partir das especificações das classes, e guardadas em um único namespace. Além disso, a maioria -- se não todas -- as estruturas possuem um prefixo "of", para ajudar na compartimentalização do seu código e do código da engine. Mesmo o código da Oficina2 é totalmente NÃO-retrocompatível com a Oficina 1.3 por uma série de razões, incluindo o uso abusivo de ponteiros de memória na versão anterior. Oficina2 segue a filosofia de que fazer uma boa gerência de recursos no seu jogo é algo essencial e estimula boas práticas de programação em geral, mas soma, ao conceito da versão 1.3, a ideia de que não é preciso dificultar o código do seu programa para fazer esta tarefa de forma eficiente.
Criando o Projeto
Para começar a programar o nosso jogo, é imprescindível que iniciemos preparando o nosso diretório de trabalho.
Para a próxima etapa, criaremos alguns arquivos importantes para o nosso projeto. Começaremos digitando código em C++ para a execução de uma aplicação básica; depois, configuraremos um Makefile simples para ambientes Linux, afim de compilar o executável do nosso jogo pela primeira vez.
Hierarquia
Comece criando um diretório para seu jogo. Por exemplo, criaremos um diretório chamado "MeuJogo". Dentro deste diretório, vamos criar alguns subdiretórios chamadas "bin", "misc", "obj", "res" e "src".
A pasta ficará com a seguinte hierarquia:
MeuJogo
├── bin
├── misc
├── obj
├── res
└── src
Eis uma explicação para o uso de cada subdiretório:
bin
: Diretório onde ficarão as compilações do nosso jogo;misc
: Diretório onde ficarão todos os arquivos extras do jogo, que não serão incluídos na distribuição dele;obj
: Diretório onde ficarão os objetos pré-compilados do jogo. No primeiro momento, não lidaremos com estes objetos;res
: Diretório onde ficarão os resources do jogo, ou seja, os arquivos que serão usados pelo jogo e incluidos na distribuição do mesmo. Isso inclui scripts, texturas, etc;src
: Diretório onde ficará o código-fonte do jogo a ser compilado.
Lembre-se de que esta hierarquia de diretórios é apenas uma convenção, que aqui usaremos para organizar melhor nosso projeto.
O arquivo "main.cpp"
Navegue até MeuJogo/src
e crie um arquivo main.cpp
(no Linux, você pode navegar até a pasta com um terminal e digitar touch main.cpp
para criar este arquivo. Ou pode simplesmente inserir o código no seu editor de texto preferido e salvá-lo nesta pasta). Insira neste arquivo o seguinte código:
#include <oficina2/oficina.hpp>
using namespace oficina;
int main(int argc, char** argv)
{
ofInit();
ofGameLoop();
ofQuit();
return 0;
}
Neste código, começamos incluindo o cabeçalho C++ oficina2/oficina.hpp
. Este é o cabeçalho geral da OficinaFramework que contém a maioria dos símbolos e definições que precisamos para criar uma aplicação rápida. Logo após, deixamos claro que usaremos símbolos do namespace oficina
, onde absolutamente todas as funções, classes, enumerações e structs da engine estão definidos.
O uso do argc
e do argv
na função main
é essencial, por requerimento da biblioteca SDL2, que usaremos para criação e gerenciamento da janela e de seus eventos.
A função ofInit
inicializa a janela e um contexto de desenho automaticamente, sem que precisemos instanciar os pormenores da engine. Esta função também aceita argumentos, que veremos a seguir.
A função ofGameLoop
realiza o loop do jogo, onde ocorrerá toda a lógica de atualização de quadros (frames) do jogo, bem como toda a lógica dos objetos e entidades do jogo em geral. É imprescindível salientar que quaisquer telas ou objetos a serem inseridos no jogo devem, preferencialmente, serem adicionados DEPOIS de ofInit
e ANTES de ofGameLoop
. Isto exemplifica-se ao criarmos um Canvas de desenho, a ser visto na próxima seção.
A função ofQuit
executa toda a lógica de limpeza do jogo após o fim da execução do mesmo, ao ser encerrado. JAMAIS chame esta esta função dentro de uma lógica a ser executada dentro de ofGameLoop
(como num método de atualização de um Canvas, por exemplo). Ao invés disso, uma bandeira para finalização do jogo pode ser levantada ao chamar a função ofSoftStop
, a ser vista também em um outro momento.
Este código bem básico vai assegurar que consigamos executar uma tela de desenho simples e sem absolutamente nada, mas será o suficiente para um início. Agora, vamos tratar de compilar este arquivo para que se torne o nosso jogo futuramente.
O arquivo "Makefile"
Agora, navegue até a pasta raiz do jogo (MeuJogo
) e crie um arquivo chamado Makefile
(sem extensões).
Caso você não esteja familiarizado com Makefiles, estes arquivos são scripts de compilação muito usados no Linux, feitos para facilitar e automatizar a compilação de projetos. Não são muito avançados no sentido de que boa parte da configuração feita é crua mas, como estamos tratando de um projeto pequeno, Makefiles nos servirão bem.
Abra este arquivo e coloque o seguinte texto. ATENÇÃO: Note que as indentações, após as linhas terminadas com ":", devem ser feitas com O CARACTERE DE TABULAÇÃO (tecla Tab), e não com espaços; do contrário, a compilação mostrará erros.
CXX = g++ --std=c++11
CXXFLAGS = -g -Wall `oficina2-config --cppflags`
CXXLIBS = `oficina2-config --libs`
CXXOUT = -o
CXXOBJ = -c
DEL = rm -rf
BIN = bin/MeuJogo
FILES = src/main.cpp
.PHONY: clean
all: $(FILES)
$(CXX) $(FILES) $(CXXFLAGS) $(CXXLIBS) $(CXXOUT) $(BIN)
clean:
$(DEL) obj/*.o
Aqui definimos, antes de mais nada, alguns símbolos (macros), sendo eles:
CXX
: Denomina o compilador que usaremos. Aqui usaremos, necessariamente, o compilador GCC, com especificações de C++11. Estas especificações também são automaticamente ativadas ao inserirmos as flags de compilação da OficinaFramework, mas aqui as adicionamos para manter a clareza. Caso você prefira usar o compilador Clang, basta substituirg++
porclang++
.CXXFLAGS
: Flags de compilação a serem usadas. Aqui adicionamos-g
para gerar símbolos de debug,-Wall
para exibir todos os avisos de compilação, e um comando entre crases (também chamadas backquotes) que será executado como um comando bash durante a compilação. Este comando chamará o programaoficina2-config
, que possui predefinições de todas as flags de compilação necessárias para a forma com a qual você compilou a OficinaFramework.CXXLIBS
: Flags de compilação a serem também usadas; mais especificamente, bibliotecas a serem linkadas pelo compilador como dependências do seu jogo. Aqui só reusamos o comandooficina2-config
, desta vez exprimindo as bibliotecas que são dependências de qualquer jogo feito com a OficinaFramework.CXXOUT
: Flag que vem antes do nome do arquivo executável a ser gerado. Por padrão, é-o
para GCC e Clang.CXXOBJ
: Flag que define a pré-compilação de um arquivo C/C++ em um arquivo de objeto (*.o
,*.obj
). Não usaremos esta flag por enquanto.DEL
: Comando da linha de comando para deleção de um arquivo ou pasta. Aqui um comando bash, para Linux.BIN
: Localização relativa e nome do arquivo binário (programa) a ser gerado na compilação.FILES
: Arquivos de código C++ a serem compilados e transformados no nosso programa, separados por espaço. Por enquanto, usaremos esta definição para compilar vários arquivos de uma vez, mas é aconselhável compilar nosso binário em partes, para que não precisemos recompilar arquivos que não alteramos..PHONY
: Define os targets que NÃO GERAM arquivos. Aqui definimosclean
, já queclean
será apenas um comando para limpar os objetos que geramos (a ser visto mais adiante).
Definimos, também, alguns alvos (targets) de compilação:
all
: Alvo de compilação padrão de um Makefile. Tem como função compilar TODOS os outros alvos, ou compilar o projeto inteiro.clean
: Alvo de compilação que funciona apenas como um comando, para limpar os objetos pré-compilados.
Após o texto all:
, vemos o símbolo FILES
(colocado entre parênteses e precedido por um cifrão). Isto faz com que esta anotação seja substituída pelo valor que guarda. Este valor, então, será tratado como uma DEPENDÊNCIA daquele target. Ou seja: all
passará a depender, quando executado, da existência de src/main.cpp
, e também passará a monitorar alterações neste arquivo. O mesmo valeria para outros arquivos, caso adicionados também a FILES
.
Abaixo do target all
e após a indentação por tabulações, temos uma linha usando praticamente apenas símbolos definidos previamente. Estes símbolos, como já explicado, serão substituídos pelo seu valor. Sendo assim, na prática, esta linha será reescrita, durante a compilação, da seguinte forma:
g++ --std=c++11 src/main.cpp -g -Wall `oficina2-config --cppflags` `oficina2-config --libs` -o bin/MeuJogo
Caso você já esteja familiarizado com compilar seus programas C/C++ via terminal do Linux, fica claro que este comando apenas invoca o compilador, dando a ele algumas flags como argumento, bem como dando a ele, também, o nosso arquivo main.cpp
para compilação. O compilador então gerará o arquivo de saída MyGame
na pasta bin
.
A ordem dos argumentos do programa g++
realmente não possui tanta relevância, EXCETO para as flags CXXLIBS
e FILES
. Ao compilar seu jogo feito com a OficinaFramework manualmente ou através de um Makefile, é IMPRESCINDÍVEL que seus arquivos de código-fonte venham ANTES das flags de dependências do projeto; do contrário, você poderá se deparar com erros de dependências indefinidas.
O target clean
realiza um trabalho parecido, porém removendo arquivos com a extensão .o
que serão eventualmente gerados e armazenados na pasta obj
. Por enquanto, não precisamos nos preocupar com isso, uma vez que não geraremos nenhum neste momento.
Compilando o jogo pela primeira vez
Feito tudo isso, basta agora compilar seu jogo.
Assumindo que você tenha o programa make
instalado, basta ir até a pasta raiz do jogo, via terminal, e executar o seguinte comando:
make
Algumas linhas de compilação devem ser exibidas e, caso não ocorra nenhum erro inesperado, um arquivo MeuJogo
deve ser gerado na pasta bin
.
Você poderá executar este arquivo digitando, a partir da pasta raiz, o comando bin/MeuJogo
ou ./bin/MeuJogo
, num sistema Linux. Também é possível ir até a pasta do jogo e executá-lo lá dentro, mas prefira executá-lo da pasta rai para que, futuramente, o jogo consiga localizar seus recursos, que estarão localizados na pasta res
.
Você verá uma simples tela preta com um tamanho de 1280x720. Por enquanto, não temos absolutamente nada desenhado, o que apenas nos dá a opção de fechar a tela.
Segue abaixo a hierarquia final da nossa pasta:
MeuJogo
├── bin
│ └── MeuJogo
├── Makefile
├── misc
├── obj
├── res
└── src
└── main.cpp
GameArgs: Pré-configurando seu jogo
Como mencionado anteriormente, a função ofInit
aceita alguns argumentos interessantes de pré-inicialização do seu jogo. Cobriremos, aqui, alguns deles, que são essenciais e vitais para a qualidade da sua aplicação.
A função ofInit
aceita, por padrão, nenhum argumento, ou um vetor de strings (std::vector<std::string>
) contendo configurações iniciais para o seu jogo, como título da janela, localização do ícone da janela em runtime, etc.
Abra o arquivo src/main.cpp
e, onde se encontra a função ofInit
, substitua-a por:
ofInit({
"wname=Meu Jogo",
"datad=MeuJogo",
"winsz=800x600",
"frmrt=60c"
});
A função, agora, recebe como argumento uma lista de strings separada por vírgula, e envolta em colchetes.
Esta é uma das formas práticas de inicialização de um vetor, e a mais sucinta para se passar o vetor de argumentos de inicialização para esta função.
É importante ressaltar que as strings devem ser cuidadosamente separadas por vírgulas, uma vez que, por padrão, C++ concatenará duas strings que não estejam separadas por algum separador padrão da linguagem (como vírgulas ou ponto-e-vírgulas).
Estes argumentos, referidos na documentação da Oficina como GameArgs, são argumentos de inicialização padrão para o seu jogo. Cada argumento é uma string de exatamente cinco caracteres, seguidos de um caractere =
, e dos argumentos que cada GameArg espera. Os GameArgs aqui usados são:
wname
: Define um nome da janela. Pode incluir espaços;datad
: Define o nome da pasta de dados do jogo. Uma pasta de dados do jogo é aquela que possui texturas e scripts de um jogo. Por padrão, o jogo primeiramente procura por seus recursos na atual pasta de execução do programa. Se não encontra, procura nas pastas onde o sistema operacional normalmente armazena dados de aplicativos. No Linux, estas pastas são/usr/share/<nome da pasta>
,/usr/local/share/<nome da pasta>
e/home/<usuário>/.local/share/<nome da pasta>
; no Windows, sãoC:\Arquivos de Programas\<nome da pasta>
ouC:\Arquivos de Programas (x86)\<nome da pasta>
, dependendo da versão e arquitetura do seu sistema operacional. No nosso caso, nossos arquivos de recursos estarão emres
, que deverá ser instalada, quando o jogo for publicado, em uma destas pastas, sob o subdiretório<nome da pasta>/res
ou, mais precisamente,MeuJogo/res
.winsz
: Define um tamanho em pixels para a janela do aplicativo. Neste caso, a janela terá um tamanho de 800x600 e estará centralizada no monitor principal.frmrt
: Define a configuração de quadros por segundo (FPS) da aplicação. Aqui, forçamos nossa aplicação (c
, de "capped"/limitado) a um máximo de 60 quadros por segundo; ao ultrapassar esse valor, o jogo força um delay na aplicação até o próximo segundo.
Para mais informações de GameArgs e outros valores/literais que podem ser usados nos GameArgs apresentados, consulte a documentação da OficinaFramework.
Após está alteração, volte à pasta raiz do seu jogo, e compile-o novamente. Você deve ver uma nova janela preta, porém desta vez menor, e com outro título:
Primeiros Passos
Agora que temos, por fim, a nossa janela básica, podemos nos concentrar no principal: desenhar algo na tela. Por enquanto, manteremo-nos em desenho básico, ou seja, aprenderemos a criar primitivas comuns, a partir de vértices, e desenharemos estas primitivas na tela.
Aprenderemos, também, a configurar o ambiente básico de desenho da Oficina. Este ambiente é comparável a uma tela de pintura (canvas), o que dá nome à estrutura na engine.
Criando uma cena (ofCanvas)
Antes de realizar quaisquer operações novas no nosso jogo, é necessário que haja algo que possa ser associado como uma "tela de pintura" na sua janela. Neste caso, a OficinaFramework possui uma classe de objetos que disponibiliza uma abstração neste sentido.
A classe oficina::ofCanvas
disponibiliza controles e métodos pré-instanciados para que possamos criar nossas próprias "telas de pintura". Esta abstração, em conjunto com o objeto estático oficina::ofCanvasManager
tornará possível a coexistência de uma ou mais cenas na tela, sendo renderizadas e tendo sua lógica atualizada ao mesmo tempo.
Declarando e compilando a cena
Para tanto, crie dois arquivos: src/MinhaCena.hpp
e src/MinhaCena.cpp
.
Em src/MinhaCena.hpp
, insira o código como mostrado abaixo:
// MinhaCena.hpp
#pragma once
#include <oficina2/canvas.hpp>
using namespace oficina;
class MinhaCena : public ofCanvas
{
private:
glm::mat4 mvp;
public:
void init();
void load();
void unload();
void update(float dt);
void draw();
};
O código consiste na declaração de uma classe, que recebe como herança os métodos e campos internos da classe oficina::ofCanvas
. Os métodos herdados são:
init
: Método para inicialização de lógica em geral, sendo executado logo antes do métodoload
;load
: Método especial para carregamento de recursos do jogo (texturas, scripts, etc), executado após o métodoinit
;unload
: Método especial para descarregamento de recursos do jogo, executado durante a remoção/deleção doofCanvas
;update
: Método para execução de lógica em geral, sendo chamado uma vez a cada quadro da aplicação. O parâmetrodt
especifica, em SEGUNDOS, a diferença de tempo entre o quadro renderizado anteriormente e o quadro renderizado atualmente. Este número pode variar livremente ou ser fixo, de acordo com o especificado no controle de quadros do jogo. Não usaremos este parâmetro neste tutorial, uma vez que sabemos que nosso jogo executará sob um limite de 60 quadros por segundo, mas é um parâmetro muito útil ao realizar interpolações na física de um jogo.draw
: Método para execução da lógica de desenho da cena.
Em adição, também declaramos um campo privado chamado mvp
, uma matriz 4x4. O uso do prefixo glm::
está relacionado à biblioteca onde ela é implementada, a GL Mathematics, da qual a Oficina faz extenso uso. Esta é uma sigla para a expressão "Model-View-Projection", relacionada à multiplicação destas três importantes matrizes no mundo da computação gráfica. Trata-se da matriz que vai especificar a forma como a cena será renderizada na tela: a câmera e o tamanho da resolução interna, bem como a "posição" da mesma. Na realidade, especificaremos aqui apenas as matrizes View e Projection, já que a matriz Model será calculada para cada entidade que colocarmos na tela.
Este não é um tutorial introdutório de computação gráfica ou de geometria analítica, mas é importante dizer que a ordem de multiplicação entre uma matriz e outra é extremamente importante. Para tanto, devemos fixar que, ao gerar a matriz mvp
em qualquer linguagem similar a C++, devemos nos certificar de que multiplicamos as matrizes nesta ordem:
mvp = Projection * View * Model
Agora, digite este código no arquivo src/MinhaCena.cpp
:
// MinhaCena.cpp
#include "MinhaCena.hpp"
void MinhaCena::init()
{
}
void MinhaCena::load()
{
}
void MinhaCena::unload()
{
}
void MinhaCena::update(float dt)
{
}
void MinhaCena::draw()
{
}
Como você pode ver, o código acima não passa de definições vazias para a classe que declaramos.
Agora, precisamos nos certificar de que o arquivo MinhaCena.cpp
seja compilado. para tanto, abra o arquivo Makefile
, e altere o macro FILES
de forma que fique assim:
FILES = src/main.cpp src/MinhaCena.cpp
Agora, daremos à engine uma instância da nossa cena, de forma que a engine gerencie-a e atualize-a como necessário.
Abra o arquivo src/main.cpp
e, entre as chamadas das funções ofInit
e ofGameLoop
, insira o seguinte:
ofCanvasManager::add(new MinhaCena, 0, "Minha Cena");
Esta chamada adicionará uma nova instância da nossa cena ao gerenciador de cenas interno da engine.
O primeiro argumento é efetivamente um PONTEIRO para nossa cena, que obrigatoriamente deve ser um objeto que herdou os métodos de ofCanvas
. O segundo argumento é a profundidade da cena, ou a ordem com a qual ela deve aparecer em relação às outras cenas; como só possuimos uma cena atualmente, esta ordem é irrelevante. O terceiro argumento é um nome opcional para a cena, que manteremos para fins didáticos.
Por último, precisamos nos certificar de que o arquivo src/main.cpp
conheça a declaração de classe da nossa cena. Ainda em src/main.cpp
, logo após o cabeçalho da OficinaFramework, insira a seguinte linha:
#include "MinhaCena.hpp"
O código final de src/main.cpp
deverá estar assim:
#include <oficina2/oficina.hpp>
using namespace oficina;
#include "MinhaCena.hpp"
int main(int argc, char** argv)
{
ofInit({
"wname=Meu Jogo",
"datad=MeuJogo",
"winsz=800x600",
"frmrt=60c"
});
ofCanvasManager::add(new MinhaCena, 0, "Minha Cena");
ofGameLoop();
ofQuit();
return 0;
}
Verificando a inserção da cena
Ao compilar e executar o jogo, você vai perceber que absolutamente nada está aparente na tela. Isso é normal, mas nossa cena já foi adicionada à lista e podemos verificar isso através de uma ferramenta muito importante: o REPL. Entraremos realmente na forma como o REPL funciona mais tarde mas, por enquanto, veremos como verificar quantas e quais cenas estão inseridas no nosso gerenciador de cenas.
Abra o programa compilado. Você será agraciado automaticamente com a tela preta que já conhece.
Agora, então, faça com que o console de debug seja exibido: pressione o atalho Ctrl + X (de agora em diante, referenciaremos este atalho da mesma forma que é referenciado em um editor de texto como o Emacs: na forma C-x
).
Ao fazer isso, será exibido um pequeno console com algumas informações gerais, como tamanho da janela, FPS, status de VSync e etc, chamado "Watcher". Pressione novamente C-x
, e este console será substituido por outro, chamado "REPL".
O REPL é sempre iniciado com uma mensagem de boas-vindas da linguagem a ser usada; verifique se a linguagem em questão é IronScheme
ou IronLua
, precisaremos desta informação a seguir.
Na borda inferior da tela, um outro mini-console terá alguns símbolos com um número de três algarismos. Este é o buffer de entrada de comandos do REPL. Use o atalho Alt + X (a partir daqui, referenciaremos como M-x
no estilo Emacs) para alternar a digitação de comandos neste buffer entre ligada e desligada. Você poderá, então, a seguir, digitar comandos e pressionar Enter para que eles sejam executados.
Dependendo da sua compilação da Oficina, o REPL poderá usar, de cara, a linguagem IronScheme
, IronLua
ou NENHUMA. Caso o REPL não use nenhuma das linguagens, você poderá PULAR ESTA SEÇÃO DO TUTORIAL.
Se o REPL está usando IronScheme
, ligue a entrada no buffer com M-x
e digite:
(canvas-list)
Caso o REPL esteja usando IronLua
, ligue a entrada no buffer com M-x
e digite:
common.canvasList()
E pressione Enter logo em seguida. Você notará que uma nova informação será exibida no REPL, como abaixo:
A imagem acima é uma foto de um comando dado na linguagem IronScheme; IronLua terá uma exibição similar. Como você pode ver, este comando exibe uma lista de todas as cenas atualmente sendo gerenciadas pela engine; neste caso, ele exibe o nome da nossa cena ("Minha Cena"), seguida de um ponteiro de memória onde a instância da cena está armazenada. Com isso, sabemos que nossa cena foi adicionada ao gerenciador e está em perfeito funcionamento, apesar de invisível.
Configurando a matriz de visão e projeção
Para desenhar nossa cena, é necessário que a placa de vídeo saiba para onde nossa câmera está apontada, e qual é o tamanho e o tipo da área de visualização a ser desenhada. Este pode ser um tópico complexo, por isso, apenas determinaremos alguns padrões fixos.
Criaremos uma cena com um tamanho exato de 800x600, como o tamanho da nossa janela. Os valores da coordenada X crescerão da ESQUERDA para a DIREITA; os valores da coordenada Y, contra-intuitivamente (porém, como normalmente empregado em engines de jogos 2D), crescerão de CIMA para BAIXO. Além disso, a origem (coordenada {0, 0} do plano) ficará no canto superior esquerdo da tela. Como na figura a seguir:
Abra o arquivo src/MinhaCena.cpp
. No método void MinhaCena::init
, insira o seguinte código:
glm::mat4 view =
glm::lookAt(
glm::vec3(0.0f, 0.0f, -1.2f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, -1.0f, 0.0f));
glm::mat4 projection =
glm::ortho(
0.0f,
800.0f, -600.0f,
0.0f, 1.0f, 10.0f);
mvp = projection * view;
Para uma rápida explicação, definimos separadamente, a princípio, duas matrizes.
A primeira é a matriz view
. Através desta matriz, definimos o lado para o qual a câmera "olha".
Ela assume que estamos usando uma representação cartesiana clássica do OpenGL, e não a representação cartesiana que estabelecemos no desenho anterior; esta representação possui a origem no centro exato da tela, e o eixo X cresce normalmente para a esquerda, mas o eixo Y cresce para CIMA e ambos os eixos têm as suas extremidades da tela entre -1.0 e 1.0 (diferente da nossa projeção, que terá as extremidades entre 0 e 800 em X, e entre 0 e 600 em Y (Caso você não tenha entendido muito bem esta projeção padrão do OpenGL, ignore-a por enquanto).
A função glm::lookAt
recebe três vetores de 3 coordenadas (X, Y, Z) como argumento:
- O primeiro vetor é a posição onde a câmera/o olho está (no caso, 1.2 unidades na direção contrária à tela, a partir do centro dela);
- O segundo vetor é a localização do centro da tela (no caso, a origem do plano);
- O terceiro vetor é o vetor que aponta PARA QUAL LADO fica a direção "para cima". No nosso caso, como queremos que nossas coordenadas Y cresçam PARA BAIXO, passamos um vetor que tem o valor Y definido como -1 (direção invertida em Y).
A segunda é a matriz projection
, que efetivamente determina o formato da nossa tela.
Esta matriz é definida pela função glm::ortho
, que cria uma projeção ortogonal (quadrada, diferente de uma projeção de perspectiva). Recebe seis argumentos:
left
,right
,bottom
etop
: Especificamente as posições de cada um dos limites da nossa tela, de acordo com seus respectivos nomes;near
,far
: Especificamente os limites de desenho no plano Z. No nosso caso, quaisquer objetos desenhados com uma coordenada Z menor que 1.0 e maior que 10.0 seriam ignorados ou cortados por estes planos.
Por fim, multiplicamos estas duas matrizes na ordem correta, e damos o resultado desta multiplicação à matriz mvp
da nossa cena.
Escrevendo texto na tela
Para um pequeno teste básico, aprenderemos, agora, a desenhar texto na tela.
Uma fonte de texto é carregada com métodos muito similares a uma textura, como veremos mais adiante. Porém, a Oficina possui algumas fontes de texto já predefinidas para uso imediato. Neste exemplo, usaremos a fonte Fixedsys Excelsior para escrever texto na tela.
Carregando e descarregando uma fonte padrão
Vá até o arquivo src/MinhaCena.hpp
.
Lidaremos, agora, diretamente, com uma estrutura de renderização, então precisaremos do cabeçalho da Oficina onde as estruturas e funções de renderização estão definidas. Abaixo da inclusão do cabeçalho oficina2/canvas.hpp
, digite o seguinte:
#include <oficina2/render.hpp>
E logo abaixo da definição da nossa matriz mvp
, ainda na região private
da classe, adicione este novo campo:
ofFont fonte;
Vá, agora, para o arquivo src/MinhaCena.cpp
. Você deverá adicionar aos métodos citados estes códigos:
// Em "void MinhaCena::load()":
fonte = ofTexturePool::loadDefaultFont(ofFontFaceFixedsysExcelsior);
// Em "void MinhaCena::unload()":
fonte.unload();
A explicação para estas linhas é bem intuitiva. No caso do código adicionado a load
, trata-se do carregamento de uma das fontes predefinidas da Oficina, alteráveis através de uma enumeração; aqui usamos a fonte Fixedsys Excelsior. No caso de unload
, o código apenas assegura que a fonte seja descarregada da GPU quando nossa cena for descarregada.
É importante notar que a Oficina descarrega toda e qualquer fonte/textura da GPU ao abandonar a aplicação, porém, para evitar uso excessivo de recursos do computador, é aconselhável descarregar tudo o que não for mais usado ao fim de uma cena.
Você pode compilar o projeto para verificar o carregamento da fonte. Caso o carregamento seja feito com sucesso, você verá, no log do console, uma linha escrita da seguinte forma:
INFO: ofLoadDefaultFont: Uploaded Fixedsys Excelsior (Hardcoded) to VRAM
Definindo e posicionando um texto
Agora, por fim, desenharemos texto na tela.
Ainda no arquivo src/MinhaCena.cpp
, vá até o método void MinhaCena::draw
e adicione o seguinte código:
fonte.write("Hello, world!", glm::vec2(50.0f), mvp, glm::vec4(1.0f));
O código acima é um método da classe ofFont, que recebe, respectivamente, como parâmetro:
- Um texto a ser exibido (pode ser uma literal ou um texto compatível com uma
std::string
). Infelizmente, atualmente, umaofFont
só suporta texto ASCII sem acentuação, exceto pelo acento grave, que também pode ser usado no REPL -- sobretudo em IronScheme; - A posição do texto (apenas eixos X e Y de coordenadas são suportados). Utilizando abstrações da GL Mathematics, definimos esta posição como 50x50;
- A matriz ModelViewProjection da cena. Teoricamente, você também pode reposicionar o texto usando diretamente essa matriz, especialmente se você tiver um texto sendo renderizado de dentro de uma
ofEntity
(que veremos a seguir), por exemplo. Neste caso, a posição do texto citada anteriormente se tornaria uma posição relativa à posição estipulada na matriz Model; - Um vetor de quatro dimensões, definindo a COR do texto a ser exibido, em RGBA (vermelho, verde, azul, alpha/transparência) normalizado (valores de 0 a 1). Utilizando abstrações da GL Mathematics, definimos esta cor como branco sólido (R: 1, G: 1, B: 1, A: 1).
Compile e execute o jogo mais uma vez. Você deverá ver texto inserido na tela.
O arquivo src/MinhaCena.cpp
deverá estar com o seguinte código, agora:
#include "MinhaCena.hpp"
void MinhaCena::init()
{
glm::mat4 view =
glm::lookAt(
glm::vec3(0.0f, 0.0f, -1.2f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, -1.0f, 0.0f));
glm::mat4 projection =
glm::ortho(
0.0f,
800.0f, -600.0f,
0.0f, 1.0f, 10.0f);
mvp = projection * view;
}
void MinhaCena::load()
{
fonte = ofTexturePool::loadDefaultFont(ofFontFaceFixedsysExcelsior);
}
void MinhaCena::unload()
{
fonte.unload();
}
void MinhaCena::update(float dt)
{
}
void MinhaCena::draw()
{
fonte.write("Hello, world!", glm::vec2(50.0f), mvp, glm::vec4(1.0f));
}
Criando uma entidade através da classe ofEntity
Após uma noção inicial da hierarquia de um ofCanvas
/cena, poderemos, então, dar prosseguimento à definição de um objeto, entidade, ou, como definido na Oficina, ofEntity
.
Entidades funcionam de forma similar a uma cena, no sentido de que também herdam certos métodos e propriedades de uma classe abstrata. Porém, entidades possuem algumas coisas específicas, como sua própria matriz Model (ou melhor dizendo, as matrizes componentes do objeto); esta matriz fica responsável por armazenar posição, rotação e escala do objeto em questão.
Vamos ao código e, logo depois, analisaremos o que precisamos.
Você vai precisar criar mais dois arquivos: um cabeçalho e um arquivo de código, ambos na pasta src
. Vá em frente e crie src/MinhaEntidade.hpp
e src/MinhaEntidade.cpp
, colocando, dentro de cada um desses arquivos, conforme mostrado abaixo:
// MinhaEntidade.hpp
#pragma once
#include <oficina2/entity.hpp>
using namespace oficina;
class MinhaEntidade : public ofEntity
{
private:
public:
void init();
void load();
void unload();
void update(float dt);
void draw(glm::mat4 mvp);
};
// MinhaEntidade.cpp
#include "MinhaEntidade.hpp"
void MinhaEntidade::init()
{
}
void MinhaEntidade::load()
{
}
void MinhaEntidade::unload()
{
}
void MinhaEntidade::update(float dt)
{
}
void MinhaEntidade::draw(glm::mat4 mvp)
{
}
Além disso, você precisará, como feito com nossa cena na subseção anterior, adicionar uma referência ao cabeçalho em src/MinhaCena.hpp
e uma referência ao arquivo de código em si no nosso Makefile
.
Em src/MinhaCena.hpp
, após a inclusão do cabeçalho oficina2/render.hpp
, digite:
#include "MinhaEntidade.hpp"
Já em Makefile
, substitua o macro FILES
de forma que ele mostre o seguinte:
FILES = src/main.cpp src/MinhaCena.cpp src/MinhaEntidade.cpp
Veja que, na realidade, uma entidade possui uma estrutura muito parecida com a cena, à exceção dos detalhes apresentados, que estão relacionados a transformações na entidade (detalhes com os quais ainda não tivemos contato direto).
A única diferença destacável é o fato de que, em um método draw
de uma entidade, recebemos, como argumento, a matriz mvp
da nossa CENA. Veremos como lidar com isso no próximo tópico.
Feito isso, você pode testar se ocorreu tudo bem, compilando sua aplicação mais uma vez.
Instanciando manualmente uma entidade
Após digitar o código, absolutamente tudo está apropriadamente definido, exceto pelo fato de que nós ainda não possuimos NENHUMA instância da entidade, que acabamos de criar, em nenhuma parte do nosso jogo.
Por enquanto, vamos consertar isso instanciando manualmente UMA intidade do nosso jogo.
Vá ao arquivo src/MinhaCena.hpp
. Abaixo da instância da nossa fonte, insira o seguinte:
MinhaEntidade entidade;
Note que não estamos criando nossa entidade dinamicamente, mas isto também não significa que ela está inicializada, muito menos incluída na lógica da cena!
Para tanto, vá agora ao arquivo src/MinhaCena.cpp
. Lá, nós vamos nos assegurar de que:
- A entidade seja inicializada (método
init
); - A entidade tenha seu conteúdo essencial carregado (método
load
); - A entidade tenha seu conteúdo essencial descarregado ao fim da cena (método
unload
); - A entidade tenha sua lógica atualizada quadro-a-quadro (método
update
); - A entidade seja desenhada quadro-a-quadro (método
draw
).
Você pode já ter percebido onde queremos chegar. Adicione as linhas de código a seguir nos respectivos métodos referidos:
// em "void MinhaCena::init", após a declaração da mvp:
entidade.init();
// em "void MinhaCena::load", após o carregamento da fonte de texto:
entidade.load();
// em "void MinhaCena::unload", após o descarregamento da fonte de texto:
entidade.unload();
// em "void MinhaCena::update":
entidade.update(dt);
// em "void MinhaCena::draw", após a escrita de texto na tela:
entidade.draw(mvp);
Você pode compilar mais uma vez e verificar se o código corre sem erros; Porém, você pode verificar se seu código esta sendo executado sem problemas de outra forma, como veremos a seguir.
Isolando bugs e colhendo informações: A função ofLog
Para verificar melhor o funcionamento do programa, é bom que tenhamos um sistema de log, que imprima texto na tela. Para isso, temos a função ofLog
.
Na realidade, ofLog
é uma função muito parecida com sprintf
e fprintf
. Porém, ofLog
é dinâmica no sentido de que permite configuração de níveis de gravidade do texto de log a ser impresso, bem como redirecionamento: você pode escolher entre imprimir no console, imprimir num arquivo de texto, ou não imprimir os logs.
Esta função pode ser usada da seguinte forma:
ofLog(nivel, formato, ...);
nivel
: Nível de gravidade do log. Os níveis suportados são:ofLogCrit
: Erro crítico, possivelmente ocasionando uma quebra no funcionamento do programa. Este tipo de erro, quando o log é suprimido, exibirá uma caixa de texto comuma mensagem de erro na tela;ofLogErr
: Mensagem de erro;ofLogWarn
: Mensagem de aviso;ofLogInfo
: Mensagem de informação;ofLogNone
: Mensagem sem nível de propósito simples.
formato
: Formato da string de saída, da mesma forma queprintf
. Recomenda-se encerrar a string com o caractere\n
;...
: Elipses; variáveis que farão parte do formato, da mesma forma que emprintf
.
Além disso, você ainda pode usar as seguintes funções para definir propriedades do seu log:
ofLogSetLevel(nivel);
Onde nivel
é o seu nível mínimo de log. Qualquer nível ABAIXO deste nível de log será automaticamente descartado da saída.
ofLogUseFile(nome_do_arquivo);
ofLogUseConsole();
ofLogDisable();
Estas três funções estabelecem onde o log será impresso.
ofLogUseFile
determina que o log seja impresso num arquivo à parte, casonome_do_arquivo
seja um arquivo válido ou possa ser criado;ofLogUseConsole
devolve o log ao console onde o jogo está sendo executado, caso o log esteja sendo feito em um arquivo ou esteja desabilitado;ofLogDisable
suprime toda e qualquer tentativa de impressão de log, a não ser janelas de mensagens de erro crítico.
Abra o arquivo src/MinhaEntidade.cpp
.
Abaixo da inclusão do arquivo MinhaEntidade.hpp
, escreva a seguinte linha:
#include <oficina2/io.hpp>
Agora, adicione as seguintes linhas aos seus respectivos métodos:
// em "void MinhaEntidade::init":
ofLog(ofLogWarn, "Entidade inicializada!\n");
// em "void MinhaEntidade::load":
ofLog(ofLogWarn, "Entidade carregada!\n");
// em "void MinhaEntidade::unload":
ofLog(ofLogWarn, "Entidade descarregada!\n");
Após inicializar e imediatamente fechar a aplicação, você verá uma mensagem de saída, no seu console, similar a este log:
INFO: Powered by OficinaFramework v2.0.11
INFO: ofInit: Creating generic display and context
INFO: ofInit: Pushing gameargs
INFO: ofInit: Opening display and context
INFO: ofDisplay.parseArgs: Window size set to (800, 600)
INFO: ofDisplay.setSwapInterval: Set a cap to 60.00FPS.
INFO: ofContext.open:
OpenGL v3.3.0 NVIDIA 378.13
GLSL Shader Model v3.30 NVIDIA via Cg compiler
GL Extension Wrangler v2.0.0
Renderer: GeForce GT 525M/PCIe/SSE2
Vendor: NVIDIA Corporation
ARB Debug Log: Yes
INFO: ofInit: Starting canvas manager
INFO: ofLoadDefaultFont: Uploaded GohuFont (Hardcoded) to VRAM
INFO: ofShader.compile: Compilation successful
INFO: ofShader.compile: Compilation successful
INFO: ofInit: Starting global REPL interpreter
INFO: Initializing Scheme REPL...
INFO: ofGameLoop
WARN: Entidade inicializada!
INFO: ofLoadDefaultFont: Uploaded Fixedsys Excelsior (Hardcoded) to VRAM
WARN: Entidade carregada!
INFO: ofSoftStop
INFO: ofQuit: Unloading canvases
INFO: ofTexturePool.unload: Deleted Fixedsys Excelsior (Hardcoded) from VRAM
WARN: Entidade descarregada!
INFO: ofTexturePool.unload: Deleted GohuFont (Hardcoded) from VRAM
INFO: ofQuit: Unloading REPL interpreter
INFO: Uninitializing Scheme REPL...
INFO: ofQuit: Unloading texture pool
INFO: ofTexturePool.clear: Clearing pool
INFO: ofTexturePool.clear: Cleared
INFO: ofQuit: Closing context and display
Observe que nossas mensagens de log na entidade receberam um prefixo WARN:
(provavelmente na cor amarela, caso seu console suporte caracteres coloridos), em conformidade com o nível de log que demos a elas (ofLogWarn
).
Por último, você pode, também, imprimir texto colorido, caso seu console suporte isso. Através do truque de concatenação de strings de C++, existem alguns macros predefinidos no cabeçalho oficina2/io.hpp
que auxiliam nesta façanha.
Por exemplo, um comando como
ofLog(ofLogInfo, OFLOG_GRN "Hello, " OFLOG_RESET "world!\n");
imprimirá a string "Hello, " em letras verdes, enquanto "world!\n" será impresso na cor normal de texto do console. Na verdade, o que ocorre é uma concatenação automática de literais de string, feitas durante a compilação do jogo.
Você pode consultar estes macros nas documentações. Apenas lembre-se de usar a macro OFLOG_RESET
ao fim da sua string!
Renderizando um ofPrimitive
Nós já sabemos que nossa entidade está em nossa cena, mas não temos NENHUMA representação visual de que ela ao menos exista!
É hora de usar um dos recursos da OficinaFramework: usaremos uma primitiva, ou ofPrimitive
, para desenhar uma forma simples na tela que possamos manipular a nosso gosto.
ofPrimitive
s são objetos que guardam informações de desenhos com vértices simples e sequenciais, derivados das primitivas de OpenGL. Estas informações são carregadas, no momento da criação, para a sua própria GPU e lá ficam armazenadas para desenho estático, o que significa que você, enquanto um desenvolvedor preocupado com a performance do seu jogo, não precisa se preocupar com a quantidade de informações que são enviadas a cada quadro para a sua GPU!
Desenhando a primitiva
Vamos começar indo ao arquivo src/MinhaEntidade.hpp
.
Precisaremos das definições de renderização para desenhar na tela, portanto, abaixo da inclusão do cabeçalho oficina2/entity.hpp
, inclua o cabeçalho de renderização:
#include <oficina2/render.hpp>
Agora, entre a linha onde está escrito private:
e a linha onde está escrito public:
, adicione o seguinte campo:
ofPrimitive* pQuadrado;
Agora, mude para o arquivo src/MinhaEntidade.cpp
. Vamos inicializar a forma da nossa primitiva.
Vá para o método void MinhaEntidade::load
. Lá, digite o seguinte código, ANTES da nossa mensagem de log:
float vertices[] = { // Vértices do quadrado
-50.0f, -50.0f, 0.0f, // topo esquerdo
50.0f, -50.0f, 0.0f, // topo direito
50.0f, 50.0f, 0.0f, // base direita
-50.0f, 50.0f, 0.0f // base esquerda
};
pQuadrado = ofPrimitiveRenderer::makePrimitive(ofTriangleFan, 4, sizeof(vertices), vertices);
Uma dica valiosa é lembrar-se de sempre descarregar um conteúdo imediatamente após criar seu código de carregamento. Para tanto, vá ao método void MinhaEntidade::unload
e, ANTES da mensagem de log, adicione esta linha:
delete pQuadrado;
Isto desalocará a estrutura da primitiva, após efetivamente deletá-la da GPU.
Entendendo a primitiva desenhada
Como você pode ver através dos comentários, estamos tentando desenhar um quadrado de tamanho 100x100, com seu centro fixado na origem.
Os vértices são descritos por quatro linhas, cada linha com TRÊS números (obrigatoriamente TRÊS por vértice, e devem ser pontos flutuantes); cada número representando uma coordenada (X, Y e Z). Como estamos lidando apenas com duas dimensões, a dimensão Z será sempre zero. Também, veja que os vértices são descritos num sentido horário; isto é um conceito importante, que será explicado a seguir.
Por fim, criamos a nossa primitiva, através da classe estática ofPrimitiveRenderer
. Nela, o método makePrimitive
recebe alguns argumentos importantes:
- O tipo de primitiva que estamos tentando desenhar;
- O número de vértices (pacotes de três pontos flutuantes) que usaremos;
- O tamanho, em bytes, do nosso vetor de vértices;
- A referência direta (subentenda-se um ponteiro de memória) aos nossos vértices.
Este método nos dará um retorno do tipo ofPrimitive*
, ou seja, um ponteiro para a nossa primitiva, após os dados serem enviados para a placa de vídeo. Esta primitiva armazenará uma referência indireta aos dados armazenados na nossa GPU.
Veja que, apesar de estarmos criando um QUADRADO, o tipo da primitiva é ofTriangleFan
. Isso ocorre porque OpenGL, a partir da versão 3.2, não possui o conceito formal de um quadrilátero; subentende-se que um quadrilátero é, na verdade, um conjunto de DOIS triângulos.
Em um nível mais baixo, quando lidamos diretamente com esse tipo de renderização, podemos escolher várias formas de desenhar um quadrado na tela. Você pode especificar diretamente os três vértices de dois triângulos e renderizá-los diretamente (com um tipo de primitiva como ofTriangles
), mas isso criaria uma redundância na quantidade de vértices, já que dois deles seriam duplicados. Você também poderia fazer a forma mais indicada, que seria especificar os vértices da forma que fizemos, e usar um buffer de elementos para especificar uma ordem de desenho dos vértices (o que não pode ser feito rapidamente, até mesmo na Oficina; para isso, você teria que usar estruturas como ofVertexBuffer
e ofElementBuffer
, além de configurar outros aspectos manualmente que não vem ao caso mencionar agora).
Uma opção mais fácil - a usada aqui por nós - seria especificar, normalmente, nossos vértices, e classificá-los como uma primitiva do tipo ofTriangleFan
- ou seja, um "ventilador" de vértices.
Isso pode ser um pouco difícil de visualizar, então peço que preste atenção: a ideia, aqui, é fixar um primeiro vértice e, à medida que os próximos vértices forem sendo dados, construir triângulos AO REDOR deste primeiro vértice, como pétalas de uma flor em torno de um centro.
Quebrando em passos o que ocorre:
- Fixamos o topo esquerdo como o centro;
- Damos o topo direito;
- Damos a base direita. Forma-se um triângulo com os vértices do TOPO ESQUERDO, TOPO DIREITO e BASE DIREITA;
- Damos a base esquerda. Forma-se um triângulo com os vértices do TOPO ESQUERDO, BASE DIREITA e BASE ESQUERDA.
Veja que, se déssemos mais um vértice, não teríamos mais um quadrilátero! Esta é a desvantagem de não se usar algo mais avançado. Infelizmente, só podemos desenhar um quadrado por vez com esta técnica, mas nos servirá por enquanto.
Caso você tenha entendido o que aconteceu, pode também utilizar ofQuad
no lugar de ofTriangleFan
que manteremos, por enquanto, por fins didáticos.
A título de curiosidade, como você pode observar, também é possível desenhar círculos usando esta técnica. Em uma aplicação, círculos são desenhados, vetorialmente, com um número de vértices grande o suficiente - ao redor de um centro - para dar a impressão de que se trata de uma figura completamente redonda.
Renderizando a primitiva desenhada
Agora, poderemos realmente renderizar nossa primitiva.
Com ajuda da mesma classe estática ofPrimitiveRenderer
, podemos usar o método draw
para desenhar nossa primitiva.
Vá ao método void MinhaEntidade::draw
e adicione esta linha de código:
ofPrimitiveRenderer::draw(pQuadrado, glm::vec4(1.0f), mvp);
Ao compilar e executar este código, você verá um quadrado no canto superior da tela, mas saberá que algo está errado: Você está vendo apenas um quarto do quadrado! Ademais, não seria melhor que o quadrado estivesse no centro da tela?
Existe uma razão para isso: primitivas não possuem sua própria matriz Model. Elas apenas são renderizadas no local especificado pela matriz mvp
a elas dada.
Lembra-se de que, anteriormente, quando definimos os vértices da nossa primitiva, definimo-nos relativos a uma origem? Pois bem, a origem do nosso plano é o mesmo canto superior esquerdo da tela, como definido na nossa matriz mvp
.
A solução seria criar uma matriz Model
, e então multiplicá-la por nossa mvp
: mvp * Model
. Mas não precisamos fazer exatamente isso.
Como mencionado anteriormente, toda entidade possui submatrizes da matriz Model, e esta matriz Model pode ser gerada com uma chamada simples de função. Portanto, basta substituir a linha de desenho da matriz por esta linha
ofPrimitiveRenderer::draw(pQuadrado, glm::vec4(1.0f), mvp * getModelMatrix());
onde a função getModelMatrix
é, na verdade, um método da nossa classe, herdado de ofEntity
. Isto fará com que nossa primitiva seja renderizada SEMPRE onde a nossa entidade verdadeiramente está.
Mas, como você pode ver (caso tenha compilado e executado o seu programa agora), só isto não é o suficiente para centralizar nosso objeto na tela. Para isso, aplicaremos uma operação de translação na própria entidade.
No método void MinhaEntidade::init
, antes do nosso log, adicione a seguinte linha de código:
translate(glm::vec3(400.0f, 300.0f, 0.0f), true);
Mais detalhes serão explicados logo no próximo tópico, mas isto fará com que nossa entidade seja posicionada no centro exato da tela (veja que os valores correspondentes a X e Y no vetor de três dimensões são 400 e 300, metades exatas do tamanho da nossa resolução interna de 800x600).
Compile e execute o código. Você verá uma tela assim:
Abaixo, o código final dos nossos arquivos src/MinhaEntidade.hpp
e src/MinhaEntidade.cpp
:
#pragma once
#include <oficina2/entity.hpp>
#include <oficina2/render.hpp>
using namespace oficina;
class MinhaEntidade : public ofEntity
{
private:
ofPrimitive* pQuadrado;
public:
void init();
void load();
void unload();
void update(float dt);
void draw(glm::mat4 mvp);
};
#include "MinhaEntidade.hpp"
#include <oficina2/io.hpp>
void MinhaEntidade::init()
{
translate(glm::vec3(400.0f, 300.0f, 0.0f), true);
ofLog(ofLogWarn, "Entidade inicializada!\n");
}
void MinhaEntidade::load()
{
float vertices[] = { // Vértices do quadrado
-50.0f, -50.0f, 0.0f, // topo esquerdo
50.0f, -50.0f, 0.0f, // topo direito
50.0f, 50.0f, 0.0f, // base direita
-50.0f, 50.0f, 0.0f // base esquerda
};
pQuadrado = ofPrimitiveRenderer::makePrimitive(ofTriangleFan, 4, sizeof(vertices), vertices);
ofLog(ofLogWarn, "Entidade carregada!\n");
}
void MinhaEntidade::unload()
{
delete pQuadrado;
ofLog(ofLogWarn, "Entidade descarregada!\n");
}
void MinhaEntidade::update(float dt)
{
}
void MinhaEntidade::draw(glm::mat4 mvp)
{
ofPrimitiveRenderer::draw(pQuadrado, glm::vec4(1.0f), mvp * getModelMatrix());
}
Movendo a ofEntity
Agora que temos um objeto renderizado na tela, vamos fazer com que este objeto se mova de acordo com o nosso pressionamento de teclas.
Entendendo os métodos de entrada e mapeamento
Por razões de padrão e portabilidade, toda a entrada básica de um jogo, ao usar a Oficina, é recuperada a partir de mapeamentos de um controle como o do Xbox 360. Ou seja, a Oficina, por padrão, possui entradas associadas a:
- Botões de controles (A, B, X, Y, LB, RB, LT, RT, LS, RS, Start, Back);
- Analógicos (dois; analógico esquerdo e analógico direito);
- Botões direcionais digitais (Digital Pad Up, Digital Pad Down...);
- Gatilhos (diferente de pressionamentos simples de LT e RT, um valor normalizado indicando a taxa de pressionamento destes gatilhos).
A Oficina suporta um máximo de quatro controles conectados (precisam ser compatíveis com a biblioteca SDL2), e inputs de teclado devem ser diretamente mapeados aos botões citados. Ao mapear botões de teclado para analógicos, a Oficina oferece ferramentas que relacionam o pressionamento de um botão à movimentação de um certo analógico em uma certa direção.
A engine também possui suporte a posição e cliques do mouse; porém, a posição do ponteiro do mouse sempre será feita a partir do valor absoluto da posição na JANELA, e não no seu viewport! Desta forma, lembre-se de normalizar esta posição, caso um dia seja necessária para você.
Tendo em vista que este mapeamento pode ser problemático para um pequeno projeto sendo feito rapidamente, a Oficina fornece uma função em especial que mapeia automaticamente algumas teclas ao input do Jogador 1. Usaremos, portanto, esta função para mapear nossos controles.
Abra o arquivo src/main.cpp
e, antes de ofGameLoop
, digite esta linha de código:
ofMapDefaultsP1();
Feito isso, poderemos mover nossa entidade, nos próximos tópicos, com as setas do teclado, após utilizarmo-nas apropriadamente. Ou, se assim achar melhor, você pode simplesmente plugar um controle compatível e usar o analógico esquerdo deste controle para interagir da mesma forma.
Para saber um pouco mais sobre as funções disponíveis e sobre o mapeamento, você pode consultar a documentação da Oficina.
Recebendo métodos de entrada
Agora que mapeamos nossa entrada adequadamente, podemos partir para a forma como devemos lidar com a entrada em si.
A Oficina possui várias funções essenciais para lidar com estes valores. Eis as essenciais, que lidam diretamente com entradas de controladores ou de teclado mapeado:
// Analógicos
ofGetLeftStick(jogador);
ofGetRightStick(jogador);
// Botões
ofButtonPress(botao, jogador);
ofButtonTap(botao, jogador);
// Gatilhos
ofGetLeftTrigger(jogador);
ofGetRightTrigger(jogador);
Note que, nestas funções, o parâmetro jogador
é sempre opcional. Caso omitido, a engine assume que você esteja se referindo ao Jogador 1 (ofPlayerOne
).
Abra o arquivo src/MinhaEntidade.cpp
.
Logo após os cabeçalhos já incluídos, adicione o cabeçalho de entradas da Oficina:
#include <oficina2/input.hpp>
Agora, no método void MinhaEntidade::update
, insira o seguinte código:
glm::vec2 velocidade = ofGetLeftStick();
Apesar de não possuir nenhum efeito gráfico, já estamos recuperando a posição do analógico esquerdo do Jogador 1 e salvando-o em um vetor de duas dimensões. Neste vetor, a coordenada X lida com o plano horizontal, e a Y lida com o plano vertical.
Mover o analógico para a esquerda total e para a direita total significa fazer com que esta coordenada X do vetor se torne -1 e 1, respectivamente; mover o analógico parcialmente também alterará o valor. Um analógico não movido no plano horizontal terá seu valor em X igual a ZERO. Da mesma forma, mover para cima e para baixo fará com que a coordenada Y assuma valores -1 e 1 respectivamente. Note que, da mesma forma como estabelecemos para nossa projeção anteriormente, o valor se torna maior na direção PARA BAIXO.
Perceba que, no caso do teclado, ele SIMULA este tipo de valor; apertar a seta para a esquerda, por exemplo, fará com que a coordenada X assuma um valor de exatamente -1; apertar a seta para a esquerda fará X assumir 1, e assim por diante. Note, porém, que por padrão não é possível simular, no teclado, algo como um meio-movimento (mover o analógico apenas um pouco para uma direção).
Calculando a velocidade do movimento
Agora, faremos com que nossa entidade efetivamente SE MOVA na tela.
Primeiramente, é interessante ressaltar que, normalmente, movimentos em jogos são interpolados, já que jogos normalmente possuem uma variação ilimitada na quantidade de quadros processados por segundo. Porém, ao iniciarmos este projeto, configuramos a engine de forma a assegurar que nosso jogo NUNCA ultrapasse a barreira dos 60 quadros por segundo, NO MÁXIMO. Por tanto, podemos operar como se nosso jogo estivesse sempre operando nesta quantidade fixa de quadros por segundo.
Abaixo da linha em que obtemos a posição do nosso analógico, adicione esta linha:
velocidade *= 5.0f;
Isto fará com que cada um dos eixos do nosso analógico esquerdo tenha seu valor multiplicado pela taxa de movimento que queremos (5 pixels por quadro ou, numa variação de 60 quadros por segundo, em torno de 300 pixels por segundo). Perceba que nos valemos de um pequeno artifício da GL Mathematics: estamos multiplicando dois valores, que fazem parte do nosso vetor, com apenas uma linha. Na prática, esta linha faz um trabalho como este:
velocidade.x = velocidade.x * 5.0f;
velocidade.y = velocidade.y * 5.0f;
Agora, portanto, podemos dizer que temos o valor em pixels para o qual sabemos que nosso objeto pode se mover. O sinal desse valor indicará a direção do movimento (por exemplo, se tivermos que nos mover APENAS para a esquerda, teremos um valor como {x = -5, y = 0}).
Transformando a entidade
Por fim, agora, só falta incorporar esta variação de posição do objeto ao próprio objeto.
Mais cedo, utilizamos o método translate
para posicionar nosso objeto na tela. Este método, em conjunto com rotate
e scale
, é herdado também da casse ofEntity
.
Estes três métodos especiais fazem com que seja possível alterar aspectos da nossa classe ou, mais especificamente: posição, ângulo de rotação e escala.
Vá ao fim do mesmo método void MinhaEntidade::update
, e adicione esta linha, depois do cálculo da velocidade:
translate(glm::vec3(velocidade, 0.0f));
O que estamos fazendo é uma translação direta na matriz de posição do nosso objeto. O argumento glm::vec3(velocidade, 0.0f)
cria um vetor tridimensional a partir dos valores X e Y de velocidade e, no que tange ao valor Z (que está faltando em velocidade
), usamos 0.0f, já que vamos nos manter em duas dimensões.
Compile e execute seu aplicativo. Você agora poderá mover o quadrado usando as setas do teclado ou, caso ache melhor, conectando a qualquer momento um controle compatível e usando o analógico esquerdo do controle.
Eis o método update
da entidade, após o experimento:
void MinhaEntidade::update(float dt)
{
glm::vec2 velocidade = ofGetLeftStick();
velocidade *= 5.0f;
translate(glm::vec3(velocidade, 0.0f));
}
Gráficos e Renderização
Já fizemos bastante até agora. Colocamos um quadrado na tela, e conseguimos movê-lo através dos comandos do próprio jogador. Mas nosso jogo ainda continua um pouco feio... por isso, colocaremos gráficos reais no nosso jogo.