Qual é a maneira correta de lidar com dados entre cenas?


52

Estou desenvolvendo meu primeiro jogo 2D no Unity e me deparei com o que parece ser uma pergunta importante.

Como lidar com dados entre cenas?

Parece haver respostas diferentes para isso:

  • Alguém mencionou o uso do PlayerPrefs , enquanto outras pessoas me disseram que isso deve ser usado para armazenar outras coisas, como brilho da tela e assim por diante.

  • Alguém me disse que a melhor maneira era certificar-se de escrever tudo em um jogo de salvamento toda vez que eu mudasse de cena e garantir que, quando a nova cena for carregada, obtenha as informações do jogo de salvamento novamente. Isso me pareceu um desperdício de desempenho. Eu estava errado?

  • A outra solução, que eu implementei até agora, é ter um objeto de jogo global que não seja destruído entre as cenas, manipulando todos os dados entre as cenas. Então, quando o jogo começa, eu carrego uma Cena Inicial em que este objeto está carregado. Depois que isso termina, ele carrega a primeira cena real do jogo, geralmente um menu principal.

Esta é a minha implementação:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

    // Make global
    public static GameController Instance {
        get;
        set;
    }

    void Awake () {
        DontDestroyOnLoad (transform.gameObject);
        Instance = this;
    }

    void Start() {
        //Load first game scene (probably main menu)
        Application.LoadLevel(2);
    }

    // Data persisted between scenes
    public int exp = 0;
    public int armor = 0;
    public int weapon = 0;
    //...
}

Este objeto pode ser tratado em minhas outras classes como esta:

private GameController gameController = GameController.Instance;

Embora isso tenha funcionado até agora, ele me apresenta um grande problema: se eu quiser carregar diretamente uma cena, digamos, por exemplo, o nível final do jogo, não posso carregá-lo diretamente, pois essa cena não contém esse objeto de jogo global .

Estou lidando com esse problema da maneira errada? Existem melhores práticas para esse tipo de desafio? Gostaria muito de ouvir suas opiniões, pensamentos e sugestões sobre este assunto.

obrigado

Respostas:


64

Listadas nesta resposta estão as formas fundamentais de lidar com essa situação. Embora a maioria desses métodos não seja adequada para projetos grandes. Se você deseja algo mais escalável e não tem medo de sujar as mãos, confira a resposta de Lea Hayes sobre as estruturas de injeção de dependência .


1. Um script estático para armazenar apenas dados

Você pode criar um script estático para armazenar apenas dados. Como é estático, você não precisa atribuí-lo a um GameObject. Você pode simplesmente acessar seus dados como ScriptName.Variable = data;etc.

Prós:

  • Nenhuma instância ou singleton necessário.
  • Você pode acessar dados de qualquer lugar do seu projeto.
  • Nenhum código extra para passar valores entre as cenas.
  • Todas as variáveis ​​e dados em um único script semelhante ao banco de dados facilitam o manuseio deles.

Contras:

  • Você não poderá usar uma Coroutine dentro do script estático.
  • Você provavelmente acabará com enormes linhas de variáveis ​​em uma única classe, se não se organizar bem.
  • Você não pode atribuir campos / variáveis ​​dentro do editor.

Um exemplo:

public static class PlayerStats
{
    private static int kills, deaths, assists, points;

    public static int Kills 
    {
        get 
        {
            return kills;
        }
        set 
        {
            kills = value;
        }
    }

    public static int Deaths 
    {
        get 
        {
            return deaths;
        }
        set 
        {
            deaths = value;
        }
    }

    public static int Assists 
    {
        get 
        {
            return assists;
        }
        set 
        {
            assists = value;
        }
    }

    public static int Points 
    {
        get 
        {
            return points;
        }
        set 
        {
            points = value;
        }
    }
}

2. DontDestroyOnLoad

