Cách thích hợp để xử lý dữ liệu giữa các cảnh là gì?


52

Tôi đang phát triển trò chơi 2D đầu tiên của mình trong Unity và tôi đã bắt gặp những gì có vẻ là một câu hỏi quan trọng.

Làm cách nào để xử lý dữ liệu giữa các cảnh?

Dường như có câu trả lời khác nhau cho điều này:

  • Ai đó đề cập đến việc sử dụng PlayerPrefs , trong khi những người khác nói với tôi rằng điều này nên được sử dụng để lưu trữ những thứ khác như độ sáng màn hình, v.v.

  • Có người nói với tôi rằng cách tốt nhất là đảm bảo ghi tất cả mọi thứ vào một trò chơi lưu trữ mỗi khi tôi thay đổi cảnh và để đảm bảo rằng khi cảnh mới tải, hãy lấy lại thông tin từ trò chơi lưu trữ. Điều này dường như lãng phí trong hiệu suất. Là tôi sai?

  • Giải pháp khác, đó là giải pháp tôi đã triển khai cho đến nay là có một đối tượng trò chơi toàn cầu không bị phá hủy giữa các cảnh, xử lý tất cả dữ liệu giữa các cảnh. Vì vậy, khi trò chơi bắt đầu, tôi tải Cảnh bắt đầu nơi đối tượng này được tải. Sau khi kết thúc, nó tải cảnh trò chơi thực sự đầu tiên, thường là một menu chính.

Đây là triển khai của tôi:

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

Đối tượng này có thể được xử lý trên các lớp khác của tôi như thế này:

private GameController gameController = GameController.Instance;

Mặc dù điều này đã hoạt động cho đến nay, nhưng nó cho tôi một vấn đề lớn: Nếu tôi muốn tải trực tiếp một cảnh, ví dụ như cấp độ cuối cùng của trò chơi, tôi không thể tải trực tiếp, vì cảnh đó không chứa điều này đối tượng trò chơi toàn cầu .

Tôi đang xử lý vấn đề này sai cách? Có thực hành tốt hơn cho loại thách thức này? Tôi rất thích nghe ý kiến, suy nghĩ và đề xuất của bạn về vấn đề này.

Cảm ơn

Câu trả lời:


64

Được liệt kê trong câu trả lời này là những cách cơ bản để xử lý tình huống này. Mặc dù, hầu hết các phương pháp này không mở rộng tốt cho các dự án lớn. Nếu bạn muốn một cái gì đó có khả năng mở rộng hơn và không sợ bị bẩn tay, hãy xem câu trả lời của Lea Hayes về các khuôn khổ Dependency Injection .


1. Tập lệnh tĩnh chỉ giữ dữ liệu

Bạn có thể tạo một tập lệnh tĩnh để chỉ giữ dữ liệu. Vì nó là tĩnh, bạn không cần gán nó cho GameObject. Bạn chỉ có thể truy cập dữ liệu của bạn như ScriptName.Variable = data;vv

Ưu điểm:

  • Không yêu cầu hoặc đơn lẻ.
  • Bạn có thể truy cập dữ liệu từ mọi nơi trong dự án của bạn.
  • Không có mã bổ sung để vượt qua các giá trị giữa các cảnh.
  • Tất cả các biến và dữ liệu trong một tập lệnh giống như cơ sở dữ liệu giúp dễ dàng xử lý chúng.

Nhược điểm:

  • Bạn sẽ không thể sử dụng một Coroutine bên trong tập lệnh tĩnh.
  • Bạn có thể sẽ kết thúc với các dòng biến lớn trong một lớp nếu bạn không tổ chức tốt.
  • Bạn không thể gán các trường / biến trong trình chỉnh sửa.

Một ví dụ:

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

Nếu bạn cần tập lệnh của mình được gán cho GameObject hoặc xuất phát từ MonoBehavior, thì bạn có thể thêm DontDestroyOnLoad(gameObject);dòng vào lớp của mình, nơi nó có thể được thực thi một lần (Đặt nó vào Awake()theo cách thông thường là cách này) .

Ưu điểm:

  • Tất cả các công việc MonoBehaviour (ví dụ Coroutines) có thể được thực hiện một cách an toàn.
  • Bạn có thể gán các trường bên trong trình chỉnh sửa.

