Unity游戏管理器。脚本仅有效一次。

62

我正在制作一个简单的游戏管理器。我有一个脚本,将可从游戏中的所有场景访问。在加载新场景后,我需要检查其变量的值。但是,我的代码仅在模拟开始后运行一次,即使该脚本存在于所有场景中的对象中。出了什么问题?为什么在加载新场景后不起作用?


1
启动只需要调用一次,由于使用了DontDestroyOnLoad,因此不会再次发生启动。该对象在所有场景中均保持存在,因为使用了DontDestroyOnLoad。不确定为什么OnLevelWasLoaded没有触发。 - Everts
1
由于“唤醒”函数在场景中的所有对象上调用,而任何对象的“开始”函数都没有被调用。可能是因为这种情况导致“开始”函数没有被触发。您是否尝试过调用“OnLevelWasLoaded”函数? - Burak Karasoy
1
请尝试使用OnEnable。并查看此链接:http://docs.unity3d.com/Manual/ExecutionOrder.html。 - Barış Çırıka
很遗憾,@BarışÇırıka,它也不起作用。在第一个场景中,它之所以能够工作,是因为在每次加载后,它都会创建一个带有脚本的新对象实例,我需要修复它。所以它对于“Start”和“Awake”也不起作用。 - Amazing User
我不确定我是否正确理解了您的意思。您想在加载新场景时执行某些方法吗?您尝试过 SceneManager.sceneLoaded 吗?(https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager-sceneLoaded.html) - YAC
显示剩余4条评论
5个回答

124

在每个Unity项目中,您必须有一个预加载场景。

令人困惑的是,Unity没有“内置”的预加载场景。

他们将来会增加这个概念。

现在,您必须点击添加自己的预加载场景。

这是尝试使用Unity的新程序员所犯的最大误解!


幸运的是,拥有预加载场景非常容易。

步骤1。

制作一个名为“preload”的场景。 它必须是Build Manager中的 0号场景

enter image description here

步骤2。

在“preload”场景中创建一个名为“__app”的空GameObject。

简单地将DontDestroyOnLoad放在'__app'上。

注意:

这是整个项目中唯一的地方,您使用DontDestroyOnLoad

就这么简单。

enter image description here

在示例中:开发人员制作了一个一行的 DDOL 脚本。

将该脚本放在“__app”对象上。

您再也不需要考虑 DDOL 了。

步骤3

您的应用程序将具有(许多)“通用行为”。 因此,诸如数据库连接、声音效果、评分等等的事情。

您必须且只能将您的通用行为放在“_app”上。

就是这么简单。

通用行为随后 - 当然 - 在整个项目中的任何地方,无论何时和哪个场景中都可以使用

否则,您怎么能做到呢?

在上面的图像示例中,请注意“Iap”(“应用内购买”)和其他内容。

您所有的“常用行为”,例如声音效果、评分等等 - 都在那个对象中。

重要...

这意味着 - 当然,自然地 -

...您的通用行为将像Unity中的其他所有游戏对象一样具有普通检查器。

您可以使用Unity的所有常规功能,这些功能您可以在任何其他游戏对象上使用。 检查器变量、拖放连接、设置等等。

(确实: 假设您被聘用来开发一个现有项目。第一件事情是浏览预加载场景。您将在预加载场景中看到所有“通用行为” - 声音效果、得分、人工智能等等。您将立即看到这些内容的设置,如检查员变量……语音音量、playstore标识等等。)

以下是一个“声音效果”通用行为的例子:

enter image description here

看起来似乎还有一个“配音”通用行为和一个“音乐”通用行为。

再次重申。 关于您的“通用行为”。(声音效果、得分、社交等等。)这些只能放置在预加载场景中的游戏对象上。

这不是可选项:没有替代方案!

就是这么简单。

有时候从其他环境过来的工程师会陷入困境,因为它看起来像“它不能那么简单”。

再次重申,Unity只是忘记了在预加载场景中“内置”一个场景。 因此,您只需单击即可添加预加载场景。不要忘记添加DDOL。

因此,在开发过程中:

始终从预加载场景开始游戏。

就是这么简单。

重要提示: 您的应用程序肯定会有“早期”场景。例如:

  • “闪屏”
  • “菜单”

