Esta página explica como usar ScriptableObjects como contêineres de dados que separam os dados da lógica no código do seu jogo.
Este é o segundo de uma série de seis miniguias criados para auxiliar os desenvolvedores do Unity com a demonstração que acompanha o e-book, Crie arquitetura de jogo modular no Unity com ScriptableObjects.
A demonstração é inspirada na mecânica clássica de jogos de arcade de bola e raquete e mostra como o ScriptableObjects pode ajudar você a criar componentes testáveis, escaláveis e fáceis de usar.
Juntos, o e-book, o projeto de demonstração e esses miniguias fornecem práticas recomendadas para usar padrões de design de programação com a classe ScriptableObject no seu projeto Unity. Essas dicas podem ajudar você a simplificar seu código, reduzir o uso de memória e promover a reutilização do código.
Esta série inclui os seguintes artigos:
Antes de mergulhar no projeto de demonstração do ScriptableObject e nesta série de miniguias, lembre-se de que, em essência, os padrões de design são apenas ideias. Elas não se aplicam a todas as situações. Essas técnicas podem ajudar você a aprender novas maneiras de trabalhar com Unity e ScriptableObjects.
Cada padrão tem prós e contras. Escolha apenas aquelas que beneficiam significativamente seu projeto específico. Seus designers dependem muito do Unity Editor? Um padrão baseado em ScriptableObject pode ser uma boa escolha para ajudá-los a colaborar com seus desenvolvedores.
Em última análise, a melhor arquitetura de código é aquela que se adapta ao seu projeto e à sua equipe.
Os desenvolvedores de software geralmente se preocupam com a modularidade – dividir um aplicativo em unidades menores e independentes. Cada módulo se torna responsável por um aspecto específico da funcionalidade do aplicativo.
No Unity, ScriptableObjects podem ajudar na separação de dados da lógica.
ScriptableObjects são excelentes para armazenar dados, especialmente quando são estáticos. Isso os torna ideais para estatísticas de jogos, valores de configuração para itens ou NPCs, diálogos de personagens e muito mais.
Isolar os dados de jogabilidade da lógica de comportamento pode tornar cada parte independente do projeto mais fácil de testar e manter. Essa “separação de preocupações” pode reduzir efeitos colaterais indesejados e não intencionais à medida que você faz as mudanças necessárias.
Se você quiser relembrar o fluxo de trabalho do ScriptableObject, este artigo do Unity Learn pode ajudar. Caso contrário, aqui vai uma explicação rápida:
Defina um ScriptableObject: Para criar um, defina uma classe C# que herde da classe base ScriptableObject com campos e propriedades para os dados que você deseja armazenar. ScriptableObjects podem armazenar os mesmos tipos de dados disponíveis para MonoBehaviours, tornando-os contêineres de dados versáteis. Adicione o CreateAssetMenuAttribute do Editor para facilitar a criação do ativo no projeto.
Criar um ativo: Depois de definir uma classe ScriptableObject, você pode criar uma instância desse ScriptableObject no projeto. Isso aparece como um recurso salvo no disco que você pode reutilizar em diferentes GameObjects e cenas.
Definir valores: Depois de criar o ativo, preencha-o com dados definindo os valores de seus campos e propriedades no Inspetor.
Use o ativo: Depois que o ativo contiver dados, faça referência a ele a partir de uma variável ou campo. Quaisquer alterações feitas no ativo ScriptableObject serão refletidas em todo o projeto.
Você pode reutilizar ScriptableObjects como contêineres de dados em diferentes partes do seu jogo. Por exemplo, você pode definir as propriedades de uma arma ou personagem dentro de um ScriptableObject e, então, referenciar esse ativo de qualquer lugar no projeto.
Observação: Você também pode gerar ScriptableObjects em tempo de execução por meio do método CreateInstance . Para armazenamento de dados, no entanto, você normalmente criará os ativos ScriptableObject com antecedência usando o CreateAssetMenuAttribute.
Para entender melhor por que ScriptableObjects são uma escolha mais adequada para armazenamento de dados do que MonoBehaviours, compare versões vazias de cada um. Certifique-se de definir sua serialização de ativos para Modo: Texto de Força nas Configurações do Projeto para visualizar a marcação YAML como texto.
Crie um novo GameObject com um MonoBehaviour vazio. Em seguida, compare isso com um ativo ScriptableObject vazio. Colocando-os lado a lado, eles devem ficar como a comparação mostrada na imagem acima.
ScriptableObjects são mais leves quando comparados aos MonoBehaviours e não carregam a sobrecarga associada a estes últimos, como o componente Transform. Isso dá aos ScriptableObjects um menor consumo de memória e os torna mais otimizados para armazenamento de dados.
ScriptableObjects são salvos como ativos, então eles persistem fora do modo Play, o que pode ser útil. Por exemplo, os dados do ScriptableObject estão disponíveis em qualquer lugar, mesmo se você carregar uma nova cena.
O exemplo de demonstração de Padrões apresenta uma tela de créditos básica que você pode testar. Modifique o ScriptableObject Credits_Data e pressione Atualizar para ver o texto armazenado aparecer.
Se você tivesse um RPG com uma grande quantidade de diálogos ou uma cena de tutorial com um roteiro pré-definido, essa seria uma maneira comum de armazenar muitos dados.
Embora os dados dentro do ScriptableObject sejam atualizados instantaneamente quando modificados, nosso projeto requer um botão Atualizar para atualizar a tela manualmente. A tela baseada no UI Toolkit é criada apenas uma vez e precisa ser notificada quando os dados forem alterados.
Crie um evento dentro do ScriptableObject se quiser sincronizar atualizações automaticamente. Por exemplo, este script ExampleSO chamaria o evento OnValueChanged toda vez que ExampleValue fosse alterado. Veja o exemplo de código abaixo.
Em seguida, faça com que seu objeto de interface de escuta assine OnValueChanged e atualize adequadamente.
ScriptableObjects brilham quando muitos objetos compartilham os mesmos dados. Por exemplo, se você estivesse criando um jogo de estratégia em que várias unidades tivessem a mesma velocidade de ataque e saúde máxima, seria ineficiente armazenar esses valores individualmente em cada GameObject.
Em vez disso, você pode consolidar dados compartilhados em um local central e fazer com que cada objeto faça referência a esse local compartilhado. No design de software, essa é uma otimização conhecida como padrão flyweight. Reestruturar seu código dessa maneira evita copiar muitos valores e reduz seu consumo de memória.
No PaddleBallSO, o GameDataSO ScriptableObject atua como armazenamento de dados compartilhado.
Em vez de manter uma cópia separada das configurações comuns (velocidade, massa, elasticidade física, etc.), os scripts Paddle and Ball fazem referência à mesma instância GameDataSO sempre que possível. Cada elemento do jogo mantém dados exclusivos, como posições e eventos de entrada, mas usa dados compartilhados como padrão quando possível.
Embora a economia de memória possa não ser perceptível com apenas dois ou três objetos, editar dados compartilhados é mais rápido e menos propenso a erros do que editar cada um manualmente.
Por exemplo, se você precisar modificar a velocidade da pá, ajustá-la em um único local atualizará ambas as pás em todas as cenas. Se você os armazenasse como campos exclusivos no MonoBehaviours, um clique incorreto poderia facilmente fazer com que dois valores ficassem fora de sincronia.
Descarregar dados em ScriptableObjects também pode ajudar no controle de versão e evitar conflitos de mesclagem quando colegas de equipe trabalham na mesma cena ou Prefab.
O GameDataSO mostra como usar um ScriptableObject como um contêiner de dados. No PaddleBallSO, isso inclui várias configurações para configurar a jogabilidade:
- Dados do remo: Atributos como velocidade, arrasto e massa da pá determinam o movimento e a física das pás durante o jogo.
- Dados da bola: Isso contém a velocidade atual da bola, a velocidade máxima e o multiplicador de salto, que controla o comportamento da bola quando ela interage com uma simulação.
- Dados da partida: O GameDataSO contém informações sobre atrasos entre pontos durante uma partida, ajudando a controlar o ritmo do jogo.
- IDs dos jogadores: Os ScriptableObjects do PlayerIDSO funcionam como uma identificação de equipe para cada jogador (por exemplo, Jogador1 e Jogador2).
- Sprites dos jogadores: Esses sprites opcionais permitem a personalização do avatar do jogador.
- Layout dos níveis: O objeto LevelLayoutSO define as posições iniciais dos jogadores e elementos do jogo, como gols e paredes.
Com essas configurações e dados em um local central, o GameDataSO permite que qualquer objeto acesse esses dados compartilhados. Isso simplifica a maneira como você gerencia esses objetos e promove maior consistência em todo o seu projeto. Mudando a física da raquete? Faça uma alteração aqui em vez de ajustar vários scripts.
Às vezes, você pode ter o bolo e comê-lo também. Com a serialização dupla, você pode armazenar dados em um ScriptableObject e, ao mesmo tempo, mantê-los em outro formato.
O script LevelLayoutSO demonstra esse conceito. Além de manter as posições iniciais das raquetes e da bola, ele armazena dados de transformação das paredes e gols em uma estrutura personalizada.
Esses valores podem ser gravados no disco por meio do método ExportToJson . Os arquivos JSON são textos legíveis por humanos, permitindo modificações diretas fora do Unity. Isso permite que você trabalhe com ScriptableObjects no Editor e então armazene seus dados em outro local, como um arquivo JSON ou XML.
Formatos de arquivo como JSON e XML podem ser desafiadores para trabalhar no Editor, mas são fáceis de modificar fora do Unity em um editor de texto. Isso abre a possibilidade de níveis personalizados ou modificados pelo usuário.
O script GameSetup pode então usar um LevelLayout ScriptableObject ou um arquivo JSON externo para gerar o nível do jogo.
Para carregar um nível modificado personalizado, o script de configuração gera um ScriptableObject em tempo de execução com CreateInstance. Em seguida, ele lê o texto do arquivo JSON para preencher o ScriptableObject.
Seus dados personalizados substituem o conteúdo do ScriptableObject e permitem que você use este nível modificado externamente como qualquer outro. O restante do aplicativo funciona normalmente, sem perceber a mudança.
Embora nosso minijogo de paddle ball não possa demonstrar todos os casos de uso para contêineres de dados ScriptableObject, considere o seguinte para seus próprios aplicativos:
- Configuração do jogo: Pense em constantes, regras do jogo ou quaisquer outros parâmetros de configuração que não precisam mudar durante o jogo. Outros componentes podem então consultar esses dados de configuração sem usar valores codificados.
- Atributos do personagem e do inimigo: Use ScriptableObjects para definir atributos como saúde, poder de ataque, velocidade, etc. Isso permite que seus designers equilibrem e ajustem elementos de jogabilidade sem um desenvolvedor.
- Sistemas de inventário e itens: Definições de itens e propriedades como nomes, descrições e ícones são perfeitas para ScriptableObjects. Você também pode usá-los como parte de um sistema de gerenciamento de inventário para rastrear itens que o jogador coleta, usa ou equipa.
- Sistemas de diálogo e narrativa: ScriptableObjects podem armazenar texto de diálogo, nomes de personagens, caminhos de diálogo ramificados e outros dados relacionados à narrativa. Eles podem estabelecer a base para sistemas de diálogo complexos.
- Dados de nível e progressão: Você pode usar ScriptableObjects para definir layouts de níveis, pontos de surgimento de inimigos, objetivos e outras informações relacionadas aos níveis.
- Clipes de áudio: Como visto no projeto PaddleBallSO , ScriptableObjects podem armazenar um ou mais clipes de áudio. Eles podem definir efeitos de áudio ou música em diversas partes do seu jogo.
- Clipes de animação: ScriptableObjects podem ser usados para armazenar clipes de animação, o que é útil para definir animações comuns que são compartilhadas entre vários GameObjects ou personagens.
À medida que você se aprofunda nos ScriptableObjects e os adapta aos seus próprios projetos, você descobrirá ainda mais aplicações para eles. Eles são especialmente úteis para gerenciar dados e facilitam a manutenção da consistência entre vários elementos do jogo.
Leia mais sobre padrões de design com ScriptableObjects no e-book Crie arquitetura de jogo modular no Unity com ScriptableObjects. Você também pode descobrir mais sobre os padrões comuns de design de desenvolvimento do Unity em Melhore seu código com padrões de programação de jogos.