Página 1 de 1

Artigo/Discussão - Programação Orientada a Eventos

Enviado: Sex Abr 29, 2016 11:36 am
por Tutoriais & Aulas
Autor original: Kabeção
Programação Orientada a Eventos

Os inimigos dependem do código do jogador, o som dependem do código dos inimigos e aquele super complexo sistema que faz algo muito legal dependem de código espalhado por todo o jogo!
Esse tipo de situação tornou-se comum por causa do jeito que o GameMaker funciona e tem sido usado ao longo dos tempos.
Todo esse código misturado, além de embaralhar seu cérebro, torna o jogo cada vez mais difícil de se programar, corrigir bugs ou fazer melhorias.
Esse artigo discuti sobre um padrão de programação muito útil para resolver esse problema e é direcionado a aqueles já com um nível avançado no GMS.

O problema

Todos as instâncias dos inimigos precisam tocar sons de passos, tocar outro som quando morrerem e criar partículas de poeira enquanto se movem.
O jogador precisa fazer o mesmo que os inimigos além de desenhar um balão de fala quando se aproximar em um NPC e ativar uma conquista quando matar 1000 deles, caminhar 10000 passos ou finalizar uma fase.
E todos os chefões precisam ativar uma conquista específica quando derrotados.

No começo tudo ia bem e todas essas coisas foram programadas em seus respectivos objetos.
Um tempo depois percebi que as partículas deviam ser removidas enquanto testo o jogo no mobile, os sons de passos deveriam tocar apenas se o volume for maior que 0 e estiverem perto do jogador, o balão de fala aparecer só quando o jogador apertar um botão, tocar uma música e enviar uma mensagem para meu servidor toda a hora que ele ativar uma conquista.

Para isso eu tenho que editar todos os objetos, procurar em que parte do código elas estão, corrigir diversos bugs e perder muito com tudo isso só para fazer ajustes!

A solução

Se cada sistema fosse totalmente independente dos diversos objetos que meu jogo possui, por exemplo, um objSystemSound cujo ó unico objetivo é tocar sons; um simples "if (volume > 0)" envolta do código desse objeto resolveria meu problema em questão de segundos.
Da mesma forma, se as conquistas fossem ativadas por um único objeto, programa-lo para tocar uma música e enviar uma mensagem para o servidor toda vez que uma nova conquista for adquirida seriam um problema trivial.
Cada objeto especial (sistema) só precisaria ter uma única instância durante todo o jogo.

Todos os objetos deveriam enviar uma ordem a esses sistemas sempre que precisassem ativar/criar algo, ou ainda melhor, os sistemas deveriam saber o que fazer por si só sempre que um objeto executar uma ação.

Mas como implementar isso?
Como os sistema iram saber o que fazer se são os objetos que executam ações?
A solução é simples: enviar uma mensagem falando sobre o que o objeto fez para todos os sistemas e deixa-los decidir se vão ou não responder a aquela ação.

Message Bus

Vamos especificar um id para cada mensagem usando um enum.
A primeira sera "inimigoDerrotado".

Código: Selecionar todos

enum mensagemId {
    inimigoDerrotado
};
Vamos chamar de ônibus aquele que transporta nossas mensagens aos sistemas.
Ele deve ser uma ds_list pois pode conter diversas mensagens e global pois todos os sistemas devem ter acesso ao ônibus.

Código: Selecionar todos

global.onibus = ds_list_create();
Criamos então um script para colocar mensagens no ônibus:

Código: Selecionar todos

/// enviar_mensagem(mensagem)
// mensagem (array) - 0 - mensagem id
//                          - 1 - objeto que enviou mensagem
//                          - 2 - opções extras

ds_list_add(global.onibus, argument0);
O jogador então, derrota um inimigo.
Uma ação aconteceu, é hora de enviar uma mensagem aos sistemas!

Código: Selecionar todos

// No Destroy Event do inimigo

minhaMensagem[0] = mensagemId.inimigoDerrotado; // tipo da mensagem
minhaMensagem[1] = id; // objeto que enviou a mensagem
minhaMensagem[2] = tipo; // se é um inimigo normal ou chefão

enviar_mensagem(minhaMensagem);
Não temos nem um código para ativar uma conquista, tocar um som ou seja o que for.
Apenas enviamos uma mensagem genérica dizendo que um inimigo foi derrotado.
Agora é a vez dos sistemas olharem essa mensagem é decidir se vão ou não fazer algo.

Para o objSistemaConsquita, a mensagem "inimigoDerrotado" é importante e precisa ser processada.
Esse sistema deve guardar quando inimigos foram derrotados:

Código: Selecionar todos

// Create
contagemDeInimigos = 0;
E então aumentar essa variável toda vez que "inimigoDerrotado" estiver no ônibus.
Quanto a contagem for 1000 a conquista deve ser ativada:

Código: Selecionar todos

var mensagens = global.onibus;
var quantidade = ds_list_size(mensagens);

