高层游戏引擎——基于OGRE所实现的高层游戏引擎框架

这 是意念自己的毕业论文,在一个具体的实践之中,意念主要负责的是物件和GUI之外的其他游戏系统。意念才学疏陋,望众位前辈不吝赐教。由于代码质量不高、 环境很难于配置、资源包过大等问题,意念暂先不提供代码和程序,未来有时间的时候组织一下这些曾经的代码,再向外发布。

文过三月,也有些新的想法,以后会慢慢跟大家聊的,欢迎拍砖哦^_^。

 

关键字与术语:

游戏、 游戏引擎 、高层引擎、规则 、场景、物件、Terrain(地形)、解释器 Application Framework(应用程序框架)、GUIGraphics User Interface 图形用户界面)、Manager(在本文中特指管理器)、触发器、

设计模式(Design Patterns)、

Singleton(单件模式,一种设计模式,使某个类在某个程序的生存期内,有且只有一份实例,而且可以在任何时候得到这份实例。)、

Adapter(适配器模式,一种设计模式,将一个类的接口转换成客户希望的另外一个接口)、

Factory(工厂模式,一种设计模式,提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类)、

Thanatos(死本能,代表恨与破坏的力量。死本能投射于外,则表现为求杀的希望,表现为侵犯和仇恨的根源;如死本能外投受挫,则为“自杀倾向”,包括自我谴责,自我惩罚,对敌手的嫉妒和对权威的反抗等。)、

OGREObject-Oriented Graphics Rendering Engine)面向对象图形渲染引擎

 

Abstract

The abstraction of game engine is an important question in game programming, and the kernel of the question is “How could we give the game engine more adaptability?” In the paper we attempt to deduce the form which our game engine must be from our daily-lives. With the help of OGRE, I completed the game engine which has the form that we have just deduced.

The major questions of this paper are: First, why we make game engine? Second, how we make a game engine? The first question was answered in Part1, while the second in Part3. And the Preview gives us a theoretic conclusion to the second question. At the end of this paper, we used the game engine which has just been completed to make a simple game.

The main idea of the paper is to take game engine as a combination of the high-level game engine and the low-level game engine. We use the high-level game engine to support game logic, and low-level game engine to support device and platform API.

摘要:

游戏引擎和框架的抽象一直是游戏制作中的一个关键问题,其核心问题是如何令抽象好的引擎具有更好的适应性。本文尝试使用演绎法从我们所生活的世界推导出了高层引擎为了适应游戏需要所需要保持的形态,并通过实做利用OGRE完成了满足这个形态的一个简单的高层引擎。

本文的关键问题有两个:一是为什么要有游戏引擎,二是我们怎样来构架一个游戏引擎。在第一部分和第三部分我们分别回答了这个问题,绪论则作为对第二个问题所进行的演绎和推导。在本文的最后,我们利用完成的游戏引擎制作了一个简单的游戏。

游戏引擎应分为为高层的逻辑提供支持和为底层的功能提供封装的两个部分,这是本文的中心论点。


目录

绪论 游戏形态与其所决定的程序形态... 5

电脑游戏的本质是模拟现实... 5

被电子竞技的现实形态所决定的程序形态... 8

第一部分 游戏引擎技术简介... 10

引擎概述... 10

高层引擎概述... 11

第二部分 OGRE图形引擎的基本构成... 14

第三部分 实作:基于OGRE图形引擎的游戏框架... 18

场景系统:OGRE场景体系的分离和重新合成... 18

场景:游戏的舞台... 20

规则:脚本系统... 22

零件组装:具体游戏层的构建过程... 26

第四部分 结论和展望... 27

附录... 28

Terrain Example. 28

参考... 33


 

Preview 游戏形态与其所决定的程序形态

电脑游戏的本质是模拟现实

“游戏是先于人类的,许多学者在关注游戏时候发现,在自然界哺乳类动物里存在着大量的游戏行为……

“尽 管游戏的历史是悠远久长的,但人类把它当作一种理论研究对象却是在近代才开始的事。德国诗人和剧作家席勒在研究艺术起源的问题上曾以‘艺术起源于游戏’的 大胆一说而语惊四海。他认为,人类在现实生活中要受到精神与物质的双重束缚,在这两种束缚中常常失去了理想和自由,人们就设法用剩余的精神创造一个自由的 世界,这个自由的世界就是游戏。”

——《游戏之王·游戏文明论》

“游戏是先于人类的,许多学者在关注游戏时候发现,在自然界哺乳类动物里存在着大量的游戏行为……

“尽 管游戏的历史是悠远久长的,但人类把它当作一种理论研究对象却是在近代才开始的事。德国诗人和剧作家席勒在研究艺术起源的问题上曾以‘艺术起源于游戏’的 大胆一说而语惊四海。他认为,人类在现实生活中要受到精神与物质的双重束缚,在这两种束缚中常常失去了理想和自由,人们就设法用剩余的精神创造一个自由的 世界,这个自由的世界就是游戏。”

——《游戏之王·游戏文明论》

人类对于游戏的研究起源于近代。关于游戏起源的问题,自近代而来就有了多种理论:例如席勒的“本能论”,斯宾塞的“剩余能量论”,谷鲁斯的“练习理论”,弗洛伊德的“宣泄理论”等等。

然而,什么是游戏?游戏的本质是什么呢?我们先看看下面几种游戏形态,看能否获得一些启发。

棋牌似乎是所能列举出的人类最早的纯智力的游戏形态;而当游戏规则开始与体力结合时,就诞生了诸多体育项目;自上世纪60年代之后,伴随着芯片处理能力的提高,又产生了电子游戏这种新的游戏形态。抛开这么多游戏的存在形态不说,精神是物质的映射,游戏这种人类的精神产物所反映的是如何的客观存在呢?

象棋、军旗和围棋,是从人类战争中找到了灵感。战争,是人类所能接触和理解到的事物。

 

02 象棋很可能来自于人类对战争的理解和认识

 

现代战略战术类电脑游戏的规则大多始祖于近代出现的军事推演,这个军事推演可以准确的说是从战争中找到的灵感。

 

