장면 간 데이터를 처리하는 올바른 방법은 무엇입니까?


52

Unity에서 첫 2D 게임을 개발 중이며 중요한 질문이 무엇인지 알게되었습니다.

장면 간 데이터를 어떻게 처리합니까?

이것에 대한 다른 대답이있는 것 같습니다.

  • 누군가는 PlayerPrefs 사용에 대해 언급 했지만 다른 사람들은 이것이 화면 밝기 등과 같은 다른 것을 저장하는 데 사용해야한다고 말했습니다.

  • 누군가 나에게 가장 좋은 방법은 장면을 바꿀 때마다 모든 것을 세이브 게임에 기록하고 새 장면이로드 될 때마다 세이브 게임에서 정보를 얻는 것입니다. 이것은 나에게 성능이 낭비되는 것처럼 보였다. 내가 틀렸어?

  • 내가 지금까지 구현 한 솔루션 인 다른 솔루션 은 장면 사이에서 모든 데이터를 처리하면서 장면간에 파괴되지 않는 전역 게임 개체 를 갖는 것입니다. 게임이 시작되면 이 오브젝트가로드 된 Start Scene을 로드합니다. 이 작업이 끝나면 일반적으로 주 메뉴 인 첫 번째 실제 게임 장면을로드합니다.

이것은 내 구현입니다.

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;

지금까지 효과가 있었지만 한 가지 큰 문제가 있습니다. 장면을 직접로드하려면 게임의 최종 레벨을 예로 들어 보겠습니다. 해당 장면에는이 장면이 포함되어 있지 않으므로 직접로드 할 수 없습니다 글로벌 게임 오브젝트 .

이 문제를 잘못 처리하고 있습니까? 이런 종류의 도전에 대한 더 나은 사례가 있습니까? 이 문제에 대한 귀하의 의견, 생각 및 제안을 듣고 싶습니다.

감사

답변:


64

이 답변에는이 상황을 처리하는 기본 방법이 나와 있습니다. 그러나 이러한 방법의 대부분은 대규모 프로젝트에 적합하지 않습니다. 더 확장 가능한 것을 원하고 손이 더러워지는 것을 두려워하지 않는 경우 Leaagees의 의존성 주입 프레임 워크 에 대한 답변을 확인하십시오 .


1. 데이터 만 보관하기위한 정적 스크립트

데이터 만 보유하는 정적 스크립트를 작성할 수 있습니다. 정적이므로 게임 오브젝트에 할당 할 필요가 없습니다. ScriptName.Variable = data;등 의 데이터에 간단히 액세스 할 수 있습니다 .

장점 :

  • 인스턴스 또는 싱글 톤이 필요하지 않습니다.
  • 프로젝트의 어느 곳에서나 데이터에 액세스 할 수 있습니다.
  • 장면 사이에 값을 전달하는 추가 코드가 없습니다.
  • 단일 데이터베이스와 유사한 스크립트의 모든 변수와 데이터를 사용하면 쉽게 처리 할 수 ​​있습니다.

단점 :

  • 정적 스크립트 내에서 코 루틴을 사용할 수 없습니다.
  • 잘 정리하지 않으면 단일 클래스에서 거대한 변수 행으로 끝날 것입니다.
  • 편집기 내에서 필드 / 변수를 할당 할 수 없습니다.

예 :

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

스크립트를 게임 오브젝트에 할당하거나 MonoBehavior에서 파생해야하는 경우, 한 DontDestroyOnLoad(gameObject);번만 실행할 수있는 클래스를 클래스에 추가 할 수 있습니다 (이를 배치하는 Awake()것이 일반적으로 사용되는 방법입니다) .

장점 :

  • 모든 MonoBehaviour 작업 (예 : Coroutines)은 안전하게 수행 할 수 있습니다.
  • 편집기 내에서 필드를 지정할 수 있습니다.

단점 :

  • 스크립트에 따라 장면을 조정해야 할 수도 있습니다.
  • 업데이트 또는 기타 일반 기능 / 방법에서 수행 할 작업을 결정하기 위해로드 된 secene을 확인해야 할 수 있습니다. 예를 들어 Update ()에서 UI를 사용하여 작업을 수행하는 경우 작업을 수행하기 위해 올바른 장면이로드되었는지 확인해야합니다. 이 경우 if-else 또는 switch-case 검사가 수행됩니다.

3. PlayerPrefs

게임이 종료 된 경우에도 데이터를 저장하려는 경우이를 구현할 수 있습니다.

장점 :

  • Unity는 모든 백그라운드 프로세스를 처리하므로 관리가 쉽습니다.
  • 장면뿐만 아니라 인스턴스 (게임 세션) 간에도 데이터를 전달할 수 있습니다.

단점 :

  • 파일 시스템을 사용합니다.
  • prefs 파일에서 데이터를 쉽게 변경할 수 있습니다.

4. 파일로 저장

이것은 장면 사이에 값을 저장하는 데 약간의 과잉입니다. 암호화가 필요하지 않은 경우이 방법을 사용하지 않는 것이 좋습니다.

장점 :

  • PlayerPrefs와 달리 저장된 데이터를 제어 할 수 있습니다.
  • 장면뿐만 아니라 인스턴스 (게임 세션) 간에도 데이터를 전달할 수 있습니다.
  • 파일을 전송할 수 있습니다 (사용자 생성 컨텐츠 개념은 이것에 의존합니다).

단점 :

  • 느린.
  • 파일 시스템을 사용합니다.
  • 저장 중 스트림 중단으로 인한 읽기 /로드 충돌 가능성.
  • 암호화를 구현하지 않으면 파일에서 데이터를 쉽게 변경할 수 있습니다 (코드를 더 느리게 만들 수 있습니다).

