Sahneler arasında veri işlemenin doğru yolu nedir?


52

Unity'deki ilk 2D oyunumu geliştiriyorum ve önemli bir soru gibi görünen şeyle karşılaştım.

Sahneler arasındaki verileri nasıl idare ederim?

Bunun farklı cevapları var gibi görünüyor:

  • Birisi PlayerPrefs'i kullanarak bahsederken , diğer insanlar bunun ekran parlaklığı ve benzeri şeyleri saklamak için kullanılması gerektiğini söyledi.

  • Birisi bana, en iyi yolun, sahneleri değiştirdiğim her seferinde her şeyi bir oyun oyununa yazmaktan emin olmak ve yeni sahne yüklendiğinde, oyun oyunundan tekrar bilgi almasını sağlamak olduğunu söyledi. Bu bana performansta savurgan görünüyordu. Hatalı mıydım?

  • Şimdiye kadar uyguladığım diğer çözüm, sahneler arasındaki tüm verileri ele alarak sahneler arasında yok edilmeyen global bir oyun nesnesine sahip olmak. Bu yüzden oyun başladığında, bu nesnenin yüklendiği bir Start Scene yüklüyorum . Bu bittikten sonra ilk ana oyun sahnesini, genellikle ana menüyü yükler.

Bu benim uygulamam:

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

Bu nesne şu diğer sınıflarımda da kullanılabilir:

private GameController gameController = GameController.Instance;

Bu şimdiye kadar çalışmış olmasına rağmen, bana büyük bir sorun sunuyor: Doğrudan bir sahne yüklemek istersem, örneğin oyunun son seviyesini söyleyeyim, doğrudan sahne yükleyemem, çünkü o sahne bu içermiyor küresel oyun nesnesi .

Bu sorunu yanlış mı ele alıyorum? Bu tür bir zorluk için daha iyi uygulamalar var mı? Bu konudaki görüş, düşünce ve önerilerinizi duymak isterim.

Teşekkürler

Yanıtlar:


64

Bu cevapta listelenen bu durumu ele almanın temel yollarıdır. Bununla birlikte, bu yöntemlerin çoğu büyük projelere iyi ölçeklenememektedir. Daha ölçeklenebilir bir şey istiyorsanız ve ellerinizi kirletmekten korkmuyorsanız, Lea Hayes'in Bağımlılık Enjeksiyonu çerçeveleriyle ilgili yanıtlarına bakın .


1. Yalnızca verileri tutmak için statik bir komut dosyası

Yalnızca verileri tutmak için statik bir komut dosyası oluşturabilirsiniz. Statik olduğundan, bir GameObject öğesine atamanıza gerek yoktur. ScriptName.Variable = data;Vb gibi verilerinize kolayca erişebilirsiniz .

Artıları:

  • Örnek veya singleton gerekmez.
  • Projenizin her yerinden verilere erişebilirsiniz.
  • Sahneler arasında değerleri iletmek için fazladan kod yok.
  • Tek bir veritabanına benzer bir betiğin içindeki tüm değişkenler ve veriler bunları işlemeyi kolaylaştırır.

Eksileri:

  • Statik betiğin içinde bir Coroutine kullanamazsınız.
  • İyi organize edemezseniz, muhtemelen tek bir sınıfta büyük değişken satırları ile karşılaşacaksınız.
  • Editör içinde alanlar / değişkenler atayamazsınız.

Bir örnek:

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

Komut dosyanızın bir GameObject öğesine atanması veya MonoBehavior'dan türetilmesi gerekiyorsa, DontDestroyOnLoad(gameObject);sınıfınıza bir kez çalıştırılabileceği bir satır ekleyebilirsiniz (Bunu yerleştirmek Awake(), genellikle bunun için gitmenin yoludur) .

Artıları:

  • Tüm MonoBehaviour işleri (örneğin Coroutines) güvenli bir şekilde yapılabilir.
  • Editörün içindeki alanları atayabilirsiniz.

