最早接触设计模式是在跟着教程造轮子,编写游戏引擎的过程中,有看到相关教程提到:使用单例模式这样一种设计模式,创建一个全局的,静态的,能够确定只生成单个实例从而避免重复生成或引用的对象。当时的教程中使用单例的主要案例是代表游戏引擎进行游戏整体统筹管理的GameEngine类,为其创建一个单例的实例指针,并在实际程序中调用该指针来进行各类操作。那个时候,自己对“设计模式”到底是个什么东西还没有一个明确的概念,仅仅将其理解为一项技巧,一个trick。后来在系统学习之后,才意识到设计模式其实并不是那种用来解决某个具体问题的屠龙之技,而是将执行各种功能的“代码们”进行组织集合的方式,更偏向于“道”而不是“术”。
不过,一般意义的设计模式是面向整个软件开发行业的,有些为常规的软件工程流程所定下的设计模式在游戏开发行业中并不那么适用。相对应的,最近看到的这本《游戏编程模式》则是非常令人满意地,专门针对游戏开发过程中的设计问题介绍了许多实用的设计模式(尽管其中一部分并不是四人帮GoF提出的经典设计模式类型,但确实有很大的实际意义)。并且, 所介绍的内容拥有相当的深度和广度,无论是为游戏的某一功能模块提供实在的设计思路,还是在整个游戏引擎的架构原理上进行探讨,都有着实用性和专业性。自己因为跟着造过游戏引擎轮子,看到书中一些观点与以前所学到的东西不谋而合,或是在其之上更进一步进行深入探讨,甚是感动。因此将书中所讨论的一些设计模式进行记录和总结,谨供参考。
游戏循环模式
既然从大场面说起,那么再造轮子,从头开始制作游戏引擎的时候最不能缺少的,就是一个最基本的游戏循环了。一开始可能只是一个最简单的while循环:
1 | while(!quit) |
但一旦游戏引擎的功能逐渐增多,需要分别处理的画面渲染、音效加工、检查并接收玩家输入、执行游戏逻辑等工作接踵而至,将它们分别封装成各个模块,再统一放入游戏循环中进行依次刷新是一个显而易见的解决方案。这也就是游戏循环的基本思路。
游戏循环需要注意的一个问题是循环与时间同步的实现。当CPU全力运行游戏引擎的while循环时,由于不同CPU的运行频率不一,导致不同计算机上运行的游戏引擎在单位时间内能执行的循环次数不同,从而每单位时间内能渲染的画面帧数、能执行的游戏逻辑次数也都不一样——最直观的感觉就会是:在不同电脑上游戏的运行速度不一样。
在之前的自制轮子教程中,对这一问题的解决方法是:手动设置一个更新频率,在游戏主循环中不断刷新,达到更新频率所指定的时间时才真正执行对应的操作。当然这仅仅是一种实现方法,而且还存在一定问题,例如即使游戏什么都不运行,游戏引擎这边仅仅是为了执行更新频率检测就要耗费一定的CPU算力。
另外,像物理引擎这样的与游戏时间密切相关的模块也需要较为精准的时间同步来保证在不同的计算机上都能够达到预期的效果。就像Unity中除了Update以外还提供有FixedUpdate这样的专门进行同步的循环操作方法,这也是需要交给游戏引擎进行分别处理的循环。
命令模式
本书中的命令模式是指,将玩家进行的指令或者请求进行封装,从而能够较为灵活地实现游戏中的许多功能或者需求。这样的定义非常拗口,实际举一个例子就非常好懂了:
刚开始进行游戏制作时,很多教程里面都会这样教:游戏需要检测用户输入的按键并执行对应的操作。例如用户按下了空格键,则游戏中的主角进行一次跳跃。那么代码可能就会这样写:
1 | if( isPressedSpaceKey() ) |
然而这样硬编码的劣势也显而易见:玩家不能根据自己的习惯修改按键操作。为了使玩家自己的按键习惯得到尊重,显然需要新增加一个抽象层,产生一个可以依玩家需求而变化的映射关系,玩家在其中进行按键和命令之间映射关系的自定义。然后,再将对应的命令和具体的功能实现方法进行对接, 从而实现玩家自定义输入按键执行操作的功能。
具体的命令模式实现方法,可以是一个管理各项命令的CommandManager类,由这个类来存储命令与按键、命令与指令的关系。玩家修改按键与命令的映射关系后,也由该类负责进行储存。最后,在游戏逻辑中需要执行命令对应的操作时,也是向该类调用命令指令,由该类执行命令对应的操作细节。
(to be continued…)