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
};
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();
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);
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);
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;
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;
}
}
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;
}
}
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;
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);
}
Código: Selecionar todos
// objSistemaConquista
updateScriptId = script_conquista_update;
// objSistemaAudio
updateScriptId = script_audio_update;
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!