游戏编程模式--单例模式

单例模式

  定义:确保一个类只有一个实例,并为其提供一个全局的访问入口。

  那么什么情况下使用单例?最常见的情况就是一个类需要与一个维持自身状态的外部系统进行交互,比如说打印机。大多数情况下都是多人共用一个打印机,这意味着可能由多个人同时向这个打印机发送打印任务,这个时候管理打印机的类就必须熟悉打印机的当前状态并协调这些任务的执行。这个时候就不允许存在多个打印机的实例,因为实例无法知道其他的实例所做的操作,也就无法进行整体的管理。

  我们先看看最常见的单例的实现方式:

class FileSystem
{
public:
    static FileSystem& instance_()
    {
        if(instance_ == nullptr)
        {
            instance_ = new FileSystem();
        }

        return instance_;
    }

private:
    FileSystem(){}
    static FileSystem* instance_;
};

  c++11保证一个局部静态变量初始化只进行一次,哪怕实在多线程的情况下也是如此,所以c++11中这样写更优雅。

class FileSystem
{
public:
    static FileSystem& instance()
    {
        static FileSystem& instance_ = new FileSystem();

        return instance_;
    }

private:
    FileSystem(){}
};

特性

  从代码实现上看,单例模式由以下几个特性:

  •   如果我们不使用它,就不会创建实例。
  •   它在运行时初始化。还有一种方法是使用静态类,但静态类有个局限就是:自动初始化。而且它是在main函数之前初始化,这也就意味了它不能使用运行时才能知道的信息,并且不能相互依赖——编译器并不能保证静态函数间初始化的顺序。
  •   你可以继承单例,这可以让我们更好的控制我们的代码,比如对于多平台的文件系统,我们定义两个子类继承FileSystem的接口,通过一个编译指令控制文件系统类型的绑定,程序的其他代码可以与文件系统解耦(因为其他代码只是用FileSystem::instance())。

后悔使用单例模式的原因

  (1)它是个全局变量

   根据前人的经验,全局变量时有害的,我们应该远离全局变量。为什么了?

  • 它令代码晦涩难懂。本来在一个函数中,我们只需要关注函数段的局部代码即可,但如果在函数中使用了全局变量,则我们就需要追踪所有能改变全局变量状态的代码,如果这样的代码由成百上千行,你就会痛恨全局变量了。
  • 全局变量促进了耦合。因为全局变量的特性,你只需要包含相应的头文件,就可以使用这个变量,这就增加了代码的耦合程度。
  • 它对并发并不友好,这个显而易见。

  (2)它是个画蛇添足的方案

   从定义上看出,单例模式其实是解决了两个问题:第一保证一个实例,第二提供已访问入口。保证一个单例是很有用的,但谁说我们希望谁都能操作它?而第二个问题,便利的访问通常是我们使用单例的主要原因。但这同时也会引出新的问题,比如一个日志类,一开始大家都使用这个单例的日志类时很方便,但随着项目的深入,对于日志的需求也复杂了起来,比如要求分类写入多个日志文件,这个时候因为你是单例,所以为了支持多个实例,你就要修改每个你调用这个类的地方,结果便利的访问也就不那么便利了。

  (3)延迟初始化剥离了你的控制

   延迟初始化也就是在第一次调用的时候初始化,这样也就不能保证你初始化的时机。这通常在对性能要求非常高的游戏中时不被允许的,设想一个音频单例单例,初始化需要几百毫秒,而且伴随着内存的分配,如果你的游戏进行中突然调用这个单例,则会进行初始化操作,这件带来不可接受的游戏掉帧和卡顿。而且也不利于内存布局的控制。

  在游戏中通常使用这样的方式来实现单例模式:

class FileSystem
{
public:
    static FileSystem& instance()
    {
        return instance_;
    }

private:
    FileSystem(){}
    static FileSystem instance_;
};

我们应该使用单例吗?

  (1)首先看看你需不需类

  在游戏中,我看见了太多的“manager”类了,它们的初衷时为了管理其它对象,虽然有时确实有用,但我更多的时看到它被滥用。比如下面的一个例子:

class Bullet
{
public:
    int getX() const {return x_;}
    int getY() const {return y_;}
    void setX(int x) {x_=x;}
    void setY(int y) {y_=y;}

private:
    int x_;
    int y_;
};

class BulletManager
{
public:
    Bullet* create(int x,int y)
    {
        Bullet* bullet = new Bullet();
        bullet->setX(x);
        bullet->setY(y);

        return bullet;
    }

    bool isOnScreen(Bullet& bullet)
    {
        return bullet.getX() >=0
            && bullet.getY >=0
            && bullet.getX() <= SCREEN_WIDTH
            && bullet.getY() <= SCREEN_HEIGHT;
    }