5. 싱글 톤 패턴

싱글 톤 패턴은 객체 지향 프로그래밍에서 매우 인기있는 주제입니다. 어떤 사람들은 그것을 제안하지만, 그렇지 않은 사람들도 있습니다. 직접 조사하고 프로젝트 조건에 따라 적절한 전화를 겁니다.

장점 :

  • 설치 및 사용이 간편합니다.
  • 프로젝트의 어느 곳에서나 데이터에 액세스 할 수 있습니다.
  • 단일 데이터베이스와 유사한 스크립트의 모든 변수와 데이터를 사용하면 쉽게 처리 할 수 ​​있습니다.

단점 :

  • 싱글 톤 인스턴스를 유지하고 보호하는 것이 유일한 역할을하는 많은 상용구 코드입니다.
  • 싱글 톤 패턴 사용에 대한 강력한 논증 이있다 . 미리 조심하고 연구하십시오.
  • 잘못된 구현으로 인한 데이터 충돌 가능성.
  • 단일 패턴 처리에 어려움이있을 수 있습니다 1 .

1 : Unify Wiki에서 제공되는 Singleton ScriptOnDestroy방법 요약 에서 런타임에서 편집기로 블리드 된 고스트 오브젝트 를 설명하는 작성자를 볼 수 있습니다 .

Unity가 종료되면 무작위 순서로 오브젝트가 삭제됩니다. 원칙적으로 싱글 톤은 응용 프로그램이 종료 될 때만 파괴됩니다. 스크립트가 파괴 된 후 인스턴스를 호출하는 스크립트는 응용 프로그램 재생을 중지 한 후에도 에디터 장면에 남아있는 버그가있는 고스트 오브젝트를 생성합니다. 정말 나빠! 그래서 이것은 버그가있는 유령 오브젝트를 생성하지 않도록하기위한 것입니다.


8

약간 고급 옵션은 Zenject 와 같은 프레임 워크를 사용하여 종속성 주입을 수행하는 것 입니다 .

따라서 원하는대로 응용 프로그램을 자유롭게 구성 할 수 있습니다. 예를 들어

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

그런 다음 유형을 IoC (inversion of control) 컨테이너에 바인딩 할 수 있습니다. Zenject를 사용하면이 작업이 a MonoInstaller또는 a 내부에서 수행됩니다 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

처음 두 옵션 모두 세션간에 게임을 저장하는 데 게임에서 필요할 수 있지만이 문제에 대한 잘못된 도구입니다.


방금 게시 한 링크 중 일부를 읽었습니다. 글로벌 게임 오브젝트를로드하는 초기 장면을 자동로드하는 방법이있는 것 같습니다. 조금 복잡해 보이므로 문제를 해결할 수 있는지 결정하는 데 약간의 시간이 필요합니다. 의견 주셔서 감사합니다!
엔리케 모레노 텐트

내가 일종의 연결 스크립트는 매번 시작 장면으로 전환하는 것을 기억하지 않고 모든 장면에서 재생할 수 있다는 점에서 문제를 해결합니다. 그래도 마지막 레벨에서 직접 시작하지 않고 처음부터 게임을 시작합니다. 어떤 레벨로든 건너 뛸 수 있도록 치트를 넣거나 자동로드 스크립트를 수정하여 레벨을 게임에 전달할 수 있습니다.
jhocking

그래 문제는 특정 레벨을 염두에두고 해킹해야하는 것만 큼 시작 장면으로 전환해야한다는 "불쾌감"이 아니 었습니다. 어쨌든 고마워!
Enrique Moreno Tent

1

장면 사이에 변수를 저장하는 이상적인 방법은 싱글 톤 관리자 클래스를 이용하는 것입니다. 영구 데이터를 저장할 클래스를 만들고 해당 클래스를로 설정 DoNotDestroyOnLoad()하면 즉시 액세스 할 수 있고 장면간에 지속될 수 있습니다.

또 다른 옵션은 PlayerPrefs클래스 를 사용하는 것 입니다. 재생 세션간에PlayerPrefs 데이터를 저장할 수 있도록 설계 되었지만 장면간에 데이터를 저장하는 수단으로 사용됩니다 .

싱글 톤 클래스를 사용하여 DoNotDestroyOnLoad()

다음 스크립트는 영구 싱글 톤 클래스를 만듭니다. 싱글 톤 클래스는 동시에 단일 인스턴스 만 실행하도록 설계된 클래스입니다. 이러한 기능을 제공함으로써 정적 자체 참조를 안전하게 생성하여 어디서나 클래스에 액세스 할 수 있습니다. 따라서 클래스 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에는라는 기본 영구 데이터를 관리하기위한 클래스가 내장되어PlayerPrefs 있습니다. PlayerPrefs파일에 커밋 된 모든 데이터 는 게임 세션 동안 지속 되므로 자연스럽게 장면 전체에서 데이터를 유지할 수 있습니다.

PlayerPrefs파일 유형의 변수를 저장할 수 string, intfloat. PlayerPrefs파일에 값을 삽입 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 PlayerPrefs 파일에 저장 될 배열을 문자열 형식으로 직렬화하는 스크립트를 제공 하는 유사한 질문에 대한 좋은 답변을 제공 합니다. 그들은 또한 우리를 가리 킵니다 의 Unify 커뮤니티 위키 , 사용자가보다 넓은 업로드 PlayerPrefsX스크립트를 같은 벡터와 배열과 같은 유형의 큰 다양한 지원을 허용 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.