Melhorar o manuseio de cenas com ScriptableObject

Olá. Neste momento, um set para o curso “Unity Game Developer. Básico " . Convidamos você a olhar para o registro do dia aberto do curso e também compartilhar uma tradução tradicionalmente interessante.










Trabalhar com várias cenas no Unity pode ser desafiador e otimizar esse fluxo de trabalho tem um grande impacto no desempenho do jogo e na produtividade da equipe. Hoje vamos compartilhar com você dicas para configurar fluxos de trabalho com o Scene que podem ser dimensionados para projetos maiores.


A maioria dos jogos tem vários níveis, e os níveis geralmente contêm mais de uma cena. Em jogos em que as cenas são relativamente pequenas, você pode dividi-las em partes diferentes usando Prefabs. No entanto, para conectá-los ou instanciá-los durante o jogo, você precisa fazer referência a todos esses pré-fabricados. Isso significa que conforme seu jogo fica maior e esses links ocupam mais espaço na memória, torna-se mais eficiente o uso de cenas.



Você pode dividir os níveis em uma ou mais cenas de unidade. Encontrar a melhor maneira de gerenciá-los torna-se o ponto chave. Você pode abrir várias cenas de uma vez no editor e no tempo de execução usando a função de edição de várias cenas . Dividir camadas em várias cenas também facilita o trabalho em equipe, pois evita conflitos de mesclagem em ferramentas de colaboração como Git, SVN, Unity Collaborate e muito mais.



Gerencie várias cenas para criar um nível



No vídeo abaixo, mostraremos como carregar um nível com mais eficiência, quebrando a lógica do jogo e as diferentes partes do nível em várias cenas separadas do Unity. Então, usando o modo Additive Scene-loading ao carregar essas cenas, carregamos e descarregamos as partes necessárias junto com a lógica do jogo que não vai a lugar nenhum. Usamos pré-fabricados como âncoras para as cenas, o que também oferece muita flexibilidade ao trabalhar em equipe, já que cada cena faz parte do nível e pode ser editada separadamente.



Você ainda pode carregar essas cenas no modo de edição e pressionar Play a qualquer momento para renderizá-las todas juntas enquanto trabalha no design dos níveis.



Mostraremos dois métodos diferentes para carregar essas cenas. O primeiro é baseado na distância, o que funciona bem para níveis não internos, como o mundo aberto. Essa técnica também é útil para alguns efeitos visuais (como névoa) para ocultar o processo de carga e descarga.



O segundo método usa um Trigger para verificar quais cenas precisam ser carregadas, o que é mais eficiente ao trabalhar com interiores.





Agora que descobrimos tudo dentro do nível, podemos adicionar uma camada adicional em cima dele para gerenciar melhor os próprios níveis.



Controle de vários níveis de jogo com ScriptableObjects



Queremos acompanhar as diferentes cenas em cada nível, bem como todos os níveis ao longo de todo o jogo. Uma maneira possível de conseguir isso é usar variáveis ​​estáticas e singletones em scripts MonoBehaviour, mas essa solução não é tão fácil. Usar um singleton implica vínculos estreitos entre seus sistemas, portanto, não é estritamente modular. Os sistemas não podem existir separadamente e sempre dependerão uns dos outros.



Outro problema está relacionado ao uso de variáveis ​​estáticas. Já que você não pode vê-los no Inspetor, você precisa defini-los por meio de código, o que torna mais difícil para os artistas ou designers de nível testarem o jogo. Quando você precisa que os dados sejam compartilhados entre cenas diferentes, você usa variáveis ​​estáticas em conjunto com DontDestroyOnLoad, mas o último deve ser evitado sempre que possível.



Para armazenar informações sobre várias cenas, você pode usar ScriptableObject , uma classe serializável que é usada principalmente para armazenar dados. Ao contrário dos scripts MonoBehaviour, que são usados ​​como componentes vinculados a GameObjects, ScriptableObjects não são vinculados a nenhum GameObject e, portanto, podem ser usados ​​por diferentes cenas ao longo do projeto.