Eksileri:

  • Senaryosuna bağlı olarak sahneni ayarlaman gerekecek.
  • Muhtemelen Güncellemede veya diğer genel fonksiyonlar / yöntemlerde ne yapılacağını belirlemek için hangi secenin yüklendiğini kontrol etmeniz gerekecektir. Örneğin, Güncelleme'de () UI ile bir şey yapıyorsanız, işi yapmak için doğru sahnenin yüklenip yüklenmediğini kontrol etmeniz gerekir. Bu, if-else veya switch-case kontrollerinin yüklenmesine neden olur.

3. PlayerPrefs

Bunu, oyun kapatılsa bile verilerinizin saklanmasını istiyorsanız uygulayabilirsiniz.

Artıları:

  • Unity tüm arka plan işlemlerini gerçekleştirdiğinden yönetimi kolaydır.
  • Verileri yalnızca sahneler arasında değil, örnekler arasında da (oyun oturumları) geçirebilirsiniz.

Eksileri:

  • Dosya sistemini kullanır.
  • Veriler prefs dosyasından kolayca değiştirilebilir.

4. Bir dosyaya kaydetme

Bu, sahneler arasında değerleri depolamak için biraz overkill. Şifrelemeye ihtiyacınız yoksa, sizi bu yöntemden vazgeçiriyorum.

Artıları:

  • PlayerPrefs'in aksine kaydedilen verilerin kontrolü sizde.
  • Verileri yalnızca sahneler arasında değil, örnekler arasında da (oyun oturumları) geçirebilirsiniz.
  • Dosyayı aktarabilirsiniz (kullanıcı tarafından oluşturulan içerik konsepti buna dayanmaktadır).

Eksileri:

  • Yavaş.
  • Dosya sistemini kullanır.
  • Kaydederken akış kesintisinin neden olduğu çatışmaları okuma / yükleme imkanı.
  • Bir şifreleme uygulamadığınız sürece veriler kolayca dosyadan değiştirilebilir (Bu, kodu daha da yavaşlatır.)

5. Singleton deseni

Singleton modeli, nesne yönelimli programlamada gerçekten sıcak bir konudur. Bazıları bunu önerdi, bazıları sunmuyor. Kendiniz araştırın ve projenizin koşullarına bağlı olarak uygun çağrı yapın.

Artıları:

  • Hem kurulumu hem de kullanımı kolaydır.
  • Projenizin her yerinden verilere erişebilirsiniz.
  • Tek bir veritabanına benzer bir betiğin içindeki tüm değişkenler ve veriler bunları işlemeyi kolaylaştırır.

Eksileri:

  • Tek işi tekton örneğini korumak ve güvence altına almak için kullanılan çok sayıda kazan plakası kodu.
  • Singleton paterni kullanımına karşı güçlü argümanlar var . Dikkatli olun ve araştırmanızı önceden yapın.
  • Kötü uygulama nedeniyle veri çatışması olasılığı.
  • Birlik tekil kalıpları 1 ele almada zorluk yaşayabilir .

1 : Unify Wiki'de verilen Singleton ScriptOnDestroy metodunun özetinde, çalışma zamanından editöre sızan hayalet nesneleri tanımlayan yazarı görebilirsiniz :

Birlik bıraktığında, nesneleri rasgele bir sırayla imha eder. Prensip olarak, bir Singleton yalnızca uygulama sona erdiğinde imha edilir. Herhangi bir komut dosyası imha edildikten sonra Örnek çağırırsa, Uygulamayı oynamayı bıraktıktan sonra bile Editör sahnesinde kalacak bir buggy hayalet nesnesi oluşturacaktır. Gerçekten kötü! Böylece, bu buggy hayalet nesnesini yaratmadığımızdan emin olmak için yapıldı.


8

Biraz daha gelişmiş bir seçenek, Zenject gibi bir çerçeve ile bağımlılık enjeksiyonunu gerçekleştirmektir .

Bu, uygulamanızı istediğiniz şekilde yapılandırmakta serbest kalmanızı sağlar; Örneğin,

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

