《游戏编程模式》--重访设计模式--学习

目录

重访设计模式

命令模式

C++虚函数,纯虚函数与抽象类

享元模式

性能如何?

参见

观察者模式

原型模式

问题:

单例模式

全局变量有害的原因:

将类限制位单一的实例 

 为了给实例提供方便的访问方法

从基类中获得:

从已经是全局的东西中获取:

状态模式

有限状态机:

一个状态接口

为每个状态写个类

状态委托


在线阅读地址:

命令模式 · Design Patterns Revisited · 游戏设计模式 (tkchu.me)

参考文章:

GameDesignPattern_U3D_Version/Assets/002FlyweightPattern at master · TYJia/GameDesignPattern_U3D_Version · GitHub

看到了没见过的观点:

  • 抽象和解耦让扩展代码更快更容易,但除非确信需要灵活性,否则不要在这上面浪费时间。

  • 在整个开发周期中为性能考虑并做好设计,但是尽可能推迟那些底层的,基于假设的优化,那会锁死代码。

相信我,发布前两个月不是开始思考“游戏运行只有1FPS”这种问题的时候。

  • 快速地探索游戏的设计空间,但不要跑得太快,在身后留下烂摊子。毕竟你总得回来打扫。

  • 如果打算抛弃这段代码,就不要尝试将其写完美。摇滚明星将旅店房间弄得一团糟,因为他们知道明天就走人了。

  • 但最重要的是,如果你想要做出让人享受的东西,那就享受做它的过程。

不止是要学,还要知道为什么要学,学了不仅是为了用,也是为了不用

重访设计模式

我认为有些模式被过度使用了(单例模式), 而另一些被冷落了(命令模式)。 有些模式在这里是因为我想探索其在游戏上的特殊应用(享元模式观察者模式)。 最后,我认为看看有些模式在更广的编程领域是如何运用的是很有趣的(原型模式状态模式)。

命令模式

将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作

当你有接口只包含一个没有返回值的方法时,很可能你可以使用命令模式

一些代码(输入控制器或者AI)产生一系列命令放入流中。 另一些代码(调度器或者角色自身)调用并消耗命令。 通过在中间加入队列,我们解耦了消费者和生产者。 

缺点:可能会产生大量的类,从而浪费内存

拓展:可以用享元模式代替大量的类

C++虚函数,纯虚函数与抽象类

  • 1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

  • 2、虚函数声明如下:virtual ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错,错误提示为:

    error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
    
  • 3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

  • 4、实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

  • 5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

  • 6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。

  • 7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。

  • 8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。

定义纯虚函数就是为了让基类不可实例化化,因为实例化这样的抽象数据结构本身并没有意义,或者给出实现也没有意义。

实际上我个人认为纯虚函数的引入,是出于两个目的:

  • 1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
  • 2、为了效率,不是程序执行的效率,而是为了编码的效率。

享元模式

 比如森林中所有的相同的树,就可以将相同的部分分离出来放到一个类中共享,节约了重复的空间,这样每个树的实例都只需要一个这个共享类的引用就可以了

 当共享对象没有有效定义的实体时,使用这种模式就不那么明显,使用它就越发显得精明

通过分享对象来节约内存的这种优化,不应该影响到应用的显性行为,因此,享元对象几乎总是不可变的

当有一些东西是固定的,或者说是上下文无关的,此时就可以使用享元模式

比如一个地形数组,可以不用每个块都储存对应的类,可以从中抽出地形类,然后写草地实例,河流实例,丘陵实例,然后在数组中存储指向实例的指针

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_;

  // 其他代码……
};

指针通常不必枚举大

性能如何?

通过解引用指针获取地形需要一次间接跳转。 为了获得移动开销这样的地形数据,你首先需要跟着网格中的指针找到地形对象, 然后再找到移动开销。跟踪这样的指针会导致缓存不命中,降低运行速度。

但是产生一堆对象也会拖慢速度(我的理解

参见

  • 在区块的例子中,我们只是为每种地形创建一个实例然后存储在World中。 这也许能更好找到和重用这些实例。 但是在多数情况下,你不会在一开始就创建所有享元。

    如果你不能预料哪些是实际上需要的,最好在需要时才创建。 为了保持共享的优势,当你需要一个时,首先看看是否已经创建了一个相同的实例。 如果确实如此,那么只需返回那个实例。

    这通常意味需要将构造函数封装在查询对象是否存在的接口之后。 像这样隐藏构造指令是工厂方法的一个例子。

  • 为了返回一个早先创建的享元,需要追踪那些已经实例化的对象池。 正如其名,这意味着对象池是存储它们的好地方。

  • 当使用状态模式时, 经常会出现一些没有任何特定字段的“状态对象”。 这个状态的标识和方法都很有用。 在这种情况下,你可以使用这个模式,然后在不同的状态机上使用相同的对象实例。

观察者模式

让代码宣称有趣的事情发生了,而不必关心到底是谁接受了通知。

原型模式

class Spawner
{
public:
  Spawner(Monster* prototype)
  : prototype_(prototype)
  {}

  Monster* spawnMonster()
  {
    return prototype_->clone();
  }

private:
  Monster* prototype_;
};

 每个类中有一个clone方法,返回一个新的与自己相同的对象的引用

这个模式的灵巧之处在于不但拷贝原型的类,还拷贝原型的状态,这就意味着可以快速产生不同类型的对象,只要创建一个合适的原型对象

问题:

深拷贝还是浅拷贝,会造成很多语义上的问题

单例模式

全局变量有害的原因:

理解代码更加困难,

促进了耦合的发生,

对并行并不友好,   我们创建了以恶搞每个线程都可以看到并访问的内存,却不知道其他线程是否正在使用这块内存。这种方式带来了死锁,竞争状态,以及其他很难解决的线程同步问题

游戏使用单例例子

采用饿汉模式,解决了惰性初始化的问题

但是在静态实例中,我们不能使用多态,我们也不能再我们不需要这个实例的时候释放实例所占的内存

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

private:
  FileSystem() {}

  static FileSystem instance_;
};

