如何在Unity场景之间传递数据(和引用)

94

如何将分数值从一个场景传递到另一个场景?

我尝试了以下方法:

场景一:

void Start () {
    score = 0;
    updateScoreView ();
    StartCoroutine (DelayLoadlevel(20));
}

public void updateScoreView(){
    score_text.text = "The Score: "+ score;
}

public void AddNewScore(int NewscoreValue){
    score = score + NewscoreValue;
    updateScoreView ();
}

IEnumerator DelayLoadlevel(float seconds){        
    yield return new WaitForSeconds(10);
    secondsLeft = seconds;
    loadingStart = true;
    do {        
        yield return new WaitForSeconds(1);
    } while(--secondsLeft >0);

    // here I should store my last score before move to level two
    PlayerPrefs.SetInt ("player_score", score);
    Application.LoadLevel (2);
}

第二场景:

public Text score_text;
private int old_score;

// Use this for initialization
void Start () {    
    old_score = PlayerPrefs.GetInt ("player_score");
    score_text.text = "new score" + old_score.ToString ();      
}

屏幕上没有任何显示,也没有错误提示。

这是传递数据的正确方式吗?

我正在使用Unity 5免费版,为Gear VR开发游戏(意味着游戏将在Android设备上运行)。

有什么建议吗?


到底是什么问题?是第二个场景没有加载,还是显示的分数不正确? - sdabet
不是加载下一个场景,而是没有我的分数,这是存储数据的正确方式吗?这类似于Android中的sharedPreference吗? - Mina Fawzy
“old_score”的值是多少?您可以在“Start()”方法中添加“Debug.Log(old_score);”来查看它。 - sdabet
哦,我真笨,我忘记将文本画布附加到我的脚本上以显示分数。 - Mina Fawzy
6个回答

184
有许多方法可以实现这一点,但解决方案取决于您想在场景之间传递的数据类型。组件/脚本和游戏对象在加载新场景时被销毁,即使被标记为静态也是如此。
在这个答案中,您可以找到:
    1. 使用static关键字
    1. 使用DontDestroyOnLoad
    1. 将数据存储在本地
    • 3a 使用PlayerPrefs
    • 3b 序列化为XML/JSON/二进制并使用FileIO

1. 使用 static 关键字。
如果要传递到下一个场景的变量不是组件,不继承自 MonoBehaviour 并且不是 GameObject,则将该变量设置为 static
内置的原始数据类型如 intboolstringfloatdouble 等都可以成为 static 变量。 可以 标记为静态的内置原始数据类型示例:
static int counter = 0;
static bool enableAudio = 0;
static float timer = 100;

这些应该没有问题。
可以标记为静态的对象示例:
public class MyTestScriptNoMonoBehaviour
{

}

那么

static MyTestScriptNoMonoBehaviour testScriptNoMono;

void Start()
{
    testScriptNoMono = new MyTestScriptNoMonoBehaviour();
}

请注意,该类没有继承自MonoBehaviour。这应该可以工作。
无法标记为静态的对象示例:
继承自ObjectComponentGameObject的任何内容都将不起作用1A。继承自MonoBehaviour的任何内容。
public class MyTestScript : MonoBehaviour 
{

}

那么

static MyTestScript testScript;

void Start()
{
    testScript = gameObject.AddComponent<MyTestScript>();
} 

这不会起作用,因为它继承自 MonoBehaviour1B. 所有 GameObject
static GameObject obj;

void Start()
{
    obj = new GameObject("My Object");
}  

这也不会起作用,因为它是一个GameObject,而 GameObject 继承自 Object
Unity 将始终销毁其 Object,即使它们声明了 static 关键字。
有一个解决方法,请参见#2
2.使用DontDestroyOnLoad函数。
只有当要保留或传递到下一个场景的数据继承自ObjectComponent或是GameObject时,才需要使用此函数。这解决了1A和1B中描述的问题。
您可以使用它使此游戏对象在场景卸载时不被销毁:
void Awake() 
{
    DontDestroyOnLoad(transform.gameObject);
}

你甚至可以使用static关键字来解决1A和1B的问题:
public class MyTestScript : MonoBehaviour 
{

}

那么

static MyTestScript testScript;

void Awake() 
{
    DontDestroyOnLoad(transform.gameObject);
}

void Start()
{
    testScript = gameObject.AddComponent<MyTestScript>();
} 

testScript变量现在将在新场景加载时被保留。

3. 保存到本地存储,然后在下一个场景中加载。

当游戏关闭并重新打开时必须保留游戏数据时,应使用此方法。例如玩家最高分、游戏设置(如音乐音量)、对象位置、摇杆配置数据等。

有两种保存方式:

3A. 使用PlayerPrefs API。

如果只需要保存几个变量,请使用这种方法。比如玩家得分:

int playerScore = 80;

我们想要保存玩家得分:
OnDisable 函数中保存得分。
void OnDisable()
{
    PlayerPrefs.SetInt("score", playerScore);
}

将其加载到OnEnable函数中。
void OnEnable()
{
    playerScore  =  PlayerPrefs.GetInt("score");
}