    void move(Bullet& bullet)
    {
        bullet.setX(bullet.getX() + 5);
    }
};

  这个例子有点极端,但现实中很多manager类简化后就是这样的一个逻辑。我们通过一个单例来管理Bullet,感觉上好像合理,但仔细分析后,发现这个manager根本就没有存在的必要,设计出这样一个类的人应该对OOP不太熟悉。首先我们分析这三个方法:

  1.   create创建一个Bullet,如果我们想要更好的管理Bullet的创建,那我们应该使用工厂模式,它会使我们的代码可维护性更高,或者直接在Bullet中提供一个静态函数来创建一个新的对象,从设计上来说更显得合理;
  2.   isOnScreen判断是否在屏幕中,这个方法既可以放在业务层代码中也可以放入Bullet中(因为可以理解我为这是bullet的一个状态),把它放在这个Manager类中显得不伦不类;
  3.   move是移动bullet,这个就好设计了,move本来就是bullet的行为,所以如果没有特别的需求,move应该放入Bullet类中。

  所以,修改后,我们只需要一个Bullet类:

class Bullet
{
public:
    Bullet(int x,int y):x_(x),y_(y)
    {

    }

    bool isOnScreen()
    {
        return x_ >=0
            && y_ >=0
            && x_ <= SCREEN_WIDTH
            && y_ <= SCREEN_HEIGHT;
    }

    void move()
    {
        x_ += 5;
    }
};

  这样修改后,类的设计显得更合理,更自然。我们完全不需要一个额外的manager单例来帮助我们管理,所以,在我们设计单例时,首先就要分析我们是否真的需要这个单例。

  (2)将类限制为单一实例

  我们使用单例模式,很多时候只是要限制该类只有一个实例,但这并不意味着我们要提供一个全局访问,我们可能只是想在某一部分代码中访问这个实例,这个时候如果使用单例模式提供一个全局的访问接口,将会削弱整体的框架。我们可以有几种方式避免这种情况的出现。比如:

class FileSystem
{
public:
    FileSystem()
    {
        assert(!instantiated);
        instantiated = true;
    }

    ~FileSystem()
    {
        instantiated = false;
    }

private:
    static bool instantiated;
};

bool FileSystem::instantiated = false;

  通过一个断言,保证FileSystem只有一个实例。

  (3)为实例提供便捷的访问方式

  使用单例模式的另一个需求就是便利的访问,它能让我们随时随地的获取这个唯一的实例。但这与我们通用的编程准则不符,我们通常是在保证功能的情况下尽量限制变量使用的一个范围,这样我们就只需要记住它的地方机会少很多(想想全局变量带来的问题)。那在不适用单例模式的时候,我们还有什么其它的途径访问一个对象了?通常我们会有这么几种方式:

  •   作为参数传递进去。这个是最简单,通常也是最好的方法。但有时我们会碰到这样的情况,即这个对象与函数的内容没什么必然的联系,比如我们执行一个渲染函数时要记录日志,如果把日志对象加入到函数的参数列表中,将会非常的奇怪,对于这种情况,我们需要一些其它的办法。
  •   在基类中获取它。这需要设计一个良好的继承体系,既然所有的子类都要访问这个对象,我们可以把这个对象放到父类中让所有的子类都能访问到它。
  •   使用其它全局对象访问它。现实中我们不太可能把所有的全局变量都移除,比如在大部分的游戏代码中我们都会定义一个代表整个游戏状态的Game或者World对象,我们可以把全局对象放入这些已有的全局变量中来减少它们的数量。
  •   使用服务定位其来访问。这是一种专门设计一个类来给对象做全局访问的,将会在服务器定位模式一节讲解。

结语

  所以,我们应该在什么时候使用单例了?老师说,单例并没有你想象的那样重要,如果你要确保类只被实例化一次,可以简单的使用一个静态类,如果还不满足要求,可以使用一个静态的标识符在运行时检查是否只有一个实例被创建。不过使用与否还是要视你自己需求来定,但一定要防止单例模式的滥用,这不会给你带来任何的好处。

原文地址:https://www.cnblogs.com/xin-lover/p/10454337.html

时间: 2024-10-21 05:32:00

游戏编程模式--单例模式的相关文章

Game Programming Patterns(游戏编程模式)

Game Programming Patterns(游戏编程模式) 大部分游戏开发者在他们游戏项目上总是一个巨大的挑战,总是东拼西凑,修修补补.很多游戏项目常常以失败告终,或者是被淹没在复杂而繁琐的代码中.如何解决这些问题? 各位看官,不管你是对游戏开发感兴趣,或者正在饱受代码不断增长带来的灾难,这本书将是你们的福音! 这本Game Programming Patterns 是由Bob Nystrom(一位在EA待过7年,有着20年游戏开发经历的工程师编写).本书将告诉你,什么模式能够帮你理清和

