Última atualização em janeiro de 2020, leitura de 10 minutos

Três modos de arquitetar seu jogo com ScriptableObjects

What you will get from this page: Tips for how to keep your game code easy to change and debug by architecting it with Scriptable Objects.

These tips come from Ryan Hipple, principal engineer at Schell Games, who has advanced experience using Scriptable Objects to architect games. You can watch Ryan’s Unite talk on Scriptable Objects here; we also recommend you see Unity engineer Richard Fine’s session for a great introduction to Scriptable Objects. Thank you Ryan!

 

 

O que são ScriptableObjects?

ScriptableObject is a serializable Unity class that allows you to store large quantities of shared data independent from script instances. Using ScriptableObjects makes it easier to manage changes and debugging. You can build in a level of flexible communication between the different systems in your game, so that it’s more manageable to change and adapt them throughout production, as well as reuse components.

Três pilares da engenharia de jogos

Use design modular:

  • Evite criar sistemas que dependam diretamente uns dos outros. Por exemplo, um sistema de inventário deve ser capaz de se comunicar com outros sistemas de seu jogo, mas não é recomendável criar uma referência rígida entre eles, pois isso dificulta a recomposição de sistemas em diferentes configurações e relações. 
  • Crie cenas como telas limpas: evite a existência de dados transientes entre as cenas. Toda vez que uma cena for alcançada, deve ocorrer quebra e carregamento limpos. Isso permite que você tenha cenas com comportamento exclusivo não presente em outras cenas, sem precisar recorrer a hacks. 
  • Configure os Prefabs para que funcionem por conta própria. Sempre que possível, cada prefab incluído em uma cena deve ter a respectiva funcionalidade contida nele. Isso ajuda muito no controle de código-fonte em equipes maiores, em que as cenas são uma lista de prefabs e os prefabs contêm a respectiva funcionalidade. Assim, a maioria das verificações ocorrem no nível do prefab, o que resulta em menos conflitos na cena. 
  • Concentre cada componente na resolução de um único problema. Isso facilita a união de vários componentes para criar algo.

 

Facilite a alteração e a edição de partes:

  • Torne o máximo possível de seu jogo orientado a dados. Ao projetar os sistemas de jogo para se comportarem como máquinas que processam dados na forma de instruções, você pode fazer alterações no jogo de maneira eficiente, mesmo em execução. 
  • Se os sistemas forem configurados para ser o mais modulares e baseados em componentes possível, isso facilita a edição deles, inclusive para artistas e designers. Se os designers puderem unir coisas no jogo sem a necessidade de solicitar um recurso explícito — em grande parte graças à implementação de componentes minúsculos com funções individuais e específicas — poderão combinar tais componentes de maneiras diferentes para encontrar nova jogabilidade/mecânica, Ryan diz que alguns dos recursos mais interessantes que sua equipe trabalhou dentro dos jogos são consequências desse processo, que ele chama de "design emergente". 
  • É crucial que sua equipe possa fazer alterações no tempo de execução do jogo. Quanto mais você puder alterar o jogo no tempo de execução, mais encontrará equilíbrio e valores e, se puder salvar o estado do tempo de execução, como os ScriptableObjects fazem, estará em uma ótima posição.

 

Facilite a depuração:

Esse é, na verdade, um subpilar dos dois primeiros. Quanto mais modular o seu jogo for, mais fácil será testar qualquer parte dele. Quanto mais editável o seu jogo for — quanto mais recursos dele tiverem suas próprias visualizações de Inspector — mais fácil será a depuração. Certifique-se de que consiga visualizar o estado de depuração no Inspector e nunca considere um recurso completo até ter algum plano sobre como vai depurá-lo. 

Arquiteto para variáveis

Uma das coisas mais simples que você pode criar com ScriptableObjects é uma variável autônoma baseada em assets. Veja abaixo um exemplo para uma FloatVariable, mas isso também vale para qualquer outro tipo serializável.

Qualquer pessoa da equipe, independentemente do nível técnico, pode definir uma nova variável de jogo criando um asset FloatVariable. Qualquer MonoBehaviour ou ScriptableObject pode usar uma FloatVariable pública em vez de um public float para fazer referência a esse novo valor compartilhado.

Ainda melhor, se um MonoBehaviour alterar o Value de uma FloatVariable, outros MonoBehaviours poderão ver essa alteração. Isso cria uma camada de mensagens entre sistemas que não precisam de referências entre si. 

FloatVariable.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}

Exemplo: pontos de vida de um jogador

Um exemplo de caso de uso para isso são os pontos de vida (HP) de um jogador. Em um jogo com um único jogador local, o HP do jogador pode ser uma FloatVariable chamada PlayerHP. Quando o jogador recebe dano, este é subtraído de PlayerHP, e quanto o jogador se cura, adiciona a PlayerHP.

Agora, imagine um Prefab de barra de vida na cena. A barra de vida monitora a variável PlayerHP para atualizar a exibição. Sem qualquer alteração de código, ela pode apontar facilmente para algo diferente, como uma variável PlayerMP. A barra de vida não sabe nada sobre o jogador na cena, ela apenas faz a leitura da mesma variável na qual o jogador grava.

