Quelle est la bonne façon de gérer les données entre les scènes?

52

Je développe mon premier jeu en 2D sous Unity et je suis tombé sur une question qui me semble importante.

Comment gérer les données entre les scènes?

Il semble y avoir différentes réponses à cela:

  • Quelqu'un a mentionné l'utilisation de PlayerPrefs , alors que d'autres personnes m'ont dit que cela devrait être utilisé pour stocker d'autres éléments tels que la luminosité de l'écran, etc.

  • Quelqu'un m'a dit que le meilleur moyen était de s'assurer de tout écrire dans une sauvegarde chaque fois que je changeais de scène, et de s'assurer que lors du chargement de la nouvelle scène, récupérait les informations à partir de la sauvegarde. Cela m'a semblé un gaspillage de performance. Étais-je trompé?

  • L’autre solution, celle que j’ai mise en œuvre jusqu’à présent, consiste à créer un objet de jeu global qui ne soit pas détruit entre les scènes et qui gère toutes les données entre les scènes. Ainsi, lorsque le jeu commence, je charge une scène de départ dans laquelle cet objet est chargé. Une fois cette opération terminée, la première scène de jeu réelle est chargée, généralement un menu principal.

Ceci est ma mise en œuvre:

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;
    //...
}

Cet objet peut être manipulé sur mes autres classes comme ceci:

private GameController gameController = GameController.Instance;

Bien que cela ait fonctionné jusqu'à présent, cela me pose un gros problème: si je veux charger directement une scène, disons par exemple le niveau final du jeu, je ne peux pas le charger directement, car cette scène ne contient pas cette objet de jeu global .

Est-ce que je traite ce problème de la mauvaise façon? Existe-t-il de meilleures pratiques pour ce type de défi? J'aimerais entendre vos opinions, vos pensées et vos suggestions sur cette question.

Merci

Tente Enrique Moreno
la source

Réponses:

64

Énumérés dans cette réponse sont les moyens fondamentaux de gérer cette situation. Bien que la plupart de ces méthodes ne s’adaptent pas bien aux grands projets. Si vous voulez quelque chose de plus évolutif et que vous n’ayez pas peur de vous salir les mains, consultez la réponse de Lea Hayes à propos des frameworks Dependency Injection .


1. Un script statique pour contenir des données uniquement

Vous pouvez créer un script statique pour contenir des données uniquement. Comme il est statique, vous n'avez pas besoin de l'assigner à un GameObject. Vous pouvez simplement accéder à vos données comme ScriptName.Variable = data;etc.

Avantages:

  • Aucune instance ou singleton requis.
  • Vous pouvez accéder aux données de partout dans votre projet.
  • Aucun code supplémentaire pour passer des valeurs entre les scènes.
  • Toutes les variables et les données contenues dans un seul script ressemblant à une base de données facilitent leur gestion.

Les inconvénients:

  • Vous ne pourrez pas utiliser de Coroutine dans le script statique.
  • Vous allez probablement vous retrouver avec d'énormes lignes de variables dans une classe si vous ne vous organisez pas bien.
  • Vous ne pouvez pas affecter de champs / variables dans l'éditeur.

Un exemple:

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

Si vous avez besoin que votre script soit assigné à un GameObject ou dérive de MonoBehavior, vous pouvez ajouter une DontDestroyOnLoad(gameObject);ligne à votre classe où il peut être exécuté une fois (le placer dans Awake()est habituellement la meilleure façon de procéder) .

Avantages:

  • Tous les travaux MonoBehaviour (par exemple Coroutines) peuvent être effectués en toute sécurité.
  • Vous pouvez affecter des champs dans l'éditeur.

Les inconvénients:

  • Vous devrez probablement ajuster votre scène en fonction du script.
  • Vous devrez probablement vérifier quel secene est chargé pour déterminer quoi faire dans Update ou dans d’autres fonctions / méthodes générales. Par exemple, si vous utilisez quelque chose avec l'interface utilisateur dans Update (), vous devez vérifier si la bonne scène est chargée pour effectuer le travail. Cela entraîne des charges de vérifications if-else ou de cas de commutation.

3. PlayerPrefs

Vous pouvez l'implémenter si vous souhaitez également que vos données soient stockées, même si le jeu est fermé.

Avantages:

  • Facile à gérer puisque Unity gère tout le processus d'arrière-plan.
  • Vous pouvez transmettre des données non seulement entre scènes mais aussi entre instances (sessions de jeu).

Les inconvénients:

  • Utilise le système de fichiers.
  • Les données peuvent facilement être modifiées à partir du fichier prefs.

4. Enregistrement dans un fichier

C'est un peu excessif pour stocker des valeurs entre les scènes. Si vous n'avez pas besoin de cryptage, je vous décourage de cette méthode.

