Projeto e estrutura de vídeo games

Apesar de existirem muitos tipos diferentes de vídeo games, existem algumas propriedades que são constantes: Todos eles requerem ao menos um jogador, todos oferecem ao jogador ao menos um desafio, todos usam uma tela, todos tem ao menos um método de entrada de dados/controle.

A Interface com o usuário

Como descrito anteriormente, a interface com o usuário é composta de sprites, menus e assim em diante. É o que o usuário tem para controlar as suas ações dentro do jogo. Esses gráficos são definidos como botões que podem ser presionados, ou um personagem que pode ser movido pelas setas de direção do teclado. Todos esses elementos são parte da interface com o usuário.

O Menu principal

Ao ser iniciado, todo jogo tem um menu principal. Este menu normalmente é um tela com algum tipo de fundom como um arranjo de botões para ações como iniciar um novo jogo ou um jogo existente, opções e sair do jogo.

Essa tela atua como um painel de controle para o jogo, permitindo que o jogador altere configurações, escolha modos de jogo, ou acesse o jogo. Algumas vezes, um jogo usará o menu principal como o menu do jogo. Esse tipo de menu é normalmente acessado pressionando a tecla Esc ou o botão Start durante o curso do jogo. O menu do jogo permite que o jogador acesse a maioria das ações do menu principal mais algumas adicionais como exibir  estatísticas de um personagem, pontos, inventário e assim em diante. Nem todos os menus tem que serem quadrados com palavras neles. O jogo “Secret of Mana” usa um criativo menu onde o nível permanece em foco enquanto as escolhas formam um circulo em torno do jogador.

Esses menus não são obrigatórios, mas tradicionalmente são incluídos em todos os jogos.

Iniciando o jogo

Quando você inicia o jogo uma série de telas de abertura são mostradas. Uma tela de abertura contém elementos como logos, filmes, e assim em diante. Elas são frequentemente usadas para informar ao jogador as companhias que contribuíram para que o jogo e algumas vezes fornece parte ou todas as instruções para o jogo.

Quando o jogo é iniciado frequentemente é exibido um filme introdutório que fornece um prólogo para a história do jogo. Esse não é um filme do tipo que você vê no cinema mas normalmente é uma renderização melhor do próprio jogo e de seus sons.

Em muitos jogos, será solicitado que você informe um nome e em outros será permitido que você personalize seu personagem, configurações e assim em diante. Esse estágio do jogo é chamado tutorial. Não é sempre considerado parte da história do jogo, mas em alguns jogos está tão integrado ao jogo que o estágio do tutorial é mesmo parte do jogo de qualquer forma. Chamamos isso de integração do tutorial. É largamente usado em jogos como The Legend of Zelda e Super Mario 64.

Jogando o jogo

Durante o curso do jogo, existem alguns conceitos básicos que todos os jogos usam. Eles são listados abaixo:

Relacionamento entre o Jogador e o personagem

A função do jogador é controlar o personagem. Como o jogador controla esse personagem? Existem normalmente três tipos de controle: terceira pessoa, primeira pessoa e influência.

  • terceira pessoa: o jogador não é o personagem mas ao invés disso controla o personagem impessoalmente.
  • primeira pessoa: o jogador é o personagem – e vê o mundo a sua volta do ponto de vista do personagem tanto pessoalmente quanto visualmente.
  • influência: o jogador não está ligado a nenhum personagem mas meramente tem uma influência no jogo. Isto é visto em jogos comoTetris,mas pode ser visto em RTS como Majestry.

O Ambiente

Qual o “mundo” que é retratado pelo jogo? Aqui, temos duas considerações a fazer:

Função do personagem: Existe a questão de qual a função que o personagem tem no jogo, que podem ser de três tipos.

  1. Protagonista: Tudo gira em torno do personagem salvar o dia. Pode ser visto em jogos como Zelda, Mario, Final Fantasy, dentro outros.
  2. Arcádico Convencional: Um personagem impessoal de arcade.
  3. Influenciador: O personagem é uma influência sem face dentro do jogo.