注意。您不能将闪屏或菜单用作预加载场景。您必须真正拥有一个单独的预加载场景

然后,预加载场景将加载您的闪屏、菜单或其他早期场景。


核心问题: 从其他脚本中“找到”它们:

所以您有一个预加载场景。

所有的“通用行为”都在预加载场景中。

接下来您面临的问题就是,很简单,就是找到“声音效果”等内容。

您必须能够轻松地从任何脚本、任何游戏对象、任何场景中找到它们。

幸运的是,这很简单明了,只需要一行代码。

Sound sound = Object.FindObjectOfType<Sound>();
Game game = Object.FindObjectOfType<Game>();

为任何需要它的脚本,在 Awake 中执行此操作。

说实话,就是这么简单。就是这样。

Sound sound = Object.FindObjectOfType<Sound>();

由于在线上看到了100多个完全错误的代码示例,因此会产生巨大的混乱。

真的很简单 - 诚实!

Unity忘记添加内置的“预加载场景”-用来附加您的系统(例如SoundEffects、GameManager等)的地方非常奇怪。这只是Unity中的一些奇怪事情之一。因此,在任何Unity项目中,您要做的第一件事就是单击一次以创建预加载场景。

就是这样!


一个细节...

请注意,如果您确实希望输入更少的代码行数(!),则可以非常容易地使用全局变量来完成每个这些事情!

此处详细说明了这一点,许多人现在使用像Grid.cs脚本这样的东西......

 using Assets.scripts.network;
 using UnityEngine;
 
 static class Grid
 {
     public static Comms comms;
     public static State state;
     public static Launch launch;
     public static INetworkCommunicator iNetworkCommunicator;
     public static Sfx sfx;
 
     static Grid()
     {
         GameObject g = GameObject.Find("_app");
 
         comms = g.GetComponent<Comms>();
         state = g.GetComponent<State>();
         launch = g.GetComponent<Launch>();
         iNetworkCommunicator = g.GetComponent<INetworkCommunicator>();
         sfx = g.GetComponent<Sfx>();
     }
 }

然后,在项目的任何地方,您都可以这样说:

Grid.sfx.Explosions();

这很容易,那就是整个事情的全部。

不要忘记,每一个“通用系统”都在预加载场景中的DDOL游戏对象上,并且只能在该对象上运行。


DylanB问:“在开发过程中,你必须每次在点击“播放”之前点击预加载场景,这很烦人。这能自动化吗?”

当然,每个团队都有不同的方法。以下是一个微不足道的例子:

// this should run absolutely first; use script-execution-order to do so.
// (of course, normally never use the script-execution-order feature,
// this is an unusual case, just for development.)
...
public class DevPreload:MonoBehaviour
 {
 void Awake()
  {
  GameObject check = GameObject.Find("__app");
  if (check==null)
   { UnityEngine.SceneManagement.SceneManager.LoadScene("_preload"); }
  }
 }

但别忘了:还有什么其他方式可以做到呢?游戏必须从预加载场景开始。除了点击进入预加载场景启动游戏外,您还能做什么呢?这就好比问“启动Unity来运行Unity很烦人——如何避免启动Unity?” 游戏当然必须从预加载场景开始,这还能怎么样呢?因此,在Unity中工作时,您必须先“点击预加载场景,然后再点击播放”-还能有其他方式吗?


