Programacao BREW - Teoria e Olá Mundo

KurtWylde
0
1 - Introdução

Demorou mas saiu, finalmente o primeiro tutorial voltado a programadores que querem
começar a programar para o Zeebo (ou qualquer plataforma que se use BREW).

Este tutorial é uma continuação do "Instalação e Configuração do kit de desenvolvimento
de jogos para plataforma Zeebo"
, e é aconselhável dar uma lida novamente nele para refrescar
a memória sobre a criação de projetos e configuração dos diretórios para os games.

Este tutorial tem como intuito ensinar um pouco sobre a BREW e como criar o mínimo necessário de código para se criar um aplicativo.
É pre requisito saber pelo menos o básico sobre C e/ou C++.


ATENÇÃO:
Para facilitar e agilizar o aprendizado sobre BREW, sugiro que você deixe sempre visível no
Visual Studio uma janela chamada "Code Definition", que pode ser visualizada indo no menu
View -> Code Definition Window ( Ctrl + Shift + V ).
Esta janela exibe onde foi definido o código que estiver sobre o cursor. Com isso, quando você
parar o cursor em um trecho de código da BREW, ela irá mostrar em que arquivo a definição foi feita
e exibirá o conteúdo do arquivo, muito útil para aprender mais sobre a BREW. Isso aumenta a velocidade
de aprendizado pois, quando o cursor estiver sobre uma função de alguma interface por exemplo, você poderá
ver todas as outras funções que a interface tem, evitando ter que parar toda hora para consultar o manual da BREW.


2 - Como funciona a BREW

BREW é um sistema cooperativo e se utiliza de uma idéia bem simples, também usada na implementação COM da Microsoft: factory e interfaces.
Pode-se dizer que o sistema é orientado a objetos, apesar de ser escrito totalmente em C, mas para entender
isso é necessário entrar um pouco mais o baixo nível da criação de uma classe em C++:

Uma classe, quando tem algum método virtual, contém uma tabela de ponteiro para funções, ou seja, a classe em C++:

Código:

class Teste
{
public:
    virtual ExibeTexto( const char* pTexto ) = 0;
};

é equivalente a estrutura em C:

Código:

typedef struct
{
    void ( *ExibeTexto )( const char* pTexto );
}
TesteVtbl;  // Vtbl = abreviação de virtual table

struct Teste
{
    TesteVtbl*  Vtbl;
};

typedef struct Teste Teste;

Quando a classe é instanciada, os ponteiros para as funções virtuais são iniciados com o endereço da função
referente aquela classe, então se uma classe é extendida e o método virtual é sobreescrito, uma instância da classe base
conterá um endereço de função diferente do ponteiro da função da classe derivada.

Exemplificando, uma classe derivada:

Código:

class Derivada : public Teste
{
public:
    virtual void EscreveTexto2( const char *pTexto, int numChars ) = 0;
};

é equivalente a estrutura em C:

Código:

typedef struct
{
    // Interface base
    void ( *ExibeTexto )( const char* pTexto );
    // Derivada
    void ( *ExibeTexto2 )( const char* pTexto, int numChars );
}
DerivadaVtbl;

struct Derivada
{
    DerivadaVtbl*  Vtbl;
};

typedef struct Derivada Derivada;

Pensando agora somente em interfaces, sabemos que elas são classes abstratas, sem construtor nem destrutor, apenas métodos virtuais públicos puros
que devem ser sobreescritos por uma classe concreta. Como então ela pode ser criada e destruída?
Aí que entra o design pattern factory.

Uma factory é uma classe ou função que é capaz de "fabricar" uma instância de uma classe concreta que implementa os métodos
públicos definidos pela interface. Normalmente quando uma fabrica é uma classe, ela é criada usando o design singleton.
Vamos usar a forma mais simples, uma função, para exemplificar a criação de uma instância de uma interface.

Em C++:

Código:

// Implementa a interface Teste
class TesteImpl : public Teste
{
public:
    void EscreveTexto( const char* pTexto )
    {
        if( pTexto )
        {
            printf( pTexto );
        }
    }
};

// Cria uma instância da interface Teste
Teste*  TESTE_CriaInstancia( void )
{
    return ( new TesteImpl );
}

Em C:

Código:

// Implementação do método virtual
static void EscreveTexto( const char* pTexto )
{
    if( pTexto )
    {
        printf( pTexto );
    }
}

// Cria uma instância da interface/estrutura Teste
Teste* TESTE_CriaInstancia( void )
{
    Teste* teste = ( Teste* ) malloc( sizeof( Teste ) );

    if( teste != NULL )
    {
        teste->Vtbl = ( TesteVtbl* ) malloc( sizeof( TesteVtbl ) );

        if( teste->Vtbl != NULL )
        {
            // Inicia tabela de funções virtuais com endereço de funções válidas:
            teste->Vtbl->EscreveTexto = &EscreveTexto;
        }
        else
        {
            free( teste );
            teste = NULL;
        }
    }
    return teste;
}

Do ponto de vista de quem usa a interface, a única diferença seria que, quando compilando o sistema em
C, o acesso as funções devem ser feitas manualmente através da tabela de funções...
Para que isso seja transparente, uma simples definição pode ser feita:

Código:

#if defined( __cplusplus )
#define  TESTE_EscreveTexto( p, t )  p->EscreveTexto( t )
#else
#define  TESTE_EscreveTexto( p, t )  p->Vtbl->EscreveTexto( t )
#endif

Usando sempre a definição criada, a aplicação acessa os métodos de forma transparente
e compatível entre projetos em C e C++ ( um exemplo dessa forma de programação pode ser encontrada
nos cabeçalhos do SDK do DirectX da Microsoft ).


Você pode estar se perguntando: para quê tudo isso?!


Bom, normalmente, as interfaces/objetos são adicionados ao sistema gradualmente, com updates e upgrades,
então é sensato pensar em construí-los separadamente, em DLLs por exemplo.
Dessa forma, podemos carregar na RAM apenas as DLLs que realmente estiverem em uso, apenas uma vez só,
e gerenciar a quantidade de referencias que temos a ela, ou seja, carregamos o código da interface na RAM
uma vez e gerenciamos quantas instâncias dela temos em tempo de execução. Quando a quantidade de referências
chegar a zero, ela não está mais sendo usada por nenhuma aplicação, tornando possível removê-la da RAM para
liberar espaço.

Para esse gerenciamento, o método adotado foi incluir 2 métodos na interface base, AddRef e Release. Cada vez
que uma nova instância é criada e mapeada as funções corretas, o contador de referências é incrementado. Quando
uma interface não é mais utilizada, chamamos Release para que ela decremente seu contador de referências e
descarregue a interface da memória se a contagem chegar em zero.

Se existir alguma dúvida sobre isso, sugiro reler o tópico e fazer alguns experimentos até que essa forma
de funcionamento esteja bem claro, caso contrário, você não terá um entendimento mais aprofundado do que
está acontecendo quando entrarmos na programação em BREW.

O sistema de interface adotado na BREW pode ser encontrada nos arquivos AEEInterface.h, AEEIBase.h e AEE.h,
e o seu funcionamento é igual ao explicado aqui, tirando algumas peculiaridades ou formas diferentes de se
chegar ao mesmo resultado.


3 - Aplicativos em BREW - a estrutura AEEApplet

Nossos programas em BREW são derivados de uma 'classe' base, chamada AEEApplet.
Ela tem a tabela de funções da interface IApplet, além de atributos úteis para nossa aplicação.
Quando eu digo derivada, é porque a nossa aplicação vai conter todos os dados da nossa instância em
uma única estrutura, que tem como primeiro atributo uma variável do tipo AEEApplet.
E daí?
E daí que com isso, vamos pedir para a fábrica de applets gerar uma instância da interface IApplet,
mas passando o tamanho da nossa estrutura e um ponteiro para a nossa função de tratamento de eventos,
fazendo a estrutura gerada ser maior, sendo necessário apenas fazer um cast de IApplet para nossa estrutura para acessar todas as nossas variáveis!


Em código isso seria o seguinte:

Código:

typedef struct
{
    AEEApplet a;
    AEEDeviceInfo  sDeviceInfo;

    /* Add suas variáveis aqui */
    uint32  dPosicaoX;
    uint32  dPosicaoY;
    IBitmap* pJogadorBmp[ NUM_FRAMES ];
    /* ... */
}
Game;