3B.将数据序列化为json、xml或二进制形式,然后使用C#文件API之一(如File.WriteAllBytesFile.ReadAllBytes)保存和加载文件。

如果有许多变量需要保存,请使用此方法。

通常情况下,您需要创建一个不继承MonoBehaviour的类。您应该使用这个类来保存游戏数据,以便可以轻松地进行序列化或反序列化。

要保存的数据示例:

[Serializable]
public class PlayerInfo
{
    public List<int> ID = new List<int>();
    public List<int> Amounts = new List<int>();
    public int life = 0;
    public float highScore = 0;
}

获取包装File.WriteAllBytesFile.ReadAllBytesDataSaver类,使得从this帖子中保存数据更加容易。

创建新实例:

PlayerInfo saveData = new PlayerInfo();
saveData.life = 99;
saveData.highScore = 40;

将PlayerInfo中的数据保存到名为“players”的文件中:
DataSaver.saveData(saveData, "players");

从名为“players”的文件中加载数据:
PlayerInfo loadedData = DataSaver.loadData<PlayerInfo>("players");

12
这个回答比被接受的回答更为全面。谢谢! - Chris Blackwell
嗨@程序员,这个#3B方法在WebGL游戏中可行吗? - Sakuna Madushanka
1
我经常使用第四种方法。请参见我的回答 - derHugo

59

还有另一种方式:

ScriptableObject

ScriptableObject基本上是数据容器,但也可以实现自己的逻辑。它们仅在Assets中存在,类似于预制体。它们不能用于永久存储数据,但它们在一个会话期间存储数据,因此可以用于在场景之间共享数据和引用...以及 - 还有件我经常需要的事情 - 在场景和AnimatorController之间!

脚本

首先,您需要一个类似于MonoBehaviour的脚本。一个简单的ScriptableObject示例可能如下所示:

// fileName is the default name when creating a new Instance
// menuName is where to find it in the context menu of Create
[CreateAssetMenu(fileName = "Data", menuName = "Examples/ExamoleScriptableObject")]
public class ExampleScriptableObject : ScriptableObject
{
    public string someStringValue = "";
    public CustomDataClass someCustomData = null;
    public Transform someTransformReference = null;

    // Could also implement some methods to set/read data,
    // do stuff with the data like parsing between types, fileIO etc

    // Especially ScriptableObjects also implement OnEnable and Awake
    // so you could still fill them with permanent data via FileIO at the beginning of your app and store the data via FileIO in OnDestroy !!
}

// If you want the data to be stored permanently in the editor
// and e.g. set it via the Inspector
// your types need to be Serializable!
//
// I intentionally used a non-serializable class here to show that also 
// non Serializable types can be passed between scenes 
public class CustomDataClass
{
    public int example;
    public Vector3 custom;
    public Dictionary<int, byte[]> data;
}

创建实例

您可以通过脚本来创建ScriptableObject的实例。

var scriptableObject = ScriptableObject.CreateInstance<ExampleScriptableObject>();

或者为了简化操作,可以像上面的示例中所示使用[CreateAssetMenu]
由于创建的ScriptabeObject实例存在于Assets中,它不与场景绑定,因此可以在任何地方引用!
当您想要在两个场景之间共享数据,或者例如在场景和AnimatorController之间共享数据时,您只需要在两者中引用这个ScriptableObject实例即可。
填充数据
我经常使用一个组件来填充数据,例如
public class ExampleWriter : MonoBehaviour
{
    // Here you drag in the ScriptableObject instance via the Inspector in Unity
    [SerializeField] private ExampleScriptableObject example;

    public void StoreData(string someString, int someInt, Vector3 someVector, List<byte[]> someDatas)
    {
        example.someStringValue = someString;
        example.someCustomData = new CustomDataClass
                                 {
                                     example = someInt;
                                     custom = someVector;
                                     data = new Dictionary<int, byte[]>();
                                 };
        for(var i = 0; i < someDatas.Count; i++)
        {
            example.someCustomData.data.Add(i, someDatas[i]);
        }
        example.someTransformReference = transform;
    }
}

消费数据

所以在你将所需的数据写入并存储到这个ExampleScriptableObject实例之后,任何场景中的其他类、AnimatorController或其他ScriptableObject都可以以同样的方式读取这些数据:

public class ExampleConsumer : MonoBehaviour
{
    // Here you drag in the same ScriptableObject instance via the Inspector in Unity
    [SerializeField] private ExampleScriptableObject example;

    public void ExampleLog()
    {
        Debug.Log($"string: {example.someString}", this);
        Debug.Log($"int: {example.someCustomData.example}", this);
        Debug.Log($"vector: {example.someCustomData.custom}", this);
        Debug.Log($"data: There are {example.someCustomData.data.Count} entries in data.", this);

        Debug.Log($"The data writer {example.someTransformReference.name} is at position {example.someTransformReference.position}", this);
    }
}

持久性

如所说,ScriptableObject本身的更改只在Unity编辑器中真正持久。

在构建过程中,它们只在同一会话期间持久。

