EDK Game Engine: Desenvolvimento de um Motor para Jogos Multiplataforma - PDF
EDK Game Engine: Desenvolvimento de um Motor para Jogos Multiplataforma
Download "EDK Game Engine: Desenvolvimento de um Motor para Jogos Multiplataforma"
Victor Gabriel Bicalho Vilarinho
1 EDK Game Engine: Desenvolvimento de um Motor para Jogos Multiplataforma Eduardo Moura Sales Martins, Márcio Garcia Martins Unidade Acadêmica de Graduação Curso de Jogos e Entretenimento Digital Universidade do Vale do Rio dos Sinos (UNISINOS) São Leopoldo RS Brasil Abstract. This paper describe the development of the first version of EDK Game Engine (Extensible Development Kit) made to build multi platform projects. Your code make your own pattern used to represent data types and functions. The use of external libraries are transparent removing the need for knowledge. Having your source code opened for use and learning, the engine is intended to provide a flexible and cheap game development technology, aimed for beginners and independent developers. Resumo. Este artigo descreve o desenvolvimento da primeira versão do motor EDK (Extensible Development Kit) voltado para a construção de projetos multiplataforma. Sua escrita idealiza uma padronização própria usada na representação de tipos de dados e funções. O uso de bibliotecas externas é feito de forma transparente removendo a necessidade de conhecimento das mesmas. Tendo o seu código aberto para utilização e aprendizado, o motor tem como objetivo proporcionar uma tecnologia de desenvolvimento de jogos maleável e barata, voltado para empresas iniciantes e desenvolvedores independentes. 1. Introdução Um motor de jogo pode ser um programa de computador, ou um conjunto de bibliotecas, construído para ajudar o desenvolvimento de um, ou mais, jogos. Também permitindo ao programador a construção de um projeto sem necessidade de conhecer o sistema operacional ou hardware alvo. Muitos motores são escritos utilizando bibliotecas externas, como exemplos da opengl[1] e directx[2], ou recebendo características de outros motores, como visto no desenvolvimento do GoldSource[3] utilizado na construção do primeiro Half Life[4]. Segundo HODOROWICZ[5], os componentes básicos existentes em um motor de jogo em três dimensões podem ser divididos em seis partes: System representando a camada de conversa direta com o sistema operacional; Console disponibilizando para o desenvolvedor a escrita de mensagens na tela usadas para debug; Support sendo o mais usado por outros componentes possuindo funções para cálculos matemáticos, gerenciamento de memória e carregamento de arquivos; Renderer/Engine Core possuindo funções para teste de colisão, posicionamento da câmera, processamento de geometria estática e dinâmica, sistema de partículas, renderização de malhas,
2 iluminação e shaders; Game Interface possibilitando a modificação das características do motor através de scripts e Tools/Data adicionando ao motor softwares para edição ou conversão de dados. De acordo com GREGORY[6], o termo motor de jogo surgiu nos anos noventa em referência ao jogo DOOM[7] tendo uma arquitetura separada do jogo principal possuindo: renderizador gráfico em três dimensões, detector de colisões e um processador de áudio. Esta separação disponibilizou o licenciamento do produto para outros desenvolvedores sendo chamado de software motor. Na mesma época iniciou se a criação de uma comunidade para modificação de jogos chamados mod. O software EDK (Extensible Development kit) é um motor de desenvolvimento de jogos multiplataforma, com versões para Windows[8] e Linux[9] para processadores com arquitetura 32 e 64 bits. O código escrito para a construção do motor será distribuído livremente sob a licença LGPL[10]. Este projeto visa construir um motor, com o seu código aberto, para a comunidade desenvolver os seus jogos com maior facilidade. Desenvolvedores independentes e empresas poderão modificá lo de acordo com a sua necessidade. O desenvolvimento multiplataforma consiste na construção de headers utilizando toda e qualquer característica presente no projeto, tendo o seu código implementado ou atribuído de uma biblioteca externa. Esta gera uma padronização nas funcionalidades do código facilitando as futuras conversões do motor para outras plataformas como videogames e dispositivos móveis. Utiliza se cinco bibliotecas externas: A SFML[11](Simple and Fast Multimedia Library), usada no gerenciamento de janelas, interpretação dos parâmetros de entrada de dados, leitura e reprodução de arquivos de áudio, e carregamento de imagens com quatro canais. Utiliza se a PugiXML[12] para interpretação e codificação de código em XML[13]. Possibilita se através da SQLite[14] a criação e manipulação de um banco de dados SQL[15] não obrigando a utilização de um servidor externo. A leitura e gravação de imagens JPEG[16] com um ou três canais é feita através da Jpeglib[17]. O motor de física Box2D[18] é usado para simular objetos, e suas conexões em um mundo em duas dimensões. Visando facilitar a compilação do projeto, todas as bibliotecas são compiladas junto com o motor, com exceção da SFML que necessita da instalação de dependências[19] para a sua compilação Organização do Documento Este documento encontra se dividido em seis capítulos, incluindo o estudo sobre motores de jogos e apresentação do projeto: A segunda seção mostra a pesquisa de mercado apresentando aplicações comerciais e didáticas. Na terceira sessão é visto a preparação do ambiente e o desenvolvimento do projeto. A seção seguinte disseca o funcionamento dos núcleos encontrados no motor. A quinta seção demonstra a utilização do motor no desenvolvimento de aplicações. A seção final transcreve as implicações e considerações finais, onde são comentados o futuro do projeto no desenvolvimento de aplicações e o gerenciamento de contribuições.
3 2. Trabalhos Relacionados A Unreal Engine[20] é uma suíte de aplicativos desenvolvido pela Epic Games, usado na construção de jogos em duas ou três dimensões para computadores, plataformas móveis e videogames. O uso da linguagem C++ em sua interface, e a disponibilidade de modificação do seu código, facilita a entrada de novos desenvolvedores que buscam um motor flexível. Este possui uma licença livre para uso comercial e não comercial necessitando, como pagamento de royalties, cinco por cento do lucro do projeto. O software Unreal Editor possui versão apenas para Windows possibilitando o porte dos jogos produzidos, para plataformas adicionais como: MacOS, Linux, HTML5(HyperText Markup Language 5), Android, IOS, Playstation 4 e XboxONE. Game Maker Studio[21] é um motor de jogo 2D proprietário, desenvolvido pela YoYo Games tendo como uma das características a sua facilidade de uso, sendo possível a construção de um jogo através de comandos pré configurados sem a necessidade de conhecimento em linguagens de programação. Projetos mais avançados podem ser escritos usando a linguagem GML(GameMaker Language). O software de edição do motor possui apenas versão para Windows, similar ao Unreal Engine, tendo a possibilidade de porte dos projetos para para IOS, Android, MacOS, Linux, HTML5 e consoles de videogames sendo eles: Playstation 3, Playstation 4, Playstation VITA e Xbox ONE. Lib Unicórnio é o nome dado ao motor de desenvolvimento de jogos criado em 2014 por JOHANN[22], sendo usado para fins acadêmicos e profissionais, tendo como alvo, programadores iniciantes na linguagem C++. Suas classes e funções recebem nomenclaturas em português brasileiro visando o seu fácil aprendizado no curso de Jogos Digitais da UNISINOS (Universidade do Vale dos Sinos). A arquitetura descentralizada estimula, a liberdade de criação do aluno, no controle total do código do seu jogo. Entre as suas funcionalidades, podemos citar: Renderização de sprites e textos, reprodução de áudio, gerenciamento de recursos, controle de tempo, captura de eventos, teste de colisão e tile map. No mesmo ano escreveu se uma contribuição ao projeto adicionando um modelo de redes Peer to Peer (P2P) com suporte a gerenciamento (WAGNER[23]), auxiliando o desenvolvimento de aplicações multiusuários em redes de computadores usando a lib Unicórnio. 3. Metodologia O motor EDK foi escrito sob a linguagem de programação C++[24] utilizando a IDE (Integrated Development Environment) multiplataforma chamada QTCreator[25]. Em ambos os sistemas operacionais, compila se o código usando a suíte GNU Compiler[26] utilizando no Windows a versão MinGW[27]. A compilação do motor necessita da biblioteca externa SFML sendo incluído os seguintes links: lsfml system lsfml window lsfml graphics lsfml audio adicionando a SFML em ambos os sistemas operacionais. No Windows é necessário o uso do comando lwsock32 lopengl32 lglu32 lglew32 adicionando a Winsock2[28] para uso de redes, a OpenGL e GLEW[29] para renderização pela GPU. No Linux atribui se o comando lpthread lgl lglu lglew ldl adicionando além da OpenGL, também usada no Windows, a Pthread[30] para processamento em paralelo e a libdl necessária para a
4 compilação da SQLite. O diagrama de dependências visto na Figura 1 apresenta as principais características do motor conectadas aos seus códigos de utilização. A plataforma alvo do projeto pode representar um sistema operacional ou hardware. As conexões na cor azul demonstram uso direto da plataforma alvo, na cor verde são apresentadas o uso de bibliotecas externas. As conexões na cor vermelha mostram as utilização de diferentes classes do motor. Os objetos de nível mais baixo são representados na cor azul, para bibliotecas externas, e cinza para código do projeto. Na cor verde é exibido o segundo nível conectado ao primeiro. As demais cores mostram os objetos de terceiro, quarto e quinto níveis respectivamente. Figura 1. Diagrama de dependências O desenvolvimento do projeto teve início com a criação dos tipos de de dados utilizados no desenvolvimento do motor seguido pelas classes de baixo nível, independente de bibliotecas externas sendo utilizadas para: manipulação de string, cálculos matemáticos, leitura do relógio do processador e controle de FPS (frames per second) disponível no núcleo watch, manipulação de Threads para processamento paralelo, leitura e gravação de arquivos, manipulador de código XML com a ajuda da pugixml e uma classe de banco de dados SQLite. Para guardar e ordenar dados na memória, foi escrito o núcleo vector possuindo uma pilha e uma árvore binária de ordenação de dados. Para facilitar o gerenciamento de ponteiros na memória, desenvolveu se um contador de referencia por ponteiro sendo usado para limpar os seus apontadores antes da remoção do dado. O seu desenvolvimento foi descontinuado na metade do desenvolvimento do projeto porque o seu uso não resolveu o problema de travamento do sistema. A necessidade de renderização gráfica foi suprida inicialmente pela classe de criação de janelas, e gerenciamento de dados de entrada. A criação da primeira janela
5 possibilitou o teste do carregador e gravador de imagens utilizando, além da SFML para PNG[31], o núcleo codecs para a leitura de arquivos em JPEG. O renderizador gráfico de baixo nível do motor recebeu o nome de GU (Graphic Unity) possuindo uma semelhança com a OpenGL para facilitar o aprendizado do usuário final. A divisão de áreas na janela foi construída através de View's, possibilitando também, a renderização de uma View dentro da outra pela ViewController. As demais foram criadas com o tempo. Também desenvolveu se o núcleo shape para a renderização de uma malha de polígonos em duas dimensões. A construção do carregamento de texturas fez se através da lista de gerenciamento de texturas sendo usada pela classe de materiais. Adicionou se ao shape o construtor de curva de Bezier[32] em duas dimensões. Com a finalização da construção da malha de polígonos, implementou se o objeto em duas dimensões possuindo uma lista de malhas guardadas de forma ordenada. Escreveu se uma classe para manipulação de uma câmera em duas dimensões representando a visão do jogador dentro da ViewGU2D. Também desenvolveu se no projeto o núcleo collision possuindo funções simples em baixo nível para teste de colisão entre polígonos e linhas. Foi adicionado a ViewButton usada para testar quando o mouse clicou encima da View gerenciando o retorno para o usuário modificando o seu sprite. Para efetuar o carregamento de áudio, construiu se o núcleo audio utilizando a SFML, sendo implementado: a lista de carregamento de áudio e um controlador responsável na reprodução do arquivo de áudio. A classe gerenciadora do buffer carrega o arquivo inteiro para a memória. Para o processamento de áudio em duas ou três dimensões, é necessário a leitura de um arquivo com apenas um canal de áudio (mono). Implementou se o núcleo de animação com a representação dos keyframes em até três dimensões usada na manipulação de um grupo de interpolações. Para esses grupos, adicionou se funções para: play, pause, stop e criação de nomes de animação guardando o segundo inicial e o segundo final. Adicionou se ao desenvolvimento do projeto a implementação do núcleo shader escrevendo o leitor do código do shader GLSL[33], uma lista de dados à serem enviados para a manipulação do programa e uma classe usada para compilar o programa e processá lo na GPU. Implementou se o núcleo physics2d utilizando a Box2D escrevendo: Objetos estáticos, cinemáticos e rígidos. Sensores estáticos e cinemáticos. Junções diferentes entre os objetos. Também construiu se o núcleo bones desenvolvendo o osso animado em duas dimensões sendo usado pelo corpo. Implementou se ao corpo um carregador de arquivo de animação em BVH[34]. Visando a construção de jogos com gameplay multiplayer em rede, implementou se o núcleo network possuindo classes para a criação de sockets sob os protocolos UDP e TCP[35]. Ambos protocolos possuem classes para a implementação de cliente e servidor. As classes de servidor guardam as conexões em uma árvore binária ordenando os pelo seu endereço IP e pelo valor da porta utilizada. Escreveu se o núcleo tiles sob a implementação do tileset sendo usado pelo mapa de tiles. Cada tile encontrado no tileset possui uma textura independente dos
6 outros tiles e pode ser adicionado à um mundo físico em duas dimensões. Após a criação do tilemap, desenvolveu se o mapa de caracteres herdando os parâmetros do tilemap, implementando o tamanho automático do tileset. A classe escrita para a criação do Cenário2D implementa funções para o recebimento de objetos em duas dimensões, objetos físicos e um tilemap. O núcleo animation recebeu a implementação das classes de animação por caminho, substituindo as interpolações por pontos a serem percorridos, usados na movimentação de objetos físicos. 4. Arquitetura O EDK possui uma tipologia própria com nomes construídos usando o tipo de dado seguido de seu tamanho em bits. Existem três tipos de dado que não seguem esta regra sendo eles: typeid usado para representar um identificador de trinta e dois bits, sockettype que guarda o identificador do tipo de socket representado por oito bits e o classid que é utilizado para setar um ponteiro de objeto com tamanho dependendo da versão do sistema operacional, possibilitando trinta e dois ou sessenta e quatro bits. Para representar o tipo boleano, utiliza se o nome bool padrão na linguagem C e C++. O diagrama de definições de tipos (Figura 2) mostra os tipos de dado usado no código do motor. Figura 2. Diagrama de Definições de tipos Os vetores bidimensionais, tridimensionais e quadridimensionais são mostrados no diagrama de definições de vetores (Figura 3). Os nomes são escritos utilizando a palavra vec seguida por: valor de eixos no vetor, primeiro caractere do tipo de dado e quantidade de bits ocupada por cada eixo. Os caracteres usados para tipologia são: 'ui' para inteiro sem sinal, 'i' para inteiro com sinal e 'f' para valores com ponto flutuante.
7 Figura 3. Diagrama de definições de vetores O motor disponibiliza seis tipos de cores encontradas no diagrama de definições de cores (Figura 4) com nomes sendo representados pela palavra color, a quantidade de canais, o tipo de dado similar ao utilizado nas classes de vetores e o tamanho de memória em bits, usado por cada canal. Por padrão, utilizam se valores entre zero e duzentos e cinquenta e cinco para inteiro sem sinal, e valores entre zero e um para ponto flutuante. Figura 4. Diagrama de definições de cores Como visto no diagrama de definições de tamanhos (Figura 5). Os vetores para representação de tamanho podem guardar tamanhos de duas ou três dimensões. O padrão para nomes de tipos de tamanho utiliza a palavra size seguida pelo valor de dimensões no tamanho, tipo de dado igual ao encontrado nos tipos de vetores e o tamanho em bits de cada eixo.
8 Figura 5. Diagrama de definições de tamanhos A Figura 6 mostra o diagrama de definições de retângulos tendo os seus nomes iniciados com a palavra rect, e o tipo de dado semelhante ao encontrado nos nomes dos vetores seguido do tamanho em cada eixo no vetor. O retângulo utiliza dois vetores sendo eles: a origem representando a posição do retângulo, e o seu tamanho. Figura 6. Diagrama de definições de retângulos A estrutura de classes do motor foi dividida em treze núcleos distintos interagindo entre si. Estes núcleos são distribuídos em diferentes pastas, com exceção do view e audio que fazem parte da pasta raiz. O núcleo raiz apresentado na Figura 7, possui classes para manipulação de vectores do tipo char8, arquivos binário e texto, conversor de valores binários, processamento no hardware de vídeo, banco de dados SQL, multi thread, cálculos matemáticos, alocação dinâmica de objetos, leitura e escrita de arquivos XML, carregamento de imagens, manipulação de texturas, renderização de objetos, manipulação de câmera, renderizador de cenário em duas dimensões e logotipo do projeto.
9 Figura 7. Diagrama de classes raiz A edk::string disponibiliza a clonagem, conversão, comparação, corte, concatenação e retorno do tamanho de strings. Também possui conversores de dados numéricos para string, e conversões de string para dados, entre eles: inteiro com sinal, inteiro sem sinal e ponto flutuante. O edk::file pode criar, apagar, ler e escrever arquivos binários e texto usando a cstdio[36] em C. A classe edk::binaryconverter possibilita a escrita de bits no terminal, inversão dos valores de um número, consulta e modificação do valor de um bit. O envio de dados para o hardware de vídeo é feito usando as classes edk::gu e edk::gu_glsl convertendo funções opengl para GU como mostrado na Tabela 1, e para GU_GLSL sendo visto na Tabela 2. As conversões são feitas apenas trocando a segunda leltra de l para u, outras funções como gualloctexture2d, gudrawtotexture e guallocframebuffer. foram criadas para facilitar o uso do hardware gráfico.
10 Tabela 1. OpenGL Graphic Unity Função GU gucolor3f32 gucolor3f64 gucolor4f32 gucolor4f64 guusingmatrix guusematrix guloadidentity gupushmatrix gupopmatrix gusetviewport gusetviewportrect guuseortho gualloctexture2d gudrawtotexture gudeletetexture gublendfunc guenable gudisable gurotate3f32 gurotate3f64 guscale3f32 guscale3f64 gutranslate3f32 gutranslate3f64 gupointsize gulinewidth gubegin guend guvertex2f32 guvertex2f64 guvertex3f32 Funções OpenGL Função GU glcolor3f guvertex3f64 glcolor3d guvertex4f32 glcolor4f guvertex4f64 glcolor4d guvertexnormal3f32 Testa se está usando a matrix guvertexnormal3f64 glmatrixmode gushademodel glloadidentity gulightf32 glpushmatrix gulighti32 glpopmatrix gulightfv32 glviewport gulightiv32 glviewport gugetlightfv32 glortho gugetlightiv32 glgentextures, glbindtexture, glteximage2d, gltexparameteri, glgeneratemipmap gumaterialf32 glbindtexture, glteximage2d, gltexparameteri, glgeneratemipmap gumateriali32 gldeletetextures gumaterialfv32 glblendfunc gumaterialiv32 glenable gugetmaterialfv32 gldisable gugetmaterialiv32 glrotatef gucolormaterial glrotated guusetexture glscalef guusetexture1d glscaled guusetexture2d gltranslatef guusetexture3d gltranslated guvertextex1f32 glpointsize guvertextex1f64 gllinewidth guvertextex2f32 glbegin guvertextex2f64 glend guvertextex3f32 glvertex2f guvertextex3f64 glvertex2d guvertextex4f32 glvertex3f guvertextex4f64 Funções OpenGL glvertex3d glvertex4f glvertex4d glnormal3f glnormal3d glshademodel gllightf gllighti gllightfv gllightiv glgetlightfv glgetlightiv glmaterialf glmateriali glmaterialfv glmaterialiv glgetmaterialfv glgetmaterialiv glcolormaterial glbindtexture glbindtexture glbindtexture glbindtexture gltexcoord1f gltexcoord1d gltexcoord2f gltexcoord2d gltexcoord3f gltexcoord3d gltexcoord4f gltexcoord4d Tabela 2. OpenGL Graphic Unity GLSL Função GU gushaderinit gushaderinitiate gustartshader gucreateshader gudeleteshader gushadersource gucompileshader gugetshaderiv gugetprogramiv gugetshaderinfolog gugetprograminfolog gucreateprogram guprogramuseshader guprogramremoveshader gudeleteprogram gulinkprogram guuseprogram gudata1i32 gudata2i32 gudata3i32 gudata4i32 Funções OpenGL glewinit return true if shader run init glewgetextension glcreateshader gldeleteshader glshadersource glcompileshader glgetshaderiv glgetprogramiv glgetshaderinfolog glgetprograminfolog glcreateprogram glattachshader gldetachshader gldeleteprogram gllinkprogram gluseprogram gluniform1i gluniform2i gluniform3i gluniform4i Função GU gudata1f32 gudata2f32 gudata3f32 gudata4f32 guvertexdata1f32 guvertexdata2f32 guvertexdata3f32 guvertexdata4f32 gugetdatalocation gugetvertexdatalocation gugeterrorstring glewgetstring guactivetexture guallocframebuffer guuseframebuffer gudeleteframebuffer guframebuffertexture gusetdrawbuffers gusetdrawbuffers gucheckframebufferstatus gucheckframebufferok Funções OpenGL gluniform1f gluniform2f gluniform3f gluniform4f glvertexattrib1f glvertexattrib2f glvertexattrib3f glvertexattrib4f glgetuniformlocation glgetattriblocation glewgeterrorstring glewgetstring glactivetexturearb glgenframebuffers, glbindframebuffer glbindframebuffer gldeleteframebuffers glframebuffertexture gldrawbuffers gusetdrawbuffers glcheckframebufferstatus glcheckframebufferstatus Substituiu se o uso da biblioteca mysql[37] pela SQLite devido a sua facilidade de compilação e utilização. A SQLite disponibiliza a criação de um servidor próprio sem a necessidade de instalação de pacotes adicionais. O código necessita o uso de um compilador para linguagem C diferente do código EDK que usa C++. Com o edk::sql
11 é possível criar, deletar, editar a base de dados através de comandos SQL. O retorno de dados dos comandos são adicionados em um edk::sqlgroup que pode guardar um conjunto de nodos. Cada nodo possui duas strings guardando nome e o valor do dado. A manipulação de threads está disponível na classe edk::thread visando padronizar do seu uso em Windows através da windows.h e no Linux através da pthread.h. Em C++ é necessário atribuir por parâmetro o ponteiro de uma função de execução seguida por um valor de entrada. Após feita a inicialização, o processo de criação pode fazer ações do tipo: testar se ainda está viva, esperar a sua finalização ou processar uma finalização forçada. O edk::math disponibiliza para o programador funções para calcular a raiz quadrada de um valor, módulo, Teorema de Pitágoras, retorno do ângulo de um vetor, retorno do ângulo da distância entre dois vetores, normalização do vetor, seno e cosseno, arco seno e arco cosseno, ângulo de um vetor 2D e resultado na rotação do vetor em um plano 2D. O motor possui um contador de referências usado na classe edk::object. A função retainobject() recebe por parâmetro o endereço do ponteiro da classe usuária do objeto, salvando o em uma árvore binária[38]. A função releaseobject() remove da árvore o endereço recebido por parâmetro limpando o após a sua remoção. Caso o objeto original seja removido da memória, todos os ponteiros existentes na árvore são zerados. Devido a sua complexidade, e a não resolução do risco de travamento da aplicação, a sua utilização ficou restrita apenas ao núcleo de animação. Inicialmente a classe de leitura e escrita de códigos em XML estava sendo desenvolvida usando a biblioteca rapidxml, mas os testes retornaram dois erros: O primeiro ocorre quando tenta se fazer a leitura de um código alocado como const char* tendo como resultando o travamento do sistema. Descobriu se o segundo adicionando um único nodo XML não sendo reconhecido pela biblioteca. Para resolver o problema, substituiu se a rapidxml pela pugixml no código edk::xml sendo usado para ler, escrever e editar código sob este formato. Através da função parse(edk::char* code), é possível interpretar um código escrito em XML. A cada interpretação, o código anterior é removido. Para a edição, utiliza se funções de seleção e edição de nodos. Se houver a necessidade de salvar o código editado, utiliza se a função savetofile() que sobrescreve o arquivo atual (se existir) gravando na memória um arquivo com o código indentado. A leitura do código pode ser feita de duas maneiras: Pode se herdar a edk::xml reimplementando as funções didstartelement(), foundcharacters() e didendelement(edk::char8* name) ou pode se fazer a leitura dos nodos presentes no código utilizando as suas funções e seleção e leitura. A classe edk::image2d disponibiliza a criação de um vetor de bytes representando uma imagem com até quatro canais. A leitura e escrita dos arquivos é feita usando o núcleo codecs para arquivos JPEG e a biblioteca SFML para apenas a leitura de arquivos PNG. As texturas no motor são alocadas utilizando opengl. A textura criada possui um código edk::uint32, um nome (obrigatório para guardar textura na lista), um filtro e um modo representando o número de canais da textura. O filtro recebe os mesmos valores usados na opengl: GU_NEAREST_MIPMAP_NEAREST,
12 GU_NEAREST_MIPMAP_LINEAR, GU_LINEAR_MIPMAP_NEAREST, GU_LINEAR_MIPMAP_LINEAR, GU_NEAREST e GU_LINEAR. O modo seta os canais usados pela textura sendo : GU_RGB (três canais), GU_RGBA (quatro canais), GU_LUMINANCE (um canal), GU_LUMINANCE_ALPHA (dois canais). A edk::texture2drender herda a edk::texture2d adicionado a alocação de um buffer usado para receber a renderização da cena. A função opengl para a criação do frame buffer está disponível apenas nas verões 3.0 ou maior[39]. As texturas podem ser lidas externamente através da leitura de arquivos JPEG ou PNG usando a edk::texture2dfile. Esta classe utiliza a edk::image2d para ler os arquivos em disco ou memória. Escreveu se um gerenciador de texturas em duas dimensões (edk::texture2dlist) sendo usado para controlar o carregamento e remoção das texturas utilizando um contador de referência, aqui chamado de referenciador. Na função de carregamento, se a textura já existir na memória, incrementa se o mesmo. Em sua remoção este é decrementado. Se o valor do referenciador for igual a zero, remove se a textura da memória. Os objetos no mundo 2D são representados pela classe Object2D possuindo um conjunto de malhas de polígonos (edk::shape::mesh2d). Os valores de posição, rotação e escala do objeto são herdados da classe edk::shape::object2dvalue podendo ser conectada à um osso edk::bones::bone2d. O edk::shape::object2d pode animar os valores herdados de edk::shape::object2dvalues usando o núcleo de animação. A edk::camera2d possui uma posição pública, e um tamanho privado (não pode ser negativo) guardando apenas a metade do tamanho adicionado aos vértices na sua renderização. A câmera possui funções para movimentação e escala nos eixos X e Y, e recebimento de um retângulo edk::rectf32 disponibilizando a atribuição da posição e tamanho usando um retângulo no mundo 2D. A classe edk::cenario2d possui um tileset, um mundo físico e um gerenciador de níveis de renderização. Cada nível pode guardar um grupo de objetos, ou um grupo de objetos físicos, ou um mapa de tiles. A ordem de renderização dos níveis é processada do menor ao maior. A classe Cenario2D virtualiza por herança as funções de chamadas para contato entre objetos físicos, objeto com sensor, objeto com tile e objeto com um tile sensor (tile convertido em um sensor). Pode se encontrar, no diretório edkimages, duas classes chamadas edk::powerededkwhite (usada com fundo branco) e edk::powerededkcolor (usada com fundo diferente de branco). Essas duas imagens são adicionadas ao código para atribuir a opção de renderização do logotipo do motor em projetos futuros. Ambas possuem atributos públicos com os seguintes valores: size usado para salvar o tamanho da imagem, name contendo o tamanho da imagem e o vector possuindo o vetor de bytes da imagem. Esses arquivos foram criados usando o software filetoclass criado no projeto Vector Usado para alocar vetores de dados, o núcleo vector apresentado na Figura 8, disponibiliza a criação e manipulação de arranjos de dados, pilha e árvore binária. As classes do núcleo vector manipulam qualquer tipo de dado através do uso de
13 templates[40]. Figura 8. Diagrama de classes Vector O arranjo de dados edk::vector::array pode criar, guardar dados e apagar arrays de diferentes tamanhos não sendo possível o redimensionamento. A edk::vector::stack empilha e desempilha dados através das funções pushback() e popback(). A pilha utiliza arranjos de dados chamados edk::vector::stackcel para alocar dados dinamicamente. Quando um arranjo estiver cheio, o próximo é criado para receber os novos dados. Após a remoção do último dado, o arranjo é removido da memória. O conjunto de arranjos são manipulados de forma unificada representando um vetor de n posições. Por padrão, cada célula é alocada com dez posições, mas este valor pode ser modificado na função clean() que remove todos os valores da pilha, ou no construtor da classe. A remoção de um valor na pilha não influencia a posição dos demais valores empilhados. Esta posição continua na memória e será preenchida na próxima chamada da função pushback(). A edk::vector::stack possui funções para troca de posição de dados sendo possível: trocar duas posições diferentes e troca de posição entre vizinhos. O motor EDK possui uma árvore binária chamada edk::vector::binarytree usada para ordenar valores e dados setados pelo desenvolvedor. Os dados são guardados em nós chamados edk::vector::binaryleaf possuindo um ponteiro de nó para a esquerda e um para a direita. As comparações para ordenação da árvore são feitas usando duas funções virtuais: firstbiggersecond() retorna verdadeiro se o primeiro dado for maior que o segundo e firstequalsecond() retornando verdadeiro se o primeiro dado for igual ao segundo. A função de adição recebe o dado, posicionando o na árvore de acordo com as funções de comparação. As consultas para remoção e retorno do elemento necessitam
14 receber o elemento consultado, ou um elemento diferente que possua uma semelhança ao original apenas para validar a função firstequalsecond(). O retorno de um valor sem a necessidade comparação é feita pela função getelementinposition(). Para facilitar a gravação de dados por nome, escreveu se as classes edk::vector::name e edk::vector::nametree adicionando as funções necessárias para a sua ordenação Watch Como visto na Figura 9, o núcleo watch possui uma classe usada para captura do clock do processador, e uma classe usada no controle de quadros por segundo. Figura 9. Diagrama de classes Vector O edk::watch::time faz o carregamento do clock do processador em microssegundos de duas maneiras: O clock real pode ser carregado usando a função getmicrosecondsreal() aonde retorna a contagem de tempo desde o início da execução da aplicação, a função getmicroseconds() retorna a contagem de tempo desde a execução da função start() usada para zerar o contador de tempo da classe. A ocorrência de overflow no contador de tempo real pode ser testado através da função overflowoccurred(), necessitando previamente do uso da função start() logo após a leitura dos microssegundos. A pausa no processamento de uma thread é feita usando as funções: sleepprocessmiliseconds() sendo usada para dormir o processo durante um tempo definido em milissegundos, e sleepprocessmicroseconds() usada para dormir o processo por um tempo em microssegundos funcionando apenas no sistema operacional Linux, sendo convertido para um valor em milissegundos no Windows. Os valores do relógio do computador podem ser carregados usando a função clockloadtime(). Após o carregamento, é possível a leitura dos dados usando as seguintes funções: clockgetmillisecond() para milissegundos, clockgetsecond() para o carregamento dos segundos, clockgetminute() para a leitura dos minutos, clockgethour() para leitura das horas, clockgetdayofmont() para descobrir o dia do mês, clockgetdayofweek() para retornar o dia da semana, clockgetdayofyear() para ler o valor do dia do ano, clockgetmonth() para ler o valor do mês e clockgetyear() para ler o ano. Para efetuar o controle de quadros por segundo, escreveu se a classe edk::watch::fps que utiliza a edk::watch::time para carregar os clock do processador e setar quanto tempo o processo precisa dormir. A contagem do clock é zerada usando a
15 função start() similar à edk::watch::time. Após o final do processamento do quadro, a função waitforfps() calcula o tempo necessário passado por parâmetro para função sleepmicroseconds() no Linux, e sleepmiliseconds() no Windows Codecs O núcleo codecs mostrado na Figura 10 padroniza a compactação e descompactação de imagens através da edk::codecs::encoderimage e edk::codecs::decoderimage. Figura 10. Diagrama Codecs O carregador de imagens da SFML converte as imagens carregadas para 4 canais por padrão. Para carregar imagens JPEG em um (GRAYSCALE) ou três canais (RGB), escreveu se as classes edk::codecs::encoderjpeg e edk::codecs::decoderjpeg utilizando o código nativo da Jpeglib Collision Como visto na Figura 11. O núcleo collision possui uma classe para processamento matemático usada para o processamento de cálculos como: intersecção entre Bounding Box, intersecção entre ponto e linha, intersecção entre linha e linha, intersecção entre linha e círculo, e intersecção entre ponto e círculo.
16 Figura 11. Diagrama Collision As classes edk::collision::linecontact2d e edk::collision::rectanglecontact2d utilizam as funções encontradas na classe edk::collision::mathcollision retornando os pontos de contato. Os pontos de contato são empilhados na classe edk::collision::vecs2f Network O núcleo network apresentado na Figura 12 possui um edk::network::adress usado para guardar o endereço e porta do host conectado. Este endereço pode ser setado usando um número (quatro dígitos para IPV4) ou um nome (string com o número IPV4 ou nome do host na rede). O valor da porta do host conectado é salvo para diferenciar host's com o mesmo IP. A classe edk::network::socket cria o socket (TCP ou UDP) disponibilizando o envio e recebimento de mensagens. Figura 12. Diagrama Network
17 As classes edk::network::udp::clientudp e edk::network::tcp::clienttcp herdam a classe edk::network::socket implementando o envio e recebimento de mensagens de acordo com a sua necessidade. O cliente TCP pode receber três tipos de valores retornados pela função receivestream(): o valor zero representando a desconexão do servidor, o tamanho da mensagem recebida sendo o tamanho total do buffer de recebimento ou o seu tamanho parcial. A classe edk::network::serversocket padroniza o envio e recebimento de mensagens de vários clientes pelo servidor. Os endereços são salvos em uma árvore binária disponibilizada para as classes herdeiras. Os objetos edk::network::udp::serverudp e edk::network::tcp::servertcp herdam as funções de envio e recebimento de mensagens, e a disponibilidade do uso da árvore de clientes conectados. O servidor UDP processa a inicialização da conexão pela função startbind(), a finalização da conexão, o envio e recebimento de mensagens. O servidor TCP pode ouvir a porta, fechar a conexão do servidor ou fechar a conexão com um cliente específico. Este pode aceitar clientes normais e clientes non block (não bloqueados) sendo possível receber uma mensagem sem bloquear o processo. O recebimento das mensagens podem retornar três tipos de valores: Tamanho total da mensagem recebida representando a mensagem total ou parcial, o valor zero representando a desconexão do cliente ou o número um negativo avisando o não recebimento de mensagem pelo cliente. A classe edk::network::package guarda na memória um pacote de tamanho dinâmico conectado à um edk::network::package::packheader sendo usado para gravar, em cada pacote, os seguintes dados: um inteiro de 64 bits com o valor 0xAA BDE8421 usado para testar se o pacote está quebrado, o tamanho do vetor do pacote (sem o PackHeader), o número identificador do pacote sendo setado pelo usuário, a posição do pacote em sua ordenação e o número total de pacotes enviados. O edk::network::packagegroup gerencia um grupo de pacotes com um identificador setado pelo usuário. É possível quebrar um vetor em vários pacotes para enviá los, ou montar pacotes recebidos pela rede. Não é possível gerenciar mas de um grupo de pacotes com uma única instância da edk::network::packagegroup Audio Como visto na Figura 13, O motor possui um edk::audiobuffer representando o carregamento do áudio na memória. É possível carregar o áudio de um arquivo, apagá lo e fazer a leitura da quantidade de canais e duração do mesmo. O processamento na saída de som necessita do controlador edk::audio, possibilitando a reprodução desde o início, a reprodução a partir de um segundo específico, modificação no volume e na velocidade de reprodução.
18 Figura 13. Diagrama Audio A edk::audiolist faz o gerenciamento de edk::audiobuffer na memória. A lista faz o carregamento do arquivo retornando o buffer para o controlador de áudio. A edk::audiolist é usada diretamente pela classe edk::audio. Os buffers na lista são guardados em uma árvore ordenada pelo nome dos arquivos. Não é possível guardar na memória dois arquivos com o mesmo nome. Ambos edk::audio2d e edk::audio3d adicionam um ouvinte representado por dois vetores com a sua posição e mira (lookat). É necessário setar a posição do áudio para calcular a sua reprodução diante da câmera. O processamento de áudio 2D e 3D é possível apenas com arquivos mono (um canal) Bones De acordo com a Figura 14. Cada osso é representado pela classe edk::bones::bone2d. Cada bone possui variáveis de posição, ângulo de rotação, escala e uma posição 2D representando a posição final do osso (para qual direção o osso está apontando). Cada osso pode possuir nenhum, ou n filhos que são desenhados de acordo com a posição do pai. Os ossos filhos são ordenados pelo nome do osso impossibilitando dois filhos com o mesmo nome. Pode se adicionar quadros de animação[41] ao osso salvando os no grupo de interpolações: edk::animation::interpolationgroup1d para animar o ângulo de rotação e edk::animation::interpolationgroup2d para a posição do osso. Cada osso pode conectar se à nenhum ou n objetos herdando a classe edk::object2dvalues. Os objetos que herdarem as propriedades da classe edk::object2dvalues poderão ser conectados a qualquer osso tendo a sua posição, rotação e escala atualizadas pela função recursiva updateobjects(). Os dados são processados usando matrizes de transformação[42].
19 Figura 14. Diagrama Bones O edk::bones::body2d representa o corpo usado para guardar os ossos. O corpo possui um osso root estático que recebe os ossos filhos. Todos os ossos do corpo são criados e salvos em uma árvore, não importando os seus pais ou filhos. É possível remover da memória cada osso independentemente, ou todos os ossos do corpo de uma só vez. O corpo pode carregar os dados de um arquivo BVH e suas animações. Também é possível escalar o tempo de animação deixando mais rápido ou mais lento. As funções de controle da animação são atribuídas em todos os ossos. Para animar diferentes ossos é necessário processar as funções de controle de animação diretamente no osso correspondente. O corpo processa o controle da posição, rotação e escala de todos os ossos. Pelo corpo herdar os atributos da classe edk::object2dvalues, um corpo pode ser conectado em um osso qualquer construindo uma árvore de corpos animados independentemente. Para a cinemática inversa no edk::animation::body2d, utiliza se o algoritmo de CCD[43] que rotaciona os ossos da árvore até a folha ficar alinhada com o alvo. A sua repetição aproxima a ponta da folha até o seu destino. O algoritmo implementado no motor consiste em uma função para carregar a posição do vetor da folha, e uma função para rotacionar o osso para alinhá lo ao alvo. A função calculateinversekinematic() encontrada na classe edk::bones::body2d recebe por parâmetro: o osso folha, a posição do alvo, o tamanho da calda (número de ossos afetados pelo algoritmo) e a quantidade de repetições para a execução do algoritmo (a repetição do algoritmo aproxima a folha ao alvo). Se a folha alcançar o alvo antes do processamento das repetições, e função é finalizada Shader O núcleo shader mostrado na Figura 15 faz a leitura de shaders escritos em GLSL através da biblioteca GLEW.
20 Figura 15. Diagrama Shader A GU_GLSL faz o carregamento de dois tipos de shaders sendo eles, vertex e fragment. Após o seu carregamento, o mesmo é compilado e conectado à um shader program, que pode possuir vários shaders. A influencia externa neste é setada através da atualização dos dados com sua localização carregada (gugetdatalocation()), e depois atualizando o seu valor (guvertexdata()). O classe edk::shd::glsl faz o carregamento dos shaders de forma transparente para o usuário necessitando apenas o nome do arquivo com a sua extensão definida. As extensões aceitas são.vert para vértex shader, e.frag para fragment shader. Após a criação do shader program, os dados podem ser atualizados. A edk::shd::glsl não disponibiliza atualização por vértice Animation De acordo com o diagrama de classes visto na Figura 16. O edk::animation::frame representa o quadro chave (keyframe) no conjunto de animações salvando o segundo do frame. O processamento da interpolação de valores é feito nas classes: edk::animation::frame1d usado para interpolar um valor, edk::animation::frame2d interpolando dois valores e edk::animation::frame3d sendo usado para três valores.
21 Figura 16. Diagrama Animation A interpolação é calculada utilizando as classes edk::animation::interpolationline disponibilizando o processamento em: uma dimensão usando edk::animation::interpolationline1d, duas dimensões através da classe edk::animation::interpolationline2d e três dimensões disponível na classe edk::animation::interpolationline3d. É possível o processamento das animações em linha reta ou curva de bezier sendo acionada nas funções: setcurvex(), setcurvey() e setcurvez(). Os valores da curva são atribuidos usando as funções setp1x(), setp2x(), setp1y(), setp2y(), setp1z(), setp2z(). O retorno da posição é feito pelo processamento das funções: getpositionx(), getpositiony() e getpositionz() sendo necessário a atribuição do tempo em segundos. O grupo de interpolações edk::animation::interpolationgroup recebe os frames e ordena os mesmos automaticamente construindo a fila de interpolações. A classe possui funções para: play na animação, play no tempo determinado, pause, stop e a disponibilização de animação com ou sem loop. O processamento da atualização da animação usando função edk::float32 updateclockanimation() faz o carregamento do
22 valor do clock do processador usando a edk::clock::time. Os grupos de interpolações com valores, sendo representados pelas classes edk::animation::interpolationgroup1d, edk::animation::interpolationgroup2d e edk::animation::interpolationgroup3d, herdam as funções da edk::animation::interpolationgroup implementando o processamento das posições através das funções: getclockx(), getclocky() e getclockz(). As funções de retorno de posição podem receber o ponteiro de um valor booleano sendo atribuído falso para fim da animação ou se a mesma não estiver sendo processada. É possível criar mini animações dentro do grupo de animações. Cada mini animação é salva com um nome tendo o seu processamento iniciado usando a função playname(). Quando a animação chega ao fim, com o valor de loop como falso, executa se a função animationend(edk::animation::interpolationgroup* animation) encontrada na classe edk::animation::animationcallback sendo setada através da função setanimationcallback(). O motor de física edk::physics2d::world2d movimenta objetos físicos através do controle da sua velocidade linear e angular. O algoritmo de movimentação não garante o reposicionamento do objeto no próximo quadro. Para resolver esse problema, escreveu se a classe edk::animation::pathgroup sendo usada para guardar apenas as posições percorridas pelo objeto. Quando a posição é alcançada, a próxima posição é carregada movimentando se o mesmo na próxima direção Shape O núcleo shape mostrado na Figura 17 possui algoritmos de renderização de polígonos em duas dimensões e posição de um ponto em uma curva. Figura 17. Diagrama Shape
23 Um edk::shape::polyon2d cria o polígono alocando o vetor de vértices. O polígono pode alocar três tipos de vértices: edk::shape::vertex2d guardando a posição e cor do vértice, edk::shape::vertex2dwithuv adicionando a posição do mapeamento UV do vértice na textura e edk::shape::vertex2danimateduv aonde processa a reposição UV do vértice dinamicamente de acordo com a animação. A animação da textura do polígono é setada pela função setpolygonuvframes() possibilitando a escolha da posição do mapa através da função usepolygonuvframe(). Criou se polígonos com quantidade de vértices estáticos sendo eles: edk::shape::rectangle2d com dois vértices, edk::shape::triangle2d com três vértices e edk::shape::quadrangle2d com quatro vértices. O polígono edk::shape::circle2d possibilita a criação de n vértices posicionados automaticamente. A edk::shape::lines2d implementa o polígono como um caminho de linhas. As linhas recebem um código de colisão através da função setcollisionid(edk::uint8 collisionid), sendo explicado no núcleo physics2d. A edk::shape::line2d representa na memória uma linha em duas dimensões sendo usada apenas no núcleo collision. Os polígonos criados são salvos em duas listas diferentes: edk::shape::polygon2dlist usada para guardar os polígonos em uma pilha e edk::shape::animatedpolygon2dlist, que herda as características da lista normal adicionando funções para animação dos polígonos. A malha edk::shape::mesh2d herda a lista animada de polígonos adicionando um material, disponibilizando assim o carregamento de texturas na malha. As curvas de bezier são calculadas utilizando a classe edk::shape::bezier2d que implementa a função getpoint() herdada da classe edk::shape::curve2d Physics 2D O núcleo de física 2D utiliza a Box2D para controlar objetos, conexões e colisões no mundo 2D. Como visto na Figura 18, o motor possui uma classe edk::physics2d::world2d sendo usada para controlar todos os objetos no mundo. A função addobject() adiciona os seguintes objetos: edk::physics2d::staticobject2d representando um objeto estático não sofrendo ação da gravidade ou forças externas aplicadas pelo mundo, edk::physics2d::kinematicobject2d que não sofre a ação da gravidade mas pode receber forças externas e o edk::physics2d::dynamicobject2d sendo manipulado como um objeto dinâmico possibilitando a movimentação aplicada pela gravidade e forças externas. Os sensores edk::physics2d::staticsensor2d e edk::physics2d::kinematicobject2d herdam os atributos das respectivas classes adicionando a característica de não influência na colisão com outros objetos. Cada objeto físico possui uma edk::physics2d::physicsmesh2d não animada e independente dos polígonos do objeto, isso diferencia a área de colisão com a área de desenho. É possível, antes de criar o objeto físico, modificar o tamanho das malhas, mas não é possível escalar círculos na Box2D. O raio do círculo criado recebe o valor da escala no eixo X multiplicado pelo seu raio. A criação de polígonos precisa obrigatoriamente atender as seguintes necessidades: ter os vértices ordenados counterclockwise (sentido anti horário), possuir menos de 8 vértices e ser um polígono convexo. Implementou se diretamente na edk::physics2d::physicsmesh2d um conversor que, após receber o polígono, remove os vértices internos gerando o seu Convex hull dividindo o em vários
24 polígonos de 8 vértices cada. Escreveu se uma versão modificada do algoritmo de Graham Scan[44] para gerar o polígono convexo. Ao invés de procurar um ponto médio entre os vértices, os vértices são ordenados em relação ao vértice na menor posição inferior esquerda do plano cartesiano. Isto facilita divisão do polígono convexo. Figura 18. Diagrama Physics 2D Todos os contatos processados pelo mundo em duas dimensões são tratados pelas funções de callback. Os contatos são filtrados e adicionados à árvore de contatos. Após a criação do contato, a interação entre os dois objetos pode ser cancelada fazendo um objeto atravessar o outro. Objetos sensores não possuem interação com os objetos normais. As funções de gerenciamento de contatos são: contactbegin() (contato iniciado), contactend() (contato finalizado), contactkeepbegin() (se o contato não for finalizado, o contato é conservado) e contactkeepend() (finaliza a conservação). As funções de gerenciamento de sensores são: sensorbegin() (iniciado o contato com o sensor), sensorend() (finalizado o contato com o sensor) e sensorkeeping() (o contato com o sensor esta sendo conservado). É possível controlar a ação de um contato através das funções disablecontact() para desabilitar o contato e enablecontact() para retornar o uso do contato. O edk::physics2d::world2d disponibiliza a criação de conexões entre dois
25 objetos, chamadas edk::shape::joint2d possibilitando a criação de joints com limite de rotação ou joint com motor. É possível a criação das seguintes joints: edk::physics2d::revolutejoint2d sendo a mais simples conectando dois pontos encontrados nos objetos, edk::physics2d::prismaticjoint2d que move o objeto em uma linha imaginária, edk::physics2d::distancejoint2d que adiciona uma distância entre os pontos da RevoluteJoint, edk::physics2d::pulleyjoint2d simulando uma corda de polia sobre quatro pontos, edk::physics2d::wheeljoint2d que adiciona limites na DistanceJoint transformando à sua linha em uma mola e a edk::physics2d::ropejoint2d aonde limita a distância máxima da DistanceJoint simulando uma corda. A função createweldjoint() conecta dois objetos transformando os em apenas um. Após a remoção do objeto físico no mundo em duas dimensões, a joint é deletada automaticamente Tiles Um edk::tiles::tile2d renderiza na tela um polígono retangular com uma textura. É possível adicionar características físicas ao tile transformando o em objeto estático, cinemático ou dinâmico (este último recebendo a ação da gravidade). O tile físico pode possuir um polígono de colisão diferente do retângulo do mesmo. O diagrama de classes do núcleo tiles visto na Figura 19 mostra dois espaços de nomes chamados tiles e font. A edk::tiles::tileset2d cria e gerencia os tiles na memória ordenando os pelo código atribuído na sua criação. O tileset possibilita a renderização do tile em qualquer posição, e em qualquer tamanho. O mesmo tileset pode ser usado por mais de um mapa. O edk::tiles::tilemap2d possui uma matriz de códigos inteiros sem sinal usado para guardar os códigos do tileset. Cada mapa utiliza apenas um tileset. A função de desenho do mapa no mundo virtual em duas dimensões renderiza os tiles nas suas devidas posições. O tilemap pode ser posicionado e escalado (não é possível rotacionar o mapa). Os tiles físicos são adicionados no mundo físico em duas dimensões através da função loadphysicstiles(). O ponteiro do ek::physics2d::world2d é atribuído por parâmetro na função setworld(edk::physics2d::world2d* world). O gerenciamento do contato de objetos físicos com tiles são implementados nas funções da edk::tiles::tilecallback: contact2dtilebegin(), contact2dtileend(), contact2dtilekeepbegin(), contact2dtilekeepend(), sensor2dtilebegin(), sensor2dtileend() e sensor2dtilekeeping().
26 Figura 19. Diagrama Tiles O edk::fonts::fontmap renderiza na tela um mapa de tiles construído com caracteres. Esses caracteres são carregados pela classe edk::fonts::fontset através a leitura da textura de caracteres ASCII, dividindo a em 16x16 tiles. Cada FontSet é guardada em uma lista global de fontes chamada edk::fonts::fontsetlist. O edk::fonts::fontmap possui uma função de construção do mapa de acordo com o texto recebido. A renderização do texto pode ser animado simulando uma conversa possibilitando animar a origem e o final do texto. O tratamento da finalização da animação do texto é processado pelas funções da classe edk::fonts::fontcallback sendo elas: originfoundend(), lastfoundend() e animationend() View A Figura 20 apresenta o diagrama do núcleo view. A edk::view renderiza uma view usando a biblioteca edk::gu através da função guviewport(). A classe possui funções virtuais usadas para: desenho da cena usando a função draw(), carregamento e descarregamento dos dados pelas funções load() e unload(), atualização dos eventos da cena pela função update() e tratamento do contato do mouse na função contact(). O tratamento de eventos específicos do mouse podem ser tratados pelas funções eventmousepressed(), eventmousemoved(), eventmousereleased(), eventmouseentryinsideview() e eventmouseleftview(). Apenas a posição da view pode ser animada, não é possível animar o seu tamanho.
27 Figura 20. Diagrama View A edk::viewcontroller herda os parâmetros da edk::view adicionando a pilha de views sendo renderizadas de acordo com a posição da viewcontroller. O ponteiro da view filha é empilhada usando função addsubview() e removida usando a função removesubview() recebendo o ponteiro da view por parâmetro. As classes edk::viewsprite e edk::viewspritecontroller herdam os atributos das classes edk::view e edk::viewcontroller implementando a renderização de um sprite no fundo da view sendo carregado como pela edk::texture2dlist. A edk::viewspritecontroller herda os parâmetros da edk::viewsprite modificando a renderização para desenhar na tela nove polígonos com diferentes mapas UV sendo mostrado na Figura 21. O mesmo código de renderização do polígono foi implementado na view::viewbutton que herda os atributos da view::viewsprite adicionando mais duas texturas: spritepressed renderizado quando o botão for pressionado e spriteup sendo renderizado quando o mouse estiver encima do botão. Dentro do botão é possível a renderização de um símbolo sendo representado pelas texturas symbol, symbolpressed, symbolup, ou a renderização de um edk::fonts::fontmap. O tratamento dos eventos de mouse são processados automaticamente.
28 Figura 21. Menu exemplo A renderização de uma cena construída com a biblioteca edk::gu pode ser feita usando a classe edk::viewgu2d que herda os atributos da edk::viewgu adicionando uma câmera 2D. As classes edk::viewgutexture e edk::viewgu2dtexture desenham a cena em uma edk::texture2drender renderizando a mesma na tela. A edk::window faz o gerenciamento de uma janela utilizando a classe de criação de janelas da SFML. A função createwindow() recebe por padrão o tamanho, nome da janela, característica de design, bits do buffer de profundidade, bits do stencil buffer e o valor do antialiasing. A janela pode receber as seguintes características de design: EDK_WINDOW_BAR para adicionar a barra da janela, EDK_WINDOW_RESIZE para adicionar a possibilidade de redimensionamento da janela, EDK_WINDOW_BUTTONS adicionando o botão e fechar a janela e botões de redimensionamento se estiver ativado, EDK_WINDOW_NOBAR forçando a remoção da barra da janela e EDK_WINDOW_FULLSCREEN usada para renderizar a janela em tela cheia. A janela possui uma edk::viewcontroller possibilitando a renderização de outras views adicionadas pelo desenvolvedor. Os eventos da janela são carregados para a classe edk::windowevents utilizando a função loadevents(). Se a janela possuir subviews, utiliza se a função updateviews() para atualizar a árvore de views passando por parâmetro os eventos carregados Atualização da SFML Após o lançamento da versão 2.0 da SFML no dia vinte e nove de abril de dois mil e treze, necessitou se a atualização do código do projeto para a versão mais nova incompatível com a sua versão 1.6. Podemos ver na Tabela 3 as conversões nas manipulações de áudio e imagem tendo suas funções convertidas apenas pela substituição do primeiro caractere para caixa alta, somada ao retorno do tamanho da imagem através da função getsize().
29 Tabela 3. Conversão para SFML 2.0 (Audio e Image) SFML 1.6 this->sound->setbuffer(*buffer->getbufferpointer()); this->sound->setplayingoffset(second); this->sound->setloop(loop); this->sound->setvolume(volume); this->sound->setpitch(speed); return (edk::audiostatus)sound->getstatus(); return this->sound->getplayingoffset(); return this->sound->getloop(); return this->sound->getvolume(); return this->sound->getpitch(); this->sound->play(); this->sound->stop(); this->sound->pause(); sf::listener::setposition(position.x,position.y,0.f); sf::listener::settarget(lookat.x,lookat.y,0.f); this->sound->setposition(sf::vector3f(this->position.x,this->position.y,0.f)); this->sound->setrelativetolistener(use); sf::listener::setposition(position.x,position.y,position.z); sf::listener::settarget(lookat.x,lookat.y,lookat.z); SFML 2.0 this->sound->setbuffer(*buffer->getbufferpointer()); this->sound->setplayingoffset(sf::seconds(second)); this->sound->setloop(loop); this->sound->setvolume(volume); this->sound->setpitch(speed); return (edk::audiostatus)sound->getstatus(); return this->sound->getplayingoffset().asseconds(); return this->sound->getloop(); return this->sound->getvolume(); return this->sound->getpitch(); this->sound->play(); this->sound->stop(); this->sound->pause(); sf::listener::setposition(position.x,position.y,0.f); sf::listener::setdirection(lookat.x,lookat.y,0.f); this->sound->setposition(sf::vector3f(this->position.x,this->position.y,0.f)); this->sound->setrelativetolistener(use); sf::listener::setposition(position.x,position.y,position.z); sf::listener::setdirection(lookat.x,lookat.y,lookat.z); this->sound->setposition(sf::vector3f(this->position.x,this->position.y,this>position.z)); this->sound->setrelativetolistener(use); if (this->buffer->loadfromfile((const char*)name) ){ this->buffer->getchannelscount(); this->buffer->getduration(); if(this->image->loadfromfile((const char*)imagefilename)){ this->size = edk::size2ui32(this->image->getwidth(),this->image>getheight()); this->image->setsmooth(false); return (const edk::uint8*) this->image->getpixelsptr(); this->sound->setposition(sf::vector3f(this->position.x,this->position.y,this>position.z)); this->sound->setrelativetolistener(use); if (this->buffer->loadfromfile((const char*)name) ){ this->buffer->getchannelcount(); this->buffer->getduration(); if(this->image->loadfromfile((const char*)imagefilename)){ this->size = edk::size2ui32(this->image->getsize().x,this->image>getsize().y); this->image->setsmooth(false); return (const edk::uint8*) this->image->getpixelsptr(); Arquivo Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio.cpp Audio2D.cpp Audio2D.cpp Audio2D.cpp Audio2D.cpp Audio3D.cpp Audio3D.cpp Audio3D.cpp Audio3D.cpp AudioBuffer.cpp AudioBuffer.cpp AudioBuffer.cpp Image2D.cpp Image2D.cpp Image2D.cpp Image2D.cpp A Tabela 4 nos mostra que a manipulação de janelas e gerenciamento de entradas do usuário, encontradas em Window.cpp, tiveram suas modificações similares ao tratamento de áudio e imagem com exceção ao: Setamento dos parâmetros da janela pela sf::contextsettings(). Controle da visualização da janela através da função setvisible(). Definição de renderização do cursor do mouse na função window.setmousecursorvisible(). Ativação da possibilidade da repetição das teclas do teclado usando setkeyrepeatenabled(). Modificação do teste de estado da janela pela a função isopen(). Manipulação direta do mouse sem a necessidade da chamada da janela através da função sf::mouse::setposition() recebendo a janela por parâmetro. Carregamento dos eventos da janela modificado para pollevent(). Leitura das teclas pressionadas necessitando a construção de um filtro setando letras e números pelo código ASCII e demais teclas reposicionadas após o limite de oito bits.
30 Tabela 4. Conversão para SFML 2.0 (edk::window) SFML 1.6 SFML 2.0 this->window.create(sf::videomode(width, height, bitsperpixel),(const char*)name,style,,sf::windowsettings(depth,stencil,antialiasing)); this->window.show(this->renderwindow); this->window.showmousecursor(this->rendermouse); this->window.enablekeyrepeat(true); if(this->window.isopened()){ this->window.close(); this->window.create(sf::videomode(width, height, bitsperpixel),,(const char*)name,style,sf::contextsettings(depth,stencil,antialiasing)); this->window.setvisible(this->renderwindow); this->window.setmousecursorvisible(this->rendermouse); this->window.setkeyrepeatenabled(true); if(this->window.isopen()){ this->window.close(); this->window.clear(sf::color((edk::uint32)(this->cleancolor.r*255), (edk::uint32)(this->cleancolor.g*255),(edk::uint32)(this>cleancolor.b*255),(edk::uint32)255)); this->window.setcursorposition(pos.x,pos.y); this->window.setposition(pos.x,pos.y); this->window.setsize(size.width,size.height); this->window.setactive(true); this->window.display(); this->window.clear(sf::color((edk::uint32)(this->cleancolor.r*255), (edk::uint32)(this->cleancolor.g*255),(edk::uint32)(this>cleancolor.b*255),(edk::uint32)255)); sf::mouse::setposition(sf::vector2i(pos.x,pos.y), this->window); this->window.setposition(sf::vector2i(pos.x,pos.y)); this->window.setsize(sf::vector2u(size.width,size.height)); this->window.setactive(true); this->window.display(); ret = edk::size2ui32(this->window.getwidth(),this->window.getheight()); return this->window.getwidth(); return this->window.getheight(); while(window.getevent(event)){ event.type ret = edk::size2ui32(this->window.getsize().x,this->window.getsize().y); return this->window.getsize().x; return this->window.getsize().y; while(window.pollevent(event)){ event.type this->events.keypressed.pushback(event.key.code); if(event.key.code>=0&&event.key.code<26) this->events.keypressed.pushback(event.key.code+'a'); else if(event.key.code>=26&&event.key.code<36) this->events.keypressed.pushback(event.key.code+ '0' - 26) else this->events.keypressed.pushback(event.key.code ); this->events.keyrelease.pushback(event.key.code); edk::size2ui32 tempview = edk::size2ui32(window.getwidth(),window.getheight()); event.mousebutton.button input.getmousex(),input.getmousey() this->events.mousescrollwheel=event.mousewheel.delta; sf::event::joybuttonpressed sf::videomode DesktopMode = sf::videomode::getdesktopmode(); sf::videomode DesktopMode = sf::videomode::getdesktopmode(); return DesktopMode.BitsPerPixel; if(event.key.code>=0&&event.key.code<26) this->events.keyrelease.pushback(event.key.code+'a'); else if(event.key.code>=26&&event.key.code<36) this->events.keyrelease.pushback(event.key.code+ '0' - 26) else this->events.keyrelease.pushback(event.key.code ); edk::size2ui32 tempview = edk::size2ui32(window.getsize().x,window.getsize().y); event.mousebutton.button sf::mouse::getposition(this->window).x,sf::mouse::getposition(this>window).y this->events.mousescrollwheel=event.mousewheel.delta; sf::event::joystickbuttonpressed sf::videomode DesktopMode = sf::videomode::getdesktopmode(); sf::videomode DesktopMode = sf::videomode::getdesktopmode(); return DesktopMode.bitsPerPixel; 5. Casos de Uso Para demonstrar as principais funcionalidades do motor, desenvolveu se quatro aplicações distintas demonstrando o uso do motor na criação de classes C++, processamento de jogos de computador e apresentação de slides: filetoclass criado para converter arquivos binários em classes, Adventure Plataform representando um jogo de plataforma com gameplay usando física em duas dimensões, Carnivorous desenvolvido como um arcade point and click e uma aplicação chamada pressconference usada para processar a apresentação de slides deste projeto. O software filetoclass recebe por parâmetro os nomes dos arquivos necessitados para conversão e grava cada arquivo em suas respectivas classes, criando dois arquivos com as extensões:.h usado como header, e.cpp recebendo o código do arquivo. Todos os atributos da classe possuem visualização pública sendo alocados estaticamente.
31 Adventure Plataform possui uma edk::viewcontroller usada para mostrar na tela a imagem edk::powerededkcolor com a transparência sendo animada. Após a apresentação, é renderizada na tela a view de início do jogo possuindo uma imagem de fundo com o nome do jogo e um botão edk::viewbutton com o nome PLAY. A view usada para o processamento do gameplay faz a leitura do cenário contido no arquivo cenario.xml, e a construção do personagem principal carregando, além das texturas dos seus membros, as animações gravadas no arquivo WoodManequinAnimation2.bvh. O cenário em duas dimensões apresenta: Objetos estáticos sendo renderizados nas cores verde e branco, um objeto cinemáticos animado desenhado em vermelho e marrom. Dois objetos de baixa fricção desenhados nas cores verde e azul piscina sendo estático e dinâmico, um objeto estático triangular amarelo, um tilemap com tiles físicos e tiles simples com textura animada, uma junção entre o triângulo amarelo e o objeto dinâmico de baixa fricção, um sensor estático representando o final da fase e objetos não físicos enfeitando a fase. Ao final da fase, o usuário é retornado à tela de início sendo disponibilizado o botão chamado PLAY WITH BOX iniciando a mesma faze com a renderização do personagem junto da sua caixa de colisão. O jogo Carnivorous foi escrito visando testar a funcionalidade de cinemática inversa. Similar ao Adventure Plataform, o projeto possui a tela de apresentação do motor, e a tela de início renderizando um sprite de fundo e um texto animado escrito click left, obrigando o jogador à clicar com o botão esquerdo do mouse para iniciar. A view de gameplay mostra uma planta carnívora na parte central da tela dançando. Após o clique do mouse, a cabeça da planta ataca na posição clicada se e mesma estiver ao seu alcance. Os inimigos são representados por moscas movimentando se pela tela de dez maneiras diferentes na mesma ordem. As animações de movimentação da mosca, e dança da planta são carregadas de arquivos.bvh. O caule da planta constitui se de oito ossos posicionados usando o algoritmo CCD. A sua suavização é processada renderizando setenta e cinco objetos quadrados inscritos em duas curvas de Bezier. A possível quebra na parte central do caule foi resolvida rotacionando o segundo ponto da segunda curva, cento e oitenta graus a mais que o ângulo do terceiro ponto da primeira curva. O projeto para hello world tem o seu desenvolvimento iniciado com a criação de um projeto usando uma IDE (Integrated Development Environment) de escolha do desenvolvedor. Adiciona se ao projeto os arquivos encontrados nas pastas: Box2D, edk, jpeglib, pugixml e sqlite. Necessita se lincar o uso de bibliotecas externas pré instaladas usando no tutorial de instalação da SFML, tendo os seus nomes encontrados no arquivos LIBS.h na pasta edk. A função principal encontrada na Figura 22 inclui a declaração da janela, seguida de sua criação, setamento da cor de fundo e inclusão da view de renderização. O looping principal é processado enquanto a janela estiver aberta, tendo em seu núcleo as seguintes ações nesta ordem: limpeza do buffer de vídeo, leitura dos eventos de entrada, atualização das view's da janela, tratamento dos eventos, renderização das view's e envio da cena para a janela do sistema operacional. Após a finalização do loop, o ponteiro da view é removido da janela. Como visto na Figura 23, a RenderView herda os parâmetros da edk::viewgu2d reimplementando as funções virtuais para: carregamento da cena adicionando o texto no fontmap e reposicionando a câmera, remoção de cena deletando o texto criado e renderização da cena desenhando a
32 mensagem Hello EDK World. A Figura 24 apresenta a imagem da execução do projeto. Figura 22. Função principal
33 Figura 23. Classe RenderView
34 Figura 24. Execução do projeto Hello World A renderização de formas geométricas na cena em duas dimensões é processada pela classe edk::object2d utilizando a função draw(). Cada objeto pode receber n malhas, e cada malha disponibiliza a renderização de n polígonos. A Figura 25 apresenta a ViewGU2D usada na apresentação da cena. A criação da malha pelo obj1 provê ao objeto, o cargo de gerenciador da alocação da classe na memória sendo necessário apenas a sua remoção na função de unload(). Os polígonos adicionados à malha são copiados pela função addpolygon() sendo removidos em seu destrutor. A animação do mapa UV dos polígonos necessita ser criada no polígono. As interpolações são adicionadas de acordo do segundos na animação. O carregamento da textura fica a cargo do material disponível na malha. Utiliza se a mesma malha nos dois objetos. As animações UV podem ser iniciadas pelo objeto, não necessitando guardar o ponteiro da classe de animação. Após a criação da malha, os objetos são posicionados e escalados na cena. A atualização das animações e renderização são processados pelas funções update() e drawscene() na View.
35 Figura 25. Criação e renderização de Polígonos
36 A Figura 26 apresenta a construção de um programa usado para testar a animação por grupo de interpolações. A herança de classes vista na Figura 16 possibilita a utilização das mesmas funções, com diferenças em seus atributos, para ambos objetos em uma, duas ou três dimensões. A função addnewinterpolationline() adiciona quadros chaves recebendo o tempo em segundos, e os valores de x y e z. Após a criação da interpolação, modifica se a interpolação nos eixos y e z para serem processados usando uma curva de bezier. A animação tem o seu início através do uso da função playforward() modificando as características de processamento para à incrementação do clock interno. O looping de animação atualiza o clock e escreve os valores enquanto o teste de reprodução estiver retornando verdadeiro. Os valores retornados na execução da aplicação são apresentados na Figura 27. Os eixos y e z representam interpolações por curva, o eixo x mostra os valores processados em uma interpolação por linha reta. Figura 26. Utilização dos objetos de animação
37 Figura 27. Resultado da animação Como visto na Figura 29, a leitura do arquivo BVH faz se através da função loadbvh() possuindo três versões para conversão de arquivos em três dimensões para ossos em duas dimensões. A classe de ossos em duas dimensões possibilita o carregamento nos eixos XY, XZ e YZ. Adiciona se animação por nome limitando o ciclo de animação. Os objetos são conectados ao osso recebendo o nome do osso por parâmetro incrementando a posição, rotação e escala do objeto. A sua atualização necessita do processamento das animações e reposicionamento dos objetos conectados. A cinemática inversa pode ser calculada pela função calculateinversekinematic() recebendo por parâmetro o nome do osso, a posição no mundo, quantidade de ossos conectados e quantidade de repetições do algoritmo. A classe para carregamento de áudio utiliza a SFML para leitura e reprodução do arquivo. O código encontrado na Figura 28 abre o arquivo little town
38 orchestral.ogg[45] setando o seu volume para 50%. Enquanto a música for reproduzida, renderiza se o segundo da animação esperando pela pressão da tecla Esc. A finalização do software acarreta na finalização da reprodução e fechamento do arquivo. Figura 28. Reprodução de áudio Os objetos físicos são similares aos objetos renderizados, sendo adicionado uma única malha física e características como: objeto estático, cinemático e dinâmico. O programa apresentado na Figura 30 adiciona dois objetos ao mundo físico sendo eles: um piso estático e uma caixa dinâmica usada para interagir com o piso. Ambos os objetos são renderizados usando a mesma malha, mas cada malha física necessita possuir o seu próprio retângulo. O valor da restituição no polígono físico da caixa é modificado para simular o movimento similar à uma bola de basquete. Após o posicionamento dos objetos, os mesmos são adicionados ao mundo físico usando seus ponteiros. A simulação possui a duração de um segundo sendo processada trinta vezes dentro do loop. O próximo passo é calculado de acordo com o clock do processador pela classe World2D. No final de sua execução, os objetos são removidos do mundo. O retorno processado pelo software pode ser visto da Figura 31.
39 Figura 29. Manipulação de ossos
40 Figura 30. Exemplo do uso de mundo físico
41 Figura 31. Posição e rotação da caixa A Figura 32 apresenta a criação e renderização do tilemap2d. Constrói se o tileset através do carregamento das texturas dividindo a em partes iguais. A criação do tile animado é feita pelas funções de divisão do mapa UV em quadros e a adição das interpolações. A atualização das animações dos tiles pode ser feita por código, ou atualizando todos de uma vez. O tilemap renderiza os tiles usando do ponteiro do tileset.