当几个类需要访问相同的数据时,应在哪里声明数据?


39

我有一个使用C ++编写的基本2D塔防游戏。

每个地图都是从GameState继承的单独的类。地图将逻辑和绘图代码委托给游戏中的每个对象,并设置数据(例如地图路径)。在伪代码中,逻辑部分可能看起来像这样:

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

对象(爬行,塔和导弹)存储在指针矢量中。这些塔必须能够进入蠕变矢量和导弹矢量,以制造新的导弹并确定目标。

问题是:我在哪里声明向量?它们是否应该是Map类的成员,并作为参数传递给tower.update()函数?还是在全球宣布?还是我完全想不到的其他解决方案?

当几个类需要访问相同的数据时,应在哪里声明数据?


1
全局成员被认为是“丑陋的”,但是敏捷并且使开发变得容易,如果这是一个小游戏,那没问题(IMHO)。您还可以创建一个处理逻辑的外部类(为什么塔需要这些向量)并可以访问所有向量。
乔纳森·康奈尔

-1如果这与游戏编程有关,那么也吃披萨。
索取

9
@Maik:软件设计与游戏编程无关吗?仅仅因为它也适用于其他编程领域并不能使其脱离主题。
BlueRaja-Danny Pflughoeft

@BlueRaja的软件设计模式列表更适合SO,这就是它的全部目的。GD.SE用于游戏编程,而不用于软件设计
Maik Semder 2011年

Answers:


53

当您在整个程序中需要一个类的单个实例时,我们将该类称为服务。有几种在程序中实现服务的标准方法:

  • 全局变量。这些是最容易实现的,但最糟糕的设计。如果使用过多的全局变量,您会很快发现自己编写的模块之间相互依赖过多(强耦合),这使得逻辑流程很难遵循。全局变量不是多线程友好的。全局变量使跟踪对象的生存期更加困难,并使名称空间混乱。但是,它们是性能最高的选项,因此有时可以并且应该使用它们,但要谨慎使用。
  • 单身人士。关于10 - 15年前,单身人大设计模式了解。但是,如今他们被鄙视了。它们更易于使用多线程,但是您必须一次将它们的使用限制为一个线程,而这并非总是想要的。跟踪生命周期与使用全局变量一样困难。
    典型的单例类将如下所示:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
    
  • 依赖注入(DI)。这仅意味着将服务作为构造函数参数传入。为了将其传递到类中,必须已经存在一个服务,因此,两个服务无法相互依赖。在98%的情况下,这就是您想要的 (对于其他2%的情况,您始终可以创建一个setWhatever()方法并稍后传递服务)。因此,DI与其他选项没有相同的耦合问题。它可以与多线程一起使用,因为每个线程可以简单地拥有每个服务的实例(并仅共享其绝对需要的实例)。如果您对此有所关注,它还可以使代码可进行单元测试。

    依赖项注入的问题在于它占用了更多的内存。现在,类的每个实例都需要引用它将使用的每个服务。而且,当您有太多服务时,使用它会很烦人。有一些框架可以缓解其他语言中的此问题,但是由于C ++缺乏反思,因此C ++中的DI框架往往比手动完成更多的工作。

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.
    

    请参阅此页面(来自C#DI框架Ninject的文档)以获取另一个示例。

    依赖注入是解决此问题的常用方法,也是您在StackOverflow.com上最喜欢此类问题的答案。DI是控制反转(IoC)的一种。

  • 服务定位器。基本上,只有一个类包含每个服务的实例。您可以使用Reflection进行操作,也可以在每次要创建新服务时向其添加新实例。您仍然遇到与以前相同的问题- 类如何访问此定位器?-可以通过上述任何一种方式解决,但是现在您只需要针对您的ServiceLocator班级即可解决此问题,而无需进行数十种服务。如果您关心这种方法,此方法也是可单元测试的。

    服务定位器是控制反转(IoC)的另一种形式。通常,执行自动依赖项注入的框架还将具有服务定位器。

    XNA (微软的C#游戏编程框架)包括一个服务定位器。要了解更多信息,请参见此答案


顺便说一句,恕我直言,塔不应该知道这些小兵。除非您打算简单地循环遍历每座塔的蠕变列表,否则您可能要实现一些非平凡的空间划分;而且这种逻辑不属于Towers类。


评论不作进一步讨论;此对话已转移至聊天
乔什

我读过的最好,最清晰的答案之一。做得好。我以为始终应该共享服务。
Nikos

5

我个人将在这里使用多态。为什么要有一个missile向量,一个tower向量和一个向量creep..当它们都调用相同的函数时;update?为什么没有指针的一些基础类的载体EntityGameObject

我发现设计的一种好方法是认为“就所有权而言这有意义吗?” 显然,塔拥有一种更新自身的方法,但是地图是否拥有其上的所有对象?如果您走向全球,您是说没有任何东西拥有塔楼和小兵吗?全局通常是一个糟糕的解决方案-它会促进不良的设计模式,但使用起来却容易得多。考虑权衡“我要完成这个吗?” 和“我想要可以重用的东西”吗?

解决此问题的一种方法是某种形式的消息传递系统。该tower可以发送消息到map(由它来访问,也许一提到它的主人?),它击中creep,而map然后告诉creep它被击中了。这非常干净,可以隔离数据。

另一种方法是仅在地图本身上搜索所需内容。但是,此处的更新顺序可能存在问题。


1
您关于多态性的建议实际上并不相关。我将它们存储在单独的向量中,因此我可以逐个遍历每种类型,例如在绘图代码中(首先要绘制某些对象)或在碰撞代码中。
多汁

对我而言,地图确实拥有实体,因为此处的地图类似于“级别”。我会考虑您对邮件的想法,谢谢。
多汁

1
在游戏中,性能至关重要。因此,相同对象时间的向量具有更好的参考位置。而且,带有虚拟指针的多态对象具有糟糕的性能,因为它们不能内联到更新循环中。
Zan Lynx

0

在这种情况下,严格的面向对象编程(OOP)会崩溃。

根据OOP的原理,应使用类将数据与相关行为分组。但是您的行为(定位)需要的数据彼此不相关(塔和塔)。在这种情况下,许多程序员将尝试将行为与所需的部分数据相关联(例如,塔式处理目标,但不了解蠕变),但还有另一种选择:不将行为与数据分组。

不用将定向行为作为塔类的方法,而应使它成为一个自由函数,该函数接受塔和蠕变作为参数。这可能需要将塔和蠕变类中剩下的更多成员公开,这没关系。数据隐藏是有用的,但是它只是一种手段,而不是目的本身,您不应该成为它的奴隶。此外,私有成员不是控制数据访问的唯一方法-如果没有将数据传递到函数中并且它不是全局的,则该函数实际上将其隐藏。如果使用此技术可以避免全局数据,则可能实际上是在改进封装。

这种方法的一个极端示例是实体系统架构。

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.