top of page
  • Foto do escritorGabriel A. Pereira

Os Benefícios de uma Arquitetura com ScriptableObjects no Unity


ScriptableObject architecture in Unity

Na altura em que comecei a aprender game design de forma independente, uma das primeiras questões com que me deparei foi: como se podem articular de forma eficaz e modular todos os sistemas que constituem um videojogo?


Quando se fala em sistemas, fala-se obviamente dos componentes individuais que operam em simultâneo para oferecer aquilo que é a experiência jogável. Por exemplo, os elementos típicos que encontramos num jogo de plataformas, estilo Super Mario, serão: o protagonista, os inimigos, os obstáculos, os colecionáveis...

No meio disto, advém a necessidade de comunicar ao jogador o resultado das suas ações, algo que acontece tipicamente mediante a apresentação de informações no ecrã através da HUD (heads up display), ou interface, quer com recurso a textos, quer a ícones, gráficos ou outros elementos visuais. Cada um destes estará ligado a um sistema nuclear que o controla: os comummente designados Managers. Assim sendo, eles são independentes uns dos outros... a interface não precisa de saber da existência do protagonista, no entanto precisa de mostrar informações sobre, por exemplo, o estado da sua saúde, ou quantas moedas este colecionou; quando o jogador chega a um checkpoint, este tem que, de alguma forma, comunicar essa informação a um sistema dedicado para que, caso o jogador perca uma vida, o protagonista possa reaparecer naquele ponto.

Mas como é que isto pode ser feito sem "quebrar" o jogo, ao mesmo tempo que se assegura a possibilidade de expansão e adição de novos elementos, evitando a repetição de passos triviais?


Um dos padrões no qual eu me tinha sustentado para interligar sistemas, no inicio da minha aprendizagem, foram os Singletons. Adorado por alguns, odiado por muitos, este parece ser um dos padrões mais arcaicos do desenvolvimento de videojogos, baseando-se na criação de um script encarregue de controlar determinado sistema, e no qual é inserida uma função genérica que permite que todos os outros componentes do jogo possam ter acesso ao mesmo através de código, sem ser necessária uma referência direta pré-atribuída. Quer isto dizer que, para atualizar a barra de saúde do protagonista na interface quando este sofre algum tipo de dano, eu teria apenas que escrever «UIManager.Instance.UpdateHealth(5)», onde "Instance" seria o termo global para referenciar a única instância desse componente existente na atual cena (ou nível).

No entanto, este padrão acarreta uma série de contratempos. Em primeiro lugar, requer que um objeto com o componente em questão exista na cena, caso contrário será criado automaticamente assim que um script lhe fizer referência. Isto terá que ser assegurado em TODAS as cenas do jogo, exigindo, da parte dos designers, certificar-se de que a instância realmente está presente e devidamente ajustada.

Mas porquê recorrer a um método tão enfadonho e repetitivo para incorporar um sistema global, quando, afinal, existem alternativas que proporcionam um fluxo de desenvolvimento muito mais modular?


Perto de setembro de 2020, a equipa do Unity lançou uma iniciativa intitulada Unity Open Projects, cujo objetivo foi convidar a comunidade a participar na criação de um videojogo de raíz, juntamente com alguns elementos destacados da empresa. Desde o processo de concetualização, à finalização e lançamento, a proposta foi de partilhar o projeto com a comunidade em código aberto, através da plataforma GitHub, para que todos os interessados, independentemente do nível de experiência, pudessem de alguma forma contribuir, ou no mínimo aprender com o processo de desenvolvimento do projeto.

Assim que me deparei com esta iniciativa, a ideia de aprender mediante o acesso livre a uma arquitetura de jogo desenvolvida por profissionais de diversos níveis de experiência entusiasmou-me bastante, dado que proporcionaria a expansão do meu próprio conhecimento. Um dos primeiros temas debatidos nos fóruns de discussão relativos ao projeto foi o estabelecimento de um método para sedimentar a arquitetura do jogo. Não tardou em surgir, como seria de esperar, a ideia dos controversos Singletons por parte de alguns participantes. No entanto, a equipa do Unity propôs a ideia de basear os sistemas do jogo e a comunicação entre eles em ScriptableObjects, e a proposta foi unanimemente aceite.


Mas o que são ScriptableObjects?

