ue3dUnity 中的游戏单例

Unity 中的游戏单例

分类:
ue3d - Unity 中的游戏单例

Unity单例是一个有争议的话题,它有时很有用,有时想用又不会用。使用单例是一把双刃剑,但对于刚尝试完成游戏的入门同学来说,单例可能是连接脚本的一种有用且简单的方法。

那么在本文中,将向大家展示如何使用单例来简化游戏构建的一些不同方法,以及其中涉及的一些潜在风险,以便可以正确决定是否在游戏中使用单例。

Unity 中的游戏单例是什么?

一般来说,Unity 中的单例是一个全局可访问的类,存在于场景中,但只有一次。

这个想法是任何其他脚本都可以访问单例,允许轻松地将对象连接到游戏的重要部分,例如玩家或其他游戏系统。

这对于将不相关的对象和脚本连接到全局系统,很有用。例如音频管理器。

那么它们是如何工作的呢?

Unity 中的单例是可以添加到游戏内对象的普通类。然而,单例的不同之处在于该类拥有对其自身类型实例的公共静态引用。

例如这样:

public class Singleton : MonoBehaviour 
{
    public static Singleton instance;
}

静态引用是使单例全局可访问的原因。

将变量设为静态意味着它由类的所有实例共享,这意味着任何脚本都可以通过其类名访问单例,而无需先引用它。

例如这样:

Singleton.instance;

这意味着单例类中存在的任何公共方法或变量都可以被游戏中的其他脚本轻松访问。

但是,因为任何脚本都可以访问它,所以通常最好使用 property 保护实例变量,这意味着它可以被其他脚本读取,但只能在自己的类中设置。

例如这样:

public class Singleton : MonoBehaviour 
{
    public static Singleton Instance { get; private set; }
}

虽然对脚本的引用是静态的,这意味着它在类的任何和所有实例中都是相同的,但它指向的实例不是。

这意味着,虽然引用只能指向脚本的一个实例,但场景中可能有多个单例实例。

这就是为什么通过检查静态引用,如果有的话,是否与脚本实例匹配来确保只有一个单例实例很重要的原因。

然后,如果它不匹配,脚本就知道它是重复的,它可以删除自己。

例如这样:

public static Singleton Instance { get; private set; }
private void Awake() 
{ 
    // If there is an instance, and it's not me, delete myself.
    
    if (Instance != null && Instance != this) 
    { 
        Destroy(this); 
    } 
    else 
    { 
        Instance = this; 
    } 
}

这是单例的两个基本要求,一个全局可访问的类,但只有一个实例。但是为什么要做一个单例,能用它们做什么呢?

为什么要使用单例?

单例可以非常方便,因为它们允许更轻松地连接游戏的各个部分。

例如,可以使用单例让敌人可以公开访问玩家的位置,或者使用单例将玩家的健康状况暴露给用户界面。

例如这样:

float healthBarValue = Singleton.Instance.playerHealth;

或者,可以从游戏中的任何脚本调用单例的音效函数。

例如这样:

Singleton.Instance.PlaySound(clipToPlay);

只要变量或函数存在于单例中,并且可以公开访问,其他脚本就可以使用它,而无需先设置对它的引用。

这可以使连接游戏的不同部分变得更加容易。但是虽然单例很容易使用,但它们也可能导致问题。

使用单例解决现在遇到的问题,以后可能会产生不同的问题,因此值得了解如果选择在项目中使用单例会发生什么。

那么,可能发生的最坏情况是什么?

单例有不好的地方?

一般来说,随着项目变得越来越大,单例可能会使管理项目变得困难。它们会使项目更难更改、更难扩展,并可能导致测试出现问题。

如果已经研究过使用单例,那么可能已经听说过其中的一些反对意见,但它们对你自己来说究竟意味着什么?

具体来说,使用单例时会出现什么问题?

单例的最大风险之一是,在设计上,首先使它们方便的东西是全局访问。例如,允许每个脚本访问游戏管理器单例可能非常有用。

那么,这有什么问题呢?我们接着往下说明。

游戏管理器单例项目

在项目中使用单例的主要好处之一是避免需要将重要的脚本,例如游戏管理器与可能需要访问它们的许多不同对象紧密连接起来。

