Apa cara yang tepat untuk menangani data antar adegan?

52

Saya sedang mengembangkan game 2D pertama saya di Unity dan saya telah menemukan apa yang tampaknya merupakan pertanyaan penting.

Bagaimana cara saya menangani data di antara adegan?

Tampaknya ada jawaban yang berbeda untuk ini:

  • Seseorang menyebutkan menggunakan PlayerPrefs , sementara orang lain mengatakan kepada saya ini harus digunakan untuk menyimpan hal-hal lain seperti kecerahan layar dan sebagainya.

  • Seseorang mengatakan kepada saya bahwa cara terbaik adalah memastikan untuk menulis semuanya menjadi savegame setiap kali saya mengubah adegan, dan untuk memastikan bahwa ketika adegan baru dimuat, dapatkan info dari savegame lagi. Bagi saya ini tampak boros dalam kinerja. Apakah saya salah?

  • Solusi lain, yang telah saya terapkan sejauh ini adalah memiliki objek permainan global yang tidak dihancurkan di antara adegan, menangani semua data di antara adegan. Jadi saat permainan dimulai, saya memuat adegan mulai di mana objek ini dimuat. Setelah ini berakhir, ini memuat adegan permainan nyata pertama, biasanya menu utama.

Ini implementasi saya:

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

Objek ini dapat ditangani di kelas saya yang lain seperti ini:

private GameController gameController = GameController.Instance;

Meskipun ini telah bekerja sejauh ini, itu memberi saya satu masalah besar: Jika saya ingin memuat adegan secara langsung, katakanlah misalnya tingkat akhir permainan, saya tidak dapat memuatnya secara langsung, karena adegan itu tidak mengandung ini objek game global .

Apakah saya menangani masalah ini dengan cara yang salah? Adakah praktik yang lebih baik untuk tantangan semacam ini? Saya ingin mendengar pendapat, pemikiran, dan saran Anda tentang masalah ini.

Terima kasih

Tenda Enrique Moreno
sumber

Jawaban:

64

Tercantum dalam jawaban ini adalah cara mendasar untuk menangani situasi ini. Meskipun, sebagian besar metode ini tidak skala baik untuk proyek-proyek besar. Jika Anda menginginkan sesuatu yang lebih skalabel dan tidak takut tangan Anda kotor, periksa jawabannya oleh Lea Hayes tentang kerangka kerja Dependency Injection .


1. Skrip statis untuk menyimpan data saja

Anda dapat membuat skrip statis untuk menyimpan data saja. Karena itu statis, Anda tidak perlu menetapkannya ke GameObject. Anda cukup mengakses data Anda seperti ScriptName.Variable = data;dll.

Pro:

  • Tidak diperlukan instance atau singleton.
  • Anda dapat mengakses data dari mana saja di proyek Anda.
  • Tidak ada kode tambahan untuk meneruskan nilai di antara adegan.
  • Semua variabel dan data dalam satu skrip mirip-basis data memudahkan penanganannya.

Kekurangan:

  • Anda tidak akan dapat menggunakan Coroutine di dalam skrip statis.
  • Anda mungkin akan berakhir dengan garis besar variabel dalam satu kelas jika Anda tidak mengatur dengan baik.
  • Anda tidak dapat menetapkan bidang / variabel di dalam editor.

Sebuah contoh:

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

Jika Anda membutuhkan skrip untuk ditugaskan ke GameObject atau berasal dari MonoBehavior, maka Anda dapat menambahkan DontDestroyOnLoad(gameObject);baris ke kelas Anda di mana ia dapat dieksekusi satu kali (Menempatkannya Awake()adalah cara yang biasa digunakan untuk ini) .

Pro:

  • Semua pekerjaan MonoBehaviour (misalnya Coroutines) dapat dilakukan dengan aman.
  • Anda dapat menetapkan bidang di dalam editor.

Kekurangan:

  • Anda mungkin perlu menyesuaikan adegan tergantung pada skripnya.
  • Anda mungkin perlu memeriksa bagian mana yang dimuat untuk menentukan apa yang harus dilakukan dalam Pembaruan atau fungsi / metode umum lainnya. Misalnya, jika Anda melakukan sesuatu dengan UI di Pembaruan (), maka Anda perlu memeriksa apakah adegan yang benar dimuat untuk melakukan pekerjaan. Ini menyebabkan banyak pemeriksaan if-else atau switch-case.

3. PlayerPrefs

Anda dapat menerapkan ini jika Anda juga ingin data Anda disimpan bahkan jika gim ditutup.

Pro:

  • Mudah dikelola karena Unity menangani semua proses latar belakang.
  • Anda dapat mengirimkan data tidak hanya di antara adegan tetapi juga di antara instance (sesi game).

Kekurangan:

  • Menggunakan sistem file.
  • Data dapat dengan mudah diubah dari file prefs.

4. Menyimpan ke file

Ini sedikit berlebihan untuk menyimpan nilai di antara adegan. Jika Anda tidak perlu enkripsi, saya tidak menyarankan Anda menggunakan metode ini.

