站务联系

内容

游戏设计模式Decoupling Patterns

发布时间:2021-03-17   来源:网络整理    
字号:

← 上一章 下一章 → ≡ 首页服务定位器游戏设计方式Decoupling Patterns

提供服务的全局接入点,避免使用者跟实现服务的详细类耦合。

一些游戏中的对象或则系统几乎出现在程序库中的每一个角落。很难找到游戏中的哪部份永远不需要显存分配,记录日志,或者随机数字。像那样的东西可以被视为整个游戏都须要的服务。

我们考虑音频作为反例。它不需要接触象显存分配这样底层的东西,但是依然要接触一大堆游戏系统。滚石撞击地面(物理)。NPC狙击手开了一枪,射出炮弹(AI)。用户选择菜单项还要响一声确认(用户界面)。

每处都须要用象下边那样的东西读取音频系统:

// 使用静态类?
AudioSystem::playSound(VERY_LOUD_BANG);
// 还是使用单例?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);

尽管每种都能荣获想要的结果,但是我们会踢倒在一些微妙的耦合上。每个读取音频系统的游戏部份直接引用了详细的AudioSystem类,和访问它的模式——是静态类还是一个单例。

这些读取点,当然,需要耦合至这些东西上来播放声音,但是直接接触至详细的音频实现,就好似给了一百个陌生人你家的地址,只是为了使它们在旁边放一封信。这不仅仅是隐私问题,在你搬家后,需要告诉每位人新地址是个格外悲哀的问题。

有个更好的解决方法:一本电话薄。需要联系我们的人可以在里面查找并找到如今的地址。当我们搬家时,我们通告电话公司。他们更新电话薄游戏服务器,每个人都晓得了新地址。事实上,我们并且无需给出真实的地址。我们可以列一个转发信箱或则其他“代表”我们的东西。通过使调用者查询电话薄找我们,我们荣获了一个控制找我们的方式的便捷地方。

这就是服务定位方式的简略介绍——它时延了还要服务的代码跟服务由谁提供(哪个详细的实现类)以及服务在那里(我们怎么荣获它的例子)。

服务 类定义了一堆操作的具象插口。具体的服务提供者实现这个插口。分离的服务定位器提供了通过查询获取服务的方式,同时掩藏了服务提供者的详细细节跟定位它的过程。

当你还要使某物在程序的各处都能被访问时,你就是在找麻烦。这是单例方式的主要问题,这个机制也没有哪些不同。我对何时使用服务定位器的最简略建议是:少用。

与其使用全局制度使这些代码接触至它,不如首先考虑将它传给代码。这超简略,也显著保持了解耦,能覆盖你大部分的需求。

但是…… 有时候自动传到对象是不或许的或则会使代码无法阅读。有些系统,比如日志或内存管理,不该是模块公开API的一部分。传给渲染代码的参数应当与渲染相关,而不是与日志之类的相关。

同样,代表外设的系统一般只存在一个。你的游戏或许只有一个音频设备或则显示设备。这是周围环境的属性,所以将它传过十个函数使一个底层读取才能使用它会为代码降低不必要的复杂度。

游戏设计模式Decoupling Patternsvirtual ~Audio() {} virtual void playSound(int soundID) = 0; virtual void stopSound(int soundID) = 0; virtual void stopAllSounds() = 0; };

当然,一个真实的音频引擎比这复杂得多,但这展示了基本的观念。要点在于它是个没有实现绑定的具象插口类。

只靠它自己,我们的音频插口不是很有用。我们还要详细的实现。这本书不是关于怎样为游戏主机写音频代码,所以你得想像这种函数中有实际的代码,了解原理就好:

class ConsoleAudio : public Audio
{
public:
  virtual void playSound(int soundID)
  {
    // 使用主机音频API播放声音……
  }
  virtual void stopSound(int soundID)
  {
    // 使用主机音频API停止声音……
  }
  virtual void stopAllSounds()
  {
    // 使用主机音频API停止所有声音……
  }
};

现在我们有插口跟实现了。剩下的部份是服务定位器——那个将二者绑在一起的类

下面的实现是你可以定义的最简略的服务定位器:

游戏设计模式Decoupling Patternsstatic Audio* getAudio() { return service_; } static void provide(Audio* service) { service_ = service; } private: static Audio* service_; };

这里用的技术被称为依赖注入,一个简略思路的复杂行话表示。假设你有一个类依赖另一个。在实例中,是我们的Locator类还要Audio的例子。通常,定位器负责构造例子。依赖注入与之相反,它指外部代码负责向对象注入它须要的依赖。

静态函数getAudio()完成了定位工作。我们可以从代码库的任何地方读取它,它会给我们一个Audio服务例子使用:

Audio *audio = Locator::getAudio();
audio->playSound(VERY_LOUD_BANG);

它“定位”的形式非常简略——依靠一些外部代码在任何东西使用服务前已注册了服务提供者。当游戏开始时,它读取一些那样的代码:

ConsoleAudio *audio = new ConsoleAudio();
Locator::provide(audio);

这里还要留意的关键部份是读取playSound()的代码没有意识到任何详细的ConsoleAudio类;它只晓得具象的Audio插口。同样重要的是,定位器 类没有与详细的服务提供者耦合。代码中只有初始化代码惟一晓得那个详细类提供了服务。

这里有更高层次的时延:Audio插口没有意识到它在通过服务定位器来接受访问。据它所知,它也是常见的具象子类。这很有用,因为这意味着我们可以将这个方式应用至现有的类上,而这些类无需因此特殊设计。这与单例产生了对比,那个会影响“服务”类本来的设计。

我们目前的实现很简单,而且也太灵活。但是它有很大的劣势:如果我们在服务提供者注册前使用服务,它会返回NULL。如果读取代码没有检测,游戏就崩溃了。

图说天下

×
二维码生成