因此,如果需要,我经常将会话持久性与一些文件IO结合使用(如this answer的第3b节所述),以便在会话开始时(或在需要时)从硬盘加载和反序列化值,并在会话结束时(OnApplicationQuit)或在需要时将其序列化并存储到文件中。

(当然,这对于引用是不起作用的。)


2
好的解决方案不需要任何额外的东西。这段代码也更适合单元测试。感谢您提供如此详细的答案。 - picolino
1
很棒的答案!我也喜欢这种方法。我只想补充一点,如果在会话期间加载了不包含特定SO引用的场景,则SO将从内存中删除,并且其当前状态将丢失。为了避免这种情况,可以在OnEnable()中设置hideFlags = HideFlags.DontUnloadUnusedAsset。 - KwahuNashoba
我认为这个答案在2021年需要更多的赞。 - MÇT

20

除了playerPrefs之外,另一种简单粗暴的方法是在级别加载期间通过调用DontDestroyOnLoad来保留一个对象。

DontDestroyOnLoad (transform.gameObject);

任何附加到游戏对象的脚本都将存活下来,其中包括脚本中的变量。

DontDestroyOnLoad函数通常用于保留整个GameObject以及与其关联的组件和层次结构中的任何子对象。

您可以创建一个空的GameObject,并仅在其中放置包含需要保留的变量的脚本。


1
还有一个不错的选择 - “信息对象”http://answers.unity3d.com/questions/532656/what-is-the-best-way-to-pass-data-between-scenes.html - Jviaches

5

我使用一种称为“无状态场景(Stateless Scenes)”的功能方法。

using UnityEngine;
public class MySceneBehaviour: MonoBehaviour {
    private static MySceneParams loadSceneRegister = null;

    public MySceneParams sceneParams;

    public static void loadMyScene(MySceneParams sceneParams, System.Action<MySceneOutcome> callback) {
        MySceneBehaviour.loadSceneRegister = sceneParams;
        sceneParams.callback = callback;
        UnityEngine.SceneManagement.SceneManager.LoadScene("MyScene");
    }

    public void Awake() {
        if (loadSceneRegister != null) sceneParams = loadSceneRegister;
        loadSceneRegister = null; // the register has served its purpose, clear the state
    }

    public void endScene (MySceneOutcome outcome) {
        if (sceneParams.callback != null) sceneParams.callback(outcome);
        sceneParams.callback = null; // Protect against double calling;
    }
}

[System.Serializable]
public class MySceneParams {
    public System.Action<MySceneOutcome> callback;
    // + inputs of the scene 
}

public class MySceneOutcome {
    // + outputs of the scene 
}

您可以将全局状态保存在调用者的范围中,这样场景输入和输出状态就可以被最小化(使测试变得容易)。要使用它,您可以使用匿名函数:-

MyBigGameServices services ...
MyBigGameState bigState ...

Splash.loadScene(bigState.player.name, () => {
   FirstLevel.loadScene(bigState.player, (firstLevelResult) => {
       // do something else
       services.savePlayer(firstLevelResult);
   })
)}

更多信息请访问https://corepox.net/devlog/unity-pattern:-stateless-scenes


3
我很喜欢这个,但你应该加上一个生命周期的例子,我花了一些时间才明白如何在实践中实现它。 - Haelle

4

有多种方式,但假设你只需要传递一些基本数据,则可以创建一个GameController的单例实例,并使用该类存储数据。

当然,DontDestroyOnLoad是必须的!

public class GameControl : MonoBehaviour
{
    //Static reference
public static GameControl control;

//Data to persist
public float health;
public float experience;

void Awake()
{
    //Let the gameobject persist over the scenes
    DontDestroyOnLoad(gameObject);
    //Check if the control instance is null
    if (control == null)
    {
        //This instance becomes the single instance available
        control = this;
    }
    //Otherwise check if the control instance is not this one
    else if (control != this)
    {
        //In case there is a different instance destroy this one.
        Destroy(gameObject);
    }
}

这里是包含其他示例的完整教程


你提供的链接中的网站被我的反病毒软件检测出感染,接着显示了404错误页面。 - dankal444

2

您有几个选项。

第一个选项是使用静态变量,这样您就不会失去它们的信息或值,从场景到场景传递(因为它们不绑定于对象)。[当关闭游戏时,您会失去信息,但在场景之间传递时不会丢失]

第二个选项是将您不想失去信息的玩家或对象通过DontDestroyOnLoad函数传递。这里我给您提供文档和示例代码。[当关闭游戏时,您会失去信息,但在场景之间传递时不会丢失]

https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html

第三个选项是使用playerPrefab [https://docs.unity3d.com/ScriptReference/PlayerPrefs.html],它允许您保存信息并在任何时候检索它,而不会挂起它,即使在关闭游戏后也是如此。[如果您计划使用它来保存数据甚至在关闭游戏后,请非常小心,因为您可能会在突然关闭游戏时丢失数据,因为player prefab会创建文件并从那里检索信息,但它会在结束时保存文件或正确关闭应用程序]


我不知道上面那些有点复杂的答案,这个非常简单并且完美地完成了工作,“静态字段”就是我所需要的。 - Alireza Jamali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接