Seria bom ser capaz de usar essa estrutura para níveis, bem como cenas de menu em seu jogo. Para fazer isso, crie uma classe GameScene que contém várias propriedades gerais para níveis e menus.



public class GameScene : ScriptableObject
{
    [Header("Information")]
    public string sceneName;
    public string shortDescription;
 
    [Header("Sounds")]
    public AudioClip music;
    [Range(0.0f, 1.0f)]
    public float musicVolume;
 
    [Header("Visuals")]
    public PostProcessProfile postprocess;
}


Observe que a classe herda de ScriptableObject, não MonoBehaviour. Você pode adicionar quantas propriedades forem necessárias para o seu jogo. Após essa etapa, você pode criar as classes Level e Menu que herdam da classe GameScene que você acabou de criar, portanto, elas também são ScriptableObjects.



[CreateAssetMenu(fileName = "NewLevel", menuName = "Scene Data/Level")]
public class Level : GameScene
{
    // ,    
    [Header("Level specific")]
    public int enemiesCount;
}


Adicionar o atributo CreateAssetMenu no topo permite que você crie um novo nível a partir do menu Ativos no Unity. Você pode fazer o mesmo para a classe Menu. Você também pode adicionar uma enumeração para poder selecionar o tipo de menu no inspetor.



public enum Type
{
    Main_Menu,
    Pause_Menu
}
 
[CreateAssetMenu(fileName = "NewMenu", menuName = "Scene Data/Menu")]
public class Menu : GameScene
{
    // ,    
    [Header("Menu specific")]
    public Type type;
}


Agora que você pode criar níveis e menus, vamos adicionar um banco de dados que os lista (níveis e menus) para sua conveniência. Você também pode adicionar um índice para acompanhar o nível atual do jogador. Você pode então adicionar métodos para carregar um novo jogo (neste caso, o primeiro nível será carregado), para repetir o nível atual e ir para o próximo nível. Observe que apenas o índice é alterado nesses três métodos, portanto, você pode criar um método que carregue o nível por índice para reutilizá-lo.



[CreateAssetMenu(fileName = "sceneDB", menuName = "Scene Data/Database")]
public class ScenesData : ScriptableObject
{
    public List<Level> levels = new List<Level>();
    public List<Menu> menus = new List<Menu>();
    public int CurrentLevelIndex=1;
 
    /*
 	* 
 	*/
 
    //     
    public void LoadLevelWithIndex(int index)
    {
        if (index <= levels.Count)
        {
            //     
            SceneManager.LoadSceneAsync("Gameplay" + index.ToString());
            //       
            SceneManager.LoadSceneAsync("Level" + index.ToString() + "Part1", LoadSceneMode.Additive);
        }
        //  ,      
        else CurrentLevelIndex =1;
    }
    //   
    public void NextLevel()
    {
        CurrentLevelIndex++;
        LoadLevelWithIndex(CurrentLevelIndex);
    }
    //   
    public void RestartLevel()
    {
        LoadLevelWithIndex(CurrentLevelIndex);
    }
    //  ,   
    public void NewGame()
    {
        LoadLevelWithIndex(1);
    }
  
    /*
 	* 
    */
 
