游戏编程模式 Game Programming Patterns (Robert Nystrom 著)

第1篇 概述

第1章 架构,性能和游戏 (已看)

第2篇 再探设计模式

第2章 命令模式 (已看)

第3章 享元模式 (已看)

第4章 观察者模式 (已看)

第5章 原型模式 (已看)

第6章 单例模式 (已看)

第7章 状态模式 (已看)

第3篇 序列型模式

第8章 双缓冲 (已看)

第9章 游戏循环 (已看)

第10章 更新方法 (已看)

第4篇 行为型模式

第11章 字节码 (已看)

第12章 子类沙盒 (已看)

第13章 类型对象 (已看)

第5篇 解耦型模式

第14章 组件模式 (已看)

第15章 事件队列 (已看)

第16章 服务定位器 (已看)

第6篇 优化型模式

第17章 数据局部性 (已看)

第18章 脏标记模式 (已看)

第19章 对象池 (已看)

第20章 空间分区 (已看)

第1章 架构,性能和游戏

  1.1 什么是软件架构

这本书关于代码的组织方式

    1.1.1 什么是好的软件架构

第一个关键部分是,架构意味着变化.衡量一个设计好坏的方法就是看它应对变化的灵活性

    1.1.2 你如何做出改变

一旦你理解了问题和它涉及的代码,则实际的编码有时是微不足道的

    1.1.3 我们如何从解耦中受益

你可以用一堆方式来定义"解耦",但我认为如果两块代码耦合,意味着你必须同时了解这两块代码.如果你让它们解耦,那么你只需了解其一.

当然,对解耦的另一个定义就是当改变了一块代码时不必更改另外一块代码.很明显,我们需要更改一些东西,但是耦合得越低,更改所波及得范围就越小

  1.2 有什么代价

良好的架构需要很大的努力及一系列准则.每当你做出一个改变或者实现一个功能时,你必须很优雅地将它们融入到程序的其余部分.你必须非常谨慎地组织代码并保证其在开发周期中经过数以千计的小变化之后仍然具有良好的组织性

  1.3 性能和速度

没有人可以在纸上设计出一个平衡的游戏.这需要迭代和实验.

  1.4 坏代码中的好代码

原型(把那些仅仅在功能上满足一个设计问题的代码融合在一起)是一个完全正确的编程实践

  1.5 寻求平衡

开发中我们有几个因素需要考虑

  1. 我们想获得一个良好的架构,这样在项目的生命周期中便会更容易理解代码

  2. 我们希望获得快速的运行时性能

  3. 我们希望快速完成今天的功能

这些目标至少部分是相冲突的.好的架构从长远来看,改进了生产力,但维护一个良好的架构就意味着每一个变化都需要更多的努力来保持代码的干净

最快编写的代码实现却很少是运行最快的.相反,优化需要消耗工程时间.一旦完成,也会使代码库僵化:高度优化过的代码缺乏灵活性,很难改变

完成今日的工作并担心明天的一切总伴随着压力.但是,如果我们尽可能快的完成功能,我们的代码库就会充满了补丁,bug和不一致的混乱,会一点点地消磨掉我们未来的生产力

这里没有简单的答案,只有权衡

  1.6 简单性

  1.7 准备出发

抽象和解耦能够使得你的程序开发变得更快和更简单.但不要浪费时间来做这件事,除非你确信存在问题的代码需要这种灵活性

在你的开发周期中要对性能进行思考和设计,但是要推迟那些降低灵活性的,底层的,详尽的优化,能晚则晚

尽快地探索你的游戏的设计空间,但是不要走得太快留下一个烂摊子给自己.毕竟你将不得不面对它

如果你将要删除代码,那么不要浪费时间将它整理得很整洁.摇滚明星把酒店房间弄得很乱是因为他们知道第二天就要结账走人.

但是,最重要得是,若要做一些有趣得玩意,那就乐在其中地做吧