Daha sonra türü IoC (kontrolün ters çevrilmesi) kabına bağlayabilirsiniz. Zenject ile bu eylem a MonoInstallerveya a içinde gerçekleştirilir ScriptableInstaller:

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

Tekil örneği PlayerProfiledaha sonra Zenject ile örneklenen başka sınıflar içine enjekte edilir. İdeal yapıcı enjeksiyon yoluyla ancak özellik ve saha enjeksiyonu da Zenject'in Injectözelliği ile açıklamalı olarak mümkündür .

İkinci öznitelik tekniği, sahnenin oyun nesnelerini otomatik olarak enjekte etmek için kullanılır çünkü Unity bu nesneleri sizin için başlatır:

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


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

Hangi nedenle olursa olsun, bir uygulamayı uygulama türünden ziyade arayüze göre bağlamak isteyebilirsiniz. (Feragatname, aşağıdakilerin şaşırtıcı bir örnek olması gerekmiyor; bu belirli konumda Kaydet / Yükle yöntemlerini isteyeceğinizden şüpheliyim ... ancak bu, uygulamaların davranış biçiminde nasıl değişiklik gösterebileceğini gösteren bir örnek gösterir).

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

Bu da daha önce olduğu gibi IoC kabına bağlanabilir:

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

İşleri iyi bir şekilde yapıyorsun. Bunu yapma şeklim ve birçok insanın yaptığı gibi açıkça yapması çünkü bu otomatik yükleyici betiği (Play tuşuna her bastığınızda otomatik olarak ilk yüklenecek bir sahne ayarlayabilirsiniz) var: http://wiki.unity3d.com/index.php/ SceneAutoLoader

İlk iki seçeneğin her ikisi de oyunun, oturumlar arasında oyunu kaydetmek için ihtiyaç duyabileceği şeylerdir, ancak bunlar bu sorun için yanlış araçlardır.


Sadece gönderdiğiniz bağlantının bir kısmını okudum. Küresel Oyun Nesnesini yüklediğim resmi sahneyi otomatik olarak yüklemenin bir yolu var gibi görünüyor. Biraz karmaşık görünüyor, bu yüzden sorunumu çözecek bir şey olup olmadığına karar vermek için biraz zamana ihtiyacım olacak. Geri bildiriminiz için teşekkürler!
Enrique Moreno Çadır,

Bağlandığım senaryo bu sorunu çözdü, çünkü her seferinde başlangıç ​​sahnesine geçmeyi hatırlamak yerine herhangi bir sahnede oyuna vurabilirsiniz. Yine de oyuna en baştan başlar, doğrudan son seviyeden başlamak yerine; Herhangi bir seviyeye atlamanıza izin vermek için bir hile koyabilir veya seviyeyi oyuna aktarmak için otomatik kod dosyasını değiştirebilirsiniz.
05'te attı

Evet iyi. Sorun, başlangıç ​​seviyesine geçmeyi hatırlamak zorunda kalmanın “rahatsızlığı” değil, aynı zamanda belirli bir seviyeyi göz önünde bulundurmak için uğraşmak kadar değildi. Yine de teşekkürler!
Enrique Moreno Çadır

1

Değişkenleri sahneler arasında saklamanın ideal bir yolu, singleton manager sınıfıdır. Kalıcı verileri depolamak için bir sınıf oluşturarak ve o sınıfı ayarlayarak, DoNotDestroyOnLoad()ona hemen erişilebildiğinden ve sahneler arasında kalıcı olduğundan emin olabilirsiniz.

Sahip olduğunuz başka bir seçenek de PlayerPrefssınıfı kullanmak . oyun oturumları arasındaPlayerPrefs veri kaydetmenize izin vermek için tasarlanmıştır , ancak yine de sahneler arasında veri kaydetme aracı olarak işlev görecektir .

Singleton sınıfı kullanma ve DoNotDestroyOnLoad()

