シーン間でデータを処理する適切な方法は何ですか?


52

私はUnityで最初の2Dゲームを開発していますが、重要な質問と思われるものに出会いました。

シーン間でデータを処理するにはどうすればよいですか?

これには異なる答えがあるようです:

  • PlayerPrefsの使用に言及している人もいれば、画面の明るさなどのその他のものを保存するために使用すべきだと他の人から言われました。

  • 誰かが私に、シーンを変更するたびにセーブゲームにすべてを書き込み、新しいシーンがロードされたときに必ずセーブゲームから情報を取得するようにすることだと言った。これはパフォーマンスが無駄に思えました。私は間違っていましたか?

  • もう1つのソリューションは、これまでに実装したものですが、シーン間で破壊されず、シーン間のすべてのデータを処理するグローバルゲームオブジェクト用意することです。したがって、ゲームが開始されると、このオブジェクトがロードされる開始シーンをロードします。これが終了すると、最初の実際のゲームシーン(通常はメインメニュー)が読み込まれます。

これは私の実装です:

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

このオブジェクトは、他のクラスで次のように処理できます。

private GameController gameController = GameController.Instance;

これは今までのところうまくいきましたが、1つの大きな問題があります:シーンを直接ロードしたい場合、たとえばゲームの最終レベルは、シーンにはこれが含まれていないため、直接ロードできませんグローバルゲームオブジェクト

この問題を間違った方法で処理していますか?この種の挑戦のためのより良い実践はありますか?この問題に関するご意見、ご意見、ご提案をお聞かせください。

ありがとう

回答:


64

この答えにリストされているのは、この状況を処理する基本的な方法です。ただし、これらの方法のほとんどは、大規模なプロジェクトにうまく対応できません。よりスケーラブルなものが必要で、手を汚すことを恐れない場合は、Lea HayesによるDependency Injectionフレームワークに関する回答をご覧ください。


1.データのみを保持する静的スクリプト

データのみを保持する静的スクリプトを作成できます。静的であるため、GameObjectに割り当てる必要はありません。ScriptName.Variable = data;などのデータに簡単にアクセスできます。

長所:

  • インスタンスやシングルトンは必要ありません。
  • プロジェクトのどこからでもデータにアクセスできます。
  • シーン間で値を渡すための余分なコードはありません。
  • 1つのデータベースのようなスクリプト内のすべての変数とデータにより、それらを簡単に処理できます。

短所:

  • 静的スクリプト内でコルーチンを使用することはできません。
  • うまく整理しないと、おそらく単一のクラスに膨大な数の変数行ができてしまいます。
  • エディター内でフィールド/変数を割り当てることはできません。

例:

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

スクリプトをGameObjectに割り当てるか、MonoBehaviorから派生させる必要がある場合はDontDestroyOnLoad(gameObject);、クラスに行を追加して1回実行できるようにします(これを配置するのAwake()が通常の方法です)

長所:

  • すべてのMonoBehaviourジョブ(コルーチンなど)は安全に実行できます。
  • エディター内でフィールドを割り当てることができます。

短所:

  • おそらく、スクリプトに応じてシーンを調整する必要があります。
  • おそらく、どのseceneがロードされているかを確認して、Updateまたはその他の一般的な関数/メソッドで何をするかを決定する必要があります。たとえば、Update()のUIで何かをしている場合、ジョブを実行するために正しいシーンがロードされているかどうかを確認する必要があります。これにより、if-elseまたはswitch-caseチェックがロードされます。

3. PlayerPrefs

ゲームが終了した場合でもデータを保存したい場合は、これを実装できます。

長所:

  • Unityはすべてのバックグラウンドプロセスを処理するため、管理が簡単です。
  • シーン間だけでなく、インスタンス間(ゲームセッション)でもデータを渡すことができます。

短所:

  • ファイルシステムを使用します。
  • データはprefsファイルから簡単に変更できます。

4.ファイルへの保存

これは、シーン間で値を保存するには少々やり過ぎです。暗号化が必要ない場合は、この方法をお勧めしません。