第2章 命令模式

"将一个请求(request)封装成一个对象,从而允许你使用不同的请求,队列或日志将客户端参数化,同时支持请求操作的撤销和恢复"

我想你也和我一样觉得这句话晦涩难懂.

首先,它的比喻不够形象.在软件界之外,一词往往多义."客户(client)"指代同你有着某种业务往来的一类人.据我查证,人类(human beings)是不可"参数化"的

其次,句子的剩余部分只是列举了这个模式可能的使用场景.而万一你遇到的用例不在其中,那么上面的阐述就不太明朗了.

我对命令模式的精炼概括如下: 命令就是一个对象化(实例化)的方法调用(A command is a reified(具象化) method call)

这个术语意味着,将某个概念(concept)转化为一块数据(data),一个对象,或者你可以认为是传入函数的变量等.

GOF后面这样补充到: 命令就是面向对象化的回调(Commands are an object-oriented replacement for callbacks)

一些语言的反射系统(Reflection system)可以让你在运行时命令式地处理系统中的类型.你可以获取到一个对象,它代表着某些其他对象的类,你可以通过它试试看这个类型能做些什么.话句话说,反射是一个对象化的类型系统

  2.1 配置输入

简单实现

void InputHandler::handleInput() {
    if (isPressed(BUTTON_X)) jump();
    else if (isPressed(BUTTON_Y)) fireGun();
    else if (isPressed(BUTTON_A)) swapWeapon():
    else if (isPressed(BUTTON_B)) lurchIneffectively();
}
View Code
用对象来代表一个游戏动作
class Command {
public:
    virtual void execute() = 0;
    virtual ~Command() {}
};

class JumpCommand: public Command {
public:
    virtual void execute() { jump(); }
};

class FireCommand: public Command {
public:
    virtual void execute() { fireGun(); }
};


class InputHandler {
public:
    void handleInput();
    // Methods to bind commands...
private:
    Command * buttonX_;
    Command * buttonY_;
    Command * buttonA_;
    Command * buttonB_;
};

void InputHandler::handleInput() {
    if (isPressed(BUTTON_X)) buttonX_->execute();
    else if (isPressed(BUTTON_Y)) buttonY_->execute();
    else if (isPressed(BUTTON_A)) buttonA_->execute();
    else if (isPressed(BUTTON_B)) buttonB_->execute();
}
View Code

  2.2 关于角色的说明

控制任意游戏角色

class Command {
public:
    virtual void execute(GameActor & actor) = 0;
    virtual void ~Command() {}
};

class JumpCommand: public Command {
public:
    virtual void execute(GameActor & actor) {
        actor.jump();
    }
};

Command * InputHandler::handleInput() {
    if (isPressed(BUTTON_X)) return buttonX_;
    if (isPressed(BUTTON_Y)) return buttonY_;
    if (isPressed(BUTTON_A)) return buttonA_;
    if (isPressed(BUTTON_B)) return buttonB_;
    
    return NULL;
}

Command * command = inputHandler.handleInput();
if (command) {
    command->execute(actor);
}
View Code

  2.3 撤销和重做

在上个例子中,我们想要从被操控的角色中抽象出命令,以便将角色和命令解耦.在这个例子中,我们特别希望将命令绑定到被移动的单位上.这个命令的实例不是一般性质的"移动某些物体"这样适用于很多情境下的操作,在游戏的回合次序中,它是一个特定具体的移动

这凸显了命令模式在实现时的一个变化.在某些情况下,像我们第一对的例子,一个命令代表了一个可重用的对象,表示一件可完成的事情(a thing that can be done).

这里,这些命令更加具体.他们表示一些可在特定时间点完成的事情.

class MoveUnitCommand: public Command {
public:
    MoveUnitCommand(Unit * unit, int x, int y) : unit_(unit),x_(x),y_(y) {}
    virtual void execute() {
        unit_->moveTo(x_, y_);
    }
private:
    Unit * unit_;
    int x_;
    int y_;
};