Nhược điểm:

  • Bạn có thể sẽ cần phải điều chỉnh cảnh của bạn tùy thuộc vào kịch bản.
  • Bạn có thể sẽ cần kiểm tra xem secene nào được tải để xác định những việc cần làm trong Cập nhật hoặc các chức năng / phương thức chung khác. Ví dụ: nếu bạn đang làm gì đó với UI trong Update (), thì bạn cần kiểm tra xem cảnh chính xác có được tải để thực hiện công việc không. Điều này gây ra vô số kiểm tra if-other hoặc switch-case.

3. PlayerPrefs

Bạn có thể thực hiện điều này nếu bạn cũng muốn dữ liệu của mình được lưu trữ ngay cả khi trò chơi bị đóng.

Ưu điểm:

  • Dễ dàng quản lý vì Unity xử lý tất cả quá trình nền.
  • Bạn có thể truyền dữ liệu không chỉ giữa các cảnh mà còn giữa các trường hợp (phiên trò chơi).

Nhược điểm:

  • Sử dụng hệ thống tập tin.
  • Dữ liệu có thể dễ dàng được thay đổi từ tập tin prefs.

4. Lưu vào một tập tin

Đây là một chút quá mức để lưu trữ giá trị giữa các cảnh. Nếu bạn không cần mã hóa, tôi không khuyến khích bạn sử dụng phương pháp này.

Ưu điểm:

  • Bạn đang kiểm soát dữ liệu được lưu trái ngược với PlayerPrefs.
  • Bạn có thể truyền dữ liệu không chỉ giữa các cảnh mà còn giữa các trường hợp (phiên trò chơi).
  • Bạn có thể chuyển tệp (khái niệm nội dung do người dùng tạo dựa trên điều này).

Nhược điểm:

  • Chậm.
  • Sử dụng hệ thống tập tin.
  • Khả năng đọc / tải xung đột do gián đoạn luồng trong khi lưu.
  • Dữ liệu có thể dễ dàng được thay đổi từ tệp trừ khi bạn thực hiện mã hóa (Điều này sẽ khiến mã chậm hơn.)

5. Mẫu đơn

Mẫu đơn là một chủ đề thực sự nóng trong lập trình hướng đối tượng. Một số gợi ý nó, và một số thì không. Tự nghiên cứu và thực hiện cuộc gọi phù hợp tùy thuộc vào điều kiện dự án của bạn.

Ưu điểm:

  • Dễ dàng để thiết lập và sử dụng.
  • Bạn có thể truy cập dữ liệu từ mọi nơi trong dự án của bạn.
  • Tất cả các biến và dữ liệu trong một tập lệnh giống như cơ sở dữ liệu giúp dễ dàng xử lý chúng.

Nhược điểm:

  • Rất nhiều mã soạn sẵn mà công việc duy nhất của họ là duy trì và bảo mật cá thể singleton.
  • Có những lập luận mạnh mẽ chống lại việc sử dụng mô hình singleton . Hãy thận trọng và thực hiện nghiên cứu của bạn trước.
  • Khả năng đụng độ dữ liệu do thực hiện kém.
  • Unity có thể gặp khó khăn khi xử lý các mẫu đơn 1 .

1 : Trong bản tóm tắt OnDestroyphương pháp của Singleton Script được cung cấp trong Unify Wiki , bạn có thể thấy tác giả mô tả các đối tượng ma chảy vào trình chỉnh sửa từ thời gian chạy:

Khi Unity thoát, nó phá hủy các đối tượng theo thứ tự ngẫu nhiên. Về nguyên tắc, một Singleton chỉ bị hủy khi ứng dụng thoát. Nếu bất kỳ tập lệnh nào gọi Instance sau khi nó bị phá hủy, nó sẽ tạo ra một đối tượng ma lỗi sẽ ở lại trong trình soạn thảo ngay cả khi đã dừng chơi Ứng dụng. Thực sự tồi tệ! Vì vậy, điều này đã được thực hiện để đảm bảo chúng tôi không tạo ra vật thể ma lỗi đó.


8

Một tùy chọn nâng cao hơn một chút là thực hiện tiêm phụ thuộc với khung như Zenject .

Điều này cho phép bạn tự do cấu trúc ứng dụng của mình theo cách bạn muốn; ví dụ,

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

Sau đó, bạn có thể liên kết loại với bộ chứa IoC (đảo ngược điều khiển). Với Zenject, hành động này được thực hiện bên trong a MonoInstallerhoặc a ScriptableInstaller:

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

