如何在C ++引擎编程中正确使用单例?


16

我知道单例很糟糕,我的旧游戏引擎使用了单例“ Game”对象,该对象处理从保存所有数据到实际游戏循环的所有事情。现在我要制作一个新的。

问题是,要在SFML中绘制某些内容,请window.draw(sprite)在window是的地方使用sf::RenderWindow。我在这里看到2个选项:

  1. 制作一个单例游戏对象,游戏中的每个实体都将检索该对象(我以前使用过的对象)
  2. 使它成为实体的构造函数:(Entity(x, y, window, view, ...etc)这只是荒谬而烦人的)

在将实体的构造函数保持为x和y的同时,执行此操作的正确方法是什么?

我可以尝试跟踪我在主游戏循环中所做的所有事情,只是在游戏循环中手动绘制其精灵,但这似乎也很麻烦,我也希望对实体的整个绘制功能具有绝对的完全控制权。


1
您可以将窗口作为'render'函数的参数传递。
dari 2015年

25
单身人士还不错!它们可能是有用的,有时甚至是必要的(当然这值得商)。
ExOfDe

3
随意用简单的全局变量替换单例。创建“按需”的全球所需资源毫无意义,也没有传递这些资源的意义。对于实体,您可以使用“级别”类来保存与所有实体相关的某些事物。
snake5

我在自己的main中声明了窗口和其他依赖项,然后在其他类中有了指针。
KaareZ 2015年

1
@JAB可以通过main()的手动初始化轻松修复。延迟初始化使它在未知的时刻发生,这对核心系统来说从来都不是一个好主意。
snake5 2015年

Answers:


3

仅将渲染精灵所需的数据存储在每个实体中,然后从实体中检索它并将其传递给窗口以进行渲染。无需在实体内部存储任何窗口或查看数据。

您可能有一个顶级GameEngine类,其中包含一个Level类(包含当前使用的所有实体)和一个Renderer类(包含用于渲染的窗口,视图以及其他任何东西)。

因此,顶级类中的游戏更新循环如下所示:

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}

3
单身人士没有理想的选择。为何不必公开实施内部?为什么要写Logger::getInstance().Log(...)而不是仅仅写Log(...)?当问到是否可以手动手动完成一次时,为什么要随机初始化该类?创建和使用引用静态全局变量的全局函数要简单得多。
snake5

@ snake5在Stack Exchange上证明单身人士就像同情希特勒。
威利山羊

30

一种简单的方法是仅仅使曾经是Singleton<T>全局的事物成为T现实。全局变量也有问题,但是它们并不能代表很多额外的工作和样板代码来强制执行琐碎的约束。这基本上是唯一不涉及(可能)涉及实体构造函数的解决方案。

比较困难但可能更好的方法是将依赖项传递到需要它们的地方。是的,这可能涉及Window *以看起来很粗糙的方式将a传递给一堆对象(例如您的实体)。它看起来很粗糙的事实应该可以告诉您一些事情:您的设计可能很粗糙。

之所以更困难(除了涉及更多的类型化),是因为这通常导致重构接口,以便更少的叶级类需要“传递”的东西。这消除了将渲染器传递给所有事物的许多固有固有的丑陋感,并且还通过减少了依赖关系和耦合的数量来提高了代码的一般可维护性,通过将依赖关系作为参数,这种变化的程度非常明显。当依赖项是单例或全局时,系统之间的互连方式不太明显。

但这可能是一项重大任务。事实发生后对系统执行此操作可能会非常痛苦。对于您而言,暂时暂时不让您的系统处于单身状态可能更为实用(特别是如果您要实际发布一款效果还不错的游戏;玩家通常不会关心您是否拥有一个或四个)。

如果您确实想对现有设计进行此操作,则可能需要发布有关当前实现的更多详细信息,因为实际上并没有进行这些更改的通用清单。或者来聊天中讨论它。