Command * handleInput() {
    Unit * unit = getSelectedUnit();
    
    if (isPressed(BUTTON_UP)) {
        int destY = unit->y() - 1;
        return new MoveUnitCommand(unit, unit->x(), destY);
    }

    if (isPressed(BUTTON_DOWN)) {
        int destY = unit->y() + 1;
        return new MoveUnitComand(unit, unit->x(), destY);
    }

    return NULL;
}
View Code

可撤销的命令

class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() {}
};

class MoveUnitCommand: public Command {
public:
    MoveUnitCommand(Unit * unit, int x, int y) : unit_(unit), x_(x), y_(y), xBefore(0), yBefore(0) {}

    virtual void execute() {
        xBefore_ = unit_->x();
        yBefore_ = unit_->y();
        unit_->moveTo(x_, y_);
    }

    virtual void undo() {
        unit_>moveTo(xBefore_, yBefore_);
    }

private:
    Unit * unit_;
    int x_, y_;
    int xBefore_, yBefore_;
};
View Code

  2.4 类风格化还是函数风格化

function makeMoveUnitCommand(unit, x, y) {
    // This function here is the command object;
    return function() {
        unit.move(x, y);
    }
}

function makeMoveUnitCommand(unit, x, y) {
    var xBefore, yBefore;
    return {
        execute: function() {
            xBefore = unit.x();
            yBefore = unit.y();
            unit.moveTo(x, y);
        },
        undo: function() {
            unit.moveTo(xBefore, yBefore);
        }
    };
}
View Code

  2.5 参考

1. 你可能最终会有很多不同的命令类.为了更容易地实现这些类,可以定义一个具体的基类,里面有着一些实用的高层次的方法,这样便可以通过对派生出来的命令组合来定义其行为,这么做通常是有帮助的.它会将命令的主要方法execute()变成子类沙盒

2. 在我们的例子中,我们明确地选择了那些会执行命令的角色.在某些情况下,尤其是在对象模型分层的情况下,它可能没有这么直观.一个对象可以响应一个命令,而它也可以决定将命令下放给其从属对象.如果你这样做,你需要了解下责任链

3. 一些命令如第一个例子中的JumpCommand是无状态的纯行为的代码块.在类似这样的情况下,拥有不止一个这样命令类的实例会浪费内存,因为所有的实例是等价的.享元模式就是解决这个问题的.

第3章 享元模式

使用共享以高效地支持大量的细粒度对象

  3.1 森林之树

用代码来表示一颗树

class Tree {
private:
    Mesh mesh_;
    Texture bark_;
    Texutre leaves_;
    Vector position_; 
    double height_;
    double thickness_;
    Color barkTinit_;
    Color leafTinit_;
};
View Code

要让GPU在每帧都显示成千上万的树数据量会很大,尤其是网格和纹理.

我们可以将对象分割成两个独立的类,游戏中每一颗树的实例都有一个指向共享的TreeModel的引用

class TreeModel {
private:
    Mesh mesh_;
    Texture bark_;
    Texture leaves_;
};

class Tree {
private:
    TreeModel * model_;
    
    Vector position_;
    double height_;
    double thickness_;
    Color barkTint_;
    Color leafTint_;
};
View Code

  3.2 一千个实例

Geometry_instancing(实例绘制)

  3.3 享元模式

享元(Flyweight),顾名思义,一般来说当你有太多对象并考虑对其进行轻量化时它便能派上用场

享元模式通过将对象数据切分成两种类型来解决问题.

第一种类型数据是那些不属于单一实例对象并且能够被所有对象共享的数据.GoF将其称为内部状态(the intrinsic state),但我更喜欢将它认为是"上下文无关"的状态.在本例中,这指的便是数木的几何形状和纹理数据等.