Leis: Quais são as leis, conceitos, regras, etc que definem o ambiente?

  1. Gráficos: O que é visto e como é visto (estilo visual).
  2. Som: O que é escutado e como é escutado (estilo sonoro).
  3. Jogabilidade: Como o jogo é jogado.

Salvamento/Carregamento

Normalmente, considera-se o salvamento e carregamento de um jogo como uma ação básica do menu, onde o jogador digita um nome e o jogo é salvo. Em alguns jogos, porém, abordagens mais criativas são usadas de forma que o jogador não precisa interromper a partida para salvar o jogo. O Metroid, por exemplo, faz isso com estações de salvamento. Carregamento, porém, é normalmente uma ação de menu.

O Loop principal

No coração de nosso jogo, está o loop principal. Como muitos programas interativos, nosso jogo é executado até que receba a ordem de parar. Cada ciclo do loop é como o batimento do coração do jogo. O loop principal de um jogo em tempo real é frequentemente vinculado a atualização do vídeo (vsync). Se nosso loop principal for sincronizado um evento de hardware baseado no tempo, como um vsync, então precisamos manter o tempo de processamento para cada atualização nesse intervalos ou nosso jogo pode “explodir”.

// a simple game loop in C++

int main( int argc, char* argv[] )
{
    game our_game;
    while ( our_game.is_running())
    {
        our_game.update();
    }
    return our_game.exit_code();
}

Cada fabricante de console tem seu próprio padrão para a publicação de jogos, mas a maioria requer que o jogo forneça um feedback visual  nos primeiros segundos de inicialização. Como regra de geral de design, é desejável dar ao jogador feedback o mais rápido possível.

Por essa razão, a maioria do código de inicialização ou encerramento é normalmente processado do loop principal. Se esse código for muito longo, pode ou rodar em uma sub-thread monitorada da função update() principal ou fatiada em pequenos pedaços e executados em ordem dentro da própria rotina update().

Máquina de estados

Mesmo sem considerar os vários modos de jogo dentro do próprio jogo, muitos códigos de jogos pertencerão a um de muitos estados. Um jogo pode conter os seguintes estados e sub-estados:

  • start up (Inicialização)
  • licenses (Licenças)
  • introductory movie (Filme introdutório)
  • front end (Tela inicial)
    • game options (Opções de jogo)
    • sound options (Opções de som)
    • video options (Opções de vídeo)
  • loading screen (Tela de carregamento)
  • main game (Jogo)
    • introduction (Introdução)
    • game play (Jogabilidade)
      • game modes (Modos de jogo)
    • pause options (Opções durante a pausa)
  • end game movie (Filme de final de jogo)
  • credits (Créditos)1
  • shut down (Encerramento)

Uma maneira de modelar isso em código é com uma máquina de estados:

class state
{
public:
    virtual void enter( void )= 0;
    virtual void update( void )= 0;
    virtual void leave( void )= 0;
};

Classes derivadas podem sobrecarregar essas funções virtuais para fornecer códigos específicos para os estados. O objeto do jogo pode então apontar um ponteiro para o estado atual e permitir que o jogo flua de um estado para outro.

extern state* shut_down;

class game
{
    state* current_state;
public:
    game( state* initial_state ): current_state( initial_state )
    {
        current_state->enter();
    }

    ~game()
    {
        current_state->leave();
    }

    void change_state( state* new_state )
    {
        current_state->leave();
        current_state= new_state;
        current_state->enter();
    }

    void update( void )
    {
        current_state->update();
    }

    bool is_running( void ) const
    {
        return current_state != shut_down;
    }
};

Tempo

Um loop de jogo precisa considerar tanto quando tempo real e quanto tempo de jogo passou. Separar os dois permite criar efeitos de câmera lenta (como BullerTime), estados de pausa e depuração de forma muito mais fácil. Se você pretende criar um jogo que permita retroceder no tempo, como Blinx ou Sands of Time, precisará ser capaz de executar o loop do jogo para frente enquanto retrocede no tempo do jogo.

Outra consideração sobre o tempo depende de você querer usar um frame rate fixo ou variável.  Um frame rate fixo pode simplificar muito a matemática e a contagem de tempo no jogo mas tornam o jogo mais difícil de portar internacionalmente (exemplo, passar dos 60 Hz da TV nos EUA para os 50 Hz das TVs da Europa). Por essa razão, é aconselhável usar um frame rate variável mesmo o valor nunca mudar.