Se você precisar que seu script seja atribuído a um GameObject ou derivado do MonoBehavior, você poderá adicionar uma DontDestroyOnLoad(gameObject);linha à sua classe onde possa ser executada uma vez (colocá-lo Awake()geralmente é o caminho a seguir) .

Prós:

  • Todos os trabalhos do MonoBehaviour (por exemplo, Coroutines) podem ser realizados com segurança.
  • Você pode atribuir campos dentro do editor.

Contras:

  • Você provavelmente precisará ajustar sua cena, dependendo do script.
  • Você provavelmente precisará verificar qual secene está carregado para determinar o que fazer no Update ou em outras funções / métodos gerais. Por exemplo, se você estiver fazendo algo com a interface do usuário em Update (), precisará verificar se a cena correta está carregada para fazer o trabalho. Isso causa um monte de verificações de caso contrário ou de caixa de opção.

3. PlayerPrefs

Você pode implementar isso se também quiser que seus dados sejam armazenados, mesmo que o jogo seja fechado.

Prós:

  • Fácil de gerenciar, pois o Unity lida com todo o processo em segundo plano.
  • Você pode transmitir dados não apenas entre cenas, mas também entre instâncias (sessões de jogos).

Contras:

  • Usa sistema de arquivos.
  • Os dados podem ser facilmente alterados a partir do arquivo prefs.

4. Salvando em um arquivo

Isso é um pouco exagerado para armazenar valores entre as cenas. Se você não precisa de criptografia, eu o desencorajo com esse método.

Prós:

  • Você está no controle dos dados salvos, em oposição aos PlayerPrefs.
  • Você pode transmitir dados não apenas entre cenas, mas também entre instâncias (sessões de jogos).
  • Você pode transferir o arquivo (o conceito de conteúdo gerado pelo usuário depende disso).

Contras:

  • Lento.
  • Usa sistema de arquivos.
  • Possibilidade de ler / carregar conflitos causados ​​pela interrupção do fluxo ao salvar.
  • Os dados podem ser facilmente alterados a partir do arquivo, a menos que você implemente uma criptografia (o que tornará o código ainda mais lento).

5. Padrão Singleton

O padrão Singleton é um tópico muito importante na programação orientada a objetos. Alguns sugerem, e outros não. Pesquise você mesmo e faça a ligação apropriada, dependendo das condições do seu projeto.

Prós:

  • Fácil de configurar e usar.
  • Você pode acessar dados de qualquer lugar do seu projeto.
  • Todas as variáveis ​​e dados em um único script semelhante ao banco de dados facilitam o manuseio deles.

Contras:

  • Muitos códigos padrão, cuja única tarefa é manter e proteger a instância singleton.
  • Existem fortes argumentos contra o uso do padrão singleton . Seja cauteloso e faça sua pesquisa com antecedência.
  • Possibilidade de conflito de dados devido a má implementação.
  • A unidade pode ter dificuldade em lidar com padrões de singleton 1 .

1 : No resumo do OnDestroymétodo do Script Singleton fornecido no Unify Wiki , é possível ver o autor descrevendo objetos fantasmas que sangram no editor a partir do tempo de execução:

Quando o Unity sai, destrói objetos em uma ordem aleatória. Em princípio, um Singleton só é destruído quando o aplicativo é encerrado. Se algum script chamar Instância depois de destruído, ele criará um objeto fantasma de buggy que permanecerá na cena do Editor, mesmo depois de parar de reproduzir o Aplicativo. Muito ruim! Portanto, isso foi feito para garantir que não estamos criando esse objeto fantasma de buggy.


8

Uma opção um pouco mais avançada é executar a injeção de dependência com uma estrutura como o Zenject .

Isso deixa você livre para estruturar seu aplicativo da maneira que desejar; por exemplo,

public class PlayerProfile
{
    public string Nick { get; set; }
    public int WinCount { get; set; }
}

Em seguida, você pode vincular o tipo ao contêiner de IoC (inversão de controle). Com o Zenject, essa ação é executada dentro de um MonoInstallerou um ScriptableInstaller:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        this.Container.Bind<PlayerProfile>()
            .ToSelf()
            .AsSingle();
    }
}

