Olá! Sou Vladimir Popov, desenvolvedor cliente do projeto War Robots. No momento em que este artigo foi escrito, os War Robots já existiam há vários anos e dezenas de novos mechs apareceram no jogo durante esse período. Naturalmente, as variadas habilidades dos robôs são importantes, pois, sem elas, os robôs perdem a sua singularidade o que torna o jogo mais interessante.
Neste post, compartilharei como funciona o sistema de habilidades de jogo em nosso jogo – e como ele evoluiu. E, para tornar as coisas mais acessíveis, explicarei as coisas em termos simples e sem muitos detalhes técnicos.
Primeiro, vamos mergulhar no histórico do projeto e observar uma implementação mais antiga que não está mais sendo usada.
Anteriormente, as habilidades eram projetadas de uma forma muito trivial: elas tinham um componente que era anexado ao robô. Esta foi uma construção onde o programador descreveu detalhadamente como a habilidade funciona: tanto seu fluxo quanto como ela interage com outras habilidades. Toda a lógica é descrita dentro de um componente, e o designer do jogo pode simplesmente anexá-lo ao robô e configurar os parâmetros conforme necessário. Vale ressaltar que não foi possível alterar o fluxo de habilidades – os designers do jogo só puderam alterar parâmetros e tempos.
Uma habilidade antiga só poderia existir em dois estados: ativo e inativo, e cada estado poderia ter sua ação atribuída a ela.
Vejamos um exemplo da habilidade “Jammer”, que o robô “Stalker” possuía anteriormente; funcionou assim:
Durante muito tempo, esta funcionalidade foi suficiente para nós, mas com o tempo tanto os designers de jogos como os programadores já não estavam satisfeitos com esta abordagem: era difícil para os programadores suportar estas capacidades porque o código se tinha tornado monstruoso; isto envolvia uma cadeia de legados muito longa, onde cada situação tinha que ser descrita. Além disso, faltava flexibilidade aos designers de jogos – para fazer qualquer alteração em uma habilidade, eles tinham que solicitar modificações aos programadores, mesmo que uma habilidade adjacente tivesse a mesma funcionalidade.
Então, percebemos que algo precisava mudar. Portanto, desenvolvemos um novo sistema, onde cada habilidade era representada como um conjunto de diversos objetos relacionados. A funcionalidade foi dividida em habilidades de estado e componentes de estado.
Como funciona? Toda habilidade tem um objetivo principal. Este objeto central conecta outros objetos de habilidade com o mundo exterior e vice-versa; também toma todas as decisões principais.
Pode haver qualquer número de estados. Essencialmente, o estado nesta iteração não é muito diferente dos estados ativo/inativo da versão antiga, mas agora pode haver qualquer número deles e seu propósito se tornou mais abstrato. Notaremos que uma habilidade só pode ter um estado ativo por vez.
A principal inovação em relação ao sistema antigo foram os componentes: um componente descreve alguma ação e cada estado pode ter qualquer número de componentes.
Como funcionam as novas habilidades? Uma habilidade só pode estar em um dos estados; o objeto principal é responsável por trocá-los. Os componentes que se vinculam a um estado reagem à ativação/desativação do estado e, dependendo disso, podem começar a realizar alguma ação ou parar de executá-la.
Todos os objetos tornaram-se personalizáveis; um designer de jogos pode misturar estados e componentes como desejar, compondo assim uma nova habilidade a partir de blocos pré-instalados. Agora, os programadores só precisam entrar na imagem para criar um novo componente ou estado, o que torna a escrita de código muito mais fácil: eles trabalham com pequenas entidades, descrevem alguns elementos simples e não constroem mais a habilidade sozinhos – os designers de jogos fazem isso agora.
O fluxo ficou assim:
Posteriormente, este procedimento é repetido continuamente. Para facilitar o uso, um estado não serve apenas como um contêiner de componentes, mas também determina quando mudar para outro estado e solicita que o objeto principal faça a mudança. Com o tempo, isso ainda não foi suficiente para nós, e o diagrama de habilidades foi transformado no seguinte:
O objeto principal, o estado e os componentes permaneceram em seus lugares, mas novos elementos também foram adicionados.
A primeira coisa que chama a atenção é que adicionamos condições a cada estado e componente: para os estados, elas definem requisitos adicionais para sair do estado; para componentes, eles determinam se o componente pode executar sua ação.
O contêiner de cobrança contém cobranças, recarrega-as, interrompe a recarga se necessário e fornece cobranças para uso dos estados.
Um temporizador é usado quando vários estados devem ter um tempo de execução comum, mas seu próprio tempo de execução não está definido.
É importante observar que todos os objetos de habilidade são opcionais. Tecnicamente, para poder funcionar, são necessários apenas um objeto principal e um estado.
Agora, embora não existam tantas habilidades inteiramente construídas sem o envolvimento dos programadores, o desenvolvimento em geral tornou-se visivelmente mais barato, porque os programadores agora só precisam escrever coisas muito pequenas: por exemplo, um novo estado ou dois componentes – o resto é reutilizado.
Vamos resumir os componentes de nossas habilidades:
O objeto principal executa as funções de uma máquina de estados. Ele fornece aos estados e componentes informações sobre o mundo e fornece ao mundo informações sobre habilidades. O objeto principal serve como um elo entre estados, componentes e partes de serviço da capacidade: cargas e temporizadores externos.
O estado escuta os comandos de ativação e desativação do objeto principal e, consequentemente, ativa e desativa componentes, e também solicita que o objeto principal mude para outro estado. O estado determina quando é necessário mudar para o próximo; para isso, utiliza sua condição interna: se o jogador clicou no botão de habilidade, se passou um certo tempo desde a ativação do estado, e assim por diante, e condições externas ligadas ao estado.
O componente escuta comandos de ativação e desativação do estado e executa alguma ação: discreta ou de longo prazo. As ações podem ser completamente diferentes: podem causar dano, curar um aliado, ativar uma animação e assim por diante.
A condição verifica em que estado o elemento desejado está e relata isso ao estado ou componente. As condições podem ser complexas. Um estado não solicita uma transição para outro estado se a condição não for atendida. O componente também não executa uma ação se a condição não for atendida. As condições são uma entidade opcional; nem toda habilidade as possui.
O contêiner de cobrança retém as cobranças, recarrega-as, interrompe a recarga quando necessário e fornece cobranças aos estados. É usado em habilidades de múltiplas cargas, quando é necessário permitir que o jogador o use várias vezes, mas não mais do que n vezes seguidas.
O cronômetro é utilizado quando vários estados têm uma duração comum, mas não se sabe quanto tempo durará cada um deles. Qualquer estado pode iniciar um cronômetro por n segundos. Todos os estados relevantes assinam o evento de término do cronômetro e fazem algo quando ele termina.
Agora vamos voltar ao diagrama de habilidades. Como sua funcionalidade mudou?
Os estados podem usar cobranças como condição adicional de transição. Se tal transição ocorrer, o número de cobranças diminui. Os estados também podem usar um cronômetro comum. Neste caso, o tempo total para sua execução será determinado por um cronômetro, e cada estado individualmente pode durar qualquer tempo.
Não reinventamos completamente a roda para as novas interfaces de habilidade.
O objeto principal possui sua própria UI. Ele define alguns elementos que devem estar sempre na UI e que não dependem do estado atualmente ativo.
Cada estado tem seu próprio par na UI, e a UI do estado é exibida somente quando seu estado está ativo. Ele recebe dados sobre seu estado e pode exibi-los de uma forma ou de outra. Por exemplo, os estados de duração geralmente têm uma barra e um texto na interface do usuário que exibe o tempo restante.
No caso em que o estado está aguardando um comando externo para continuar uma habilidade, sua UI exibe um botão e pressioná-lo envia o comando para o estado.
Veremos como as habilidades funcionam usando exemplos específicos; primeiro, vamos dar uma olhada em um robô chamado “Inquisidor”. Temos quatro estados que se sucedem – acima dos estados você pode ver sua exibição na IU. Para dois deles, vemos também os componentes que lhes pertencem; os outros dois estados simplesmente não possuem componentes.
Aqui está o fluxo da habilidade:
Tudo começa com o estado “WaitForClick”. Neste momento, a habilidade não faz nada; apenas espera por comandos.
Assim que tal comando é recebido, o objeto principal muda de estado. O próximo estado ativo é “WaitForGrounded”.
Este estado possui alguns componentes e, portanto, quando ativado, o robô salta e reproduz um som e uma animação. Entre outras coisas, enquanto o estado está ativo, o robô é afetado pelo efeito Jammer, que proíbe mirar no robô.
Quando o robô pousa, sua habilidade passa para o próximo estado.
Este estado possui três componentes: o já familiar Sound e Jammer, além do Shake, que faz a câmera tremer para todos os jogadores em um raio de n .
Como este estado tem Duration , ele funciona por n segundos, então a habilidade passa para o próximo estado.
O último estado também vem com uma Duração, mas não possui nenhum componente: está em um tempo de espera normal.
Após a conclusão, a habilidade retorna ao primeiro estado.
Outro exemplo é “Fantasma”. É muito parecido com o Inquisidor, mas existem algumas nuances:
Começamos com WaitForClick.
Em seguida, a Duração, na qual o teletransporte é instalado, as estatísticas do mech são alteradas e o som e a animação são reproduzidos.
Depois disso: DurationOrClick, no qual as estatísticas do mech são alteradas, a animação e os efeitos são reproduzidos.
Se for feito um clique, passamos para outra Duração, na qual o mech se teletransporta, as estatísticas mudam e a animação, efeitos e sons são reproduzidos.
Após esse estado (ou após o tempo expirar para DurationOrClick), passamos para Duration.
A principal diferença aqui é que vemos estados com ramificação: DurationOrClick vai para o estado A se o tempo especificado tiver passado, ou para o estado B se o jogador já pressionou o botão de habilidade.
Embora pareça que o nosso sistema evoluiu de algo simples para algo bastante complexo, esta mudança simplificou a vida tanto dos programadores como dos designers de jogos. A assistência dos programadores é agora necessária principalmente ao adicionar pequenos componentes, enquanto o último grupo de membros da equipa ganhou maior autonomia e pode agora reunir de forma independente novas capacidades a partir de estados e componentes existentes. Como outro bônus, ao mesmo tempo, os jogadores também receberam lucro na forma de habilidades mais diversas e complexas dos mechs.