Pro:

  • Anda mengendalikan data yang disimpan sebagai lawan PlayerPrefs.
  • Anda dapat mengirimkan data tidak hanya di antara adegan tetapi juga di antara instance (sesi game).
  • Anda dapat mentransfer file (konsep konten yang dibuat pengguna bergantung pada ini).

Kekurangan:

  • Lambat.
  • Menggunakan sistem file.
  • Kemungkinan membaca / memuat konflik yang disebabkan oleh gangguan aliran saat menyimpan.
  • Data dapat dengan mudah diubah dari file kecuali jika Anda menerapkan enkripsi (Yang akan membuat kode lebih lambat.)

5. Pola singleton

Pola singleton adalah topik yang sangat panas dalam pemrograman berorientasi objek. Beberapa menyarankan itu, dan beberapa tidak. Cari sendiri dan lakukan panggilan yang sesuai tergantung pada kondisi proyek Anda.

Pro:

  • Mudah diatur dan digunakan.
  • Anda dapat mengakses data dari mana saja di proyek Anda.
  • Semua variabel dan data dalam satu skrip mirip-basis data memudahkan penanganannya.

Kekurangan:

  • Banyak kode boilerplate yang tugasnya hanya memelihara dan mengamankan instance tunggal.
  • Ada argumen kuat yang menentang penggunaan pola singleton . Berhati-hatilah dan lakukan riset sebelumnya.
  • Kemungkinan bentrokan data karena implementasi yang buruk.
  • Persatuan mungkin memiliki kesulitan menangani pola tunggal 1 .

1 : Dalam ringkasan OnDestroymetode Singleton Script yang disediakan di Unify Wiki , Anda dapat melihat penulis menggambarkan objek hantu yang berdarah ke dalam editor dari runtime:

Ketika Unity berhenti, ia menghancurkan objek dalam urutan acak. Pada prinsipnya, Singleton hanya dihancurkan ketika aplikasi berhenti. Jika ada skrip yang memanggil Instance setelah dihancurkan, ia akan membuat objek hantu buggy yang akan tetap berada di adegan Editor bahkan setelah berhenti memainkan Aplikasi. Sangat buruk! Jadi, ini dibuat untuk memastikan kita tidak menciptakan objek hantu kereta itu.

S. Tarık Çetin
sumber
8

Opsi yang sedikit lebih maju adalah melakukan injeksi ketergantungan dengan kerangka kerja seperti Zenject .

Ini membuat Anda bebas untuk menyusun aplikasi sesuai keinginan Anda; misalnya,

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

Anda kemudian dapat mengikat tipe ke wadah IoC (inversion of control). Dengan Zenject, tindakan ini dilakukan di dalam a MonoInstalleratau a ScriptableInstaller:

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

Contoh tunggal PlayerProfilekemudian disuntikkan ke kelas lain yang dipakai melalui Zenject. Idealnya melalui injeksi konstruktor tetapi injeksi properti dan lapangan juga dimungkinkan dengan menjelaskannya dengan Injectatribut Zenject .

Teknik atribut yang terakhir digunakan untuk secara otomatis menyuntikkan objek permainan dari adegan Anda karena Unity instantiate objek ini untuk Anda:

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


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

Untuk alasan apa pun Anda mungkin juga ingin mengikat implementasi dengan antarmuka daripada oleh jenis implementasi. (Penafian, yang berikut ini tidak seharusnya menjadi contoh yang luar biasa; Saya ragu Anda ingin menyimpan / memuat metode di lokasi tertentu ini ... tapi ini hanya menunjukkan contoh bagaimana implementasi dapat bervariasi dalam perilaku).

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

Yang kemudian dapat diikat ke wadah IoC dengan cara yang sama seperti sebelumnya:

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
sumber
3

Anda melakukan banyak hal dengan baik. Begitulah cara saya melakukannya, dan jelas cara banyak orang melakukannya karena skrip autoloader ini (Anda dapat mengatur adegan untuk memuat pertama secara otomatis setiap kali Anda menekan Play) ada: http://wiki.unity3d.com/index.php/ SceneAutoLoader

Kedua opsi pertama juga merupakan hal yang mungkin dibutuhkan gim Anda untuk menyimpan gim di antara sesi, tetapi itu adalah alat yang salah untuk masalah ini.

jhocking
sumber
Saya baru saja membaca sedikit tautan yang Anda poskan. Sepertinya ada cara untuk memuat ulang adegan inisial secara otomatis di mana saya memuat Objek Game global. Itu terlihat sedikit rumit sehingga saya perlu waktu untuk memutuskan apakah itu adalah sesuatu yang menyelesaikan masalah saya. Terima kasih atas tanggapan Anda!
Enrique Moreno Tent
Script yang saya tautkan untuk menyelesaikan masalah itu, yaitu Anda dapat menekan tombol play di adegan apa pun alih-alih harus ingat untuk beralih ke adegan startup setiap saat. Ini masih memulai permainan dari awal, daripada memulai langsung di tingkat terakhir; Anda bisa memasukkan cheat untuk memungkinkan Anda melompat ke level apa pun, atau hanya memodifikasi skrip autoload untuk meneruskan level ke permainan.
jhocking
Ya, baik. Masalahnya bukanlah "gangguan" karena harus ingat untuk beralih ke adegan awal, seperti halnya harus kembali untuk memuat level tertentu dalam pikiran. Bagaimanapun, terima kasih!
Enrique Moreno Tent
1

