【问题标题】:Using singletons inside a factory class (C++)在工厂类中使用单例 (C++)
【发布时间】:2018-06-19 04:28:24
【问题描述】:

我正在使用 DirectX 11 和 C++(使用默认 Visual Studio 模板)开发游戏引擎,我有一些问题无法在线找到答案。

所以游戏有三个主要组件,实际游戏类、渲染器和逻辑类。游戏类应该是这样的

class Game {
    Game();
    ~Game();

    Logic* m_Logic;
    Renderer* m_Renderer
}

(如果你很好奇,我这样做有两个原因,1. 玩类,我以前用传统的方法做过,所以我想尝试一下其他方法,看看它是否有任何真正的好处。 2.使两者的代码完全相互抽象)

我的问题是,将两个子类变成单例(假设游戏类是)是否是一个坏主意。我理解人们何时以及为什么会使用它们,但考虑到游戏类中只会有一个实例,这可以正常工作吗?如果游戏类不是单例,而是可以同时运行多个副本(因此允许同时运行多个游戏,再次出于好奇)

注意:因为每个人都一直在指出这一点。我知道他们不是一个好主意。这是一个测试/实验。我正在享受有趣的编程。让我玩得开心:P

示例:当游戏类可以有多个实例时,我可能希望渲染器成为单例的一个示例。由于游戏的不同实例之间的渲染器是相同的,因此不需要有多个实例。把它想象成一个包含所有函数的容器,它只接受存储在游戏类中的各种变量,并对它进行处理以输出到屏幕。逻辑类也是如此。

【问题讨论】:

  • 单身从来都不是一个好主意。
  • 在游戏类的情况下,由于我(通常)一次只希望运行一个,因此将其作为单例运行是有意义的。无论哪种方式,就像我解释的那样,这都是一个实验。如果我想制作游戏,我宁愿只使用 Unreal 或 Unity,我很喜欢几年前制作引擎并且想再做一次,但更好/不同
  • 你为什么使用指针?
  • 我再说一遍,单身从来都不是一个好主意。单例只是伪装的全局变量,你用来欺骗自己认为你没有使用全局变量。至于为什么它们是个坏主意,全局变量是 Minecraft 在 Beta 1.6 之前无法在服务器上拥有下界的原因。到处都在使用MinecraftServer.get().theWorld,必须更新才能在多个世界中正常工作。
  • 我也会重复一遍。我正在试验和玩弄一切。我不在乎这是否是“一个坏主意”,我想尝试一下,看看会发生什么,然后从中学习。如果您了解范围并努力解决,就很容易避免概述的那种问题。我知道一个游戏永远只有一个渲染器,所以我只需要一个,而且只有一个。 @Sid S 因为我不希望堆栈中的整个类,我更希望游戏类指向实际类(将自身作为参考)。我是如何学到东西的

标签: c++ singleton


【解决方案1】:

有些人会说“单机从来都不是一个好主意”,但直到今天,许多流行游戏引擎背后的工程师并不持有这种说法。我会建议您尽可能减少它们的使用,但在某些情况下它们非常适合。事实上,Jason Gregory 在 Naughty Dog 上写的“游戏引擎架构”有一整节介绍了它们的使用。我将举一个我认为是混合方法的例子。

如果您通读虚幻引擎的代码库(或至少使用该引擎),您会发现类似以下内容:

extern class UEngine * GEngine

UEngine 实际上是所有 Engine 实现的基类(并且有多个实现,因为需要不同的变体)。在启动期间,启动器确定它需要加载哪个变体,然后继续构建整个系统将使用的a single instance。这与您的方法类似,但您可以在初始化期间设置一次对 Game 对象的全局引用,而不是制作多个 Singletons:

extern std::unique_ptr<Game> game; // Global game reference in some .h file

(in .cpp)
std::unique_ptr<Game> game;
int main(int numArgs, char ** args) {
    game = std::make_unique<Game>( ... ); // Initialize game exactly once!!
    ...
}

这种方法允许您做一些有趣的事情,特别是如果您像虚幻引擎背后的工程师所决定的那样将 Game 制作成一个界面,然后根据其背后的需要进行多种实现。

Game 维护的每个对象都应该是 Singleton 吗?我不这么认为,因为它实际上限制了你以后可以做的事情。游戏至少需要支持以下内容:

  • 以正确的顺序创建、启动和关闭诸如 Logic 和 Renderer 之类的东西(它拥有它创建的所有这些对象,因此与 Game 接口的所有代码都不可避免地与它们交互!)
  • 能够检索对 Logic 和 Renderer 等事物的引用,或者尽可能将它们隐藏,但提供有限的接口来访问它们的功能
  • 由于 Game 现在是您唯一的/唯一的全局变量之一,它成为进入引擎环境的网关,因此您可以做很多事情来封装它维护的组件,而不是制作许多单例对象

我认为这比将所有核心类都设为单例更好。

【讨论】:

  • 感谢您的回答。您提供的代码是我通常用来创建游戏类的代码,但它让我思考,为什么不使用游戏维护的其他一些核心元素。由于只需要存在一个,它不一定是有害的。虽然这确实引出了一个问题,但如果我随后创建了游戏类的多个实例(带有单例元素),它会有什么反应?显然这只是一个坏主意,但我喜欢搞砸代码并看看我能用它做什么
  • 是的,您可以采取一些措施来缓解这种情况,即让您的游戏的构造函数私有并让它列出一个朋友类(可能是“GameLauncher”或其他)。 GameLauncher 将负责创建唯一的 Game 实例,没有其他代码可以做到这一点。另外,如果你将每个核心类都设为 Singleton,那么没有人真正拥有它们,甚至 Game 也没有。明确谁负责初始化/关闭它们就更难定义和执行了。
  • 这是有道理的。我对上一个游戏引擎做了类似的事情。在您概述的情况下,我将在外部创建这些核心类,然后将其输入到构造函数中,例如logic = new Logic(); renderer = new Renderer(), game = new Game(logic, renderer),然后再由同一个游戏启动器清除这些核心类。这些只是我的想法,不知道这有多糟糕(如果有的话)。我知道一个与虚幻引擎并行的方法,即输入控制器。只有一个存在,并且只是根据需要输入到每个类中。
  • 对于单身人士的另一个看法,请阅读the Scott Meyers singleton
  • 我实际上是在不久前读到的,这就是促使我提出这个问题的原因。很好的阅读,有很多有价值的信息
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-27
相关资源
最近更新 更多