Avantages:

  • Vous contrôlez les données enregistrées par opposition à PlayerPrefs.
  • Vous pouvez transmettre des données non seulement entre scènes mais aussi entre instances (sessions de jeu).
  • Vous pouvez transférer le fichier (le concept de contenu généré par l'utilisateur en dépend).

Les inconvénients:

  • Lent.
  • Utilise le système de fichiers.
  • Possibilité de lecture / chargement des conflits causés par une interruption de flux lors de la sauvegarde.
  • Les données peuvent facilement être modifiées à partir du fichier, sauf si vous implémentez un cryptage (ce qui ralentira encore le code).

5. Motif Singleton

Singleton Pattern est un sujet d'actualité dans la programmation orientée objet. Certains le suggèrent, d'autres non. Étudiez-le vous-même et appelez-le en fonction des conditions de votre projet.

Avantages:

  • Facile à installer et à utiliser.
  • Vous pouvez accéder aux données de partout dans votre projet.
  • Toutes les variables et les données contenues dans un seul script ressemblant à une base de données facilitent leur gestion.

Les inconvénients:

  • Beaucoup de code standard dont le seul travail est de maintenir et de sécuriser l'instance singleton.
  • Il existe de solides arguments contre l’utilisation du motif singleton . Soyez prudent et faites vos recherches à l'avance.
  • Possibilité de conflit de données en raison d'une mauvaise mise en œuvre.
  • Unity peut avoir des difficultés à gérer les motifs de singleton 1 .

1 : Dans le résumé de la OnDestroyméthode de script singleton fourni dans Unify Wiki , vous pouvez voir l’auteur décrivant les objets fantômes qui pénètrent dans l’éditeur à partir de l’exécution:

Quand Unity quitte, il détruit les objets dans un ordre aléatoire. En principe, un Singleton n'est détruit qu'à la fermeture de l'application. Si un script appelle Instance après sa destruction, il créera un objet fantôme défectueux qui restera sur la scène de l'éditeur, même après avoir arrêté la lecture de l'application. Vraiment mauvais! Donc, ceci a été fait pour nous assurer que nous ne créons pas cet objet fantôme buggy.

S. Tarık Çetin
la source
8

Une option légèrement plus avancée consiste à effectuer une injection de dépendance avec un framework tel que Zenject .

Cela vous laisse libre de structurer votre application comme bon vous semble; par exemple,

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

Vous pouvez ensuite lier le type au conteneur IoC (inversion of control). Avec Zenject, cette action est effectuée à l'intérieur d'un MonoInstallerou d'un ScriptableInstaller:

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

L'instance singleton de PlayerProfileest ensuite injectée dans d'autres classes instanciées via Zenject. Idéalement par injection de constructeur, l'injection de propriété et de champ est également possible en les annotant avec l' Injectattribut de Zenject .

La dernière technique d'attribut est utilisée pour injecter automatiquement les objets de jeu de votre scène puisque Unity instancie ces objets pour vous:

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


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

Pour une raison quelconque, vous pouvez également vouloir lier une implémentation par interface plutôt que par type d'implémentation. (Avertissement, ce qui suit n'est pas censé être un exemple étonnant; je doute que vous souhaitiez des méthodes de sauvegarde / chargement à cet emplacement particulier ... mais cela montre simplement un exemple de la façon dont le comportement des implémentations peut varier).

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()
    {
        ...
    }
}

Ce qui peut ensuite être lié au conteneur IoC de la même manière qu'auparavant:

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,
    }
}
Lea Hayes
la source
3

Vous faites les choses dans le bon sens. C’est ce que je fais et ce que beaucoup de gens font parce que ce script d’autochargeur (vous pouvez définir le chargement automatique d’une scène à chaque fois que vous appuyez sur Play) existe: http://wiki.unity3d.com/index.php/ SceneAutoLoader

Les deux premières options sont également des éléments dont votre jeu a peut-être besoin pour le sauvegarder entre les sessions, mais ce sont de mauvais outils pour résoudre ce problème.

jhocking
la source
Je viens de lire un peu du lien que vous avez posté. On dirait qu'il existe un moyen de charger automatiquement la scène inicial où je charge l'objet de jeu global. Cela semble un peu complexe, il me faudra donc un peu de temps pour décider si cela résout mon problème. Merci pour vos commentaires!
Tente Enrique Moreno
Le script auquel je me suis associé résout en quelque sorte ce problème, en ce sens que vous pouvez jouer dans n'importe quelle scène plutôt que de devoir vous rappeler de basculer vers la scène de démarrage à chaque fois. Il commence toujours le jeu depuis le début, plutôt que de commencer directement dans le dernier niveau; vous pouvez tricher pour vous permettre de passer à n’importe quel niveau, ou simplement modifier le script de chargement automatique pour transmettre le niveau au jeu.
Jalousie
Ouais bien. Le problème n'était pas tant le "désagrément" de devoir se rappeler de basculer vers la scène de départ que de devoir se déplacer pour charger le niveau spécifique à l'esprit. Merci quand même!
Tente Enrique Moreno
1