【游戏设计模式】之四 《游戏编程模式》读书笔记:全书内容梗概总结

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://blog.csdn.net/poem_qianmo/article/details/53240330 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 本文的Github版本:QianMo/Reading-Notes/<游戏编程模式>读书笔记 这是一篇超过万字读书笔记,总结了<游戏编程模式>一书中所有章节与内容的知识梗概. 我们知道,游戏行业其实一直很缺一本系

Game Programming Patterns(游戏编程模式)-简介

游戏编程模式-简介 本系列博客是:Game Programming Patterns 的中文翻译版本. 翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人. 博客虽然水分很足,但是也算是博主的苦劳了, 如需转载,请附上本文链接http://blog.csdn.net/cyh_24/article/details/46868419,不甚感激! 本系列博客 目录,可点击进入. 简介 ============================ 在我五年级

《游戏编程模式》(8)

<游戏编程模式>最后一篇,刚从英国玩了一圈,春节又要到啦 Chapter 19 对象池 使用固定的对象池重用对象,取代单独地分配和释放对象,达到提升性能和优化内存使用的目的. 使用情境: 频繁创建销毁对象: 对象大小基本一致: 堆上分配内存较慢或可能产生内存碎片: 粒子类: 用union节省内存:粒子使用时用live结构体,不使用时用next指针 1 class Particle 2 { 3 4 public: 5 Particle() 6 : framesLeft_(0) 7 {} 8 9

Game Programming Patterns(游戏编程模式)-架构,性能与游戏

游戏编程模式- 架构,性能与游戏 本系列博客是:Game Programming Patterns 的中文翻译版本. 翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人. 博客虽然水分很足,但是也算是博主的苦劳了, 如需转载,请附上本文链接,不甚感激! 本系列博客 <游戏编程模式>– 目录,可点击进入. 架构,性能与游戏 ============================ 在我们埋头研究一堆的设计模式之前,我想先告诉你,对于软件架构,我个

《游戏编程模式》记录

写在前面 这本书长这样 我还没有看过“GOF”,我所读到的设计模式都是这本书(游戏角度)给出的定义,害怕GOF中的定义过于抽象. 没有在项目代码晃来晃去经历的,或者没有工作至少半年的,不用着急买这本书,因为估计看不懂. 本文用来重点记录“我觉得XX设计模式是什么”,以及“当我在看XX设计模式时,我在想什么” 这本书的写作方式,属于我喜欢的“谈话口吻” 命令模式 书面定义:“将一个请求封装成一个对象,从而允许你使用不同的请求.队列或日志将客户端参数化,同时支持请求操作的撤销和恢复” 他的举例:按键

游戏编程模式-事件队列

“对消息或事件的发送与受理进行事件上的解耦.” 动机 如果你曾从事过用户界面编程,那肯定对“事件”不陌生了.每当你在界面中点击一个按钮或下拉菜单,系统都会生成一个事件,系统会把这个事件抛给你的应用程序,你的任务就是获取到这些事件并将其与你自定义的行为关联起来.那么为了获取到这些事件,你的代码通常都会由个事件循环.就像这样: while(running) { Event e = pollEvent(); //handle event } 可以看到,这段代码会不断的获取事件,然后处理.但如果期间再事

游戏编程模式-子类沙盒

“使用基类提供的操作集合来定义子类中的行为.“ 动机 在游戏中,我们可以实现各种各样的想法,比如说创造一个超级英雄, 我们为超级英雄创造各种能力.这个时候我们可以怎么做了?建立一个superpower的基类,然后使用派生的想法,构建各种派生类来实现超能力.但这里会很快的出现问题,因为超能力的多种多样,我们可能需要在派生类中做各种可能的事情:比如播放音效.产生视觉效果.与AI交互.创建和销毁其它游戏实体以及产生物理效果.它们可能触及代码库的每一个角落.很明显,这样会: 产生大量的冗余代码.虽然各种

游戏编程模式-对象池

“使用固定的对象池重用对象,取代单独的分配和释放对象,以此来达到提升性能和优化内存使用的目的.” 动机 假设我们正在致力于游戏的视觉效果优化.当英雄释放魔法时,我们想让一个火花在屏幕上炸裂.这通常需要一个粒子系统(一个用来生成大量小的图形并在它们生存周期产生动画的引擎)来实现.而这个粒子系统实现这个火花的时候会产生大量的粒子,我们需要非常快速的创建这些粒子同时在这些粒子“死亡”的时候释放这些粒子对象.在这里,我们会碰到一个严重的问题——内存碎片化. 碎片化地害处 为游戏和移动设备编程在很多方面都