03 左:《文明》的截图    右:1894年,清政府派人参加美国军方军事推演时的照片

04 左:《地球帝国》的截图    右:1910年,美国海军军事学院的一场海战推演 

05 左:《帝国时代2》的截图        右:近代图版战争游戏的包装盒

 

最早的电子游戏是1962年在PDP1型电子计算机上,由一个名叫斯蒂夫·拉塞尔的大学生开发出来的《Space War》,单从名字我们就可以看出这个游戏所立足的,是人类对于空间的幻想,和对于战争的体会。

 

06 左:在这台电脑上,诞生了人类历史上第一款电子游戏  右:利用计算机绘制的太空幻想图

07 左:战术FPS游戏《CS  右:提出了ThanatosEros的奥地利心理学家弗洛伊德

08 左:传统图版RPG游戏的人物属性表格  右:玩图版RPG能激发玩者丰富的想象力

09 左:某网络RPG游戏场景截图  右:正在玩图版RPG游戏的玩者,右下为裁判

 

而被冠以“暴力”等罪名的FPS,尤其是万人瞩目的QuakeHalfLife,似乎也是为了满足弗洛伊德所说的人类对于Thanatos的本能追求,其表现形态往往来自于真实的杀戮场面,这些杀戮场面在一些网站、文学、新闻和电视剧中比比皆是。 

 

下面来看看另一种游戏类型——RPG游戏。欧美RPG的 始祖是托尔金的名著《指环王》,作为一部文学作品,《指环王》似乎是从人类各个集团的政经军事的交互中寻找到的灵感,同时在这之中体现出了一个幻想的世界 和幻想的世界观。此后发展而成的《龙与地下城》规则,更是继承和发扬了这个体系,刻画了一个世界的各个集团之间的政治、经济、哲学和战争的体系。幻想的事 物,也作为人类所能接触到的事物,成为了游戏开发者赖以开发游戏的灵感。《龙与地下城》远在电脑游戏出现之前就已经被做成了图版游戏。后来,电脑游戏介入 了这个规则体系,用一种崭新的面貌重现着以往的图版游戏中刻板的教条和变化无穷的世界。

 

由上面的叙述我们可以看到,游戏的开发都离不开“人类所能接触到的事物”,这中间包括人类所能接触到的客观世界和精神(艺术、幻想)世界。《电脑之王·游戏文明论》中说到:“电脑游戏的本质就是模拟现实”,这也是图灵所揭示的计算机的本质特征:“模拟人类的思维”。

被电子竞技的现实形态所决定的程序形态

游戏既然是对客观世界的认识和理解,那么就可以站在最为抽象的角度,来分解这个认识和理解。

一是远古以来就业已存在的世界:山水、森林……诸如此类,他们有一个共同的特点,是很少或几乎不会因为人类的精神活动而发生变化,基本上可以认为在整个游戏期间很少或没有变化。这一部分我们用一个词语来描绘就是地图地形。一般在三维中都叫地形,二维中都叫地图。

二是这个世界的居住者以及他们所使用的各种工具:人、动物、汽车……,他们有一个共同特点是会发生强烈的相互影响相互制约的关系。由于变动明显,这一部分的物体经常会发生变动。这一部分我们也用一个词语来描绘就是物件。在另外的分类中,往往也把地图和地形当作一种物件,但在大部分游戏中,这两者属性和方法都不同,因此我们在此将地图独立出来考虑。

第一二世界的组合就成了我们肉体可以感知到的物质世界,而这个世界在我们心灵中的映象,就叫做场景。换句话说,广义的场景就是地形和地形之上的物件的集合。从一个简单的角度来考虑,我们可以把场景看作是舞台布景,舞台背景等等就是地形,而舞台上的演员和一些杂物等等就是物件。

第三世界是一种比较难于感知的世界,它肉眼无法看见,但却在很长的时间期限内处于相对静止状态的规律集合。例如 “生命体失血过多会死亡”、“在其他条件相同的前提下,有效率的人比没效率的人在单位时间内收益要大”,等等等等。这一部分很大的可能会在暗中影响到“第一第二部分的世界”,我们用一个词语来描述它就是规则或者规律。这一部分还有一个重要的内容就是人类精神领域的活动,包括思维世界情感世界,我们可以把这种活动认为成一种特殊的规则和规律。

游戏作为一种交互娱乐手段,还有一层层面就是交互。交互中首当其冲被大家接受的是通过这个人一切可能的方式来向游戏系统输入,来从游戏系统接收输出。因此I/O控制是一个游戏系统所必须的系统。例如我们下象棋时,我们的手可认为是游戏系统的输入设备,可见的棋盘和棋子从游戏系统向我们发送输出,可认为是游戏系统的输出设备。

另一方面,在电脑游戏中。I/O系统除了键盘鼠标显示器这些基本的设备之外,还有一个交互的重要组分就是GUI界面,对于现在越来越复杂的游戏系统,一个仅通过鼠标和键盘,靠输入命令来维持的I/O是很难想象的。因此GUI用它的强大功能弥补了标准I/O设备的不足,因为GUI系统的重要性,因此在这次我们也必须把它考虑在内。

通过上面的论述我们可以得出的结论是:一个游戏可分解为地图、物件、规则、I/O控制和GUI的集合。作为棋牌类游戏可以考虑之为地图、物件、I/OGUI部分退化而规则相对进化的游戏,古代没有那样强大的系统来完美体现地图、物件,因此规则就相对强大得多。近代出现的军事推演已经是一种有完整地图、物件和规则的游戏系统了。而作为GUI,似乎只有电子游戏才有这样的东西,它是为了方便交互而产生的。

大部分游戏引擎中,会把第一部分映射为地形(地图)系统来处理,对其支持至少有:地图文件的读取、导出、渲染等。

而第二部分,各个不同游戏的实现也不尽相同,一般引擎对物件的支持除了渲染外,至少会提供如下支持:物件碰撞检测、模型编辑等。

第三部分,这有很大一部分是受游戏类型的限制,而且即便是在相同游戏类型里面,由于程序员的个人划分不一样,也会出现不同的结果。比较基本的技术包括状态机等。