将类限制位单一的实例 

 希望有种方式保证同时只有一个实例而无需提供全局接触点

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

  ~FileSystem() { instantiated_ = false; }

private:
  static bool instantiated_;
};

bool FileSystem::instantiated_ = false;

 这个类允许任何人构建它,如果你试图构建超过一个实例,它会断言并失败。 只要正确的代码首先创建了实例,那么就保证了没有其他代码可以接触实例或者创建自己的实例。 这个类保证满足了它关注的单一实例,但是它没有指定类该如何被使用。

 这个实现的缺点是只在运行时检查并阻止多重实例化。 单例模式正相反,通过类的自然结构,在编译时就能确定实例是单一的。

断言 函数是一种向你的代码中添加限制的方法。 当assert()被调用时,它计算传入的表达式。 如果结果为true,那么什么都不做,游戏继续。 如果结果为false,它立刻停止游戏。 在debug build时,这通常会启动调试器,或至少打印失败断言所在的文件和行号。

assert()表示, “我断言这个总该是真的。如果不是,那就是漏洞,我想立刻停止并处理它。” 这使得你可以在代码区域之间定义约束。 如果函数断言它的某个参数不能为NULL,那就是说,“我和调用者定下了协议:传入的参数不会NULL。”

断言帮助我们在游戏发生预期以外的事时立刻追踪漏洞, 而不是等到错误最终显现在用户可见的某些事物上。 它们是代码中的栅栏,围住漏洞,这样漏洞就不能从制造它的代码边逃开。

 为了给实例提供方便的访问方法

通用原则是在能完成工作的同时,将变量写得尽可能局部。 对象影响的范围越小,在处理它时,我们需要放在脑子里的东西就越少

“依赖注入”不是代码出来调用某些全局量来确认依赖, 而是依赖通过参数被传进到需要它的代码中去。 其他人将“依赖注入”保留为对代码提供更复杂依赖的方法。

方法:

传进来,

从基类中获得:

class GameObject
{
protected:
  Log& getLog() { return log_; }

private:
  static Log& log_;
};

class Enemy : public GameObject
{
  void doSomething()
  {
    getLog().write("I can log!");
  }
};

这保证任何GameObject之外的代码都不能接触Log对象,但是每个派生的实体都确实能使用getLog()

从已经是全局的东西中获取:

我们可以让现有的全局对象捎带需要的东西,来减少全局变量的数目

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

  // 设置log_, et. al. ……

  Log&         getLog()         { return *log_; }
  FileSystem&  getFileSystem()  { return *fileSystem_; }
  AudioPlayer& getAudioPlayer() { return *audioPlayer_; }

private:
  static Game instance_;

  Log         *log_;
  FileSystem  *fileSystem_;
  AudioPlayer *audioPlayer_;
};

状态模式

有限状态机:

你拥有状态机所有可能状态的集合,状态机同时只能由一个状态,一连串的输入或事件被发送给状态机,每个状态都有一系列的转移,每个转移与输入和另一状态相关

通过一堆bool判断可能得到不合法的状态值,使用enum之后,每个值都是合法的

状态模式描述:允许一个对象在其内部状态发生变化时改变自己的行为,该对象看起来好像修改了它的类型

一个状态接口

首先,我们为状态定义接口。 状态相关的行为——之前用switch的每一处——都成为了接口中的虚方法。 在我们的例子中,那是handleInput()update()

class HeroineState
{
public:
  virtual ~HeroineState() {}
  virtual void handleInput(Heroine& heroine, Input input) {}
  virtual void update(Heroine& heroine) {}
};

为每个状态写个类

对于每个状态,我们定义一个类实现接口。它的方法定义了英雄在状态的行为。 换言之,从之前的switch中取出每个case,将它们移动到状态类中。举个例子:

class DuckingState : public HeroineState
{
public:
  DuckingState()
  : chargeTime_(0)
  {}

  virtual void handleInput(Heroine& heroine, Input input) {
    if (input == RELEASE_DOWN)
    {
      // 改回站立状态……
      heroine.setGraphics(IMAGE_STAND);
    }
  }

  virtual void update(Heroine& heroine) {
    chargeTime_++;
    if (chargeTime_ > MAX_CHARGE)
    {
      heroine.superBomb();
    }
  }

private:
  int chargeTime_;
};

状态委托

class Heroine
{
public:
  virtual void handleInput(Input input)
  {
    state_->handleInput(*this, input);
  }

  virtual void update()
  {
    state_->update(*this);
  }

  // 其他方法……
private:
  HeroineState* state_;
};

为了“改变状态”,我们只需要将state_声明指向不同的HeroineState对象。 这就是状态模式的全部了。

如果你的状态没有字段,只有一个虚方法,你可以再简化这个模式。 将每个状态替换成状态函数——只是一个普通的顶层函数。 然后,主类中的state_字段变成一个简单的函数指针

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值