quinta-feira, 3 de janeiro de 2008

Pilha de telas

Oi povo, desculpem a demora para responder, final do ano é sempre uma correria, e no trabalho não é diferente. Estou de volta com novos posts dicas e informações.

Sei que falei que iria falar da classe BaseGame da engine, mas decidi passar pra algo mais pratico. Várias pessoas nas ultimas semanas vieram conversar comigo sobre gerenciamento de telas em engines de jogos. Aqui na Tecnodata, desenvolvemos uma solução bem elegante e de conceito simples - uma pilha.

A engine tem, internamente, uma pilha controlada por um gerenciador de telas. Esse gerenciador contem os metodos de push() e pop(), assim como metodos para acessar a tela no topo da pilha ( topScreen() ), e delegar as chamadas dos metodos Update() e Draw() à tela mais visivel. Para que isso seja viável, uma tela é um conceito encapsulado em uma classe Tela. Vamos ver um exemplo de código de como isso poderia funcionar em C++. O esqueleto da classe Tela:
class Tela
{
public:
virtual void update(dword msTime) = 0;
virtual void draw(dword msTime) = 0;
};

A classe Tela é bem simples, basicamente apenas declarando uma interface minima que todas as telas devem ter para que possam ser gerenciadas. (nota: o tipo de dados dword é simplesmente um valor 32bits sem sinal - ou seja - um unsigned int numa maquina 32bits).

Agora vamos ver o esqueleto do gerenciador:

class GerenciadorTelas
{
private:
stack _pilha;

public:
// construtor/destrutor omitidos

void push(Tela *tela);
Tela* pop();
Tela* telaAtual();

void update(dword msTime);
void draw(dword msTime);
};
Como eu tinha dito, o gerenciador internamente trabalha com uma pilha de telas. Para adicionar uma nova tela visivel no jogo, basta criar o objeto dessa tela (ie. Tela *t = new TelaQualquer() ) e adicionar na pilha. Desse jeito, no proximo frame da engine, a tela irá trocar automaticamente da tela anterior para a nova, pois estará chamando update e draw na nova tela.

O interessante é que, como estamos falando de pilha, a tela anterior ainda existe, entao voltar para a tela anterior é tao simples quanto dar um pop() no gerenciador. Isso garante que voce possa ter uma hierarquia de telas no seu jogo faceis de gerenciar e navegar.

O exemplo mostrado aqui é extremamente simples, apenas para demonstrar a idea básica. Porém, no nosso sistema, telas incluem máquina de estados, serialização e transparência (caso uma tela tenha uma transparência inferior a 1.0, a tela abaixo dela na pilha também é visivel e, portanto, também recebe chamadas a update() e draw() ). O gerenciador de telas não apenas gerencia a pilha, como também tem opções específicas para transição entre-telas (não apenas troca), acesso a telas por índice e por tipo (não apenas ao topo da pilha), e tem todos seus métodos como virtuais, possibilitando sua extensão fácil caso necessário.

Leitura extra

Um artigo bem interessante sobre organização de telas em classes pode ser lido neste post do blog Ponto V. Nao deixem de visitar.

segunda-feira, 12 de novembro de 2007

Biblioteca Tecnodata3D.Utility - Classe Global

Opa! gente bacana,

Aqui estou entao mais uma vez para explicar a nossa engine, e começaremos pela assembly básica - Utility.

Como expliquei antes, a Utility contem classes que representam a base da engine, assim como várias classes de apoio. Vamos começar por ententer alguns conceitos

O que é um singleton?

Um singleton é uma classe da qual existe apenas uma instancia dela em toda a vida do projeto. Na nossa engine, poucas classes sao singletons, de bom exemplo a classe Global. Nesta classe ficam agregados os varios modulos da Utility necessarios para o uso da engine. Ela tem uma estrutura bem simples:

Tecnodata3D.Utility.Global
- private static Global _default;
- private BaseGame _game;
- private List _listModules;
- public AssetManager AssetManager { get(); set(); }
- public Camera Camera { get(); set(); }
- public static Global Default { get(); }
- public BaseGame Game { get(); }
- public Input Input { get(); set(); }
- public Log Log { get(); set(); }
- public ScreenManager { get(); set(); }
- public UserProfileManager UserProfileManager { get(); set(); }
- private Global ();
- public static void CreateInstance(BaseGame game);
- public void Initialize();
- public void AddModule ( AModule oModule );
- public AModule GetModule ( string sModule );
- public void ReplaceModule ( AModule oModule );
- public void RemoveModule ( AModule oModule );
- public void Update ( GameTime gameTime );
- public void Draw ( GameTime gameTime );

Como falei anteriormente, para usar a engine, deve-se criar uma classe de jogo que herda a classe BaseGame. A propria BaseGame se preocupa em criar a instancia de Global, entao nao existe necessidade de se preocupar com isso. Esta classe, entao, é uma interface global para uso da biblioteca Utility da engine - atravez dela temos acesso aos modulos principais, alem de possibilidade de criar e adicionar novos modulos, ou substituir os padrao por outros (por exemplo, voce pode criar uma classe CameraRTS, herdando-a da classe Camera, e utilizar do metodo ReplaceModule() para substituir a camera padrao pela customizada).

Isso nos dá uma poderosa biblioteca básica para usar para a maioria dos projetos (por exemplo, Camera vem já com 4 tipos de camera diferentes), e caso exista necessidade de substituir ou extender essas funcionalidades, temos a flexibilidade de o fazer facilmente.