O termo em si é bastante explicativo. Sendo que o Unity se baseia numa arquitetura voltada para o objeto, os ScriptableObjects não são mais do que pequenos excertos de código desenhados para suportar um comportamento isolado, condensado num objeto, o que permite que estes possam ser ajustados através da exposição de parâmetros, ao mesmo tempo que permitem a referenciação por outros objetos. Digamos que eles são como peças de Lego que podem ser livremente modificadas por quem as quiser utilizar, oferecendo uma ampla gama de ligações que evitam a recorrência constante ao código.

Sim, um ScriptableObject tem que ser programado de antemão. No entanto, ele pode ser criado e armazenado nas pastas do projeto para que designers os utilizem livremente a seu gosto, dentro das potencialidades a este inerentes.

Trata-se de um elemento que, na minha opinião, só deverá ser introduzido a quem já tenha um sólido conhecimento dos conceitos básicos gerais que constituem o design de jogos em Unity, e que esteja minimamente familiarizado com o funcionamento da engine. Não conto que alguém os utilize no seu primeiro projeto amador. Contudo, assim que descobrimos as suas potencialidades - e falo por experiência própria - os ScriptableObjects vêm abrir uma nova porta de oportunidades no longo e árduo processo de desenvolvimento de um videojogo, não só cobrindo lacunas a nível de estruturação, como também evitando dores de cabeça e processos de design morosos.


Voltando ao Open Project lançado pela Unity, uma das primeiras coisas que cativou a minha atenção quando abri o projeto e comecei a explorar o que já tinha sido feito por algumas contribuições foi a implementação de um sistema de eventos baseado em ScriptableObjects. Os eventos são um dos patamares fundamentais do game design para criar sistemas independentes, no entanto capacitados para o envio e receção de mensagens que desencadeiam reações em resposta às mesmas.

A produção de eventos no processo de desenvolvimento de um videojogo requer alguma planificação de antemão, já que estes podem apresentar alguns problemas. Por exemplo, é preciso assegurar que, no que toca a ordem de execução, eventos não sejam invocados antes da criação dos objetos que a estes necessitam de subscrever, para evitar cenários em que a mensagem difundida não é recebida.

Com a recorrência a ScriptableObjects, podemos sempre programar um sistema de eventos modular capaz de ser referenciado diretamente no inspetor de objetos do Unity. Assim, o evento em ScriptableObject será o intermediário que liga a instância que envia mensagens àquelas que a querem receber, sem que seja necessário estabelecer referências diretas entre tais instâncias.

ScriptableObjects event architecture in Unity
Demonstração da implementação de um sistema de eventos baseado em ScriptableObjects

Portanto, reformulando a questão da transmissão de informações sobre atualizações de saúde do protagonista, um evento em forma de ScriptableObject seria uma alternativa eficiente ao Singleton. Assim, o protagonista não precisaria de referenciar diretamente o UI Manager a partir do código; neste caso, ele apenas teria que referenciar eventos como OnPlayerDamaged, OnPlayerHealed, ou OnPlayerDied, aos quais o UI Manager estaria subscrito e se encarregaria de atualizar a imagem da barra de vida cada vez que recebesse uma mensagem.


Com a abertura e liberdade criativa que os ScriptableObjects oferecem, tenho procurado cada vez mais introduzi-los nos meus projetos para diversos fins que outrora se apresentavam como uma barreira. Para além dos mencionados eventos, uma das funcionalidades que implementei com recurso aos ScriptableObjects num protótipo no qual tenho estado a trabalhar há já alguns meses, foi o sistema de habilidades. Este sistema permite ao protagonista do jogo adquirir determinadas habilidades quando coleciona um certo tipo de objetos.

Primeiro criei um componente chamado Ability System, que se encontra anexado ao protagonista e está encarregue de controlar as habilidades disponíveis. Cada habilidade é um ScriptableObject individual que, a nível de código, condensa todas as operações lógicas a esta inerentes, enquanto que no inspetor de objetos expõe uma série de parâmetros reajustáveis, permitindo assim a criação de diversas variantes. E desta forma podem adicionar-se e remover-se habilidades ao protagonista sem qualquer tipo de problema ou falha, mesmo em modo debug, uma vez que fiz questão de capacitar o sistema com a funcionalidade de, diretamente a partir do editor Unity, se poder testar habilidades por via de simples acoplação direta.

Ability System based on ScriptableObjects in Unity
Protótipo de jogo com sistema de habilidades baseado em ScriptableObjects

Comparados com alternativas menos "amigas do designer", os ScriptableObjects acarretam a grande vantagem de poder visualizar-se toda a informação e modificações diretamente a partir do inspetor de objetos, sem que seja necessário constantemente recorrer ao método "Debug.Log" para registar informações de jogo na consola de mensagens.