其他数据便是外部状态(the extrinsic state), 对于每一个实例它们都是唯一的.在本例中,指的是每颗树的位置,缩放比例和颜色.

  3.4 扎根之地

简陋的实现

enum Terrain {
    TERRAIN_GRASS,
    TERRAIN_HILL,
    TERRAIN_RIVER
    // Other terrains...
};

class World {
private:
    Terrain tiles_[WIDTH][HEIGHT];
};

int World::getMovementCost(int x, int y) {
    switch (tiles_[x][y]) {
        case TERRAIN_GRASS: return 1;
        case TERRAIN_HILL: return 3;
        case TERRAIN_RIVER: return 2;
        // Other terrains...
    }
}

bool World::isWater(int x, int y) {
    switch (tiles_[x][y]) {
        case TERRAIN_GRASS: return false;
        case TERRAIN_HILL: return false;
        case TERRAIN_RIVER: return true;
        // Other terrains...
    }
}
View Code

使用享元

class Terrain {
public:
    Terrain(int movementCost, bool isWater, Texture texture): moveCost_(moveCost), isWater_(isWater), texture_(texture) {}
    int getMoveCost() const { return moveCost_; }
    bool isWater() const { return isWater_; }
    const Texture & getTexture() const {
        return texture_;
    }

private:
    int moveCost_;
    bool isWater_;
    Texture texture_;
};

class World {
public:
    World() : grassTerrain_(1, false,GRASS_TEXTURE), hillTerrain_(3, false, HILL_TEXTURE), riverTerrain_(2, true, RIVER_TEXTURE) {}
private:
    Terrain grassTerrain_;
    Terrain hillTerrain_;
    Terrain riverTerrain_;
    // Other stuff...
};

void World::generateTerrain() {
    for (int x = 0; x < WIDTH; x++) {
        for (int y = 0; y < HEIGHT; y++) {
            if (random(10) == 0) {
                tiles_[x][y] = &hillTerrain_;
            } else {
                tiles_[x][y] = &grassTerrain_;
            }
        }
    }
    
    int x = random(WIDTH);
    for (int y = 0; y < HEIGHT; y++) {
        tiles_[x][y] = &riverTerrain_;
    }
}
View Code

  3.5 性能表现如何

  3.6 参考

1. 在上面草地瓦片的例子中,我们只是匆忙地为每个地形类型创建一个实例然后将之存储到World中.这使得查找和重用共享实例变得很简单.然而在许多情况下,你并不会在一开始便创建所有的享元.如果你不能预测哪些是你真正需要的,则最好按需创建它们.为了获得共享优势,当你需要一个对象时,你要先看看你是否已经创建了一个相同的对象.如果是,则只需返回这个实例.这通常意味着在一些用来查找现有对象的接口背后,你必须做些结构上的封装.像这样隐藏构造函数,其中一个例子就是工厂方法模式

2. 为了找到以前创建的享元,你必须追踪哪些你已经实例化过的对象的池(pool).正如其名,这意味着,对象池模式对于存储它们会很有用

3. 在使用状态模式时,你经常会拥有一些"状态"对象,对于状态所处的状态机而言它们没有特定的字段.状态的标识和方法也足够有用.在这种情况下,你可以同时在多个状态机中始使用这种模式,并且重用这个相同的状态实例并不会带来任何问题

第4章 观察者模式

在对象间定义一种一对多的依赖关系,以便当某对象的状态改变时,与它存在依赖关系的所有对象都能收到通知并自动进行更新

在计算机上随便打开一个应用,它就很有可能就是采用Model-View-Controller架构开发,而其底层就是观察者模式.观察者模式应用十分广泛,Java甚至直接把它集成到了系统库里面(java.util.Observer),C#更是直接将它集成在了语言层面(event关键字)

  4.1 解锁成就

简陋实现