for (i = 0; i < quantidade ; ++i)
{
    msg = ds_list_find_value(mensagens, i); // pegar mensagem
    
    // Executar ação apenas para mensagem que interessam ao sistema
    switch (msg[0]) // olhe qual é a mensagem
    {
        case mensagemId.inimigoDerrotado:
            ++contagemDeInimigos;
            
            if (contagemDeInimigos = 1000) ativar_conquista(conquistaId.milDerrotados);
        break;
    }
}
Pronto!
Não importa quando, onde ou qual inimigo foi derrotado.
Eu poderia desativar totalmente o sistema de conquistas usando um "if (ativado == true)" em volta desse bloco de código.
Eu poderia até mesmo deletar o objeto do sistema e meu jogo funcionaria perfeitamente sem nem uma edição.

2 meses depois, decido que um som deve ser tocado quando um inimigo é derrotado.
Para isso, tudo o que tenho que fazer é ir no objSistemaAudio e programar:

Código: Selecionar todos

var mensagens = global.onibus;
var quantidade = ds_list_size(mensagens);

for (i = 0; i < quantidade ; ++i)
{
    msg = ds_list_find_value(mensagens, i); // pegar mensagem
    
    // Executar ação apenas para mensagem que interessam ao sistema
    switch (msg[0]) // olhe qual é a mensagem
    {
        case mensagemId.inimigoDerrotado:
            if (msg[2] == 1) // olhe a informação extra
                tocarSom(sndInimigoChefaoDerrotado);
            else
                tocarSom(sndInimigoNormalDerrotado);
        break;
    }
}
Novamente, mudanças nesse objeto são totalmente independentes do resto do jogo e funcionaria para todos os inimigo.

Funcionamento do ônibus

Para o ônibus funcionar propriamente é necessário fazer as coisa em uma ordem clara, por exemplo:
Todos os objetos devem enviar mensagens no Step Event, todos os sistema devem processar mensagens no Step End e o ônibus deve ser limpo depois que passar por todos os sistemas ao completar um step de jogo.

Indo Além e Aprimorando

E se os sistemas pudessem enviar mensagens?
Eu preciso seguir uma ordem para o ônibus funcionar mas se o sistema de conquistas enviasse uma mensagem do tipo "conquistaAtivada" e o sistema de audio tiver que tocar uma música quando isso acontecer?

Código: Selecionar todos

// objSistemaConquista
case mensagemId.inimigoDerrotado:
    ++contagemDeInimigos;
    
    if (contagemDeInimigos > 1000) {
        ativar_conquista_milDerrotados();

        minhaMensagem[0] = mensagemId.conquistaAtivada;
        minhaMensagem[1] = id;
        minhaMensagem[2] = conquistaId.milDerrotados; // o indice 2 agora pode carregar informação opicional
    
        enviar_mensagem(minhaMensagem);
    }
break;

Código: Selecionar todos

// objSistemaAudio
case mensagemId.conquistaAtivada:
    tocarMusica(sndConquistaBGM);
break;
Se o sistema de audio executar primeiro, a mensagem do sistema de conquistas seria perdida.

Para isso funcionar você pode fazer as mensagens serem executas assim que enviadas:

Código: Selecionar todos

// enviar_mensagem(mensagem)

// Todos os sistemas são parentes de do objeto parSistema
with (parSistema) {
    script_execute(updateScriptId, argument0);
}
updateScriptId representa o script responsável por processar as mensagens daquele sistemas, por exemplo:

Código: Selecionar todos

// objSistemaConquista
updateScriptId = script_conquista_update;
// objSistemaAudio
updateScriptId = script_audio_update;
Tem muitos outros métodos como por exemplo fazer a função enviar_mensagem(mensagem) enviar cópias das mensagem localmente para cada sistema ao invés de um ônibus global.

Conclusão

Desconectar uma coisa da outra e faze-las reagir automaticamente a um acontecimento independente de quem o provocou torna seu projeto muito mais fácil de se programar, organizar e fazer mudanças.
Uma simples mensagem definida de forma genérica pode fazer diversas coisas acontecerem ou absolutamente nada dependendo de quem estiver interessado nela.

Imagine algo como um inventário.
Isso parece bem complicado de se fazer, no entanto se o inventário enviasse mensagens como "celulaClicada", fazer partículas aparecerem, tocar um som, mudar a cor de um ícone, ativar uma habilidade, ativar uma conquista ou atualizar o banco de dados no servidor não te faria editar todo o projeto de novo e de novo.
Você nem precisaria se preocupar com coisas que não afetam diretamente a jogabilidade como efeitos sonoros e visuais e deixar tudo isso para quando o inventário estiver funcionando bem ou no final do projeto.

Esse método é praticamente obrigatório para jogos online onde o servidor deve controlar grande parte o jogo pois nunca se sabe quando uma mensagem vai chegar ao cliente e por isso, tudo deveria funcionar sem dependências entre sistemas.

Essa é uma das soluções que tenho usado para resolver problemas de organização de código e facilitar estender o que o já foi feito.

Sugestões, dúvidas ou tem algo a acrescentar?
Poste um comentário!