Note que apesar de em C/C++ nós acessarmos a estrutura AEEApplet via variável a, internamente essa estrutura é o mesmo que:

Código:

typedef struct
{
    /* AEEApplet a; expandida abaixo */

    IAppletVtbl*  vtbl;

    AEECLSID      clsID;
    uint32        m_nRefs;
    IShell    *    m_pIShell;
    IModule  *    m_pIModule;
    IDisplay  *    m_pIDisplay;
    AEEHANDLER    pAppHandleEvent;
    PFNFREEAPPDATA pFreeAppData;

    /* Não expandida aqui pq é gigante -.- */

    AEEDeviceInfo  sDeviceInfo;

    /* Add suas variáveis aqui */

    uint32  dPosicaoX;
    uint32  dPosicaoY;
    IBitmap* pJogadorBmp0;
    IBitmap* pJogadorBmp1;
    IBitmap* pJogadorBmp2;
    IBitmap* pJogadorBmp3;
    IBitmap* pJogadorBmp4;
    IBitmap* pJogadorBmp5;
    /* IBitmap* pJogadorBmp ...  NUM_FRAMES vezes  */
}
Game;

Provando assim que 'derivamos' da classe IApplet (ou AEEApplet se quiser Wink )


NOTA:
Esta estrutura é onde TODAS as suas variáveis globais ao seu aplicativo devem ficar, pois em BREW não existe variável global! Nunca declare uma variável global, pois vai funcionar no simulador,
mas não vai compilar em um compilador para celular.

NOTA2:
Zeebo NÃO tem suporte a ponto flutuante, ou seja, simularemos frações usando números inteiros ;D

NOTA3:
NADA NUNCA NEM SE ATREVA A USAR LAÇOS INFINITOS DE REPETIÇÃO OU MUITO DEMORADOS!
BREW é um sistema cooperativo, o que significa que temos que colaborar com os outros aplicativos que estiverem rodando também, deixando o sistema operacional gerenciar tudo e abrir a boca apenas quando ele quiser que a gente trate alguma mensagem =)

NOTA4:
SEMPRE faça casts explícitos para evitar problemas se um dia for compilar seu game para o Zeebo real.
Preste sempre atenção no tipo das variáveis, tanto quando compilando para o simulador quanto para um target, pois a maioria das variáveis tem 32 bits de tamanho no simulador para facilitar o alinhamento dos dados, quando no target original ela tem apenas 8 bits...

NOTA5:
Zeebo é big endian. PC é little endian, o que significa que se você ler um int de um arquivo,
ele estará com os Bytes em ordem invertida, podendo causar várias dores de cabeça!


4 - Olá mundo BREW


Como vamos conseguir instanciar uma aplicação nossa? Como vamos saber que realmente é a nossa aplicação que foi instanciada?

Toda interface na BREW tem um identificador único, que é definido no arquivo .bid.
A nossa aplicação não é exceção, e isso foi explicado no tutorial anterior.

Toda implementação também deve criar uma fábrica, mas deve seguir a seguinte assinatura:

Código:

int AEEClsCreateInstance( AEECLSID dId, IShell* pIShell, IModule* pMod, void** ppObj );

AEECLSID (aee class identifier) é o identificador da interface que o sistema está pedindo para instanciar.
Se este valor for diferente do qual nós informamos no arquivo .bid, simplesmente retornamos erro e fingimos que não sabemos de nada Wink.

Se o valor estiver correto, devemos alocar memória para nossa instância, mapear a tabela da IApplet para as funções corretas, adquirir uma instância da interface de gerenciamento do display (IDisplay) e registrar nossa rotina para finalização do applet e rotina de tratamento de eventos para sermos notificados sobre novos eventos enquanto nosso aplicativo estiver com o foco:

Código:

int AEEClsCreateInstance( AEECLSID dId, IShell* pIShell, IModule* pMod, void** ppObj )
{
    if( AEEApplet_New( sizeof( Game ), dId, pIShell, pMod, ( IApplet** )ppObj, ( AEEHANDLER )GAME_TrataEvento, ( PFNFREEAPPDATA )GAME_Finaliza ) )
    {
        return AEE_SUCCESS;
    }
    return EFAILED;
}