void Physics::updateEntity(Entity & entity) {
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if (wasOnSurface && !entity.isOnSurface()) {
        notify(entity, EVENT_START_FAILE);
    }
}
View Code

  4.2 这一切是怎么工作的

    4.2.1 观察者

class Observer {
public:
    virtual void onNotify(const Entity & entity, Event event) = 0;
    virtual void ~Observer() {}
};

class Achievements: public Observer {
public:
    virtual void onNotify(const Entity & entity, Event event) {
        switch( event) {
            case EVENT_ENTITY_FELL:
                if (entity.isHero() && heroIsOnBridge_) {
                    unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
                }
            break;
            // Handle other events...
            // Update heroIsOnBridge...
        }
     }

private:
    void unlock(Achievement achievenment) {
        // Unlock if not already unlocked...
    }

    bool heroIsOnBridge_;
};
View Code

    4.2.2 被观察者

通知方法会被正在被观察的对象调用.在GoF的术语里,这个对象被称为"被观察对象(Subject)".它有两个职责.首先,它拥有观察者的一个列表,这些观察者在随时候命接收各种各样的通知,其次就是发送通知

class Subject {
private:
    Observer * observers_[MAX_OBSERVERS];
    int numObservers_;
};

class Subject {
public:
    void addObserver(Observer * observer) {
        // Add to array...
    }
    void removeObserver(Observer * observer) {
        // Remove from array...
    }

protected:
    void notify(const Entity & entity, Event event) {
        for (int i = 0; i < numObservers_; i++) {
            observers_[i]->onNotify(entity, event);
        }
    }
    // Other stuff...
};
View Code

允许外部的代码来控制谁可以接收通知.这个被观察者对象负责和观察者对象进行沟通,但是,它并不与它们耦合
同时,被观察者对象拥有一个观察者对象的集合,而不是单个观察者,这也是很重要的.它保证了观察者们并不会隐式地耦合在一起.例如,声音引擎也注册了落水事件,这样在该成就达成的时候,就可以播放一个合适的声音.如果被观察者对象不支持多个观察者的话,当声音引擎注册这个事件的时候,成就系统就无法注册该事件了
这意味着,两个系统会相互干扰对方----而且是以一种很不恰当的方式,因为第二个观察者使第一个观察者失效了.观察者集合的存在,可以让每一个观察者都互相不干扰.在它们各自的眼里,都认为被观察者对象眼里只有它自己

    4.2.3 可被观察的物理模块

class Physics: public Subject {
public:
    void updateEntity(Entity & entity);
};
View Code

在实际代码中,我会尽量避免使用继承.取而代之的是,我们让Physics系统有一个Subject实例.与观察物理引擎相反,我们的被观察者对象会是一个单独的"下落事件"对象.观察者会使用下面的代码 physics.entityFell().addObserver(this);

对我而言,这就是"观察者"系统和"事件"系统的区别.前者,你观察一个事情,它做了一些你感兴趣的事.后者,你观察一个对象,这个对象代表了已经发生的有趣的事情.

  4.3 它太慢了

发送一个通知,只不过需要遍历一个列表,然后调用一些虚函数.老实讲,它比普通的函数调用会慢一些,但是虚函数带来的开销几乎可以忽略不计,除了对性能要求极其高的程序

  4.4 太多的动态内存分配

    4.4.1 链式观察者

class Subject {
    Subject(): head_(NULL) {}
   
    // Methods... 
private:
    Observer * head_;
};

void Subject::addObserver(Observer * observer) {
    observer->next_ = head_;
    head_ = observer;
}

void Subject::removeObserver(Observer * observer) {
    if (head_ == observer) {
        head_ = observer->next_;
        observer->next_ = NULL;
        return;
    }
    
    Observer * current = head_;

    while (current != NULL) {
        if (current->next_ == observer) {
            current->next_ = observer->next_;
            observer->next_ = NULL;
            return;
        }

        current = current->next_;
    }
}