A instância singleton de PlayerProfileé então injetada em outras classes que são instanciadas via Zenject. Idealmente através da injeção de construtor, mas também é possível a injeção de propriedades e campos anotando-as com o Injectatributo de Zenject .

A última técnica de atributo é usada para injetar automaticamente os objetos de jogo da sua cena, pois o Unity instancia esses objetos para você:

public class WinDetector : MonoBehaviour
{
    [Inject]
    private PlayerProfile playerProfile = null;


    private void OnCollisionEnter(Collision collision)
    {
        this.playerProfile.WinCount += 1;
        // other stuff...
    }
}

Por qualquer motivo, convém vincular uma implementação pela interface, e não pelo tipo de implementação. (Isenção de responsabilidade, o seguinte não deve ser um exemplo incrível; duvido que você queira métodos Save / Load nesse local específico ... mas isso apenas mostra um exemplo de como as implementações podem variar no comportamento).

public interface IPlayerProfile
{
    string Nick { get; set; }
    int WinCount { get; set; }

    void Save();
    void Load();
}

[JsonObject]
public class PlayerProfile_Json : IPlayerProfile
{
    [JsonProperty]
    public string Nick { get; set; }
    [JsonProperty]
    public int WinCount { get; set; }


    public void Save()
    {
        ...
    }

    public void Load()
    {
        ...
    }
}

[ProtoContract]
public class PlayerProfile_Protobuf : IPlayerProfile
{
    [ProtoMember(1)]
    public string Nick { get; set; }
    [ProtoMember(2)]
    public int WinCount { get; set; }


    public void Save()
    {
        ...
    }

    public void Load()
    {
        ...
    }
}

Que pode então ser vinculado ao contêiner de IoC da mesma maneira que antes:

public class GameInstaller : MonoInstaller
{
    // The following field can be adjusted using the inspector of the
    // installer component (in this case) or asset (in the case of using
    // a ScriptableInstaller).
    [SerializeField]
    private PlayerProfileFormat playerProfileFormat = PlayerProfileFormat.Json;


    public override void InstallBindings()
    {
        switch (playerProfileFormat) {
            case PlayerProfileFormat.Json:
                this.Container.Bind<IPlayerProfile>()
                    .To<PlayerProfile_Json>()
                    .AsSingle();
                break;

            case PlayerProfileFormat.Protobuf:
                this.Container.Bind<IPlayerProfile>()
                    .To<PlayerProfile_Protobuf>()
                    .AsSingle();
                break;

            default:
                throw new InvalidOperationException("Unexpected player profile format.");
        }
    }


    public enum PlayerProfileFormat
    {
        Json,
        Protobuf,
    }
}

3

Você está fazendo as coisas de uma maneira boa. É do jeito que eu faço, e claramente do jeito que muitas pessoas fazem, porque esse script de carregador automático (você pode definir uma cena para carregar automaticamente primeiro sempre que clicar em Play) existe: http://wiki.unity3d.com/index.php/ SceneAutoLoader

As duas primeiras opções também são coisas que o seu jogo pode precisar para salvar o jogo entre as sessões, mas essas são ferramentas erradas para esse problema.


Acabei de ler um pouco do link que você postou. Parece que existe uma maneira de carregar automaticamente a cena inicial em que estou carregando o Game Game global. Parece um pouco complexo, então precisarei de um tempo para decidir se é algo que resolve meu problema. Obrigado pelo seu feedback!
Enrique Moreno Tent

O script que eu vinculei resolveu esse problema, em que você pode tocar em qualquer cena em vez de ter que se lembrar de mudar para a cena de inicialização toda vez. Ele ainda inicia o jogo desde o início, em vez de começar diretamente no último nível; você pode colocar um truque para permitir que você pule para qualquer nível ou apenas modificar o script de carregamento automático para passar o nível para o jogo.
Jhocking 07/11