8
那么从工作流程角度来看,这是如何运作的呢?如果你想测试当前正在编辑的场景,那么你是否需要在编辑器中加载预加载场景并在其上点击播放,因为预加载场景是具有传播到后续场景的初始对象的场景?如果是这样,那么这个工作流程似乎非常笨拙。我觉得我在这里忽略了什么重要的东西。 - Dylan Bennett
21
每个Unity项目都必须有一个"预加载场景"。为什么?请解释原因。在静态类/单例中懒惰地初始化对象并访问它们似乎更容易——特别是在测试场景方面,因为您可以轻松地逐个启动每个场景。 - SePröbläm
21
@Fattie,我得不同意你的看法。确实,预加载场景有时是必要的(但非常罕见),但大多数情况下,你所描述的一切只是懒而简单的做法。你已经提到了第一个缺点……你必须始终从场景0开始。我曾经以这种方式开发过游戏,因此失去了无数个小时。始终将你的游戏开发成可以从任何场景开始。这样做的好处应该是显而易见的。 - Squeazer
11
@Fattie 其次,你建议使用FindObjectOfType来获取实例。是的,这很简单,但又一次,这是一种懒惰的做法。调用非常慢,甚至Unity也建议不要使用它:https://docs.unity3d.com/ScriptReference/Object.FindObjectOfType.html 在使用Unity开发时,你被提供了太多的自由,因此你必须非常小心地设计你的架构。如果你不小心,以后会遇到无尽的问题...相信我,我也曾经历过这样的情况。有很多适用于Unity的好的做法,但这种方法不是其中之一。 - Squeazer
4
如果您需要快速生成大量对象,或者需要在短时间内产生1000个以上的对象,或者其他无数场景中需要快速创建新对象的情况下,使用这样慢的调用方法最终会积累起来,尤其是如果您正在为Android / iOS构建项目。即使频繁地(例如在每个Update调用中)使用Camera.main(基本上是相同的操作),也会严重降低复杂场景下的FPS。请问您有何想法? - Squeazer
显示剩余41条评论

31

@Fattie: 感谢您详细解释这些,太棒了!不过有一个观点人们试图向你传达,我也会试着说一下:

我们不想让移动游戏中的每个实例都为每个“全局类”执行“FindObjectOfType”操作!

相反,您可以立即使用静态/单例模式的实例,而无需查找它!

而且这很简单: 在您想要从任何地方访问的类中编写此内容,其中XXXXX是类的名称,例如“Sound”

public static XXXXX Instance { get; private set; }
void Awake()
{
if (Instance == null) { Instance = this; } else { Debug.Log("Warning: multiple " + this + " in scene!"); }
}

现在,取代你的例子

Sound sound = Object.FindObjectOfType<Sound>();

简单地使用它,不需要查看,也不需要额外的变量,就像这样,在任何地方直接使用:

Just simply use it, without looking, and no extra variables, simply like this, right off from anywhere:


Sound.Instance.someWickedFunction();

或者(从技术上来说是相同的),只需使用一个全局类,通常称为Grid,来“容纳”它们中的每一个。 如何操作。因此,


Grid.sound.someWickedFunction();
Grid.networking.blah();
Grid.ai.blah();

3
嘿,弗里茨!1000%正确 :) 我想我发明了Grid.cs的想法,你可以在这里看到 https://answers.unity.com/answers/663681/view.html 现在它已经被广泛应用了。这正是你所描述的。过去我曾向新的Unity用户描述“网格”系统......就像你所描述的一样。但随着时间的推移,我意识到这很令人困惑——更好的方法是简单地解释您有一个预加载场景。这是问题的“核心”。当然,如果您熟悉并且愿意,稍后可以添加一些语法糖。你的代码非常棒。 - Fattie
1
由于这个QA非常受欢迎,我在这里设置了100点赏金,因为这是一份非常重要的信息,建立在这个非常重要的QA之上。不要把它全部花在一个地方,Frits! :) - Fattie
说@fritslyneborg,如果你还在阅读......有趣的是,我在iOS中做的事情几乎完全相同!在这里...https://dev59.com/bVwY5IYBdhLWcg3waXEF - Fattie

10

以下是您可以启动任何场景并确保在Unity编辑器中点击播放按钮时重新整合_preload场景的方法。自从Unity 2017以来,有一个新的属性可用,名为RuntimeInitializeOnLoadMethod,有关它的更多信息,请点击此处

基本上,您有一个简单的平面c#类和一个带有RuntimeInitializeOnLoadMethod的静态方法。现在每次开始游戏时,这个方法将为您加载预加载场景。

using UnityEngine;
using UnityEngine.SceneManagement;

public class LoadingSceneIntegration {

#if UNITY_EDITOR 
    public static int otherScene = -2;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void InitLoadingScene()
    {
        Debug.Log("InitLoadingScene()");
        int sceneIndex = SceneManager.GetActiveScene().buildIndex;
        if (sceneIndex == 0) return;

        Debug.Log("Loading _preload scene");
        otherScene = sceneIndex;
        //make sure your _preload scene is the first in scene build list
        SceneManager.LoadScene(0); 
    }
#endif
}