Mas como usar a Global? É simples, em qualquer local do projeto, basta acessar a propriedade estática Default, a qual guarda a instancia "singleton" da classe Global. Entao, acessamos facilmente a Camera da biblioteca através do formato Global.Default.Camera. Embora possamos acessar qualquer módulo existente na Global atraves do metodo GetModule(), criamos algumas propriedades para facilitar a digitação para acesso a alguns modulos mais utilizados (internamente eles usam o metodo GetModule).

E como é a organização interna da Global?

Internamente, todos os módulos sao guardados numa lista, obedecendo a uma ordem definida por uma propriedade OrderID da classe abstrata AModule. Se um módulo necessita ser executado antes de outro, basta alterar sua propria OrderID antes de ser adicionado na Global. Por padrao, todos os módulos que tem uma propriedade de facil acesso já sao criados e adicionados na Global pela BaseGame.

Embora seja a BaseGame que recebe primeiramente as chamadas a Update() e Draw(), elas sao repassadas imediatamente para Global, que trata de iterar por todos os modulos em ordem correta, repassando assim as chamadas a cada um dos módulos. Este sistema é extremamente eficiente e simples de trabalhar - e remove a necessidade de chamar individualmente Update() e Draw() para os modulos necessarios - tudo é cuidado automaticamente pela engine.

E é isso aí gente - como tinha dito, a Global é uma classe bem simples, porém facilita muito a vida na hora de acessar dados globais do projeto. Próximo post estarei explicando detalhadamente o funcionamento da classe BaseGame - que é o coração da parte Utility da engine.

Abraço e até mais!

quarta-feira, 7 de novembro de 2007

Estrutura da engine Tecnodata3D

A engine tem uma estrutura simples, porém bem flexivel.

Divimos a engine em varias assemblies, sendo atualmente as seguintes:

- Tecnodata3D.Utility
Engloba metodos e classes básicas necessárias ao funcionamento da engine. Nela se encontram singletons como a Global ( disponibiliza uma interface geral de acesso a todos os subsistemas da utility ), Input, Camera, AssetManager (gerencia o conteúdo do jogo), Log, ScreenManager (gerencia as telas do jogo) e UserProfileManager. Possui classes que representam entidades, luzes, coleções (ex. Pair), perfis de usuário e configurações, exceções, efeitos e módulos. Além de tudo isso, inclui também a classe BaseGame que é necessário herdar para a criação de um aplicativo usando a engine;

- Tecnodata3D.Additional
Inclui vários componentes como contador de FPS, capturador de imagem de tela, e telas padrão de jogo (explicarei esta parte em mais detalhe num novo post);

- Tecnodata3D.ContentPipeline
Todas as classes necessárias para a criação de tipos próprios de dados, como por exemplo nossa T3DModel e Terrain;

- Tecnodata3D.Physics
Toda a lógica de mundo físico é definida aqui nesta assembly. Por hora estamos utilizando a biblioteca Newton para simulação de física, mas nossa estrutura de classes é flexivel ao ponto de nos permitir utilizar outras bibliotecas, ou mesmo criar uma lógica do zero;

- Tecnodata3D.Renderer
Aqui se encontram as classes responsáveis pelo desenho 2D e 3D na tela, incluindo um SceneManager (que utiliza de um SceneGraph internamente para controle de cena). Inclui também facilidades pra criação de interfaces gráficas 2D, e controle de cursor de mouse;

Esta estrutura nos possibilitou uma fácil visualização do escopo da engine, e uma flexibilidade enorme na hora de alterar e adicionar módulos de engine. Eu estarei explicando mais detalhadamente cada uma das assemblies em posts seguintes - então até breve.

Model do XNA = Martelo

Depois de considerar bastante as opções, decidimos por criar nossas próprias classes de modelo no XNA, em detrimento da classe Model padrão do framework. Aqui listo algumas das razões que nos levaram a tomar essa decisão:

- Suporte em tempo de compilação para geração do nosso próprio modelo de objeto físico - bounding box, sphere ou mesh;
- Nosso próprio formato de Vertices - isto é crucial para alguns dos shaders mais avançados que planejamos utilizar (por exemplo, bump mapping);
- Como criamos uma classe própria de shader, o modelo pode vir já do content pipeline com um efeito apropriado, e não o padrao BasicEffect;
- A classe Model padrão não inclui informações de animação, e utilizar a propriedade Tag para armazenamento desses dados não nos pareceu uma opção elegante;
- Extensibilidade - usando um modelo próprio, temos a possibilidade de extender nosso código com novas funcionalidades, sem necessidade de alterar (fora algumas exceções) a caixa preta que define nosso sistema de modelos, meshes, shaders e animações;

Por enquanto estamos em fase de planejar essas classes novas ( a engine está, por hora, baseada na Model padrão), assim como entender claramente o funcionamento do content pipeline. Em breve terei uma versão funcional que estarei explicando detalhadamente aqui neste blog.

Próximo post vou explicar um pouco sobre a estrutura da engine Tecnodata3D, e algumas das decisões que tomamos e porquê.

Tecnodata CFC

Como primeiro post, eu queria explicar um pouco sobre o meu trabalho aqui na Tecnodata.

A Tecnodata é uma empresa especializada em educação de trânsito, onde eu trabalho no setor de TI, mais especificamente no setor de desenvolvimento de software interativo e jogos educativos.

Neste blog eu irei postar informações e dicas sobre problemas e soluções encontradas no meu dia a dia de trabalho com C++, C# com XNA e Flash CS3. Estarei colocando links para outros blogs de meus colegas que trabalham com algumas tecnologias diferentes tambem (exemplo do Flávio com Flex e Java, ou do Jefferson com ASP.NET).

Espero que com este blog eu possa estar ajudando outras pessoas a superar problemas que eu tenha encontrado, alem de que possam aproveitar positivamente as dicas que planejo postar.