例如音频管理器尝试手动将每个想要播放声音的对象连接到音频管理器脚本通常是糟糕的设计。同样,每次创建可能需要它的新对象时尝试在场景中查找音频管理器可能对性能非常不利。

出于这个原因,音频管理器单例会很有意义。因此,假设正是这样做的,即创建了一个具有公共播放音效功能的音频管理器单例。

例如这样:

public void PlaySound(AudioClip clip)
{
    audioSource.PlayOneShot(clip);
}

现在,每当想播放声音时,只需调用音频管理器并传入声音效果即可。

例如这样:

public AudioClip soundEffect;
void Start()
{
    Singleton.Instance.PlaySound(soundEffect);
}

使用这样的单例时,可能会遇到的问题是,是否以及何时决定需要更改声音效果的触发方式。

例如,可能开始使用此方法,在需要的任何地方添加音效触发器,从许多不同的脚本中调用音效函数。

取决于游戏的大小,可能有几十个或几百个不同的地方,都以完全相同的方式调用脚本。

然后,当开始向游戏中添加声音时,可能会注意到有些声音比其他声音大,而另一些声音则开始重复。

因此,假设决定要在触发时调整声音的音量,以便可以在不同的级别播放不同的剪辑,并且还想在某些声音时添加一些额外的变体被触发,以防止它们变得重复。

单独来看,这些都是相对容易创建的简单功能。

除了现在这样做可能是一项艰巨的任务,因为游戏中的每个声音触发器都有一条直接连接到音频管理器的线路,并且只需按原样调用该函数,只传递一个音频剪辑引用而不是其他任何东西。

这意味着,要更改音频管理器现在的工作方式,还需要更改调用它的每个脚本,这样就让我们加重了许多工作负担。

模块化的 Unity 事件触发器

使用单例时,很容易在早期锁定某些功能,使以后很难更改。

然而,根据设计,事件可能是一种有用的方式来改变对游戏中发生的事情的响应,但不会改变触发它的原因。

例如,可以创建一个基本的事件系统,允许一个对象,任何对象,触发一个音效事件,但是对该事件的响应,例如播放什么声音以及如何处理,发生在音频管理器,无需更改触发它的内容即可轻松更改。

虽然这也可以用于单例,但当你想稍后更改某些内容时,事件可能会更宽容,因为可以更轻松地从响应中删除触发器。

虽然在你完成后必须改变单例的行为并不容易,但这不一定是一个主要问题。例如,虽然以后要更改音效功能肯定会很困难,但也不是不可能。

但是,了解这一点后,将使用单例公开给其他对象的每个公共函数和变量视为无法更改的东西会很有帮助。

然后,通过在早期构建你期望需要的功能,可以最大限度地降低以后需要更改其工作方式的风险。

音频管理器可能非常适合单例,因为全局访问和现场存在使使用它更容易。

简而言之,使用单例的主要好处与任务的问题相匹配。

但是…仅仅因为单例似乎是解决问题的好方法,并不总是意味着它会如此。而且,就像以前一样,你现在如何选择使用单例,将影响你以后可以用你的游戏做什么。在使用玩家单例时尤其如此。

播放器对象单例

单例的一个常见用途是将玩家的某些特征暴露给游戏中的其他脚本,例如玩家的位置或他们当前的健康状况。

当你考虑它时,这很有意义。毕竟,几乎游戏中的每个对象都可能以某种方式响应玩家的动作,因此让他们更容易访问玩家和他们的脚本似乎是个好主意。

但是,使用播放器对象单例会导致什么问题?

一般来说,决定何时可以使用单例播放器对象可能很像决定是否可以使用静态变量。使用本质上是全局变量的静态变量通常没问题,只要你只打算在游戏中使用该类型的一个变量即可。

单例也有同样的漏洞。因为需要直接引用单例的脚本这样做,所以每个脚本本质上都与播放器有紧密的联系。

这意味着添加第二个播放器可能非常困难或不可能,这取决于播放器单例与需要访问它的脚本的紧密集成程度。

一般来说,使用单例来管理一个独特的个体对象,例如玩家,比使用单例来管理游戏系统具有更大的风险。

虽然现在对你和你的项目来说可能不是问题,但如果你不确定游戏会是什么样子或可能喜欢哪些功能,那么以这种方式使用单例可能会导致更严重的问题将来添加。

可编写脚本的对象变量与单例