Ví dụ đơn lẻ PlayerProfilesau đó được đưa vào các lớp khác được khởi tạo thông qua Zenject. Lý tưởng nhất là thông qua hàm tạo, nhưng cũng có thể tiêm thuộc tính và trường bằng cách chú thích chúng với Injectthuộc tính của Zenject .

Kỹ thuật thuộc tính thứ hai được sử dụng để tự động chèn các đối tượng trò chơi trong cảnh của bạn kể từ khi Unity khởi tạo các đối tượng này cho bạn:

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


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

Vì bất kỳ lý do gì, bạn cũng có thể muốn ràng buộc một triển khai bằng giao diện chứ không phải bởi loại thực hiện. (Tuyên bố miễn trừ trách nhiệm, đây không phải là một ví dụ tuyệt vời; tôi nghi ngờ bạn muốn các phương thức Lưu / Tải ở vị trí cụ thể này ... nhưng điều này chỉ cho thấy một ví dụ về cách triển khai có thể khác nhau trong hành vi).

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

Mà sau đó có thể được liên kết với bộ chứa IoC theo cách tương tự như trước đây:

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

Bạn đang làm mọi thứ một cách tốt. Đó là cách tôi làm và rõ ràng là cách nhiều người thực hiện vì tập lệnh tự động tải này (bạn có thể đặt cảnh để tự động tải trước mỗi khi bạn nhấn Play) tồn tại: http://wiki.unity3d.com/index.php/ CảnhAutoLoader

Cả hai tùy chọn đầu tiên cũng là những thứ mà trò chơi của bạn có thể cần để lưu trò chơi giữa các phiên, nhưng đó là những công cụ sai cho vấn đề này.


Tôi chỉ đọc một chút các liên kết bạn đăng. Có vẻ như có một cách để tự động tải cảnh nội bộ nơi tôi đang tải Đối tượng trò chơi toàn cầu. Có vẻ hơi phức tạp vì vậy tôi sẽ cần một chút thời gian để quyết định xem nó có phải là thứ giải quyết vấn đề của tôi không. Cảm ơn phản hồi của bạn!
Lều Enrique Moreno

Kịch bản tôi liên kết để giải quyết vấn đề đó, trong đó bạn có thể nhấn play trong bất kỳ cảnh nào thay vì phải nhớ chuyển sang cảnh khởi động mỗi lần. Nó vẫn bắt đầu trò chơi từ đầu, thay vì bắt đầu trực tiếp ở cấp độ cuối cùng; bạn có thể đặt một mánh gian lận để cho phép bạn bỏ qua bất kỳ cấp độ nào, hoặc chỉ sửa đổi tập lệnh tự động tải để chuyển cấp cho trò chơi.
jhocking

Vâng tốt. Rắc rối không phải là "sự phiền toái" của việc phải nhớ chuyển sang cảnh bắt đầu, cũng như phải hack xung quanh để tải mức độ cụ thể trong tâm trí. Dù sao cũng cảm ơn bạn!
Lều Enrique Moreno

1

Một cách lý tưởng để lưu trữ các biến giữa các cảnh là thông qua lớp trình quản lý đơn. Bằng cách tạo một lớp để lưu trữ dữ liệu liên tục và đặt lớp đó thành DoNotDestroyOnLoad(), bạn có thể đảm bảo nó có thể truy cập ngay lập tức và tồn tại giữa các cảnh.

Một lựa chọn khác bạn có là sử dụng PlayerPrefslớp. PlayerPrefsđược thiết kế để cho phép bạn lưu dữ liệu giữa các phiên phát , nhưng nó vẫn sẽ phục vụ như một phương tiện để lưu dữ liệu giữa các cảnh .

Sử dụng một lớp đơn và DoNotDestroyOnLoad()

Kịch bản sau đây tạo ra một lớp singleton liên tục. Một lớp singleton là một lớp được thiết kế để chỉ chạy một thể hiện cùng một lúc. Bằng cách cung cấp chức năng như vậy, chúng ta có thể tạo một tham chiếu tự tĩnh một cách an toàn, để truy cập lớp từ bất cứ đâu. Điều này có nghĩa là bạn có thể truy cập trực tiếp vào lớp DataManager.instance, bao gồm mọi biến công khai trong lớp.

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