Frame rate fixo sofrem de balbuciações quando a carga de trabalho por frame alcança os limites e isso pode ser pior do que um frame rate baixo. Frame rate variável, por outro lado, compensam automaticamente as frequências de TVs diferentes. Mas rates variáveis frequentemente parecem mais pesados em comparação com jogos que usam um frame rate fixo. A depuração, particularmente a depuração de problema de tempo e da física, é normalmente mais difícil  com tempo variável. Quando implementar tempo em seu código, existem frequentemente vários timers de hardware disponíveis para uma plataforma, frequentemente com resoluções diferentes, custos para acessá-los e latências. Dê atenção especial para os relógios de tempo real disponíveis. Você precisa usar um relógio com uma resolução alta o suficiente, enquanto não usa uma precisão excessiva. Você pode ter que lidar com casos onde a contagem do relógio é finalizada (por exemplo, um timer de nanosegundos de 32 bits irá estourar de volta para zero a cada 2^32 nanosegundos, o que é apenas 4.2949673 segundos).

const float game::NTSC_interval= 1.f / 59.94f;
const float game::PAL_interval=  1.f / 50.f;

float game::frame_interval( void )
{
    if ( time_system() == FIXED_RATE )
    {
        if ( region() == NTSC )
        {
            return NTSC_interval;
        }
        else
        {
            return PAL_interval;
        }
    }
    else
    {
        float current_time= get_system_time();
        float interval= current_time - last_time;
        last_time= current_time;
        if ( interval < 0.f || interval > MAX_interval )
        {
            return MAX_interval;
        }
        else
        {
            return interval;
        }
    }
}

void game::update( void )
{
    current_state->update( frame_interval());
}

Carregando

Jogos modernos são normalmente carregados ou diretamente do CD ou indiretamente do disco rígido. De qualquer forma, seu jogo pode gastar uma quantidade significativa de tempo em acesso a Entrada/Saída. Acesso a disco, especialmente a CD e DVD, é um bocado mais lento do que o resto do jogo. Muitos fabricantes de console estabelecem que todo o acesso a disco deve ser indicado visualmente; e isso não é uma má escolha de projeto, de qualquer forma.

Porém, muitas das funções da API de acesso a disco (particularmente aquelas que são mapeadas através das bibliotecas de entrada/saída do C) prendem o processador até que a transferência esteja finalizada. Isso é chamado de acesso síncrono.

Acesso a disco multi-thread

Uma forma de obter um feedback enquanto estiver acessando o disco é executar operações com disco em sua própria thread. Isso tem a vantagem de permitir que outras threads continuem a ser processadas, incluindo o desenho de algum feedback visual da operação com o disco. Mas o custo disso é que haverá um bocado de código para escrever e acesso a recursos necessários para a sincronização.

Acesso a disco assíncrono

Alguns APIs dos sistemas operacionais dos consoles manipulam alguns dos códigos de multi-thread para você através da permissão de acesso ao disco  de ser programado com operações de leitura assíncrona. Leituras assíncronas podem informar o que está sendo feito ou mantendo um canal de comunicação com o manipulador do arquivo ou por uma função de callback.

Renderizando objetos

Independente de um jogo usar gráficos 2D, 3D ou uma combinação de ambos, o motor do jogo deve manipula-los de forma similar. Existem três coisas importantes a considerar. Certos objetos podem demorar um pouco para carregar, e podem momentaneamente congelar o jogo. Algumas máquinas rodam de forma lenta que outras, e a jogabilidade precisa ser a mesma com um frame rate mais baixo. Algumas máquinas rodam de forma mais rápida, e a animação pode ser mais suave do que o intervalo de tempo em um frame rate maior.

Assim, é uma boa idéia criar uma classe base com uma interface que separe essas funções. Dessa forma, cada objeto que precisa ser desenhado pode ser tratado da mesma forma, e todo o carregamento pode ser feito ao mesmo tempo (por telas de carregamento), e todo desenho pode ser feito independentemente do intervalo de tempo. O OpenGL requer também que  lista de exibição de objetos tenham um identificador único, assim também precisaremos de suprote para associação desse valor.