Com esse tipo de configuração, fica fácil adicionar mais coisas para observar PlayerHP. O sistema de música pode mudar conforme PlayerHP fica baixa, os inimigos podem mudar os padrões de ataque quando sabem que o jogador está fraco, efeitos de espaço de tela podem enfatizar o perigo do próximo ataque e assim por diante. O importante aqui é que o script Player não envia mensagens para esses sistemas e esses sistemas não precisam conhecer o GameObject do jogador. Você também pode acessar o Inspector com o jogo em execução e alterar o valor de PlayerHP para realizar testes. 

Ao editar o Value de uma FloatVariable, pode ser uma boa ideia copiar os dados para um valor de tempo de execução a fim de não alterar o valor armazenado em disco para o ScriptableObject. Se fizer isso, os MonoBehaviours deverão acessar RuntimeValue para impedir a edição do InitialValue salvo no disco.

RuntimeValue.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
	public float InitialValue;

	[NonSerialized]
	public float RuntimeValue;

public void OnAfterDeserialize()
{
		RuntimeValue = InitialValue;
}

public void OnBeforeSerialize() { }
}

Arquiteto para eventos

Um dos recursos favoritos de Ryan para desenvolver com base em ScriptableObjects é um sistema de eventos. Arquiteturas de eventos ajuda a modularizar o código ao enviar mensagens entre sistemas que não necessariamente precisam se conhecer. Elas permitem que as coisas respondam a uma alteração no estado sem monitoramento constante em um ciclo de atualização.

Os exemplos de código a seguir foram obtidos de um sistema de eventos que consiste em duas partes: um GameEvent ScriptableObject e um GameEventListener MonoBehaviour. Os designers podem criar qualquer quantidade de GameEvents no projeto para representar mensagens importantes que podem ser enviadas. Um GameEventListener aguarda até um GameEvent específico ser gerado e responde invocando um UnityEvent (que não é um evento de fato, está mais para uma chamada de função serializada).

Exemplo de código: GameEvent ScriptableObject

GameEvent ScriptableObject: 

GameEvent ScriptableObject.cs (C#)
[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
	private List<GameEventListener> listeners = 
		new List<GameEventListener>();

public void Raise()
{
	for(int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}

public void RegisterListener(GameEventListener listener)
{ listeners.Add(listener); }

public void UnregisterListener(GameEventListener listener)
{ listeners.Remove(listener); }
}

Exemplo de código: GameEventListener

GameEventListener:

GameEventListener.cs (C#)
public class GameEventListener : MonoBehaviour
{
public GameEvent Event;
public UnityEvent Response;

private void OnEnable()
{ Event.RegisterListener(this); }

private void OnDisable()
{ Event.UnregisterListener(this); }

public void OnEventRaised()
{ Response.Invoke(); }
}

Sistema de eventos que lida com a morte do jogador

Um exemplo disso é o tratamento da morte do jogador em um jogo. Esse é um ponto em que grande parte da execução pode mudar, mas pode ser difícil determinar onde programar toda a lógica. O script Player deve acionar a Game Over UI ou uma alteração na música? Os inimigos devem verificar se o jogador está vivo em cada quadro? Um sistema de eventos permite que você evite dependências problemáticas como essas.

Quando o jogador morre, o script Player chama Raise no evento OnPlayerDied. O script Player não precisa saber quais sistemas se importam com isso, já que é apenas uma transmissão. A Game Over UI está escutando o evento OnPlayerDied e inicia a animação, um script de câmera pode aguardar isso e iniciar o escurecimento e um sistema de música pode responder com uma alteração na música. Cada inimigo pode estar aguardando OnPlayerDied também, acionando uma animação de insulto ou uma alteração de estado a fim de voltar para um comportamento ocioso.

Esse padrão facilita muito a adição de novas respostas à morte do jogador. Além disso, é fácil testar a resposta à morte do jogador chamando Raise no evento por meio de algum código de teste ou um botão no Inspector.

O sistema de eventos desenvolvido na Schell Games se expandiu para algo muito mais complexo e possui recursos que permitem a passagem de dados e a geração automática de tipos. Esse exemplo foi, essencialmente, o ponto de partida para o que usam hoje.

Arquiteto para outros sistemas

ScriptableObjects não precisam ser apenas dados. Considere qualquer sistema implementado em um MonoBehaviour e veja se você consegue mover a implementação para um ScriptableObject. Em vez de ter um InventoryManager em um DontDestroyOnLoad MonoBehaviour, experimente colocá-lo em um ScriptableObject.

Como não está vinculado à cena, não possui um Transform e não recebe as funções Update, mas manterá o estado entre carregamentos de cenas sem qualquer inicialização especial. Em vez de um singleton, use uma referência público ao seu objeto de sistema de inventário quando precisar de um script para acessar o inventário. Isso facilita a troca de um inventário de testes ou um inventário de tutoriais em relação ao uso de um singleton.

Aqui você pode imaginar um script Player recebendo uma referência ao sistema do inventário. Quando o jogador surge, pode solicitar ao inventário todos os objetos possuídos e gerar qualquer equipamento. A IU de equipamentos também pode fazer referência ao inventário e navegar pelos itens para determinar o que deve ser sacado. 

Você gostou deste conteúdo?

Usamos cookies para garantir a melhor experiência no nosso site. Visite nossa página da política de cookies para obter mais informações.

Eu entendi