Bạn có thể thấy singleton trong hành động, dưới đây. Lưu ý rằng ngay khi tôi chạy cảnh ban đầu, đối tượng DataManager chuyển từ tiêu đề cụ thể của cảnh sang tiêu đề "DontDestroyOnLoad", trên chế độ xem phân cấp.

Một bản ghi màn hình của nhiều cảnh đang được tải, trong khi Trình quản lý dữ liệu vẫn tồn tại dưới tiêu đề "DoNotDestroyOnLoad".

Sử dụng PlayerPrefslớp học

Unity có một lớp được xây dựng để quản lý dữ liệu liên tục cơ bản được gọiPlayerPrefs . Bất kỳ dữ liệu nào được cam kết với PlayerPrefstệp sẽ tồn tại trong các phiên trò chơi , do đó, một cách tự nhiên, nó có khả năng duy trì dữ liệu qua các cảnh.

Các PlayerPrefstập tin có thể lưu trữ các biến của các loại string, intfloat. Khi chúng tôi chèn giá trị vào PlayerPrefstệp, chúng tôi cung cấp bổ sung stringlàm khóa. Chúng tôi sử dụng cùng một khóa để sau đó lấy các giá trị của chúng tôi từ PlayerPreftệp.

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

Lưu ý rằng tôi có biện pháp phòng ngừa bổ sung, khi xử lý PlayerPrefstệp:

  • Tôi đã lưu từng khóa là a private static string. Điều này cho phép tôi đảm bảo tôi luôn sử dụng đúng khóa và điều đó có nghĩa là nếu tôi phải thay đổi khóa vì bất kỳ lý do gì, tôi không cần phải đảm bảo tôi thay đổi tất cả các tham chiếu đến nó.
  • Tôi lưu PlayerPrefstập tin vào đĩa sau khi ghi vào nó. Điều này có thể sẽ không tạo ra sự khác biệt, nếu bạn không thực hiện kiên trì dữ liệu qua các phiên chơi. PlayerPrefs sẽ lưu vào đĩa trong khi đóng ứng dụng bình thường, nhưng nó có thể không tự nhiên gọi nếu trò chơi của bạn gặp sự cố.
  • Tôi thực sự kiểm tra xem mỗi khóa tồn tại trong PlayerPrefs, trước khi tôi cố gắng truy xuất một giá trị được liên kết với nó. Điều này có vẻ như kiểm tra hai lần vô nghĩa, nhưng nó là một thực hành tốt để có.
  • Tôi có một Deletephương pháp ngay lập tức xóa PlayerPrefstập tin. Nếu bạn không có ý định bao gồm dữ liệu kiên trì trong các phiên chơi, bạn có thể cân nhắc gọi phương thức này vào Awake. Bằng cách xóa các PlayerPrefstập tin khi bắt đầu mỗi trận đấu, bạn đảm bảo rằng bất kỳ dữ liệu đã tồn tại so với phiên trước không được nhầm lẫn xử lý như dữ liệu từ hiện tại phiên làm việc.

Bạn có thể thấy PlayerPrefstrong hành động, dưới đây. Lưu ý rằng khi tôi nhấp vào "Lưu dữ liệu", tôi đang gọi trực tiếp Savephương thức và khi tôi nhấp vào "Tải dữ liệu", tôi đang gọi trực tiếp Loadphương thức. Việc thực hiện của riêng bạn có thể sẽ khác nhau, nhưng nó thể hiện những điều cơ bản.

Một bản ghi màn hình của dữ liệu vẫn tồn tại được ghi đè từ thanh tra, thông qua các chức năng Lưu () và Tải ().


Như một lưu ý cuối cùng, tôi nên chỉ ra rằng bạn có thể mở rộng dựa trên cơ bản PlayerPrefs, để lưu trữ các loại hữu ích hơn. JPTheK9 cung cấp một câu trả lời tốt cho một câu hỏi tương tự , trong đó họ cung cấp một tập lệnh để sắp xếp các mảng thành dạng chuỗi, được lưu trữ trong một PlayerPrefstệp. Họ cũng chỉ cho chúng tôi đến Unify Community Wiki , nơi người dùng đã tải lên một PlayerPrefsXtập lệnh mở rộng hơn để cho phép hỗ trợ nhiều loại hơn, chẳng hạn như vectơ và mảng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.