我正在制作一个简单的游戏管理器。我有一个脚本,将可从游戏中的所有场景访问。在加载新场景后,我需要检查其变量的值。但是,我的代码仅在模拟开始后运行一次,即使该脚本存在于所有场景中的对象中。出了什么问题?为什么在加载新场景后不起作用?
我正在制作一个简单的游戏管理器。我有一个脚本,将可从游戏中的所有场景访问。在加载新场景后,我需要检查其变量的值。但是,我的代码仅在模拟开始后运行一次,即使该脚本存在于所有场景中的对象中。出了什么问题?为什么在加载新场景后不起作用?
令人困惑的是,Unity没有“内置”的预加载场景。
他们将来会增加这个概念。
现在,您必须点击添加自己的预加载场景。
幸运的是,拥有预加载场景非常容易。
步骤1。
制作一个名为“preload”的场景。 它必须是Build Manager中的 0号场景。
步骤2。
在“preload”场景中创建一个名为“__app”的空GameObject。
简单地将DontDestroyOnLoad
放在'__app'上。
注意:
这是整个项目中唯一的地方,您使用DontDestroyOnLoad
。
就这么简单。
在示例中:开发人员制作了一个一行的 DDOL 脚本。
将该脚本放在“__app”对象上。
您再也不需要考虑 DDOL 了。
步骤3
您的应用程序将具有(许多)“通用行为”。 因此,诸如数据库连接、声音效果、评分等等的事情。
您必须且只能将您的通用行为放在“_app”上。
就是这么简单。
通用行为随后 - 当然 - 在整个项目中的任何地方,无论何时和哪个场景中都可以使用。
否则,您怎么能做到呢?
在上面的图像示例中,请注意“Iap”(“应用内购买”)和其他内容。
您所有的“常用行为”,例如声音效果、评分等等 - 都在那个对象中。
这意味着 - 当然,自然地 -
您可以使用Unity的所有常规功能,这些功能您可以在任何其他游戏对象上使用。 检查器变量、拖放连接、设置等等。
(确实: 假设您被聘用来开发一个现有项目。第一件事情是浏览预加载场景。您将在预加载场景中看到所有“通用行为” - 声音效果、得分、人工智能等等。您将立即看到这些内容的设置,如检查员变量……语音音量、playstore标识等等。)
以下是一个“声音效果”通用行为的例子:
看起来似乎还有一个“配音”通用行为和一个“音乐”通用行为。
再次重申。 关于您的“通用行为”。(声音效果、得分、社交等等。)这些只能放置在预加载场景中的游戏对象上。
就是这么简单。
有时候从其他环境过来的工程师会陷入困境,因为它看起来像“它不能那么简单”。
再次重申,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中工作时,您必须先“点击预加载场景,然后再点击播放”-还能有其他方式吗?
@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();
以下是您可以启动任何场景并确保在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
...
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
。Start
函数中使用类似于以下内容的东西:if(score == null)
score = FindObjectOfType<Score>();
if(playerDamage == null)
playerDamage = GameObject.Find("Player").GetComponent<HitDamage>();
或者更好的方式是使用资产管理系统,例如 Addressable Assets 或者 Asset Bundles。
作为一个来到Unity世界的程序员,我没有看到这些方法中的任何一种是标准的。
最简单和标准的方法:创建一个预制件,根据Unity文档:
Unity的预制系统允许您创建、配置和存储一个GameObject,包括所有组件、属性值和子GameObject,作为一个可重用的资源。预制资源作为模板,可以从中在场景中创建新的预制实例。
细节:
在您的Resources文件夹中创建一个预制件:
Resources
,因为它是一个Unity特殊文件夹名称创建一个类似以下内容的脚本:
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");
}
}
同时使用预制件和命名空间:
在您的预制件脚本中:
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()
{
}
}
_ = Instantiate(Resources.Load("Main")) as GameObject;
可以完成任务,正如Phyatt所建议的那样。至于DontDestroyOnLoad,我只需将一个单独的DDOL脚本附加到预制件本身,这将单独触发该行为。 - undefined
SceneManager.sceneLoaded
吗?(https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager-sceneLoaded.html) - YAC