Aşağıdaki komut dosyası kalıcı bir singleton sınıfı oluşturur. Bir singleton sınıfı, aynı anda sadece tek bir örneği çalıştırmak için tasarlanmış bir sınıftır. Bu tür bir işlevselliği sağlayarak, sınıfa her yerden erişmek için güvenli bir statik referans oluşturabiliriz. Bu, sınıf DataManager.instanceiçindeki genel değişkenler dahil olmak üzere sınıfa doğrudan erişebileceğiniz anlamına gelir .

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

Singleton'u çalışırken görebilirsiniz. İlk sahneyi çalıştırdığımda, DataManager nesnesinin sahneye özgü başlıktan hiyerarşi görünümünde "DontDestroyOnLoad" başlığına geçtiğini unutmayın.

DataManager "DoNotDestroyOnLoad" başlığı altında kalmaya devam ederken, yüklenen birden fazla sahnenin ekran kaydı.

PlayerPrefsSınıfı kullanma

Birlik, çağrılan temel kalıcı verileri yönetmek için bir sınıf içinde yerleşiktirPlayerPrefs . PlayerPrefsDosyaya işlenen herhangi bir veri oyun oturumları boyunca sürecek , bu yüzden doğal olarak, sahneler arasında verileri devam ettirme yeteneğine sahip.

PlayerPrefsDosya türlerinin değişkenleri saklayabilir string, intve float. PlayerPrefsDosyaya değerler eklediğimizde string, anahtar olarak bir ek sağlıyoruz . Aynı anahtarı daha sonra PlayerPrefdosyadan değerlerimizi almak için kullanırız .

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

PlayerPrefsDosyayı kullanırken ek önlemler aldığımı unutmayın :

  • Her anahtarı a olarak kaydettim private static string. Bu, her zaman doğru anahtarı kullandığımı garanti etmeme izin verir ve bu, herhangi bir nedenle anahtarı değiştirmek zorunda kalırsam, tüm referansları değiştirdiğimden emin olmam gerekmediği anlamına gelir.
  • Kurtarmam PlayerPrefsyazdıktan sonra diske dosyayı. Oyun oturumlarında veri kalıcılığını uygulamazsanız, bu muhtemelen bir fark yaratmayacaktır. PlayerPrefs olacak normal bir uygulama yakın sırasında diske kaydetmek, ancak oyun çökerse doğal çağrı olmayabilir.
  • Aslında kontrol her anahtar o var içinde PlayerPrefsonunla ilişkili bir değer almak için denemeden önce. Bu anlamsız çift kontrol gibi gözükebilir, ancak olması iyi bir uygulamadır.
  • DeleteHemen PlayerPrefsdosyayı silecek bir yöntemim var . Oyun oturumları arasında veri kalıcılığı eklemeyi düşünmüyorsanız, bu yöntemi çağırmayı düşünebilirsiniz Awake. Temizleyerek PlayerPrefsher oyun başlangıcında dosya, herhangi bir veri sağlamak vermedi önceki oturumdan devam yanlışlıkla gelen veri olarak işlenmiyor geçerli oturumda.

PlayerPrefsAşağıda eylemde görebilirsiniz . "Veri Kaydet" e tıkladığımda, doğrudan Saveyöntemi çağırdığımı ve "Veri Yükle" yi tıklattığımda, Loadyöntemi doğrudan çağırdığımı unutmayın . Kendi uygulamanız büyük olasılıkla değişecektir, ancak temellerini gösterir.

Devam eden verilerin ekran kaydının, Müfettişin, Save () ve Load () işlevleri aracılığıyla üzerine yazılmasıyla geçti.


Son bir not olarak, PlayerPrefsdaha faydalı türler depolamak için temelde genişleyebileceğinizi belirtmeliyim. JPTheK9 , dizileri dizgede dizmek için bir dosyada saklamak için bir betik sağlayan benzer bir soruya iyi bir cevap sunarPlayerPrefs . Ayrıca , bir kullanıcının vektörler ve diziler gibi daha çeşitli türler için desteğe izin vermek için daha geniş bir komut dosyası yüklediği Unify Community Wiki'ye de işaret ediyorlar .PlayerPrefsX

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.