Un moyen idéal de stocker des variables entre les scènes consiste à utiliser une classe de gestionnaire singleton. En créant une classe pour stocker des données persistantes et en lui affectant une valeur DoNotDestroyOnLoad(), vous pouvez vous assurer qu'elle est immédiatement accessible et persiste entre les scènes.

Une autre option consiste à utiliser la PlayerPrefsclasse. PlayerPrefsest conçu pour vous permettre de sauvegarder des données entre les sessions de lecture , mais il servira toujours de moyen de sauvegarder des données entre des scènes .

Utiliser une classe singleton et DoNotDestroyOnLoad()

Le script suivant crée une classe singleton persistante. Une classe singleton est une classe conçue pour n'exécuter qu'une seule instance à la fois. En fournissant une telle fonctionnalité, nous pouvons créer en toute sécurité une référence de soi statique, pour accéder à la classe de n'importe où. Cela signifie que vous pouvez accéder directement à la classe avec DataManager.instance, y compris toutes les variables publiques de la 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);
    }
}

Vous pouvez voir le singleton en action, ci-dessous. Notez que dès que j'exécute la scène initiale, l'objet DataManager passe de l'en-tête spécifique à la scène à l'en-tête "DontDestroyOnLoad", dans la vue hiérarchique.

Un enregistrement d'écran de plusieurs scènes en cours de chargement, tandis que le DataManager persiste sous l'en-tête "DoNotDestroyOnLoad".

Utiliser la PlayerPrefsclasse

Unity a une classe intégrée pour gérer les données persistantes de base appeléesPlayerPrefs . Toutes les données validées dans le PlayerPrefsfichier persisteront au cours des sessions de jeu . Il est donc naturellement capable de conserver des données dans des scènes.

Le PlayerPrefsfichier peut stocker des variables de types string, intet float. Lorsque nous insérons des valeurs dans le PlayerPrefsfichier, nous fournissons une stringclé supplémentaire . Nous utilisons la même clé pour extraire ultérieurement nos valeurs du PlayerPreffichier.

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();
    }
}

Notez que je prends des précautions supplémentaires lors de la manipulation du PlayerPrefsfichier:

  • J'ai enregistré chaque clé en tant que private static string. Cela me permet de garantir que j'utilise toujours la bonne clé. Cela signifie que si je dois changer la clé pour une raison quelconque, je n'ai pas besoin de changer toutes les références à la clé.
  • Je sauvegarde le PlayerPrefsfichier sur le disque après y avoir écrit. Cela ne fera probablement pas de différence si vous n'implémentez pas la persistance des données dans les sessions de lecture. PlayerPrefs sera sauvegardé sur le disque lors d’une fermeture d’application normale, mais il se peut qu’il n’appelle pas naturellement si votre jeu plante.
  • Je vérifie réellement que chaque clé existe dans le PlayerPrefs, avant d'essayer de récupérer une valeur qui lui est associée. Cela peut sembler une double vérification inutile, mais c'est une bonne pratique.
  • J'ai une Deleteméthode qui efface immédiatement le PlayerPrefsfichier. Si vous n'avez pas l'intention d'inclure la persistance des données entre les sessions de lecture, vous pouvez envisager d'appeler cette méthode sur Awake. En effaçant le PlayerPrefsfichier au début de chaque jeu, assurez - vous que toutes les données qui ne persistent de la session précédente ne sont pas traitées à tort que les données de la actuelle session.

Vous pouvez voir PlayerPrefsen action, ci-dessous. Notez que lorsque je clique sur "Enregistrer les données", j'appelle directement la Saveméthode et lorsque je clique sur "Charger les données", j'appelle directement la Loadméthode. Votre propre implémentation variera probablement, mais elle démontrera les bases.

Un enregistrement d'écran des données persistantes transmises à l'inspecteur est écrasé via les fonctions Save () et Load ().


Pour terminer, je dois souligner que vous pouvez développer la base PlayerPrefspour stocker des types plus utiles. JPTheK9 fournit une bonne réponse à une question similaire , dans laquelle ils fournissent un script pour la sérialisation de tableaux sous forme de chaîne, à stocker dans un PlayerPrefsfichier. Ils nous indiquent également le wiki de la communauté Unify , où un utilisateur a téléchargé un PlayerPrefsXscript plus volumineux pour permettre la prise en charge d'une plus grande variété de types, tels que les vecteurs et les tableaux.

Gnemlock
la source