É por isso que no meu projeto decidi utilizar também os ScriptableObjects para transportar informações relevantes acerca do progresso do jogo, não só para que vários sistemas possam ter acesso direto a elas, mas sobretudo porque torna muito fácil para mim visualizar todas essas informações de forma organizada e, por extensão, identificar um lapso quando algo falhar.

Um desses exemplos é um ScriptableObject ao qual chamei de Gameplay Stream. Ele apenas contém dois campos referenciáveis: um que indica o nível em que o jogador se encontra atualmente, outro que indica qual foi o último nível concluído (mais concretamente, aquele em que o protagonista chegou à meta).

E quem precisa de aceder a estas informações? Quanto à primeira, o nível atual pode ser acedido pelo UI Manager, para que este tenha a possibilidade de atualizar de acordo com, por exemplo, a tipologia de nível em questão (travessia, boss, etc...) e assim apresentar informações correspondentes. Já a referência ao último nível concluído pode ser utilizada pelo World Map (a área onde se o jogador pode selecionar o nível que quer jogar), para que o protagonista reapareça no ponto onde acedeu a tal nível assim que este é concluído, transmitindo a ideia de que tal como ali entrou, dali saiu após a sua jornada.

Using a ScriptableObject to store relevant Player Data in Unity
Transmissão de informações de progresso do jogo sob a forma de ScriptableObject

Outra vertente de ScriptableObject que desenvolvi foi um chamado Player Data, o qual condensa toda a informação relativa ao progresso do jogador, como os colecionáveis que este encontrou em cada nível, as personagens desbloqueadas, entre outros dados relevantes que podem ser guardados e recuperados para retomar o progresso numa próxima sessão de jogo.


Como se pode ver, os ScriptableObjects dispõem de diversas aplicações em prol de um fluxo de game design produtivo, intuitivo e modular. Contudo, eles não se cingem apenas aos métodos aqui demonstrados. De facto, estes componentes proporcionam uma gama de possibilidades infinita, cujos únicos limites serão a imaginação e capacidade de invenção do programador.

Uma das aplicações presentes também no Open Project da Unity é a implementação de State Machines, isto é, componentes que regulam operações lógicas consoante o estado em que um objeto do jogo se encontra, nomeadamente no que toca ao comportamento de personagens. A contribuição para este mecanismo por parte dos participantes do projeto passa por implementar o comportamento de cada estado através de um ScriptableObject específico, aplicando-se, da mesma forma, a funcionalidade de transição entre estados mediante a análise de condições.

O protagonista encontra-se no estado "Caminhar", mas o jogador premiu o botão "saltar"? Então o sistema transita para o estado "No Ar". O protagonista perdeu velocidade vertical e tocou no chão? Então transita-se de volta para o estado "Caminhar"... Tudo isto implementado de forma dinâmica com os ScriptableObjects, que ficam ao dispor de qualquer designer, sem necessidade de conhecimentos de código, para este modificar e unificar da forma que pretender.


No meu ainda curto percurso de aprendizagem de game design em Unity e programação C#, devo dizer que os ScriptableObjects foram um dos componentes que mais me entusiasmaram para procurar novas fontes de conhecimento na área, partilhar opiniões e dinamizar a arquitetura dos projetos amadores que tenho desenvolvido como forma de aprofundar conhecimentos e aprender a solucionar problemas.

Devo dizer que desconheço por completo se existe uma alternativa semelhante noutras engines populares, com as quais não estou familiarizado a nível prático. No entanto, posso afirmar que pretendo continuar a descobrir a multifuncionalidade desta ferramenta que o Unity oferece e se tem tornado cada vez mais falada.


Como muitos afirmam, enquanto que uma arquitetura fortemente apoiada e restrita ao código é preferida por veteranos da área, o foco no objeto e no design modular - que é o que os ScriptableObjects do Unity oferecem - tem vindo a tornar-se uma tendência por parte da nova geração de programadores e designers, que preferem uma abordagem mais pragmática.

É certo que podem surgir percalços em ambas as vertentes, assim como algum tipo de incoerência lógica, mas se tivermos em conta o velho ditado de que "a máquina tem sempre razão", então diria que uma abordagem modular nos ajuda a encontrar mais depressa essa razão no processo de combate ao bug, porque nos permite seguir o "rasto de migalhas" deixado para trás, o qual nos guiará ao potencial cerne do problema. Assim evita-se a alternativa árdua de uma arquitetura exclusivamente assente em código e fórmulas complexas.



Links Recomendados:

Comments


bottom of page