Hoje falaremos sobre como armazenar, receber e transmitir dados dentro do jogo. Sobre a coisa maravilhosa chamada ScriptableObject e por que é maravilhosa. Vamos falar um pouco sobre os benefícios de singletones ao organizar cenas e transições entre eles.
Este artigo descreve uma parte de uma maneira longa e dolorosa de desenvolver um jogo, várias abordagens usadas no processo. Provavelmente, haverá muitas informações úteis para iniciantes e nada de novo para "veteranos".
Links entre scripts e objetos
A primeira pergunta que um desenvolvedor iniciante enfrenta é como vincular todas as classes escritas e configurar as interações entre elas.
A maneira mais fácil é especificar um link para a classe diretamente:
public class MyScript : MonoBehaviour { public OtherScript otherScript; }
E então - vincule manualmente o script através do inspetor.
Essa abordagem tem pelo menos uma desvantagem significativa - quando o número de scripts excede várias dezenas e cada um deles exige dois ou três links entre si, o jogo rapidamente se transforma em uma rede. Um simples olhar para ela é suficiente para causar dor de cabeça.
É muito melhor (na minha opinião) organizar um sistema de mensagens e assinaturas, dentro do qual nossos objetos receberão as informações de que precisam - e somente isso! - sem exigir meia dúzia de links um para o outro.
No entanto, tendo sondado o tópico, descobri que soluções prontas no Unity são censuradas por todos que não são preguiçosos. Pareceu-me uma tarefa não trivial escrever um sistema desse tipo do zero e, portanto, vou procurar soluções mais simples.
Scriptableobject
Existem basicamente duas coisas a saber sobre o ScriptableObject:
- Eles fazem parte da funcionalidade implementada no Unity, como o MonoBehaviour.
- Ao contrário do MonoBehaviour, eles não estão vinculados a objetos de cena, mas existem como ativos separados e podem armazenar e transferir dados entre as sessões do jogo .
Eu imediatamente me apaixonei por seu amor quente. De certa forma, eles se tornaram minha panacéia por qualquer problema:
- Precisa armazenar as configurações do jogo? ScriptableObject!
- Criar um inventário? ScriptableObject!
- Escreva AI? ScriptableObject!
- Registrar informações sobre um personagem, inimigo, item? ScriptableObject nunca o decepcionará!
Sem pensar duas vezes, criei várias classes do tipo ScriptableObject e, em seguida, um repositório para elas:
public class Database: ScriptableObject { public PlayerData playerData; public GameSettings gameSettings; public SpellController spellController; }
Cada um deles armazena em si todas as informações úteis e, possivelmente, links para outros objetos. Cada um deles é suficiente para ligar uma vez através do inspetor - eles não irão a nenhum outro lugar.
Agora não preciso especificar um número infinito de links entre scripts! Para cada script, posso especificar um link para o meu repositório - e ele receberá todas as informações a partir daí.
Assim, o cálculo da velocidade do personagem tem uma aparência muito elegante:
E se, digamos, uma armadilha deve disparar apenas em um personagem em execução:
if (database.playerData.isSprinting) Activate();
Além disso, o personagem não precisa saber nada sobre feitiços ou armadilhas. Ele apenas recupera dados do armazenamento. Nada mal? Nada mal.
Mas quase imediatamente me deparo com um problema. ScriptableOnjects não pode armazenar links para objetos de cena diretamente. Em outras palavras, não consigo criar um link para o jogador, vinculá-lo através do inspetor e esquecer a questão das coordenadas do jogador para sempre.
E se você pensar sobre isso, faz sentido! Os ativos existem fora do palco e podem ser acessados em qualquer uma das cenas. E o que acontece se você deixar um link para um objeto localizado em outra cena dentro do ativo?
Nada de bom.
Por um longo tempo, uma muleta funcionou para mim: um link público é criado no repositório e, em seguida, cada objeto, o link do qual você deseja se lembrar, preencheu este link:
public class PlayerController : MonoBehaviour { void Awake() { database.playerData.player = this.gameObject; } }
Assim, independentemente da cena, meu repositório recebe, antes de tudo, um link para o player e se lembra dele. Agora, qualquer um, digamos, que o inimigo não deva armazenar um link para o jogador, não deve procurá-lo através do FindWithTag () (que é um processo que consome muitos recursos). Tudo o que ele faz é acessar o repositório:
public Database database; Vector3 destination; void Update () { destination = database.playerData.player.transform.position; }
Parece: o sistema é perfeito! Mas não. Ainda temos 2 problemas.
- Ainda preciso especificar manualmente um link para o repositório para cada script.
- É estranho atribuir referências a objetos de cena dentro de um ScriptableObject.
Sobre o segundo em mais detalhes. Suponha que um jogador tenha um feitiço de luz. O jogador lança e o jogo diz para a loja: a luz é lançada!
database.spellController.light.CastSpell();
E isso dá origem a uma série de reações:
- Uma nova luz de objeto de jogo (ou antiga) é criada no ponto do cursor.
- Um módulo GUI é lançado, informando que a luz está ativa.
- Os inimigos recebem, digamos, um bônus temporário por encontrar um jogador.
Como fazer tudo isso?
É possível para cada objeto interessado na luz, diretamente em Update () e escrever, eles dizem, desta maneira e daquela, cada quadro segue a luz (se (database.spellController.light.isActive)) e, quando acende, reaja! E não se importe que 90% das vezes essa verificação funcione ociosa. Em várias centenas de objetos.
Ou organize tudo isso na forma de links preparados. Acontece que a simples função CastSpell () deve ter acesso a links para o player, à luz e à lista de inimigos. E isso é o melhor. Muitos links, hein?
Obviamente, você pode salvar tudo o que é importante em nosso repositório quando a cena começa, dispersar links em ativos, que, em geral, não são destinados a isso ... Mas, novamente, violei o princípio de um único repositório, transformando-o em uma rede de links.
Singleton
É aqui que o singleton entra em cena. De fato, este é um objeto que existe (e pode existir) apenas em uma única cópia.
public class GameController : MonoBehaviour { public static GameController Instance;
Eu encaixo em um objeto de cena vazio. Vamos chamá-lo de GameController.
Portanto, tenho um objeto na cena que armazena
todas as informações sobre o jogo. Além disso, ele pode se mover entre cenas, destruir suas duplas (se já houver outro GameController na nova cena), transferir dados entre cenas e, se desejado, implementar salvar / carregar o jogo.
De todos os scripts já gravados, você pode remover o link para o armazém de dados. Afinal, agora não preciso configurá-lo manualmente. Todas as referências a objetos de cena são excluídas da loja e transferidas para o nosso GameController (provavelmente precisaremos delas para salvar o estado da cena quando sairmos do jogo). E então preencho com todas as informações necessárias de uma maneira conveniente para mim. Por exemplo, em Desperta () do jogador e dos inimigos (e objetos importantes da cena), é prescrito adicionar um link para eles no GameController. Como agora estou trabalhando com o Monobehaviour, as referências a objetos na cena se encaixam muito organicamente.
O que nós ganhamos?
Qualquer objeto pode obter
qualquer informação sobre o jogo que ele precisa:
if (GameController.Instance.database.playerData.isSprinting) ActivateTrap();
Nesse caso, não há absolutamente nenhuma necessidade de configurar links entre objetos, tudo é armazenado em nosso GameController.
Agora não haverá nada complicado em manter o estado da cena. Afinal, já temos todas as informações necessárias: inimigos, itens, posição do jogador, data warehouse. Basta selecionar as informações sobre a cena que você deseja salvar e gravá-las em um arquivo usando o FileStream ao sair da cena.
Os perigos
Se você ler até este ponto, devo alertá-lo sobre os perigos dessa abordagem.
Uma situação muito ruim é quando muitos scripts fazem referência a uma variável dentro do nosso ScriptableObject. Não há nada errado em obter um valor, mas quando eles começam a afetar uma variável de lugares diferentes, isso é uma ameaça em potencial.
Se tivermos uma variável playerSpeed salva e precisarmos que ele se mova em velocidades diferentes, não devemos alterar o playerSpeed no armazenamento, devemos obtê-lo, salvá-lo em uma variável temporária e aplicar modificadores de velocidade a ele já.
O segundo ponto - se algum objeto tem acesso a qualquer coisa - essa é uma grande potência. E uma grande responsabilidade. E você precisa abordá-lo com cautela, para que algum script de fungo acidentalmente não quebre completamente todos os seus inimigos de IA. O encapsulamento configurado corretamente reduzirá os riscos, mas não salvará você deles.
Além disso, não esqueça que singletones são criaturas gentis. Não abuse deles.
Isso é tudo por hoje.
Muito foi aprendido nos tutoriais oficiais do Unity, alguns dos não oficiais. Eu mesmo tive que fazer alguma coisa. Portanto, as abordagens acima podem ter seus perigos e desvantagens, que eu perdi.
E, portanto - a discussão é bem-vinda!