Cara ideal untuk menyimpan variabel antar adegan adalah melalui kelas manajer tunggal. Dengan membuat kelas untuk menyimpan data yang persisten, dan mengatur kelas itu untuk DoNotDestroyOnLoad(), Anda dapat memastikan itu segera diakses dan bertahan di antara adegan.

Opsi lain yang Anda miliki adalah menggunakan PlayerPrefskelas. PlayerPrefsdirancang untuk memungkinkan Anda menyimpan data di antara sesi bermain , tetapi tetap berfungsi sebagai sarana untuk menyimpan data di antara adegan .

Menggunakan kelas singleton dan DoNotDestroyOnLoad()

Script berikut menciptakan kelas singleton persisten. Kelas singleton adalah kelas yang dirancang untuk hanya menjalankan satu instance pada saat yang sama. Dengan menyediakan fungsionalitas seperti itu, kita dapat dengan aman membuat referensi mandiri statis, untuk mengakses kelas dari mana saja. Ini berarti Anda dapat langsung mengakses kelas DataManager.instance, termasuk variabel publik apa pun di dalam kelas.

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

Anda dapat melihat singleton beraksi, di bawah ini. Perhatikan bahwa segera setelah saya menjalankan adegan awal, objek DataManager bergerak dari heading khusus adegan ke heading "DontDestroyOnLoad", pada tampilan hierarki.

Rekaman layar dari beberapa adegan dimuat, sementara DataManager tetap di bawah tajuk "DoNotDestroyOnLoad".

Menggunakan PlayerPrefskelas

Unity memiliki built in class untuk mengelola data persisten dasar yang dipanggilPlayerPrefs . Setiap data yang dikomit ke PlayerPrefsfile akan bertahan di seluruh sesi permainan , jadi tentu saja, itu mampu bertahan data di seluruh adegan.

The PlayerPrefsFile dapat menyimpan variabel jenis string, intdan float. Ketika kami memasukkan nilai ke dalam PlayerPrefsfile, kami memberikan tambahan stringsebagai kunci. Kami menggunakan kunci yang sama untuk kemudian mengambil nilai kami dari PlayerPreffile.

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

Perhatikan bahwa saya mengambil tindakan pencegahan tambahan, saat menangani PlayerPrefsfile:

  • Saya telah menyimpan setiap tombol sebagai private static string. Ini memungkinkan saya untuk menjamin bahwa saya selalu menggunakan kunci yang benar, dan itu berarti bahwa jika saya harus mengubah kunci untuk alasan apa pun, saya tidak perlu memastikan saya mengubah semua referensi untuk itu.
  • Saya menyimpan PlayerPrefsfile ke disk setelah menulis ke sana. Ini mungkin tidak akan membuat perbedaan, jika Anda tidak menerapkan kegigihan data di seluruh sesi bermain. PlayerPrefs akan menyimpan ke disk saat aplikasi normal ditutup, tetapi mungkin tidak secara otomatis memanggil jika game Anda crash.
  • Saya benar-benar memeriksa bahwa setiap kunci ada di PlayerPrefs, sebelum saya mencoba untuk mengambil nilai yang terkait dengannya. Ini mungkin tampak seperti mengecek tidak ada gunanya, tetapi ini adalah praktik yang baik untuk dilakukan.
  • Saya punya Deletemetode yang segera menghapus PlayerPrefsfile. Jika Anda tidak ingin memasukkan data yang persisten di seluruh sesi permainan, Anda dapat mempertimbangkan untuk memanggil metode ini Awake. Dengan membersihkan PlayerPrefsfile pada awal setiap pertandingan, Anda memastikan bahwa setiap data yang tidak bertahan dari sesi sebelumnya tidak keliru ditangani sebagai data dari saat sesi.

Anda dapat melihat PlayerPrefsaksi, di bawah. Perhatikan bahwa ketika saya mengklik "Simpan Data", saya langsung memanggil Savemetode, dan ketika saya mengklik "Muat Data", saya langsung memanggil Loadmetode. Implementasi Anda sendiri kemungkinan akan bervariasi, tetapi ini menunjukkan dasar-dasarnya.

Rekaman layar dari data yang bertahan berlalu ditimpa dari inspektur, melalui fungsi Simpan () dan Muat ().


Sebagai catatan terakhir, saya harus menunjukkan bahwa Anda dapat memperluas dasar PlayerPrefs, untuk menyimpan jenis yang lebih berguna. JPTheK9 memberikan jawaban yang baik untuk pertanyaan serupa , di mana mereka menyediakan skrip untuk membuat serial array ke dalam bentuk string, untuk disimpan dalam PlayerPrefsfile. Mereka juga mengarahkan kami ke Unify Community Wiki , di mana pengguna telah mengunggah PlayerPrefsXskrip yang lebih luas untuk memungkinkan dukungan untuk berbagai jenis yang lebih besar, seperti vektor dan array.

Gnemlock
sumber