第四部分,GUI和控制则有很多参考,MFC等都是很好的参考,因此这一般不是经常会被拿来讨论的技术。

本文所主要讨论的就是利用一个现有的底层引擎来构架高层引擎,并借之讨论高层引擎构架的一般思路。


 

第一部分 游戏引擎技术简介

第一部分所需所有图片

引擎概述

曾经有一段时期,游戏开发者关心的只是如何尽量多地开发出新的游戏并把它们推销给玩家。尽管那时的游戏大多简单粗糙,但每款游戏的平均开发周期也要达到810个 月以上,这一方面是由于技术的原因,另一方面则是因为几乎每款游戏都要从头编写代码,造成了大量的重复劳动。渐渐地,一些有经验的开发者摸索出了一条偷懒 的方法,他们借用上一款类似题材的游戏中的部分代码作为新游戏的基本框架,以节省开发时间和开发费用。于是就慢慢产生了游戏引擎。人对于游戏引擎的概念是 逐步深入理解的,这个过程类似于其他技术的进步过程——毕竟游戏引擎也是一个程序。这个理解所立足的就是对“封装性”的理解。实际上在引擎这个概念下面更 多的是每个人对引擎各自不同的理解:游戏引擎只是一个说法,至今为止没有一个公认的定义。

近几年一部分初学者所理解的引擎是“对底层功能的简单封装”,这个底层功能包括平台API、渲染API、音频API、流媒体API等,这样的引擎往往是一种C语言时代的思路,其划分是来自于各个不同部分之间的“功能”关系,而非“逻辑”关系。经典概念包括:渲染核心、内存管理、骨骼动画、帧动画、文件操作、物理库、网络库等等。这个在广为传诵的网文《游戏引擎剖析》(参考4)里面有最为明确的体系划分:

1、“渲染和构造3D世界,3D环境的光照和纹理”。渲染永远是引擎最具有技术含量的部分,就不说那动辄千百块钱的图形卡了,单是图形渲染相关技术的进步速率,就已经足以让人瞠目结舌了。“什么是渲染器,为什么它又这么重要呢?好吧,如果没有它,你将什么也看不到。它让游戏场景可视化,让玩家/观众可以看见场景,从而让玩家能够根据屏幕上所看到的东西作出适当的决断。”渲染所需的主要底层功能就是来支持OpenGLDirectX的最新技术。由于这些技术不断更改,导致渲染器的更新换代也相当明显。好在OGRE本身就是一个很巧妙的渲染器,它为我们隐藏了很多渲染器的复杂性,让我们可以用近乎自然语言的方式来进行图形处理。

2、“内存使用,特效和API”。图形研究到高层次就不得不考虑到芯片的一些特性:例如显存和内存管理、Shader和其它重要的参数。这也是属于引擎必须染指的内容。

3、“模型与动画,细节级别LOD”。游戏引擎应该支持常见的模型文件格式并很好地渲染他们,如果游戏引擎需要用到自己的数据格式,那么它需要为几个主要的模型文件格式做导出插件,以满足美工的需要。

4、“物理,运动,效果”。物理系统可以让游戏尽可能地逼真。“作为游戏开发者来说,无论我们做什么,我们需要能够检测墙壁,检测地板,在世界中处理和其他对象的碰撞。这些是现代游戏引擎的必备。”先进的物理系统如ODE,可以在保证效率的前提下精确处理物理和运动学理论和公式,其中甚至包括流体力学。

5、“声音系统,音频APIs”。耳朵也是人的一个重要的感觉和信息获得器官,这一点应该很好理解。

6、“网 络和连线游戏环境”。网络游戏必备。如今大多数真正有长久生命力的游戏都至少有一些连线成分。“最纯粹的单人游戏容易玩一次,也许两次,或者甚至三次如果 它是非常好的游戏,但一旦游戏结束,就被束之高阁了。如果你想要有任何长久生命力,那么多人连线游戏就是形势的核心所在。”

7、“脚本系统”。你可以把游戏脚本认为是电影脚本,它们两者实质上是相同的。

8、“人工智能和导航”。

当按照这个思路建立了自己的引擎后,我们的引擎只是一个功能引擎,它没有任何逻辑关系。包括场景、地图、物件、规则等一系列游戏逻辑所直接相关的东西,它都没法直接提供。这个时候我们所具有的引擎大约是如同下图所示:

 

11 基本的的底层引擎核心结构

一 种可怕的平铺性的结构,互相之间没有关联或很少关联。也就是说,它基本什么逻辑都没有实现,每一个游戏你可以重用这些底层功能,除此之外,你需要重新写所 有逻辑,即便两个游戏在基本逻辑上基本相同。国外的游戏引擎已经可以让你脱离代码,只用脚本和编辑器就可以做游戏了(这种开发手段叫做MOD),这种简单的平铺结构,没有纵深,根本无法架起这样一栋充斥了逻辑的大楼!

高层引擎概述

我们拿2D地图来做一个例子,在这样的引擎思路下,地图只是诸多图元的拼接、Blt(发音Blit,位图位块传输)和互相遮挡。这个思路确实反映出来了地图的本质,但是对于游戏逻辑来说,它太细了。因为游戏逻辑是不需要管你地图图元如何拼接、Blt和遮挡的。下图左就是针对这种设计思路的,而下图右则是提供了高层引擎的设计思路。通过对比可以发现,右边的设计思路更符合OO的封装原则,而左边的主要是比较古老的过程式填鸭。

12 左边是直接在应用程序里硬编码底层功能,右边是在应用程序和底层引擎之间建立一个抽象层,有这个抽象层划分和承担游戏的基本逻辑。在OO大行其道的今天,你会用哪一种方法?

 