Sim, bem. O problema não era tanto o "aborrecimento" de ter que se lembrar de mudar para a cena inicial, mas também o de ter que invadir para carregar o nível específico em mente. Obrigado mesmo assim!
Enrique Moreno Tent

1

Uma maneira ideal de armazenar variáveis ​​entre cenas é através de uma classe de gerenciador único. Criando uma classe para armazenar dados persistentes e configurando-a para DoNotDestroyOnLoad(), você pode garantir que ele seja imediatamente acessível e persista entre as cenas.

Outra opção que você tem é usar a PlayerPrefsclasse. PlayerPrefsfoi projetado para permitir que você salve dados entre sessões de reprodução , mas ainda servirá como um meio para salvar dados entre cenas .

Usando uma classe singleton e DoNotDestroyOnLoad()

O script a seguir cria uma classe singleton persistente. Uma classe singleton é uma classe projetada para executar apenas uma única instância ao mesmo tempo. Ao fornecer essa funcionalidade, podemos criar com segurança uma auto-referência estática, para acessar a classe de qualquer lugar. Isso significa que você pode acessar diretamente a classe DataManager.instance, incluindo quaisquer variáveis ​​públicas dentro da classe.

using UnityEngine;

/// <summary>Manages data for persistance between levels.</summary>
public class DataManager : MonoBehaviour 
{
    /// <summary>Static reference to the instance of our DataManager</summary>
    public static DataManager instance;

    /// <summary>The player's current score.</summary>
    public int score;
    /// <summary>The player's remaining health.</summary>
    public int health;
    /// <summary>The player's remaining lives.</summary>
    public int lives;

    /// <summary>Awake is called when the script instance is being loaded.</summary>
    void Awake()
    {
        // If the instance reference has not been set, yet, 
        if (instance == null)
        {
            // Set this instance as the instance reference.
            instance = this;
        }
        else if(instance != this)
        {
            // If the instance reference has already been set, and this is not the
            // the instance reference, destroy this game object.
            Destroy(gameObject);
        }

        // Do not destroy this object, when we load a new scene.
        DontDestroyOnLoad(gameObject);
    }
}

Você pode ver o singleton em ação, abaixo. Observe que, assim que executo a cena inicial, o objeto DataManager passa do cabeçalho específico da cena para o cabeçalho "DontDestroyOnLoad", na exibição da hierarquia.

Uma gravação de tela de várias cenas sendo carregadas, enquanto o DataManager persiste sob o cabeçalho "DoNotDestroyOnLoad".

Usando a PlayerPrefsclasse

O Unity foi construído em uma classe para gerenciar dados persistentes básicos chamadosPlayerPrefs . Quaisquer dados confirmados no PlayerPrefsarquivo persistirão nas sessões do jogo ; portanto, naturalmente, ele é capaz de persistir os dados nas cenas.

O PlayerPrefsarquivo pode armazenar variáveis ​​dos tipos string, inte float. Quando inserimos valores no PlayerPrefsarquivo, fornecemos um adicional stringcomo chave. Usamos a mesma chave para recuperar posteriormente nossos valores do PlayerPrefarquivo.

using UnityEngine;

/// <summary>Manages data for persistance between play sessions.</summary>
public class SaveManager : MonoBehaviour 
{
    /// <summary>The player's name.</summary>
    public string playerName = "";
    /// <summary>The player's score.</summary>
    public int playerScore = 0;
    /// <summary>The player's health value.</summary>
    public float playerHealth = 0f;

    /// <summary>Static record of the key for saving and loading playerName.</summary>
    private static string playerNameKey = "PLAYER_NAME";
    /// <summary>Static record of the key for saving and loading playerScore.</summary>
    private static string playerScoreKey = "PLAYER_SCORE";
    /// <summary>Static record of the key for saving and loading playerHealth.</summary>
    private static string playerHealthKey = "PLAYER_HEALTH";