通常,脚本的变量是明确输入的,这意味着如果你想要一个健康栏来检查玩家的健康状况,通常需要引用该玩家健康脚本和变量的特定实例。

虽然单例使这更容易做到,但它们并没有改变你仍然直接引用场景中特定对象上的特定脚本的事实。如果你需要更改它,则必须重写代码,这就是为什么单例会使以后难以重构代码。

另一方面,可编写脚本的对象变量以不同的方式工作,它们可用于引用变量类型而不是特定变量。

这意味着,健康变量可以很容易地在 Inspector 中换成相同类型但在不同对象上的变量。这意味可以创建一个完整的第二个玩家,拥有他们自己的健康栏,你所要做的就是改变健康栏正在读取的健康变量类型。

这就像在检查器中换出可编写脚本的对象一样简单,而无需更改脚本。

使用单例有时会使管理项目变得更加困难,因为你使用的引用通常是对单例对象的直接、显式引用,例如玩家或某种类型的游戏系统。

更重要的是,如果你走上了使用单例的路线,你的游戏中可能会有多个单例系统。这使得跟踪哪个脚本正在做什么变得非常困难,因为任何脚本都可以做任何事情。

但是,可以限制全局系统访问可能导致的一些问题,同时仍然首先利用使用单例的好处。

例如,使用主单例,通过单点访问提供对其他游戏系统的访问。

主单例

这种方法虽然使用单例,但在技术上是一种不同的设计模式,称为服务定位器,它使用网关单例来管理对多个其他游戏系统的访问。

当你想要单例可以提供的功能和易于访问但具有额外的控制层时,这可能很有用。这通过在游戏系统和可能想要访问它们的对象之间添加单个主单例来实现。

例如这样:

public class Singleton : MonoBehaviour
{
    public static Singleton Instance { get; private set; }
    public AudioManager AudioManager { get; private set; }
    public UIManager UIManager { get; private set; }
    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(this);
            return;
        }
        Instance = this;
        AudioManager = GetComponentInChildren<AudioManager>();
        UIManager = GetComponentInChildren<UIManager>();
    }
}

在这个例子中,Unity用户界面和音频管理器是单例的子对象。

设置主单例

使用此方法时,需要使用 Get Component in Children 设置单例的引用,因为无法在 Inspector 中设置只读引用。

此外,由于在下一个更新循环中删除了已破坏的组件,因此即使要删除脚本,Get Component 调用仍将发生。

这就是为什么如果脚本要被销毁,使用 return 关键字会很有帮助,如果它是重复的,它可以阻止函数继续前进。

然后,你将能够通过引用单例实例及其子系统,通过单个入口点访问游戏的各个系统。

例如这样:

Singleton.Instance.AudioManager.PlaySound();

虽然这并不能防止以后需要更改代码可能导致的问题,但它确实在单例系统和可能想要访问它们的对象之间添加了一层抽象。

这可以更容易地以一种扩展难度较小的方式构建自己的游戏。

在 Unity 中使用单例的正确方法是什么?

虽然没有一种正确的方法来使用单例,但使用单例可能导致的问题通常取决于项目以及打算用它做什么。

这是因为使用单例的风险更多地与自己选择使用它的方式有关,而不是与单例本身有关。例如,当你以后可能想要添加额外的玩家时使用玩家对象单例可能会给自己带来重大问题。

如果两个脚本在不知情的情况下尝试同时保存游戏,则允许全局访问文件保存系统可能会导致问题。

在你知道想用它们做什么之前构建单例可能会使以后需要更改时更新它们变得更加困难。

总而言之有时使用单例可能很有意义,仅仅只是游戏发布于不发布的区别。

所以,虽然很多人可能会引导你远离单例,虽然绝对有充分的理由不使用它们,但如果你了解风险并谨慎使用它们,它们作为帮助你更好地管理游戏的工具会非常有用的。

相关信息

  • 类型:知识
  • 字数:3791
  • 字符:9551
  • 适用软件:Unity
  • 说明:无
  • 编号:61472

热门内容

提示:3D天堂作为服务提供者,尊重网络版权及知识产权,对某些行为的发生不具备充分的监控能力,若无意间侵犯到您的权利,请 联系我们,我们会在收到信息后尽快给予处理。

本站文章版权归本站自创作者所有,未经允许不得转载!