    //   
    public void LoadMainMenu()
    {
        SceneManager.LoadSceneAsync(menus[(int)Type.Main_Menu].sceneName);
    }
    //   
    public void LoadPauseMenu()
    {
        SceneManager.LoadSceneAsync(menus[(int)Type.Pause_Menu].sceneName);
    }


Existem também métodos de menu, e você pode usar o tipo de enumeração criado anteriormente para carregar o menu específico desejado - apenas certifique-se de que a ordem na enumeração e a ordem na lista de menus são as mesmas.



Finalmente, agora você pode criar um nível de banco de dados, menu ou ScriptableObject a partir do menu Assets clicando com o botão direito do mouse na janela Project.







A partir daí, continue adicionando os níveis e menus que você deseja, ajustando os parâmetros e, em seguida, adicionando-os ao banco de dados da cena. O exemplo abaixo mostra a aparência dos dados de Level1, MainMenu e Scenes.







É hora de chamar esses métodos. Neste exemplo, o botão Próximo nível na interface do usuário (IU) que aparece quando o jogador atinge o final do nível chama o método NextLevel. Para vincular um método a um botão, clique no botão com o evento On Click mais o componente Button para adicionar um novo evento, então arraste o Scene Data ScriptableObject para o campo do objeto e selecione o método NextLevel em ScenesData como mostrado abaixo.







Agora você pode fazer o mesmo processo para outros botões - repetir o nível ou ir para o menu principal e assim por diante. Você também pode consultar ScriptableObject de qualquer outro script para acessar várias propriedades, como AudioClip para música de fundo ou perfil de pós-processamento e usá-los no nível.



Dicas para minimizar erros em seus processos



Minimizando o carregamento /



descarregamento No script ScenePartLoader mostrado no vídeo, você pode ver que o jogador pode continuar entrando e saindo do colisor várias vezes, fazendo com que a cena seja recarregada e descarregada. Para evitar isso, você pode adicionar uma co-rotina antes de chamar os métodos de carregamento e descarregamento de cena no script e parar a co-rotina se o jogador sair do gatilho.



Convenções de nomenclatura



Outra dica global é usar convenções de nomenclatura fortes em seu projeto. A equipe deve concordar com antecedência sobre como nomear os diferentes tipos de ativos, de roteiros e cenas a materiais e outras coisas no projeto. Isso tornará mais fácil trabalhar no projeto e apoiá-lo não só para você, mas também para seus colegas de equipe. É sempre uma boa ideia, mas, neste caso específico, é muito importante para gerenciar cenas com ScriptableObjects. Nosso exemplo usou uma abordagem simples baseada no nome da cena, mas existem muitas soluções diferentes que dependem menos do nome da cena. Você deve evitar uma abordagem baseada em string porque se você renomear uma cena do Unity neste contexto, essa cena não será carregada em outro lugar no jogo.



Ferramentas especiais



Uma maneira de evitar depender de nomes em todo o jogo é configurar seu script para se referir a cenas como sendo do tipo Object . Isso permite que você arraste e solte um recurso de cena no inspetor e, em seguida, silenciosamente obter seu nome no script. No entanto, como é uma classe Editor, você não tem acesso à classe AssetDatabase em tempo de execução, portanto, é necessário combinar as duas partes de dados para uma solução que funcione no editor, evite erros humanos e ainda funcione em tempo de execução. Você pode consultar a interface ISerializationCallbackReceiver para obter um exemplo de como implementar um objeto que, após a serialização, pode recuperar o caminho da string do ativo Scene e armazená-lo para uso em tempo de execução.



Alternativamente, você também pode criar seu próprio inspetor para tornar mais fácil adicionar cenas rapidamente às Configurações de construção usando botões, em vez de adicioná-las manualmente por meio deste menu e mantê-las sincronizadas.



Para obter um exemplo desse tipo de ferramenta, verifique esta incrível implementação de código aberto do desenvolvedor JohannesMP (este não é um recurso oficial do Unity).



Deixe-nos saber o que você pensa



Esta postagem mostra apenas uma maneira como ScriptableObjects pode melhorar seu fluxo de trabalho ao trabalhar com várias cenas combinadas com pré-fabricados. Jogos diferentes usam maneiras completamente diferentes de controlar as cenas - nenhuma solução se adapta a todas as estruturas do jogo de uma vez. Faz sentido implementar suas próprias ferramentas de acordo com a organização do projeto.



Esperamos que esta informação o ajude com seu projeto, ou talvez o inspire a criar suas próprias ferramentas de gerenciamento de cena.



Deixe-nos saber nos comentários se você tiver alguma dúvida. Adoraríamos saber quais técnicas você usa para manipular as cenas do jogo. E sinta-se à vontade para sugerir outros casos de uso que gostaria de sugerir para consideração em postagens futuras.












All Articles