void Subject::notify(const Entity & entity, Event event) {
    Observer * observer = head_;
    while (observer != NULL) {
        observer->onNotify(entity, event);
        observer = observer->next_;
    }
}

class Observer {
    friend class Subject;

public:
    Observer(): next_(NULL) {}
    
    // Other stuff...
private:
    Observer * next_;
};
View Code

    4.4.2 链表节点池

  4.5 余下的问题

设计模式会遭人诟病,大部分是由于人们用一个好的设计模式去处理错误的问题,所以事情变得更加糟糕了

    4.5.1 销毁被观察者和观察者

当一个被观察者对象被删除时,观察者本身应该负责把它自己从被观察者对象中移除.通常情况下,观察者都知道它在观察着哪些被观察者,所以需要做的只是在析构器中添加一个removeObserver()方法

当一个被观察者对象被删除时,如果不我们不想让观察者来处理问题,则可以修改以下做法.我们只需要在被观察者对象被删除之前,给所有的观察者发送一个"死亡通知"就可以了.这样,所有已注册的观察者都可以收到通知并进行相应的处理

    4.5.2 不用担心,我们有GC

    4.5.3 接下来呢

观察者模式非常适合于一些不相关的模块之间的通信问题.它不适合于单个紧凑的模块内部的通信.

这也是为什么它适合我们的例子: 成就系统和物理系统是完全不相关的领域,而且很有可能是由不同的人实现的.我们想让它们的通信尽可能地减少,这样任何一个模块都不用依赖另一个模块就可以工作

  4.6 观察者模式的现状

  4.7 观察者模式的未来

第5章 原型模式

使用特定原型实例来创建特定种类的对象,并且通过拷贝原型来创建新的对象.

  5.1 原型设计模式

初始实现

class Monster {
    // Stuff...
};

class Ghost: public Monster {};
class Demon: public Monster {};
class Sorcerer: public Monster {};

class Spawner {
public:
    virtual Monster * spawnMonster() = 0;
    virtual ~Spawner() {}
};

class GhostSpawner: public Spawner {
public:
    virtual Monster * spawnMonster() {
        return new Ghost();
    }
};

class DemonSpawner: public Spawner {
public:
    virtual Monster * spawnMonster() {
        return new Demo();
    }
};

// Other
View Code

原型模式提供了一种解决方案.其核心思想是一个对象可以生成与自身相似的其他对象.如果你有一个幽灵,则你可以通过这个幽灵制作出更多的幽灵,如果你有一个魔鬼,那你就能制作出其他魔鬼.任何怪物都能被看作是一个原型,用这个原型就可以复制出更多不同版本的怪物

class Monster {
public:
    virtual Monster * clone() = 0;
    virtual ~Monster() {}
    // Other stuff...
};

class Ghost: public Monster {
public:
    Ghost(int health, int speed): health_(health), speed_(speed) {}

    virtual Monster * clone() {
        return new Ghost(health_, speed_);
    }

private:
    int health_;
    int speed_;
};

class Spawner {
public:
    Spawner(Monster * prototype): prototype_(prototype) {}
    
    Monster * spawnMonster() {
        return prototype_->clone();
    }
private:
    Monster * prototype_;
};

Monster * ghostPrototype = new Ghost(15, 3);
Spawner * ghostSpawner = new Spawner(ghostPrototype);
View Code

关于这个模式,有点比较优雅的是,它不仅克隆原型类,而且它也克隆了对象的状态.

    5.1.1 原型模式效果如何

    5.1.2 生成器函数

Monster * spawnGhost() {
    return new Ghost();
}

typedef Monster * (*SpawnCallback)();

class Spawner {
public:
    Spawner(SpawnCallback spawn): spawn_(spawn) {}

    Monster * spawnMonster() { return spawn_(); }
private:
    SpawnCallback spawn_;
};