class IDrawable
{
public:
    virtual void load( void ) {};
    virtual void draw( void ) {};
    virtual void step( void ) {};

    int listID()            {return m_list_id;}
    void setListID(int id)  {m_list_id = id;}
protected:
    int m_list_id;
};

Caixas delimitadoras

Uma método comum de detectar colisões é pelo uso de caixas delimitadoras alinhadas pelos eixos. Para implementar isso, construiremos sobre nossa interface anterior, IDrawable. Deve permanecer separada de IDrawable, porque apesar de tudo, nem todos os objetos que são desenhados na tela requerem detecção de colisões. Uma caixa 3D deve ser definida por seis valores: x, y, z, width, height e depth. A caixa deve também retornar os valores minimo e máximo do objeto no espaço. Abaixo segue um exemplo de uma classe para uma caixa delimitadora 3D:

class IBox : public IDrawable {
    public:
        IBox();
        IBox(CVector loc, CVector size);
        ~IBox();
        float X()       {return m_loc.X();}
        float XMin()    {return m_loc.X() - m_width / 2.;}
        float XMax()    {return m_loc.X() + m_width / 2.;}
        float Y()       {return m_loc.Y();}
        float YMin()    {return m_loc.Y() - m_height / 2.;}
        float YMax()    {return m_loc.Y() + m_height / 2.;}
        float Z()       {return m_loc.Z();}
        float ZMin()    {return m_loc.Z() - m_depth / 2.;}
        float ZMax()    {return m_loc.Z() + m_depth / 2.;}
    protected:
        float m_x, m_y, m_z;
        float m_width, m_height, m_depth;
};

IBox::IBox() {
        m_x = m_y = m_z = 0;
        m_width = m_height = m_depth = 0;
}

IBox::IBox(CVector loc, CVector size) {
        m_x      = loc.X();
        m_y      = loc.Y();
        m_z      = loc.Z();
        m_width  = size.X();
        m_height = size.Y();
        m_depth  = size.Z();
}

Criando o seu próprio engine de jogo

Complexidade

Enquanto é bastante simples na maioria das APIs exibir uma imagem, ou um cubo texturizado, a medida que você começa a adicionar mais complexidade a seu jogo, a tarefa começa a se tornar cada vez mais difícil. Com um motor pobremente estruturado essa complexidade de torna muito maior a medida que ele vai crescendo em tamanho. Pode se tornar obscuro quais mudanças são necessárias, e você pode acabar com um grande bloco case onde algumas abstrações simples teriam simplificado o problema.

Extensibilidade

Esse ponto se amarra com o citada acima – a medida que o seu motor evolui, você irá desejar adicionar novos recursos. Com um motor desestruturado, esses novos recursos são difíceis de serem adicionados, e um bocado de tempo pode ser gasto encontrando o porquê o recurso não está funcionando como esperado. Talvez alguma função estranha esteja interrompendo-o. Um motor cuidadosamente criado separa tarefas de tal forma que estender uma área em particular é só isso – e não é necessário modificar código não relacionado.

Conheça o seu código

Com um projeto de motor bem pensado, você começará a conhecer o seu código. Você se achará gastando menos tempo encarando uma tela em branco imaginando o porquê de seu código não estar fazendo o que você achou que tinha dito para ele fazer.

DRY

DRY é um acrônimo frequentemente usado (especialmente no ambiente de programação extrema) que significa Don´t Repeat Yourself (Não de repita). Soa simples mas pode fornecer a você muito tempo para cuidar de outras coisas. Além disso, o código que faz uma tarefa específica está em um local centralizado de forma que você pode modificar essa pequena seção e ver as mudanças surtirem efeito em qualquer lugar.

De fato, é senso comum

Os pontos acima provavelmente não parecem assim tão surpreendentes para você – eles realmente são bem senso comum. Mas sem o planejamento necessário no projeto do motor do jogo, você descobrirá que atingir esses alvos muito difícil.

Fonte: Wikibooks