長所:

  • PlayerPrefsとは対照的に、保存されるデータを制御します。
  • シーン間だけでなく、インスタンス間(ゲームセッション)でもデータを渡すことができます。
  • ファイルを転送できます(ユーザー生成コンテンツの概念はこれに依存します)。

短所:

  • スロー。
  • ファイルシステムを使用します。
  • 保存中のストリームの中断によって引き起こされる読み込み/読み込みの競合の可能性。
  • 暗号化を実装しない限り、ファイルからデータを簡単に変更できます(コードをさらに遅くします)。

5.シングルトンパターン

シングルトンパターンは、オブジェクト指向プログラミングの非常にホットなトピックです。提案する人もいれば、しない人もいます。自分で調べて、プロジェクトの条件に応じて適切な呼び出しを行ってください。

長所:

  • セットアップと使用の両方が簡単です。
  • プロジェクトのどこからでもデータにアクセスできます。
  • 1つのデータベースのようなスクリプト内のすべての変数とデータにより、それらを簡単に処理できます。

短所:

  • 唯一の仕事がシングルトンインスタンスを維持および保護することである定型コードの多く。
  • シングルトンパターンの使用に強い議論があります。事前に注意して研究を行ってください。
  • 実装が不十分なため、データが衝突する可能性があります。
  • Unityでは、シングルトンパターン1の処理が困難な場合があります。

1Unify Wiki提供されるシングルトンスクリプトOnDestroyメソッドの要約では、ランタイムからエディターにブリードするゴーストオブジェクトを説明する著者を見ることができます。

Unityが終了すると、オブジェクトはランダムな順序で破棄されます。原則として、シングルトンはアプリケーションが終了したときにのみ破棄されます。スクリプトが破棄された後にインスタンスを呼び出すと、アプリケーションの再生を停止した後でもエディターシーンに残るバグのあるゴーストオブジェクトが作成されます。すごく悪い!そのため、これは、そのバギーゴーストオブジェクトを作成していないことを確認するために行われました。


8

やや高度なオプションは、Zenjectのようなフレームワークで依存性注入を実行することです

これにより、自由にアプリケーションを構築できます。例えば、

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

その後、タイプをIoC(コントロールの反転)コンテナーにバインドできます。Zenjectを使用すると、このアクションはMonoInstallerまたは内で実行されScriptableInstallerます。

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

のシングルトンインスタンスPlayerProfileは、Zenjectを介してインスタンス化される他のクラスに注入されます。理想的にはコンストラクター注入を使用しますが、プロパティとフィールドの注入はZenjectのInject属性で注釈を付けることによっても可能です。

後者の属性手法は、Unityがこれらのオブジェクトをインスタンス化するため、シーンのゲームオブジェクトを自動的に注入するために使用されます。

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


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

何らかの理由で、実装タイプではなくインターフェイスで実装をバインドすることもできます。(免責事項、次の例は驚くべき例ではありません。この特定の場所でSave / Loadメソッドが必要になるとは思いませんが、これは実装の動作の変化の例を示しています)。

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

これは、以前と同様の方法でIoCコンテナーにバインドできます。

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

あなたは良い方法で物事をやっています。これは私が行う方法であり、明らかにこのオートローダースクリプト(Playを押すたびに最初にシーンを自動的にロードするように設定できます)が存在するため、多くの人が行う方法です:http : //wiki.unity3d.com/index.php/ SceneAutoLoader

最初の2つのオプションはどちらも、セッション間でゲームを保存するためにゲームで必要になる場合がありますが、これらはこの問題の間違ったツールです。


あなたが投稿したリンクを少し読みました。グローバルゲームオブジェクトを読み込んでいる初期シーンを自動読み込みする方法があるようです。それは少し複雑に見えるので、それが私の問題を解決するものであるかどうかを判断するのに時間が必要です。ご意見ありがとうございます!
エンリケモレノテント

私がリンクしたスクリプトはその問題を解決します。毎回スタートアップシーンに切り替えることを忘れずに、どのシーンでもプレイをヒットできます。ただし、最後のレベルで直接開始するのではなく、まだ最初からゲームを開始します。チートを入れて任意のレベルにスキップできるようにするか、オートロードスクリプトを変更してレベルをゲームに渡すだけです。
-jhocking