Spawner * ghostSpawner = new Spawner(spawnGhost);
View Code

    5.1.3 模板

class Spawner {
public:
    virtual Monster * spawnMonster() = 0;
    virtual ~Spawner() {}
};

template <class T>
class SpawnerFor: public Spawner {
public:
    virtual Monster * spawnMonster() { return new T(); }
};

Spawner * ghostSpawner = new SpawnerFor<Ghost>();
View Code

    5.1.4 头等公民类型(First-class types)

  5.2 原型语言范式

    5.2.1 Self语言

    5.2.2 结果如何

    5.2.3 JavaScript如何

function Weapon(range, damage) {
    this.range = range;
    this.damage = damage;
}

var sword = new Weapon(10, 16);

Weapon.prototype.attack = function(target) {
    if (distanceTo(target) > this.range) {
        console.log("Out of range!");
    } else {
        target.health -= this.damage;
    }
}
View Code

  5.3 原型数据建模

第6章 单例模式

确保一个类只有一个实例,并为其提供一个全局访问入口 http://wiki.c2.com/?SingletonPattern 

  6.1 单例模式

    6.1.1 确保一个类只有一个实例

在有些情况下,一个类如果有多个实例就不能正常运作.最常见的就是,这个类与一个维持着自身全局状态的外部系统进行交互的情况.

    6.1.2 提供一个全局指针以访问唯一实例

class FileSystem {
public:
    static FileSystem & instance() {
        // Lazy initialize
        if (instance_ == NULL) {
            instance_ = new FileSystem();
        }
        return *instance_;
    }
private:
    FileSystem() {}
    static FileSystem * instance_;
};

// 更现代的版本
class FileSystem {
public:
    static FileSystem & instance() {
        static FileSystem * instance = new FileSystem();
        return *instance;
    }

private:
    FileSystem() {}
};
View Code

  6.2 使用情境

优点

  1. 如果我们不使用它,就不会创建实例

  2. 它在运行时初始化

  3. 你可以继承单例,这是一个强大但是经常被忽视的特性

class FileSystem {
public:
    static FileSystem & instance();

    virtual char * read(char * path) = 0;
    virtual void write(char * path, char * text) = 0;
    virtual ~FileSystem() {}
protected:
    FileSystem() {}
};

class PS3FileSystem: public FileSystem {
public:
    virtual char * read(char * path) {
        // Use Sony file IO API...  
    }
    virtual void write(char * path, char * text) {
        // Use sony file IO API...
    }
};

class WiiFileSystem: public FileSystem {
public:
    virtual char * read(char * path) {
        // Use Nintendo file IO API...
    }
    virtual void write(char * path, char * text) {
        // Use Nintendo file IO API...
    }
};

FileSystem & FileSystem::instance() {
#if PLATFORM == PLAYSTATION3
    static FileSystem * instance = new PS3FileSystem();
#elif PLATFORM == WII
    static FileSystem * instance = new WiiFileSystem();
#endif

    return *instance;
}
View Code

  6.3 后悔使用单例的原因

    6.3.1 它是一个全局变量

我们学到的一个教训就是,全局变量是有害的,理由如下

  1. 它们令代码晦涩难懂

  2. 全局变量促进了耦合

  3. 它对并发不友好

    6.3.2 它是个画蛇添足的解决方案

    6.3.3 延迟初始化剥离了你的控制

  6.4 那么我们该怎么做

    6.4.1 看你究竟是否需要类

我见过的游戏中的许多单例类都是"managers"----这些保姆类只是为了管理其他对象.我见识过一个代码库,里面好像每个类都有一个管理者: Monster, MonsterManager, Particle, ParticleManager, Sound, SoundManager, ManagerManager.有时为了区别,它们叫做"System"或“Engine",不过只是改了名字而已

尽管保姆类有时是有用的,不过这通常反映出它们对OOP不熟悉.比如下面这两个虚构的类

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();
        Bu
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值