而在这里我们理解的引擎除了功能元素之外,同时包括一些逻辑意义的部分,即部分开发者交流中所说的“游戏层引擎”或“高层引擎”,为何会存在这部分引擎呢?答案是为了方便我们表达游戏的上层逻辑。底层游戏引擎所立足的都是平台API,是与API严格相关的。目的就是为了要让外界看不见API,专心做外界的逻辑部分,但底层引擎只完成了一个目的就是通过封装API来完成一定功能,封装好的API是 否就表明一定适应上层逻辑的要求呢?这根本不可能,因为它不是为了这个目的而存在的,例如骨骼动画和上层逻辑有什么关系呢?因此人们又提出了高层引擎的概 念。这就回答了刚刚的问题,骨骼动画是应当包含在物件逻辑内部实现的,对外部应该是透明的。如果游戏逻辑需要细化到“谁谁谁,按照骨骼动作‘Walk2’来行走”,那就太麻烦了,这种情况下,比较普遍的做法是我们由来实现一个物件,然后为其设置一种状态叫做STATE_WALK2,在物件自己的逻辑里面当发现物件是处于这种状态的时候就开始引发“Walk2”动作,这样,最后的游戏逻辑只用简化到说“那个谁,向前方走一步”就可以了。实际的处理是,引擎层获取到了这个消息以后,向物件“谁”发送一个TranslateState(“”)的消息,而物件“谁”获得这个消息后,根据当前状态自动进行状态机的切换。对于逻辑的开发者来说,这一切都是封装好的,透明的,他们只需要知道“当我说‘A向前走’,A就会向前走”就可以了,这样的引擎就不再简简单单是功能平铺的平房,而是具有一定逻辑保障的大厦了。STATE_WALK2Walk2的对应关系在不同游戏引擎里面可以通过不同方式实现,最初也是最简单的方法是硬编码(Hard-Code),这种方法速度快,然而牺牲了程序的维护性,会给测试带来很大麻烦。现在,大部分的游戏引擎可以通过配置文件甚至是编辑器来解决此问题,以及与此类似的问题,这种数据驱动的方式使编码逻辑更加简单,同时也使设计者和导演工作更加方便。

下图是我们使用一款外国引擎的编辑器时的场面,在这个编辑器里面,既有物件编辑器,也有场景编辑器,同时也包括脚本——这个编辑器里用它来实现我们所说的规则——的编辑器:

1-4 看着很像3DMax的一款游戏编辑器,中国目前大部分游戏
工作室还没有自己的实力开发这种高度集成的编辑器

 

把话题引回来,对比前面我们得出的结论,做一个游戏,实际上就是在做场景(地图+物件)、规则系统、GUI系统和I/O控 制系统。那么我们该怎么做呢?构建一个过于集中的,把所有功能都实现了的高层系统,只会降低高层引擎的可适应性,因此属于高层引擎更多的是对它们提供支 持,这些支持包括:基本数据结构和组织方式(例如物件链表及查询操作、特殊的文件数据)、工具集等。通过这一层的存在,最高层逻辑只需要写:在场景中放置 几只飞鸟,按照Sin函 数路线飞行。至于飞鸟飞行中是怎么振翅,怎么偏航,这是在物件系统的具体物件类——这里是飞鸟——里可以决定的。为了最终产品的逻辑需要,我们迫不及待的 需要一个“高层游戏引擎”,这是源自于一个很重要的思想,同时也是软件工程的基础思想:“软件产生于需求”。底层引擎层次的划分完全来自于平台和API的限制,因为毕竟我们要做的游戏必须跟某一个平台相关。而高层次的引擎结构则是跟需要达到的目的严格相关的,因为这是它的存在动机。

实际上现在大部分引擎都是或多或少地包括了高层引擎部分的,然而高层引擎的划分却并不容易,大部分引擎所面向的还是FPS这种游戏类型,做一款普遍适应的引擎是难上加难,因为不同游戏所需要的高层不一样。

我们这篇文章的基本目的,就是试验当拥有一个现有的底层引擎的时候,如何构建一个高层引擎,以及如何让这个高层引擎具有更强的适应性

现在我们具有的引擎构造大抵如下:

14 按照现在的划分诞生的高层引擎层的基本框架


 

第二部分 OGRE图形引擎的基本构成

第二部分所需所有图片

OGREObject-oriented Graphics Rendering Engine,面向对象的图形渲染引擎),是国际上比较知名的开源图形渲染引擎。OGRE是用C++开发的面向对象且使用灵活的3D引擎。它的目的是让开发者能更方便和直接地开发基于3D硬件设备的应用程序或游戏。引擎中的类库对更底层的系统库(如:Direct3DOpenGL)的全部使用细节进行了抽象,并提供了基于现实世界对象的接口和其它类。

OGRE系统主要包括:Render系统和Render插件、Material系统和Material脚本、Entity(主要是物件系统)、GUI系统和Overlay脚本、Texture和图片解码器、Archive系统和文件解码器、Scene插件(主要是地形系统)、粒子系统、日志、Dll动态导入和插件系统等等。而最后所有的系统全部归一个总管管理,这个总管就是Ogre::Root

下图是Root的关联关系, Root是整个OGRE的核心部分,它关联着其他所有的组件,并把这些组件封装其中。

 

22  OGRE核心部分框图(引用自OGRE的开发框图)

 

2-2是整个OGRE的核心成分框图。下面我们引用Mage小组的《Ogre使用指南》里对这个框图的描述:

Root:整个Ogre系统的入口点和管理器,它必须第一个被创建,最后一个被消毁。通过Root对象你可以配置系统,还可以获得系统内的其它对象。

RenderSystem3D API的抽象层,它负责设置所有的渲染属性,并调用3D API执行渲染操作。

SceneManager:场景管理器,它负责组织组织场景,包括场景中MaterialLightMovable ObjectEntity)和场景本身。

Material:定义场景中几何体的表面属性。

Entity:场景中的可运动物体。

SceneNode:代表位置和方向,AttachSceneNode上的Entity可以继承其位置和方向。场景中的SceneNode以树的形式来组织。

Camera:场景中的视点。

 

