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

Ver o tópico anterior Ver o tópico seguinte Ir em baixo

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

Mensagem por Kabeção em Qua 15 Abr 2015, 21:16

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:
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:
global.onibus = ds_list_create();

Criamos então um script para colocar mensagens no ônibus:
Código:
/// 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:
// 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:
// 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:
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:
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:
// 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:
// 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:
// 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:
// 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!

Kabeção

Ranking : Sem avaliações
Número de Mensagens : 2314
Data de inscrição : 08/06/2008
Reputação : 100
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 3
   : 0
   : 1

http://blackcapapps.blogspot.com.br/

Voltar ao Topo Ir em baixo

Re: Artigo|Discussão - Programação Orientada a Eventos

Mensagem por saim em Qui 16 Abr 2015, 21:48

Eu tinha tentado inventar algo assim, embora bem mais rudimentar. Era pra sincronizar - forçar um ritmo, na verdade - dos sons do jogo.
Um único objeto recebia as informações (tocar_tal_som = true/false) dos demais e marcava o ritmo. Se chegasse no momento de tocar o som e fosse pra tocar, ele tocava. Só uma instância, pra não ter um volume mais alto no caso de vários objetos fazerem a mesma coisa, só no momento certo (se o evento acontecesse no momento errado, a variável ficava true até a hora certa, que era rapidinho o suficiente pra não ficar estranho).
Fora essa situação, não achei que um objeto controlador poderia ter esse tipo de utilidade. Vi que estava errado. A ideia era válida, vou tentar usar nos próximos projetos.

Agora, tem o outro lado da coisa. Um sistema assim pressupõe que eu saiba o que deve acontecer ao longo do jogo, de antemão, não? Tipo, porque criar um sistema-conquistas se eu só vou pensar em conquistas quando o jogo estiver em fase de testes? Porque enviar a mensagem "ocorreu uma colisão com a parede enquanto a personagem estava no ar" se eu só vou perceber que é legal tocar um som nesse evento depois que o beta tester disser que precisava de um feedback ali? Tem situações em que parece que o sistema precisa de tanto planejamento quanto fazer a coisa na unha.

Agora, eu não entendi porque separar as coisas... Porque não criar um objeto-efeitos? Um só objeto recebe as mensagens e faz tudo o que tem que fazer com elas. Abateu 1000 inimigos? Desbloqueia um item, toca o som, ativa um efeito de partículas. Porque separar as coisas? Claro, cada evento ou efeito tem seu script, e um script pode ter diversas ações, como chamar outros scripts, mas tudo dentro do mesmo objeto.

Outra coisa... como esse sistema poderia enviar argumentos para o objeto-efeito? Por exemplo, eu quero que na posição milésimo inimigo caído um efeito de partículas aconteça e uma animação mostrando um fantasma DAQUELE INIMIGO. Eu precisaria enviar, no mínimo, a id do objeto ou, o mais provável, a sprite, a posição em x e em y.
...relendo o tuto, vi que você pensou nisso e colocou as msg[0, 1 e 2]... Mas não entendi como colocar isso na ds_list. Pelo código, parece que está faltando alguma coisa. Você define o valor das células da array "minhaMensagem[n]" e joga na ds_list só o nome da array (sem o índice da célula). Isso não faria que só a primeira célula (índice zero) entrasse na list?
(E, supondo que você passe a array pra list na mesma ordem que a array é elaborada: no caso de 2 objetos passarem mensagens, como saber qual é a mensagem[0] do segundo objeto?)

saim

Ranking : Nota B
Número de Mensagens : 2964
Idade : 38
Data de inscrição : 14/01/2011
Notas recebidas : C-D-A-B
Reputação : 121
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 1
   : 0
   : 3

Voltar ao Topo Ir em baixo

Re: Artigo|Discussão - Programação Orientada a Eventos

Mensagem por Kabeção em Sex 17 Abr 2015, 21:25

Agora, tem o outro lado da coisa. Um sistema assim pressupõe que eu saiba o que deve acontecer ao longo do jogo, de antemão, não? Tipo, porque criar um sistema-conquistas se eu só vou pensar em conquistas quando o jogo estiver em fase de testes? Porque enviar a mensagem "ocorreu uma colisão com a parede enquanto a personagem estava no ar" se eu só vou perceber que é legal tocar um som nesse evento depois que o beta tester disser que precisava de um feedback ali? Tem situações em que parece que o sistema precisa de tanto planejamento quanto fazer a coisa na unha.
É o contrário. Não tem necessidade nem uma de saber o que vai acontecer.
Você simplesmente envia mensagens genéricas quando estiver programando alguma funcionalidade.
Por Exemplo: eu quero tocar um som quando um inimigo morre, então a primeira coisa que faço é mandar os inimigos enviarem a mensagem "inimigoDerrotado" e fazer o sistema de som processar isso.
O significado da mensagem não tem nada a haver com conquistas, criar efeitos ou até mesmo tocar som. É apenas um acontecimento/evento gerado pelos objetos do jogo.
Se vou ou não usar isso para conquistas depois de meses de desenvolvimento não faz diferença.

Já enviar mensagens do tipo "tocarSomDeColisao" é algo muito especifico, não é um evento geral e por isso deveria ser evitado.
No entanto "colisaoComSolidos" poderia ser usado para diversos propósitos.
Essa é a ideia por trás do sistema.