Ahhh tá bom vai, eu quis assustar um pouquinho =P
A função AEEApplet_New faz todo o trabalho sujo para nós, e está disponível no arquivo AEEAppGen.c para os curiosos de plantão verem como ela faz toda a inicialização.

As funções GAME_TrataEvento e GAME_Finaliza serão discutidas a seguir:

Para termos a chance de liberar a memória utilizada durante a execução do nosso programa, podemos, não obrigatoriamente, registrar uma função para tal, sem retorno e com um único parâmetro que receberá a instância da aplicação que está sendo finalizada

Código:

void FreeAppData( IApplet* pIApplet );

No exemplo da criação da instância eu nomeei esta rotina de GAME_Finaliza.
Lembre-se que sempre que o parâmetro vindo do sistema for uma instância da interface IApplet, significa que esta instância na realidade é um ponteiro da nossa estrutura, podendo ser convertida com um cast sem medo.


Para ouvirmos os eventos do sistema registramos a rotina que eu nomeei no exemplo acima como GAME_TrataEvento, que tem o formato

Código:

boolean AppHandler( void* pData, AEEEvent evt, uint16 wParam, uint32 lParam );

Devemos retornar TRUE se tratarmos o evento e FALSE se não tratarmos, para que o sistema saiba quando ele mesmo deve tratar o evento ou não.
A lista de eventos possíveis está no arquivo AEEEvent.h.
Os eventos que devem ser tratados no mínimo incluem EVT_APP_START, EVT_APP_STOP, EVT_APP_NO_SLEEP, EVT_APP_SUSPEND E EVT_APP_RESUME:

Código:


/*
    Rotina de tratamento dos eventos do game.
    Todo game para BREW deve conter essa rotina para tratar
    os eventos gerados pelo sistema operacional, como
    tecla pressionada, tecla solta, etc.

    Se o evento for tratado, retorne TRUE para avisar
    o sistema operacional que você tratou o evento.
*/
boolean GAME_TrataEvento( Game* app, AEEEvent dEvento, uint16 wParam, uint32 dParam )
{
    switch( dEvento )
    {
        /* Evento recebido quando criamos nossa instância.
          Devemos carregar os dados necessários para rodar o game aqui
        */
        case EVT_APP_START:
        {
            if( GAME_Inicia( app ) == FALSE )
            {
                ISHELL_CloseApplet( app->a.m_pIShell, FALSE );
            }

            /* Escreve um olá mundo e atualiza a tela.
              É realmente aconselhável que todos tentem estudar e entender como funcionam
              as interfaces que mais utilizaremos como a IDisplay, IShell, IFileMgr, IBitmap e IDIB.
            */
            IDISPLAY_ClearScreen( app->a.m_pIDisplay );
            IDISPLAY_SetColor( app->a.m_pIDisplay, CLR_USER_TEXT, 0 );
            IDISPLAY_DrawText( app->a.m_pIDisplay, AEE_FONT_NORMAL, L"Olá mundo BREW", -1, 0, 0, NULL, 0 );
            IDISPLAY_Update( app->a.m_pIDisplay );

            return TRUE;
        }
        /* Finalizamos? se sim retornamos verdadeiro */
        case EVT_APP_STOP:
        {
            return TRUE;
        }
        /* Diz q não queremos que o sistema entre em sleep mode */
        case EVT_APP_NO_SLEEP:
        {
            return TRUE;
        }
        /* Nossa aplicação não vai tratar suspend e resume, vai simplesmente
          finalizar caso isso ocorra */
        case EVT_APP_SUSPEND:
        case EVT_APP_RESUME:
        {
            ISHELL_CloseApplet( app->a.m_pIShell, FALSE );
            return TRUE;
        }
        default:
        {
        }
    }
    return FALSE;
}

No arquivo AEEEvent.h tem a lista de todos os eventos possiveis e o que esperar nos seus parâmetros (wParam e dParam).


Com isso encerramos esta segunda parte de uma série sobre programação para o Zeebo.

O próximo artigo terá a explicação detalhada da inicialização do OpenGL ES em cima da inicialização do nosso aplicativo em BREW.

Mario

Postar um comentário

0Comentários

Postar um comentário (0)