然后在您的 _preload 场景中,您还有另一个脚本可以加载所需的场景(从您开始的位置):

...
#if UNITY_EDITOR 
    private void Awake()
    {

        if (LoadingSceneIntegration.otherScene > 0)
        {
            Debug.Log("Returning again to the scene: " + LoadingSceneIntegration.otherScene);
            SceneManager.LoadScene(LoadingSceneIntegration.otherScene);
        }
    }
#endif
...

6

2019年5月提出的另一种无需使用_preload的解决方案:

https://low-scope.com/unity-tips-1-dont-use-your-first-scene-for-global-script-initialization/

我已经从上面的博客中改编出下面的操作步骤:

为所有场景加载静态资源预制件

项目 > 资源中创建一个名为Resources的文件夹。

从一个空的GameObject创建一个Main预制件,并将其放置在Resources文件夹中。

Assets > Scripts或其他位置中创建一个Main.cs C#脚本。

using UnityEngine;

public class Main : MonoBehaviour
{
    // Runs before a scene gets loaded
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void LoadMain()
    {
        GameObject main = GameObject.Instantiate(Resources.Load("Main")) as GameObject;
        GameObject.DontDestroyOnLoad(main);
    }
    // You can choose to add any "Service" component to the Main prefab.
    // Examples are: Input, Saving, Sound, Config, Asset Bundles, Advertisements
}

Main.cs 添加到您的 Resources 文件夹中的 Main Prefab 中。
请注意,它使用了 RuntimeInitializeOnLoadMethod 以及 Resources.Load("Main")DontDestroyOnLoad
将需要在场景中全局使用的任何其他脚本附加到此 prefab 上。
请注意,如果您将这些脚本链接到其他场景游戏对象,则可能需要在这些脚本的 Start 函数中使用类似于以下内容的东西:
if(score == null)
    score = FindObjectOfType<Score>();
if(playerDamage == null)
    playerDamage = GameObject.Find("Player").GetComponent<HitDamage>();

或者更好的方式是使用资产管理系统,例如 Addressable Assets 或者 Asset Bundles


你的建议非常好,但是关于Asset Bundles的建议,我不太确定,是不是应该在之前建议的DDOL对象的基础上使用,或者它们能否作为一个完全替代品呢? - undefined

3

作为一个来到Unity世界的程序员,我没有看到这些方法中的任何一种是标准的。

最简单和标准的方法:创建一个预制件,根据Unity文档

Unity的预制系统允许您创建、配置和存储一个GameObject,包括所有组件、属性值和子GameObject,作为一个可重用的资源。预制资源作为模板,可以从中在场景中创建新的预制实例。

细节:

  1. 在您的Resources文件夹中创建一个预制件:

  2. 创建一个类似以下内容的脚本:

using UnityEngine;

public class Globals : MonoBehaviour // change Globals (it should be the same name of your script)
{
    // loads before any other scene:
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void LoadMain()
    {
        Debug.Log("i am before everything else");
    }
}
  1. 将其分配给您的预制件

并且您可以让它变得更好:

同时使用预制件和命名空间:

在您的预制件脚本中:

using UnityEngine;

namespace Globals {
    public class UserSettings
    {
        static string language = "per";
        public static string GetLanguage()
        {
            return language;
        }
        public static void SetLanguage (string inputLang)
        {
            language = inputLang;
        }
    }
}

在您的其他脚本中:

using Globals;

public class ManageInGameScene : MonoBehaviour
{
    void Start()
    {
        string language = UserSettings.GetLanguage();
    }

    void Update()
    {
        
    }
}


我发现只需将Globals脚本附加到我想要加载的预制件上,它确实会打印调试日志(方法运行正常),但这并不会将预制件作为克隆物带入场景。使用_ = Instantiate(Resources.Load("Main")) as GameObject;可以完成任务,正如Phyatt所建议的那样。至于DontDestroyOnLoad,我只需将一个单独的DDOL脚本附加到预制件本身,这将单独触发该行为。 - undefined

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