使用OGRE很简单,因为OGRE提供了自己的Application Framework,如果有兴趣可以参考一下附录里面一段使用OGRE Application Framework的标准代码:(示例来自OGRE自带的地形DemoTerrain

但细细分起来使用Application Framework开发,需要写的代码主要还是集中在下面三个方面:初始化、处理输入以及运行时帧循环。大部分时候,我们所需要做的主要是初始化和输入处理,而对于帧循环几乎不必要改动。

既然Application Framework本身封装的就这么好,为什么不能直接使用Application Framework来做游戏呢?来看看Application Framework里面都是些什么吧:

 

 23 Ogre Application Framework 工程

 

我们从中可以看到高层的封装包括有:一些物件类(AppBoxAppBall等)、物件碰撞检测支持(ODE,一个国际知名的开源物理引擎)、以及一个简单到不能再简单的RefAppWorld(这里面的World类似于我们前面讨论的场景系统),这种封装很难以满足我们的要求。不过看来OGRE的作者似乎也察觉到了OGRE本身的场景系统不能满足需要,因此在Application Framework中又重新构建了我们意义上的场景系统,这种场景系统对于单个场景的演示和Demo是够用了,但是在游戏普遍要求的多场景、甚至是大量场景前面,这种构架似乎又缺乏说服力。而且把ODEOGRE放在这一层次硬性结合实际效果也并不好,经常出现碰撞检测错误导致穿墙、撞飞的尴尬场面。曾经我们准备在其基础之上建立自己的游戏框架,最后发现越来越陷入到OGRE为我们框死的条框之中。毕竟OGRE是为了OGRE开发者的目的而开发的,不是为了游戏而开发的,更不是为了我们的目的而开发的。

现在,我们准备彻底抛弃这个构架,转而制作自己新的构架,来亲手实践前面纯理论推导的“高层引擎”。当然,我们用的思路也并不超前,同样是Application Framework中已经利用过的“场景=地形+物件”思路,但是我们的构架需要考虑到更多的情况,因为我们所要做的游戏并不只是“第一第二世界”(场景),同时它还包括“第三世界”(规则),这一点是OGRE没有重点支持的。


 

第三部分 实作:基于OGRE图形引擎的游戏框架

第三部分所有图片

场景系统:OGRE场景体系的分离和重新合成

首先我们发现,OGRE场景系统似乎现在和我们所理解的场景系统有点不合。OGRE是用一种渲染方面的理解来考虑场景的,而作为一个游戏似乎需要考虑得更远。“游戏需要渲染,但游戏不仅仅是渲染”。

要融入OGRE图形系统,需要程序结构和习惯的调整,而且所写的所有代码都需要受限于OGRE,以至于我们依照OGRE来写的高层游戏引擎很有可能会成为离开了OGRE就什么都做不了的东西。而且即使我们不离开OGRE框架,那么当OGRE以后翻新版本、做大的体系调整的时候,我们所做的高层游戏引擎也需要作极大调整,这当然不是我们想看到的。高层引擎是立足于需求的,OGRE底层改动了,只要需求没有改动,就应该保证高层引擎尽量不要改动,这首先是软件工程的原则。

怎么办呢?我们先从表面上来推导一下OGRE引擎与我们前面的层次化引擎体系的接合关系。

在现有的接合下,我们有很多框架安全方面的问题都没有考虑到,如果OGRE中的某个组件迫使我们更改上层架构,那将是危险的事情,因为上层架构即游戏逻辑不是为了OGRE而存在的,应该把这些事情都封装到底层来做。我们最希望的是让最终使用这个框架的人看不到一点跟OGRE相关的东西,他只需要考虑他自己的东西:游戏逻辑。就是为AB一封情书会怎么样,以及DC的车撞了一下会怎么样诸如此类的问题,如果在这最高层还迫使使用者考虑OGRE——把CD的 包围盒进行检测——那么只能说我们没有划分好、搭建好我们的引擎,换句直接一点的话说,我们的实作以失败告终了。这是我们对自己所做框架的最起码要求。因 为只有当高层引擎留不下底层引擎的一点痕迹,我们最上层的需求和最底层的平台才是被高层引擎完全隔绝的,也就是说,无论底层平台如何变更,具体游戏逻辑是 不需要改动的,需要作出改动的只是高层引擎。如图:

如 图,理论上,高层引擎将底层和应用层完全隔离,对底层的修改将牵动高层修改,但不会牵动应用层的修改。这对于引擎是很关键的,当引擎改动的时候,如果使用 这个引擎的所有应用层都需要修改的话,那么不知道全世界会有多少工作室、甚至是公司会发出鬼哭狼嚎的叫声。因此,模块化、层次化的思路早就是软件工程界的 一个共识。

在我们现有的划分下,高层引擎需要完成下面的工作:

32 基本的的高层引擎结构

 

我们把OGRE本身提供的功能列举一下,全部提供的用黑色块,部分提供的用浅绿色块。

33 基本的的高层引擎结构与OGRE的切和关系

 

在这个划分中,我所负责的主要是地形系统、地形、场景和规则系统,而GUII/O控制系统、物件和物件系统、应用程序主要由另一位同学负责。在这里我主要也只讲述场景、地形、地形系统和规则系统。

场景:游戏的舞台

场 景中,舞台是地形系统所支持的,而赋予场景生机活力的则是物件系统。物件系统和场景系统间的组织是有所联系的,例如超大场景管理器和普通室外场景管理器所 要求的物件系统数据结构也是不一样的,前者由于可能存储海量的物件,因此可能对物件做分区处理;而后者则不同,因此可能会用统一的一张表(Map)或者哈希表(HashMap) 来管理。物件和地形系统的相关性,可以在场景这一层次来解决,当场景调入的是这样的地形系统,它就需要调入合适的物件系统。什么样的物件系统最适配于某某 地形系统?这是一个仁者见仁智者见智的问题,没有唯一的答案。物件系统最耗费效率的无非两点:自身逻辑和搜索算法,物件系统每一帧都会走自己的逻辑,而且 外界经常会从物件数据结构里索引某一个具体物件,甚至是一帧索引十几遍物件,这两者对于物件系统的数据结构都有很高要求。

OGRE对于地形的支持比较庞大,实际上OGRE本身是没有具体的地形系统的,但我们可以通过写Plugin为原有的OGRE系统增添帮助。现有的几个Plugin包括:BSP管理器(plugin_BspSceneManager)、超大场景管理器(plugin_NatureSceneManager)、和我们这次用来作试验的四叉树室外场景管理器(plugin_OctreeSceneManager)。OGRE由于抽象度很高,因此在高层的代码层面上几乎察觉不到各个之间的区别,这当然方便了我们的抽象。只是OGRE地形系统是集成在Root里面的,没法随便打破,这样,我们所提供的地形系统相当于一个“壳”,只是重新封装了OGRE的场景管理功能,这就是设计模式中的Adapter(适配器)模式。

因此,这次所写出的Terrain就相当于OGRE::SceneManager之上的一层Adapter,基本上没有什么新的功能,这也是在图23中说这个系统已经是OGRE完全处理的原因。

Scene的一个功能是用来管理Terrain的,这一般发生在多Terrain的情况下,需要对诸多的Terrain资源统一管理,Scene掌管Terrain的生杀大权,正如舞台的形态决定了布景如何摆放一样。实际上OGRE::SceneManager中也有一部分功能是用来做这些事情的。由于需要的功能比较少,因此Scene掌握了下面这些基本方法:包括载入Terrain、销毁Terrain和更换Terrain等。

利用Adapter模式,将Terrain上升为一个接口类,以后无论OGRE内部对于SceneManager的变动有多大,Terrain由于是接口只需要更改接口的实现就可以了。而Scene则成为了这一部分的管理类,与底层OGRE在逻辑上无关。至此我们SceneTerrain结构的简单场景系统就算是构架完毕,现有的这一部分类和接口如下:

关于Scene的 另一个重要部分物件系统,由另一位同学向大家细细说明,这里只是稍稍提一下一些基本的物件设计思路。前面说过,物件是一个比较难于划分的体系,因为物件的 属性比较多,而且无论何种属性都可以成体系。例如“生物体还是非生物体”、“生命期长还是短”等等。举个例子来说,对于一般生命期比较长的物件来说,可以 按照Map或者Vector来存储,这样由于不会经常从数据结构中调入调出,而且查询算法又相对要快,使得这种数据结构显得比较有优势;但是生命期非常短的物件就不同了,例如子弹碰到墙上溅出的火花,火花的存在时间往往在1/10秒一级的,而且同时可能出现很多火花,如果用Vector或者Map,那将是一件非常恐怖的事情,且不说疯狂调入调出会有多大的时间损耗,本身火花根本就没必要对其进行查询操作,MapVector相对于List的唯一优势就此不复。因此对于这种生命期非常短的物件,用List就比用Map等数据结构优势要明显。这个划分仅仅是来自于“生命期长短”这个属性,而物件所具有的属性何止着一种呢?!即便是都按照Map或者MultiMap存储,也有按物件名称存储、按物件属性存储,等等很多种存储方式。如何抽象一个适用于游戏的物件系统,这是很多人心目中共同的问题。关于这个系统也有很多现行的方法,但是很难统一,毕竟物件的规则体系太复杂了。

规则:脚本系统

规 则系统虽然并不难划分,但却是一个比较难于把握的系统,如前所述,规则系统是一个肉体所无法感觉到的世界,这样,只能用意识去感知的这个世界就充满了诸多 变数。实际上规则系统并不是一个成形的系统,而是所有“游戏逻辑”的统称。这些逻辑或自成系统,或分布在其它系统内,构成了一个游戏严密而严谨的逻辑体 系。

从 功能上理解这个系统是一个普遍的方法,因为无论规则是多么多变,最终我们需要关注的那些总是会对感官世界产生影响,这个影响就是这些规则的功能。但这种划 分办法并非是规则系统构建的全部,而仅仅是一种方向。用白话文说就是:“无论你怎样划分这个系统,最后只要完成这个功能就可以了。”

在 做引擎的时候,很少有人会知道这个引擎会用到哪里,更不用说引擎应该满足哪些逻辑和哪些功能了。因此这些功能大部分是最后开发者拿到了引擎开始写游戏程序 的时候才会考虑到的。对于引擎开发人员来说,它无形、充满变数,因此这是规则系统难于把握的重要原因。大部分游戏逻辑都是在引擎之外写的,而且中国很多游 戏DEMO的逻辑都是靠硬编码实现的。

但 是这并不表明引擎的开发人员就无事可做,因为你要对规则系统予以底层支持,有些东西是缺不了的。这主要包括:消息系统、游戏脚本、寻路算法和状态机等等, 其中大部分是人工智能的标志性研究课题。这中间我认为最为重要的是脚本系统和消息系统。对于国外游戏引擎来说,强大的脚本系统早已成为了一个必备的利器, 而国内的开发者还是处于脚本系统的教材和资料都很难找的阶段。

这里我们的引擎将为规则系统提供一套脚本支持,在后面的组装中你将会看到这个脚本是如何作为规则应用在游戏中的。对于规则系统也有其他很多种支持,例如状态机等等,好在各个逻辑体系之间是相对独立的,因此以后可以陆续增加。

脚本分为编译型脚本和解释型教本,对于外国很多游戏引擎所提供的都是编译型脚本分析器。我这里所提供的是一套解释型的脚本分析器,一是因为开发一个编译型分析器往往所需时间过长;二是对于我们的DEMO,解释型的已经足够用了,而且速度不慢。

脚 本分析器提供的基本功能就是分析脚本,这就牵扯到了编译原理的词法分析和语法分析。在读入一行并对本行文本中的注释和空格成分予以消除后,剩下的部分转入 词法分析,进一步被断为一个个独立的有意义的单词,最后通过语法分析来解释这些单词的意义。这里我们的语法比较简单,每一个独立的语句都类如下面的语句:

Index:

Funciton( param1 , “string param 2” );

第一个语句是标号语句,主要用于跳转的,例如Goto(Index)就可以从程序的任何一个位置跳转到Index,因此在我们的语法分析中,当发现了单词“:”之前有独立存在的单词时,就把这个独立的单词存储到一张Index表里面,以备跳转。而如果发现了“(”则把之前的独立单词作为Function,每一个Function唯一对应一段C++程序,从“(”到“)”之间的部分按“,”断开做多个Param,不带””的看作是常数参数,被””所包裹的是字符串参数,这些参数用做执行Function时的一些必须数据。语法分析的关键就是FunctionC++代码的一一对应,即函数匹配,这里我们可以使用if来处理:

strCmd = ParseLine()      //分析一行

if(strCmd == “Function”)

{

         doFunction( getIntParam1() , getStrParam2() );

}

使用if可读性最好,但是比较慢,因为String比较会比常数比较要慢得多。因此也有的方法就是通过把脚本函数映射为唯一的数字,再通过数字来做比较。

例如我们建立如下的对应关系:Function 101,并把这个对应关系存储到脚本解释器里面,这样,当解释器发现用作函数的单词Function的时候,就会把他翻译为101,然后再进行匹配:

nCmd = ParseLine()        //分析一行,注意返回值不同了

switch(nCmd)

{

         case 101:

         {

                doFunction( getIntParam1() , getStrParam2() );

         }

}

这样就比原来快了很多。只是麻烦的一步就是需要一个个为脚本预先对应上这些数字。这些实际上都是在解释器的Run()函数里面运行的,在需要的地方,只需要调用Run(脚本文件名)Run就会自己去检测不同的脚本名称,然后实现各自的功能。

脚本分析器还需要有一个功能就是“功能注册”。开发引擎的时候我们几乎没有办法写出具体的脚本功能,做游戏的人拿到引擎后需要写一些具体的脚本功能,这时候需要提供给他们一个注册机制,来把脚本函数名称和功能一一对应起来。这里我们提供的唯一的注册机制就是这个switch(nCmd),如果添加了什么新的脚本,就需要为对应的编号增添新的实现。例如我们除了Function以外又添加了一个新的函数Walkto,对应编号102,文法是Walkto(param1 , param2 , param3),那么我们需要做的就是:

switch(nCmd)

{

         case 101: // Function

         {

                doFunction( getIntParam1() , getStrParam2() );

         }

         case 102: // Walkto

         {

                WalkTo( getIntParam1() , getIntParam2() , getIntParam3() );

         }

}

现在的注册机由于是隶属于引擎代码层面的,每一次添加新的脚本都会引起引擎更改和变动,前面我们说了,应该尽量避免引擎变动,怎么解决这个问题呢?关于脚本注册机的更好实现就是通过C++的多态。这样我们可以把Command实现为一个抽象类,各个具体Commond继承之并实现相应接口。例如:

class Command{ virtual void do() = 0; };      //抽象类

class Function : public Command{         //具体的一个Command

         virtual void do()

         {

                doFunction( getIntParam1() , getStrParam2() );

         }

};

但这些新加的类如何注册到解释器里面呢?因为在做解释器的时候我不可能知道会加哪些类进来啊!有办法,设计模式的工厂模式(Factory)给我们提供了明确的行动指南。我们可以另外实现一套Factory并指明Factory类型:

class CommandFactory{virtual string getType() = 0; virtual Command* create() = 0;};

class FunctionFactory : public CommandFactory{

         virtual string getType() { return “Function”; }

         virtual void create(){ return new Function;} //Factory生成具体的Command

};

最后,我们需要在Run里面通过std::Map注册Factory,把Type和具体的Factory关联起来。这里我们就通过std::Map把“Function”和FunctionFactory关联起来。最后在进行函数匹配的时候,我们只需要:(伪代码)

//分析一行,得到strCmd

strCmd = ParseLine();

//map里寻找对应strCmdFactory,假设strCmd”Function”it->second里面就会存放FunctionFactory

iterator      it = Map.find(strCmd);

if(it != Map.end() )

{

         // 通过Factory生成Command对象

         Command*     Cmd = it->second->create();

         // 执行Command对象的do方法

         Cmd->do( );

         // 销毁Command对象

         Delete(Cmd);

}

OK,现在无论怎么往里面加Factory和具体的Command,这段属于引擎层的解释器代码都不需要改动了!Great。我们把这套流程画成图:

36 利用工厂模式解决的解释器函数匹配图,会发生改变的用黑色标出,可见现在这套引擎几乎不会发生变动,以后需要的在别的地方写就可以了,健壮性相当高!

 

好了,到这里,脚本解释器本身就基本解决了。剩下的工作就是不断根据需要注册新的脚本功能了。

脚本怎样最终应用于规则呢?在我们现有的解释器下,谁想用脚本,就保留脚本文件的名称,然后调用CScript::Run( const string& filename )就可以完成任务。这样,我们需要为需要走脚本的每一个类都挂接一个成员:std::string m_strFile,来存储脚本文件的名称。而且,在这些类的Logic里面,我们需要手动调用CScript::Run来运行所储存的脚本:

if( m_strFile != “” )

{

         CScript::getSingleton().Run(m_strFile);

}

如果一切无误,脚本就会运行。对于我们这个脚本机,有一个提速的手段。我们的脚本机每一次Run都会调入一次文件,分析后再关闭文件。如果一帧需要有10个物件走脚本逻辑,那么每一帧就起码会有10次 磁盘操作,对于大量物件尤其是触发器(后面会提到)存在的情况,这是件严重的事情。提速的手段就是在最开始就按照文件名把文件内容一次提取到一个缓冲区 内,这样走脚本逻辑的时候就不会走磁盘操作了,而是从内存缓冲区读取数据,对于动辄一个场景几十个物件的游戏来说,这种提速已经是普遍的做法。但每一帧都 进行磁盘操作也并非一无是处,在调试的时候有时候需要经常改动脚本文件,对于一次调入的情况,每一次修改后必须重新启动游戏,而每帧重新读一次磁盘就不会 遇到这种问题。如何选择合适的运行方式,这就需要看是在什么情况下运行了。

零件组装:具体游戏层的构建过程

框架搭完后需要一个具体的东西来证明我们的框架是否达到了预期目标。综上,我们对这个框架的要求是:

1、           利用框架开发的开发者不必要关注框架的底层细节,不必要关注OGRE,只需要关注各个组分之间的逻辑关系和存在方式。即满足框架良好的封装性。

2、           上层逻辑允且只允许与框架打交道。同时,框架中可以包含OGRE中无法绕开的重要组分。

3、           如果是框架没有完成的功能,应该可以通过对框架的临时扩展很好的完成任务。如果实在需要修改框架结构,接口也应该尽量避免改动。即满足接口安全。

这里有一点例外,就是所未完成的功能是OGRE未实现的功能,因为这个情况需要交给OGRE的维护人员去扩展OGRE库或我们自己来扩展OGRE库。除此之外,如果我们的测试没有完成既定目标,我可以认为自己失败了。

先看看我们的需求:

1、建立一个室外场景。即我们有一个地图体系。

2、有一个Player。即我们有一个物件系统和起码一个物件。

3、有若干怪物,怪物具有一定的智能,这个智能我们将用挂接在怪物身上的脚本来处理。即我们可以为怪物挂接怪物规则体系。

4、当Player杀死所有怪物的时候,游戏成功结束,否则当Player被杀死的时候游戏失败结束。即我们为世界挂接世界规则体系。

首先我们建立Application类,其主要功能是管理应用程序运行时的所有重要组件的初始化和删除工作。所有的Singleton单件都会在Application最开始的时候创建并分配堆内存,并在Application结束的时候销毁。这个Application就类似于Ogre的总管Root。而后是一些游戏层物件的准备工作,例如Player类、怪物类等等,并将这些类和类工厂注册到ObjectManager物件管理器里面去。这些工作都是立足于扩展的,不会修改原有的代码。Player类重点在于对键盘和鼠标的控制作出响应,使摄像机等随这些控制运动,而Monster则重点在于实现一些基本的AI逻辑,例如“搜索”、“索敌”、“攻击”,以供状态机或者脚本的需要。

Application初始化的时候,我们在Scene创建时为其载入Scene.cfg场景配置文件,这个文件里包括了Terrain.cfg地形配置文件和object.cfg物件配置文件,以及一些其它与场景相关的内容,例如雾和Light等。Terrain.cfg就是Ogre的地形配置文件,而Object.cfg则确定了每一个物件所挂接的规则体系。这些规则体系的关键就是作为物件成员的脚本文件。

下面是一个标准的脚本文件:

ObjectNumber=1

#Object-1

ObjectType=7

ObjectPositionX=220

ObjectPositionY=220

ObjectPositionZ=220

ObjectScript=Insanity.AI

这里的ObjectScript=Insanity.ai就是为物件挂接了“疯狂”规则体系(AI)。

通过更改Terrain.cfgScene.cfg我们创建出来了一个室外场景,然后通过更改object.cfg为场景添加一个Player和几个怪物,并为每一个怪物挂一套AI规则。这样前三步就满足了。

那么世界的规则体系应该挂接到哪里呢?挂接到Scene里吗?

很多人是这么做的,而且这样做很简单,但是我并不决定采用这种方法。因为Scene的功能很明确,就是“管理”物件和地形,Scene应该厚厚道道地作一个管理员,而不是游戏逻辑的参与者。因此我决定采用另一种方法:就是物件系统所提供的触发器。

触发器的思路来自于一句论述:“肉眼所看不见的客观实在。”就是说,不能用肉体感知的,但是却在暗地里起作用的客观规律。对于我们程序员而言,触发器说白了就是表面上不可感知,在合适的时候检测当前的条件,当满足条件的时候按照预先的设定反馈给系统的特殊物件。使用触发器的另一个原因是在原来的作品中曾经使用过它,触发器所体现出来的的移植性和模块化比直接挂在场景上要好一些,而且性能影响并不多。

从唯理的论调中离开,回到我们的实践中。由于触发器属于物件系统,因此它每一帧都会检测自己内部的条件情况,并根据条件产生出相应的结果。我们这里的条件有两个,一个是if( Player杀死了所有的Monster ),结果就是“游戏成功结束”,另一个是if( Player的生命值低于0 ),结果就是“游戏失败”。这两个条件我们通过物件脚本系统挂接到触发器上,这样触发器每一帧都会来检测自己是否满足条件,当满足条件的时候:“BANG!”

当把触发器加上并注册到物件系统里面后,我们需求的第4点也就满足了,最后的工作就是不断测试和调试了。当一切无误的时候,就可以让它与大家见面了。

第四部分 结论和展望

通 过对游戏世界的演绎,对游戏逻辑的归纳,以及对游戏底层工具的融合,逐步诞生了我们现在的框架。为了使框架更能经得起检验,我们使用了一些设计模式提供的 方法来保证框架的安全性。现在的高层游戏引擎说明了一点,对于这个游戏,我们的框架达标了。但用这个框架开发一个新的游戏是否也会达标?答案是否定的。如果用这个框架去开发一个RPG游戏,那我们还缺技能、道具等诸多系统;如果去开发一个纸牌游戏,似乎我们的框架对于规则支持还不是太方便;如果去开发一个足球游戏,似乎我们的框架对AI的支持太差劲……而且就OGRE本身也在不断更新、提供新的功能,甚至有时候否定原有的类和接口。不过把话说回来,现在什么游戏引擎没有这种问题呢?写底层引擎容易,但是写一个通用的高层引擎层却很复杂,因为底层引擎只是跟变化缓慢的平台技术相关,但高层引擎层却是跟丰富多彩的游戏相关的。不仅如此,高层引擎的存在同时受底层引擎的功能限制

需 要走的路还很长,即便是对于国际知名的大公司,也是在不断的探索和实践中。但这并不表明前途就是渺茫的,我们通过这次实践作出了一次从底而上和自上而下的 归纳和演绎的过程,在变化多端的具体游戏和变化缓慢的引擎中间寻找到了一个引擎层契合点。内容繁复、采用的表现手段多姿多彩的游戏世界,即便是再复杂,也 可以通过一定程度的归纳演绎得出适应其规则和表现的契合点和高层引擎层架构。

参考

(1)《游戏之王》——孙百英主编,科学普及出版社,ISBN7-110-04493-9

(2)《设计模式》——Erich Gamma等,机械工业出版社,ISBN7-111-07575-7

(3)OGRE文档和源代码——Ogre制作组(英国)

(4)《游戏引擎剖析》——Jake Simpson

(5)《圣剑群英传》文档和源代码——金点工作室(中国)

(6)Ogre使用指南》——mage工作室(中国)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值