ええ、まあ。問題は、特定のレベルを念頭に置くためにハックしなければならないのと同じくらい、開始シーンに切り替えることを覚えなければならないという「迷惑」ではありませんでした。とにかくありがとう!
エンリケモレノテント

1

シーン間で変数を保存する理想的な方法は、シングルトンマネージャークラスを使用することです。永続データを保存するクラスを作成し、そのクラスをDoNotDestroyOnLoad()に設定することで、すぐにアクセスでき、シーン間で永続することを保証できます。

もう1つの選択肢は、PlayerPrefsクラスを使用することです。プレイセッション間でPlayerPrefsデータを保存できるように設計されていますがシーン間でデータを保存する手段としてまだ機能します。

シングルトンクラスを使用し、 DoNotDestroyOnLoad()

次のスクリプトは、永続的なシングルトンクラスを作成します。シングルトンクラスは、同時に1つのインスタンスのみを実行するように設計されたクラスです。このような機能を提供することで、静的な自己参照を安全に作成し、どこからでもクラスにアクセスできます。これはDataManager.instance、クラス内のすべてのパブリック変数を含めて、クラスに直接アクセスできることを意味します。

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

以下のシングルトンの動作を確認できます。最初のシーンを実行すると、DataManagerオブジェクトが階層ビューでシーン固有の見出しから「DontDestroyOnLoad」見出しに移動することに注意してください。

DataManagerが「DoNotDestroyOnLoad」という見出しの下に保持されている間、ロードされる複数のシーンの画面記録。

PlayerPrefsクラスを使用する

Unityには、という基本的な永続データを管理する組み込みクラスがありますPlayerPrefsPlayerPrefsファイルにコミットされたデータはゲームセッション全体で保持されるため、当然、シーン全体でデータを保持できます。

PlayerPrefsファイルは、種類の変数を格納することができstringintそしてfloatPlayerPrefsファイルに値を挿入するときstring、キーとして追加を提供します。同じキーを使用して、後でPlayerPrefファイルから値を取得します。

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

PlayerPrefsファイルを処理する際には、追加の予防措置を講じていることに注意してください。

  • 各キーをとして保存しましたprivate static string。これにより、常に正しいキーを使用していることを保証できます。つまり、何らかの理由でキーを変更する必要がある場合、そのキーへのすべての参照を変更する必要はありません。
  • PlayerPrefsファイルに書き込んだ後、ファイルをディスクに保存します。再生セッション間でデータの永続化を実装しない場合、これはおそらく違いを生みません。PlayerPrefs 、通常のアプリケーションのクローズ時にディスクに保存しますが、あなたのゲームがクラッシュした場合、それは自然に呼び出すことはできません。
  • 実際に、キーに関連付けられた値を取得しようとする前に、各キーがに存在することを確認します。これは無意味なダブルチェックのように思えるかもしれませんが、持っておくのは良い習慣です。PlayerPrefs
  • ファイルDeleteをすぐに消去する方法がありPlayerPrefsます。プレイセッション全体でデータの永続性を含めるつもりがない場合は、でこのメソッドを呼び出すことを検討してくださいAwake。クリアすることにより、PlayerPrefs各ゲームの開始時にファイルを、あなたはどのようなデータていることを確認しなかった前のセッションからの持続が誤ってからデータとして扱われていない現在のセッション。

PlayerPrefs以下の動作をご覧ください。[データの保存]をクリックすると、Saveメソッドが直接呼び出され、[データの読み込み]をクリックすると、メソッドが直接呼び出されることに注意してくださいLoad。実装方法はさまざまですが、基本を示しています。

Save()およびLoad()関数を介して、インスペクターから上書きされた、永続的なデータの画面記録。


最後に、基本的なを拡張して、PlayerPrefsより便利な型を格納できることを指摘する必要があります。JPTheK9 は、同様の質問に対する適切な回答を提供します。JPTheK9 は、配列を文字列形式にシリアル化し、PlayerPrefsファイルに保存するためのスクリプトを提供します。彼らはまたに私たちを指す統一コミュニティのWikiユーザーはより拡張アップロードしたPlayerPrefsXスクリプトをそのようなベクターや配列などの種類、の大きい品種のサポートを可能にするために。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.