    /// <summary>Saves playerName, playerScore and 
    /// playerHealth to the PlayerPrefs file.</summary>
    public void Save()
    {
        // Set the values to the PlayerPrefs file using their corresponding keys.
        PlayerPrefs.SetString(playerNameKey, playerName);
        PlayerPrefs.SetInt(playerScoreKey, playerScore);
        PlayerPrefs.SetFloat(playerHealthKey, playerHealth);

        // Manually save the PlayerPrefs file to disk, in case we experience a crash
        PlayerPrefs.Save();
    }

    /// <summary>Saves playerName, playerScore and playerHealth 
    // from the PlayerPrefs file.</summary>
    public void Load()
    {
        // If the PlayerPrefs file currently has a value registered to the playerNameKey, 
        if (PlayerPrefs.HasKey(playerNameKey))
        {
            // load playerName from the PlayerPrefs file.
            playerName = PlayerPrefs.GetString(playerNameKey);
        }

        // If the PlayerPrefs file currently has a value registered to the playerScoreKey, 
        if (PlayerPrefs.HasKey(playerScoreKey))
        {
            // load playerScore from the PlayerPrefs file.
            playerScore = PlayerPrefs.GetInt(playerScoreKey);
        }

        // If the PlayerPrefs file currently has a value registered to the playerHealthKey,
        if (PlayerPrefs.HasKey(playerHealthKey))
        {
            // load playerHealth from the PlayerPrefs file.
            playerHealth = PlayerPrefs.GetFloat(playerHealthKey);
        }
    }

    /// <summary>Deletes all values from the PlayerPrefs file.</summary>
    public void Delete()
    {
        // Delete all values from the PlayerPrefs file.
        PlayerPrefs.DeleteAll();
    }
}

Observe que tomo precauções adicionais ao manipular o PlayerPrefsarquivo:

  • Eu salvei cada chave como um private static string. Isso me permite garantir que estou sempre usando a chave certa e significa que, se eu precisar alterar a chave por qualquer motivo, não preciso garantir que altere todas as referências a ela.
  • Eu salvo o PlayerPrefsarquivo no disco depois de gravá -lo. Provavelmente, isso não fará diferença se você não implementar a persistência de dados nas sessões de reprodução. PlayerPrefs será salvo no disco durante o fechamento normal de um aplicativo, mas poderá não ser chamado naturalmente se o jogo travar.
  • Na verdade, verifico se cada chave existe no PlayerPrefs, antes de tentar recuperar um valor associado a ele. Isso pode parecer uma verificação dupla inútil, mas é uma boa prática.
  • Eu tenho um Deletemétodo que limpa imediatamente o PlayerPrefsarquivo. Se você não pretende incluir persistência de dados nas sessões de reprodução, considere chamar esse método Awake. Ao limpar o PlayerPrefsarquivo no início de cada jogo, você garante que todos os dados que se persistem desde a sessão anterior não é erroneamente tratados como os dados da atual sessão.

Você pode ver PlayerPrefsem ação abaixo. Observe que, quando clico em "Salvar dados", estou chamando o Savemétodo diretamente e, quando clico em "Carregar dados", estou chamando o Loadmétodo diretamente . Sua própria implementação provavelmente varia, mas demonstra o básico.

Uma gravação de tela dos dados persistentes passados ​​é substituída pelo inspetor, por meio das funções Salvar () e Carregar ().


Como nota final, devo salientar que você pode expandir o básico PlayerPrefs, para armazenar tipos mais úteis. O JPTheK9 fornece uma boa resposta para uma pergunta semelhante , na qual eles fornecem um script para serializar matrizes na forma de cadeia de caracteres, para serem armazenadas em um PlayerPrefsarquivo. Eles também nos apontam para o Wiki da Comunidade Unify , onde um usuário enviou um PlayerPrefsXscript mais abrangente para permitir o suporte a uma variedade maior de tipos, como vetores e matrizes.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.