Agora, eu não entendi porque separar as coisas... Porque não criar um objeto-efeitos? Um só objeto recebe as mensagens e faz tudo o que tem que fazer com elas. Abateu 1000 inimigos? Desbloqueia um item, toca o som, ativa um efeito de partículas. Porque separar as coisas? Claro, cada evento ou efeito tem seu script, e um script pode ter diversas ações, como chamar outros scripts, mas tudo dentro do mesmo objeto.
O mesmo motivo para se usar esse padrão de programação: organização.
Tem diversos tipos de sistemas que precisam usar variáveis com tempo de vida longo (contagem de inimigos, por exemplo) ou fazer algo continuamente, por exemplo: um sistema de networking que envia a quantidade de dinheiro do jogar ao receber o evento "dinheiroMudou" mas precisa aguardar uma confirmação do servidor dizendo se correr um erro ou não.
Em outras palavras, apesar de não serem interativos como os objetos normais do jogo, eles ainda podem fazer muito mais do que processar mensagens e executar coisa em um único step.
Misturar funções totalmente diferentes vai apenas te levar ao mesmo problema de antes.

Outra coisa... como esse sistema poderia enviar argumentos para o objeto-efeito? Por exemplo, eu quero que na posição milésimo inimigo caído um efeito de partículas aconteça e uma animação mostrando um fantasma DAQUELE INIMIGO. Eu precisaria enviar, no mínimo, a id do objeto ou, o mais provável, a sprite, a posição em x e em y.
Umas das melhores mudanças no GML em si com o GMS é novo jeito de usar arrays.
Agora elas são como em outras linguagens: ponteiros para endereços de memória.
Você pode passar arrays como argumento ou como um único item para estruturadas de dados:
Código:
arrayA[0] = 2;
arrayA[1] = 5;

list = ds_list_create();
list[|0] = arrayA;   // ou ds_list_add(list, arrayA)

arrayB = list[|0]; // pega o endereço de arrayA
show_message(arrayB[0]) // mostra 2
show_message(arrayB[1]) // mostra 5

arrayB[@0] = 27; // @ muda o conteudo do endereço
show_message(arrayA[0] == arrayB[0]) // true, porque as duas variaveis apontam para o mesmo lugar

arrayB[0] = 58;
show_message(arrayA[0] == arrayB[0]) // false, tirando o @, GMS copia o conteudo de arrayA em arrayB e agora não apontam para o mesmo lugar

// Guarda o endereço de arrays nos indices de outra array
arrayC[0] = arrayA;
arrayC[1] = arrayB;

show_message(string(arrayC[0])); // mostra algo como "{27, 5}"

Com isso você tem a possibilidade de enviar mais 31998 informações extras além dos dois primeiros que são o padrão (id da mensagem e id do objeto que a enviou).
Mas eu aconselho não abusar disso pois a ideia é ser genérico.

Kabeção

Ranking : Sem avaliações
Número de Mensagens : 2314
Data de inscrição : 08/06/2008
Reputação : 100
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 3
   : 0
   : 1

http://blackcapapps.blogspot.com.br/

Voltar ao Topo Ir em baixo

Re: Artigo|Discussão - Programação Orientada a Eventos

Mensagem por Mr.Rafael em Sab 18 Abr 2015, 17:48

Aproveitando o assunto, se alguém tiver curiosidade em saber onde isso é melhor aplicado, podem ver este tutorial que eu fiz, no qual ensina a montar uma biblioteca de jogos em JavaScript.

O princípio é o mesmo: basicamente, você possui um objeto e vários eventos atrelados a ele, nos quais você pode associar uma função que rodará quando tal situação ocorrer. O game loop então se encarrega de ler todos esses objetos e chamar as funções de evento quando for necessário. Smile

A maior vantagem desse sistema é que você só codifica o que for necessário, e tudo faz parte do objeto em si. Tentar fazer a mesma coisa de maneira estruturada é um pesadelo (eu já fiz e é horrível). yes

o/

Mr.Rafael

Ranking : Nota A
Número de Mensagens : 383
Data de inscrição : 05/10/2010
Notas recebidas : A-C-B-A
Reputação : 57
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 0
   : 1
   : 2

Voltar ao Topo Ir em baixo

Re: Artigo|Discussão - Programação Orientada a Eventos

Mensagem por Kabeção em Seg 20 Abr 2015, 20:14

@Mr.Rafael
Esse tipo de coisa é muito usado em linguagens funcionais como Javascript.
No caso, o seu método é um pouco diferente.
No meu exemplo o sistema do jogo processa o que acontece por si só independente dos objetos e de forma global. Já no seu tutorial os objetos ativam eventos que diz respeito a apenas eles mesmos e tem uma função anexada para executar quando isso acontece.
Mas esses dois métodos são normalmente usados juntos.
Em um framework que usei recentemente, por exemplo, objetos ativam eventos globais que são executados por diversos sistemas ao mesmo tempo mas também possuem eventos locais para cara objeto.

Tem até linguagens inteiramente baseadas em mensagens e eventos como por exemplo: Smalltalk.

Kabeção

Ranking : Sem avaliações
Número de Mensagens : 2314
Data de inscrição : 08/06/2008
Reputação : 100
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 3
   : 0
   : 1

http://blackcapapps.blogspot.com.br/

Voltar ao Topo Ir em baixo

Re: Artigo|Discussão - Programação Orientada a Eventos

Mensagem por Conteúdo patrocinado Hoje à(s) 09:39


Conteúdo patrocinado


Voltar ao Topo Ir em baixo

Ver o tópico anterior Ver o tópico seguinte Voltar ao Topo

- Tópicos similares

 
Permissão deste fórum:
Você não pode responder aos tópicos neste fórum