从您发布的内容来看,我认为朝“无单例”方向迈出的一大步就是避免您的实体需要访问窗口或视图。它表明它们是自己绘制的,而您不必让实体自己绘制。您可以采用一种方法,其中实体仅包含允许它们由某些外部系统(具有窗口和视图引用)绘制。实体只是公开其位置以及应该使用的子画面(或某种引用的子画面,如果要在渲染器中缓存实际的子画面以避免重复的实例)。简单地告诉渲染器绘制一个特定的实体列表,它循环遍历,从中读取数据并使用其内部保存的窗口对象来调用draw查找该实体的精灵。


3
我对C ++不熟悉,但是对于这种语言,没有舒适的依赖注入框架吗?
bgusach 2015年

1
我不会将它们描述为“舒适的”,并且我觉得它们通常没有特别的用处,但是其他人可能对它们有不同的经验,因此提出它们是一个很好的建议。

1
他描述的这种方法使实体不会自行绘制它们,而是保留信息,并且由一个系统处理所有实体的绘制在当今大多数流行的游戏引擎中被大量使用。
Patrick W. McMahon 2015年

1
对于“看起来很粗糙的事实,应该告诉您一些信息:您的设计可能很粗糙”,为此+1。
Shadow503

+1可同时提供理想的案例和务实的答案。

6

继承自sf :: RenderWindow

SFML实际上鼓励您从其类继承。

class GameWindow: public sf::RenderWindow{};

在这里,您可以为工程图实体创建成员工程图功能。

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

现在您可以执行以下操作:

GameWindow window;
Entity entity;

window.draw(entity);

如果您的实体要通过使Entity继承自sf :: Sprite来保留自己的唯一子画面,则可以更进一步。

class Entity: public sf::Sprite{};

现在sf::RenderWindow可以只绘制实体,实体现在具有setTexture()和的功能setColor()。实体甚至可以将子画面的位置用作其自己的位置,从而允许您使用该setPosition()函数来移动实体及其子画面。


最后,如果您只有:

window.draw(game);

以下是一些快速的示例实现

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

要么

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};

3

您在游戏开发中避免单例的方式与在其他每种软件开发中避免单例的方式相同:传递依赖项

有了这样的方式,你可以选择直接通过依赖裸类型(如intWindow*等),或者你可以选择把它们放入一个或多个定制的包装类型(如EntityInitializationOptions)。

前一种方法可能很烦人(如您所知),而后一种方法则允许您在一个对象中传递所有内容并修改字段(甚至专门化选项类型),而无需四处更改每个实体构造函数。我认为后一种方法更好。


3

单身人士还不错。相反,它们很容易被滥用。另一方面,全局变量更容易被滥用,并带来更多的问题。

用整体代替单身人士的唯一正当理由是平息宗教上的单身人士。

问题在于设计中包含的类仅存在一个全局实例,并且需要从任何地方进行访问。当您最终拥有多个单例实例时,这种情况就会破裂,例如在实现分屏的游戏中,或者在足够大的企业应用程序中,您发现单个记录器毕竟并不总是一个好主意时。

最重要的是,如果您确实有一个类,其中有一个无法通过引用合理传递的单个全局实例,则单例通常是次优解决方案池中较好的解决方案之一。


1
我是一个虔诚的单身恨者,我也不认为全局解决方案。:S
Dan Pantry 2015年

1

注入依赖项。这样做的好处是现在您可以通过工厂创建各种类型的依赖关系。不幸的是,将单身人士从使用它们的班级中剔除就像将一只猫的后腿划过地毯一样。但是,如果注入它们,则可以随时交换实现。

RenderSystem(IWindow* window);

现在,您可以注入各种类型的窗口。这样,您就可以使用各种类型的窗口针对RenderSystem编写测试,从而可以查看RenderSystem如何损坏或执行。如果您直接在“ RenderSystem”内部使用单例,则这不可能或更困难。

现在,它更具可测试性,模块化,并且还与特定的实现分离。它仅取决于接口,而不取决于具体的实现。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.