游戏插件复习 震撼内测
“插件体系结构”程序说明
这是一个 VC6 程序,每一个VC6程序有一个IPlugin类型的指针, CreatePlugin函数名本工作空间文件(.dsw文件)和若干个项目文件身也是CREATEPLUGIN类型的函数指针。
(.dsp),一个项目文件对应一个项目。
这个程序由三个项目(project)组成,其中主体程序还包含有插件管理器,插件管理器由PluginA和PluginB这两个项目是动态链接库,PluginManager类实现。 实现的插件对象位于这两个动态链接库中,每个动在PluginManager这个类中,有装载一个目态链接库包含一个插件; 录下所有插件的函数LoadPlugins,以及卸载所有
pluginsample项目是整个程序的框架,它涉插件的函数UnloadAll,以及获得插件数目的函数及到MFC程序的基本构成要素(文档-视图-框架),GetNumPlugins和获得其中某个插件的指针
GetPlugin 插件要遵循的抽象接口IPlugin的定义也在这个
项目中。pluginsample项目相当于是主体程序另外,PluginManager实现了单件模式,提(host)。 供了唯一一个插件管理器对象,通过这个插件管理
(说明: MFC程序是一种MVC的架构。MFC器对象完成插件的管理工作。
程序的Model部分称为‚文档?Document,View部
分称为‚视图?View,Controller部分称为‚框这个例子是把主体程序pluginsample、插件架?Frame。从pluginsample项目的组成文件可pluginA、pluginB都放到同一个工作空间以看出,这是一个MFC SDI应用程序 SDI --- 单(workspace)中,一起进行编译、连接,生成可执文档界面) 行文件和动态链接库。也可以把它们放到三个不同
的工作空间(workspace)中,分别编译,得到可执
主体程序包含有插件要实现的抽象接口的声行文件(主体程序)和动态链接库(插件),然后通过明。在本例中,插件要实现的抽象接口为IPlugin。配置文件,由主体程序在需要时动态地装载插件。
Pluginsample项目中的plugin.h文件里对
IPlugin 接口进行了声明。 在pluginsample项目中,ChildView、
在 IPlugin 这个最基本的抽象接口中,声明MainFrame、PluginSample、PluginView是MFC了Initialize 、Shutdown、Export、GetName、程序开发框架的相关文件,与我们这里讲的插件体GetExportName、About 等接口函数(也就是纯系结构没有关系,这里就不详细介绍。感兴趣想了虚函数),所有继承IPlugin抽象接口的插件都要解的同学可以参考MFC相关的编程书籍。我们在课实现这几个接口函数。从接口函数中可以看出,程实验中,会要求大家实现一个插件管理器,那么IPlugin这个接口具有初始化、关闭、导出、获得可以在这个例子的基础上进行扩充。ChildView、插件名、获得导出插件名和插件描述等功能。 MainFrame、PluginSample、PluginView这些与
在plugin.h这个文件中,使用typedef MFC界面相关的文件可以不作变动,而只需改变IPlugin * (* IPlugin中包含的接口函数,以及PluginA和CREATEPLUGIN)(PluginManager&mgr);这条语PluginB中对这些接口函数的具体实现就可以了。 句定义了一个函数指针类型CREATEPLUGIN,这个我们再来看插件管理器和插件的具体实现。 函数指针类型以PluginManager类的引用为参插件管理器是由pluginsample项目的数,以IPlugin*为返回值。 PluginManager类实现的。这个类中,
在 plugin.h 这个文件中,还声明了插件模LoadPlugins函数是装载所有插件,UnloadAll块要提供的创建插件对象的工厂函数函数是卸载所有插件,GetNumPlugins是获得插CreatePlugin,这个函数的定义是在插件对象所件管理器所管理的插件的个数,GetPlugin是获得在的动态链接库中,具体来说,就是 PluginA 和 插件管理器管理的某一个插件。静态函数PluginB 项目中。插件管理器可以通过调用这个工GetInstance主要用来实现单件模式,向调用者返厂函数创建相应插件对象。 回插件管理器的唯一实例。上面这些函数都是公有
CreatePlugin这个工厂函数会创建一个指向函数。
1
在PluginManager类的私有部分,在使用插件时,可以不必关心具体插件类型的差GetFilenames函数是获得某一个文件夹下所有文异,而直接通过抽象接口IPlugin来使用这些插件的文件名,LoadPlugin函数是根据文件名是装件。
载特定的插件;还定义了一个结构类型每个具体插件类型都实现了抽象接口所包含PluginInfo,用于描述每个插件的信息,包括指向的接口函数。其中,在PluginA和PluginB类的插件的指针和插件的句柄;还有一个向量Export成员函数中,仅仅只产生了一个对话框,m_plugins用来存放多个插件的插件信息。
示当前插件是PluginA类型的插件或是s_pInstance这个私有静态变量用于和公有的PluginB类型的插件。对于Initialize、GetInstance函数配合,实现单件模式。 Shutdown、GetName和GetExportName这些函数,
我们再特别看一下PluginManager类的函数体基本没有太多处理。我们各位同学在实现这LoadPlugin函数,这个函数首先是使用Windows 些函数的时候,可以根据自己的需要添加相应的处API函数LoadLibrary装载插件所在的动态链接理,另外除了这4个函数外,还可以添加自己所需库,获得动态链接库的句柄;然后使用Windows API要的其它函数。
函数GetProcAddress在动态链接库中寻找指定对于PluginA和PluginB所在的动态链接的函数CreatePlugin,返回此函数的函数指针库,都有CreatePlugin这个导出函数。对于pFunc;然后使用这个函数指针来创建插件,返回PluginA所在的动态链接库,这个函数创建的是指向插件的指针pPlugin。最后,将插件的指针和PluginA类型的插件对象;对于PluginB所在 动动态链接库的句柄赋值给PluginInfo类型的结构态链接库,这个函数创建的是PluginB类型的插件变量info,并把info变量添加到存放插件信息的对象。
向量m_plugins中。 对于使用PluginA和PluginB插件的插件管
理器,它们可以从动态链接库中查找
在这个例子中,有两个插件类型PluginA和CreatePlugin导出函数,并使用这个函数来创建PluginB,它们都实现了插件抽象接口IPlugin。相应的插件。
课程介绍
插件开发技术属于组件开发技术的一种 写程序。 当时的计算机硬件性能很低,为了充分本课程是一门探索性的课程 利用硬件特性,这些机器指令与计算机硬件紧密结本课程是一门介绍性的课程 合,融为一体,难以区分硬件与软件。 本课程的目标:使同学们能够 汇编语言
, 理解插件的概念和体系结构 随着硬件的改进,程序指令数增多。由于以二进制, 提高阅读软件源代码的能力 表示的机器指令很难阅读和理解,在机器指令的基, 掌握用C++语言开发插件的方法 础上,人们用字符和十进制数来代替二进制代码,, 将插件思想应用于软件开发(包括游戏开发) 产生了用助记符来代替机器指令的汇编语言。
汇编语言与特定的机器指令有一一对应的关系,但程序设计语言的代际划分 它毕竟不同于由二进制数码组成的机器指令,它还第1代语言:机器语言 需要由汇编程序翻译成为机器指令后才能运行。 第2代语言:汇编语言 高级语言(过程性语言和面向对象语言) 第3代语言:高级语言(过程性语言和面向对象语高级语言是指提供给程序员的语言指令更类似与言) 人类语言的程序语言,这类语言与机器的硬件特性第4代语言:非过程性语言(说明性语言) 的联系已经不那么紧密了,它能比较容易地描述一第5代语言:自然语言 些抽象化程度高的现实问题。 程序设计语言变迁 计算机不能直接识别高级程序语言,执行前需要进机器语言 行编译,转换成计算机能够执行的二进制指令。 在计算机发展的早期,人们最初使用机器指令来编高级语言包括普通的过程性语言,如Pascal,
2
Fortran, C等以及面向对象语言C++、Java等 只能称为程序开发,不能称为软件开发 非过程性语言(说明性语言) 注重工程性
这属于第4代语言,只描述‚做什么?,至于‚如何适合于大规模软件开发
做?则由语言本身自动完成。 强调软件开发过程的标准化 在编程时,只要把问题用语言描述出来,就可以在强调开发人员之间的沟通、交流与协作 语言平台内部自动生成解决问题的过程化代码。这有一套软件工程的实施方法
减少了编程的工作量。
象SQL等一些与数据库相关的软件语言可以看做依赖特定机器的简单程序 , 小规模通用软件 , 是这类语言 大规模通用软件 , 各种类型、不同规模的通用自然语言 与专用相结合的软件
程序设计语言的发展终极目标是最终机器能理解依赖特定机器的简单程序
人类的语言,通过自然语言,人类能和机器进行无计算机硬件十分简陋;程序用二进制机器指令直接障碍交流,指挥机器完成各种操作,使机器成为人写成;只能用于特定目的和特定机器;程序规模小,类的助手和伙伴。 复杂性低
第5代语言的研究与人工智能的重要分支——计算小规模通用软件
语言学联系紧密,虽然取得了不少的进展,但目前计算机性能提高;程序用汇编语言或早期的高级语离实用还有很长的距离。 言开发;具有一定的规模;开发方法较自由;GOTO程序设计语言的发展趋势: 语句多,结构不清晰
易用性、高效性、安全性、可移植性 大规模通用软件
计算机性能飞速提升;程序用高级语言开发;软件软件开发方式变迁 规模较大(几百兆以上);使用软件工程开发方法;个人化开发 , 小作坊式开发 , 大规模开发 对开发时间和质量要求严
个人化开发 通用与专用相结合的软件
早期的计算机硬件只用来执行单一的程序,而这个各种信息化设备涌现;用于特定信息设备的专用软程序又是为特定目的而编制的。程序逻辑简单、规件和在PC机上运行的通用软件协同;开发工具众模很小,所以这个时候程序的开发完全是个人化的多,开发类型丰富,软件工程方法广泛运用。
行为。
小作坊式开发 个人自由发挥式程序开发 ,结构化程序开发 , 随着计算机硬件性能的不断增强,程序的规模和复面向对象软件开发
杂性都明显提高,光靠一个人难以完成程序开发任个人自由发挥式程序开发
务,所以需要小组来进行开发工作。 软件开发接近于艺术创作;开发不规范;软件质量大规模开发 依赖于经验和灵感;开发进度滞后,质量常出问题,计算机广泛应用于社会生活的各个领域,解决的问可维护性差;是导致‚软件危机?的直接诱因 题涉及面更广,涉及程度更深。软件的规模和复杂结构化程序开发
度也随之急剧增加。必须要运用软件工程的原则,上世纪70年代提出;顺序、选择和循环三种基本合适地确定工期、有效地内部沟通 结构;以数据结构和算法为核心;自顶向下逐步求
精;以处理问题的步骤为基点来分析问题 注重程序开发的艺术性 , 注重软件开发的工程面向对象软件开发
性 上世纪80年代提出;把软件系统看成是离散对象注重艺术性 的集合;从软件分析到软件设计再到软件编码能够早期计算机硬件性能有限,必须使用技巧来节省资实现无缝转换;封装、继承和多态提高了软件的复源 用性,从而提供了软件的生产率 把程序当成是一件充满技巧和窍门的‚艺术品? 软件体系结构变迁
虽然能完成任务,但可维护性极差 小规模、单个模块 , 大规模、众多模块
3
早期的程序一般用汇编语言开发;程序代码几十行发、维护、运行、管理都很方便;软件之间信息交到上百行;只有一个模块;没有子程序的概念; 流困难
现代的程序开发语言多种多样;普通应用软件的程分布式软件
序代码几万行,几十万、几百万行的软件也不罕见;各个模块根据需要位于不同计算机上;基于网络协有众多的组成模块,通过模块调用完成各种各样复同运行;功能强大、运行灵活;对网络环境适应性杂的功能;子程序已成为软件开发最基本的概念; 强;开发难度高
集中式与分布式相结合的软件 使用过程式程序语言开发的结构化软件, 使用面C/S架构软件,如腾讯QQ;服务器部分采用集中式向对象程序语言开发的面向对象软件 运行管理、客户端部分采用分布式运行管理;既易结构化软件的基本组成单元是子程序;通过子程序于管理,又高度灵活;开发难度高
之间的调用来完成各种复杂的功能;结构化软件=
数据结构+算法;逻辑清晰,实现简单;但难以适功能紧耦合的大规模软件 , 基于组件技术的规应用户需求的变更 模可分解软件
面向对象软件的基本组成单元是对象;通过对象之功能紧耦合的大规模软件
间消息的发送来完成各种复杂的功能;面向对象软在源代码级进行软件代码重用;使用函数库或类件=对象+消息;软件内部耦合程度低,容易适应需库;如果软件体系结构没有经过精心设计,模块没求的变化,可复用程度高;但开发难度较结构化软有进行精心划分,软件在源代码层面相互紧密关件高 联,耦合程度高
基于组件技术的规模可分解软件 集中式运行和管理的软件 , 分布式运行和管理在二进制代码级进行软件代码重用;使用组件接口的软件 , 集中式与分布式相结合的软件 标准;组件可以独立开发;通过组合组件形成完整集中式软件 软件;组件在源代码层面没有多少关联,耦合程度所有模块都位于一台计算机上;单机独立运行;开低
插件开发技术介绍
什么是插件, 集成和代码维护都需要重新编译与链接源代码,重插件从技术上讲,是组件技术的一部分。但是从概新发布整个软件,且软件系统各个模块之间相互影念上讲,它和组件还有所不同。 响,相互依赖,耦合程度高。 插件是从日常生活中提取出的一个概念。 新的开发思路:将需要开发的软件系统分为若干功硬插件; 软插件 能部件, 各部件之间遵循着标准接口规定,这样在插件是遵循一定的应用程序接口规范编写出来的各个部件按要求开发完成之后,进行软件集成时只程序模块,它可以在不修改程序主体的情况下对软是将需要的部件进行组装, 而不是对所有源代码件功能进行扩充或增强。 或库进行重新编译和链接。这就是?主体程序+插件一个插件框架包括两个部分:主体程序(host)和插‚的开发思路。这种开发思路更加灵活,开发出的件(plugin)。主体程序即‚包含?插件的程序。插软件系统健壮性好、更易扩展、更易维护。
件必须实现若干标准接口,由主体程序与插件通信
时调用。 插件式软件体系结构
通过将不同的插件(plugin)接入主体程序为什么要使用插件技术, (host),可以灵活地、松散地组装成功能完整的软对于大规模的软件系统往往需要十几个人甚至上件。
百个人协作进行软件设计与开发。目前比较流行的插件和主体程序之间通过预定的标准接口进行通是使用源代码管理工具(如:VSS、CVS、SVN等)信。
进行源代码的集成、维护及其管理。每一次的程序插件和主体程序分开开发,在运行时将插件动态装
4
载到主体程序中,共同完成软件功能。 主体程序是一个很小的内核,其功能仅是管理插有两种体系结构: 件,软件的主要功能都由内核之上的一组相互协作主体程序满足了大部分应用需求,而插件仅仅是对的插件来实现。如Java开发平台软件 Eclipse应用程序的扩展和补充。如IE浏览器、Photoshop、的插件。
暴风影音等软件的第三方插件。
插件的特征 插件的作用
插件作为一种软件集成机制,必须具有以下特征: , 便于用户扩展和增强软件功能 , 模块性好,独立性强。 , 有助于编写易于扩充和有丰富定制功能的应, 可靠性高。 用程序
, 使用方便。 , 支持二次开发,可以定制软件的界面、功能或, 封装功能。 操作
, 功能高效实现。 , 便于软件系统集成,可以将中小规模插件集成, 简洁清晰的说明。 为大规模软件系统
基于脚本语言开发的插件(专用型插件) 插件的分类 有些软件公布了用于开发插件的专用脚本语言,而基于抽象接口开发的插件(通用型插件) 不提供插件开发包(SDK)和抽象接口,利用特定的只要软件公开了用于插件开发的抽象接口,用户和脚本语言可以开发出特定于这种软件的插件。例如第三方厂商可以根据这个抽象接口用C++语言开发魔兽世界的插件是用Lua语言开发,3DSMax的插出相应的插件。这种方法原则上说对于任何软件都件是用Maxscript语言开发,Dreamweaver的插适用,所以是一种与特定软件无关的通用性插件开件是用Javascript语言开发。发方法。例如IE插件、Firfox插件等
游戏插件与游戏外挂的区别
在某些招聘启事中,是把游戏外挂等同于游戏插件的
区别
, 插件是符合授权的,接口是公开的,属于扩展的范畴,为游戏公司所欢迎
, 游戏外挂是不被授权的,接口是不公开的,属于破解的范畴,为游戏公司所禁止 , 技术上说,游戏插件一般用C/C++语言开发,游戏外挂用汇编语言开发
请大家在做游戏插件开发时,注意知识产权的问题
QQ珊瑚虫案例
多做扩展工作,不做破解工作
第三章 插件体系结构
C++语言特性 继承与组合
封装性:通过类把数据和函数关联起来,对外提供两个类A和B只有满足is-a关系,也就是说,所接口,隐藏实现细节。(三种访问权限:公有、保有A类对象也都是B类的对象,这时,类A才应该护、私有) 继承类B。
继承性:允许在已经存在的类的基础上创建新的如果两个类A和B满足的是has关系,也就是说,类。(基类和派生类) 类A中包含类B的对象,这时,类A与类B应该是多态性:同一个接口,不同的实现。(函数重载、组合关系。即:类A不应该继承类B,而应该以B运算符重载、虚函数) 类的对象作为类A的一个成员。 封装性减少代码耦合;继承性提供代码重用;多态避免不必要的继承
性使代码灵活; 只有类A改变了类B的行为,也就是说,只有A类
5
在B类的基础上增加了函数成员或修改了函数成而转向多重继承后,不仅不同的环境下重用某个类员,才使用继承,使A类继承B类。 太麻烦,而且使得维护现存代码和为代码增添新特如果仅仅是属性的区别,那么应该在类中添加新的性变得困难,还将导致编译时间和链接时间一定程数据成员,而不应该使用继承。比如,白马虽然是度的增加。
一种马,但是不应该从马类派生出白马类,而应该如果应用得当的话,多重继承会是一个很好的为马类新添加一个‚颜色?数据成员。 工具。
多重继承 对于成员函数全是纯虚函数的特殊类——抽象接口,
尽量使用单继承,而不用或少用多重继承。 使用多重继承不存在上述的问题,还可以带来以下单继承不会使继承树层次过深,接口不至于过于膨好处:在运行期切换类的实现,软件发行后扩展软胀,类之间的耦合也不会太紧。 件的功能,为程序创建插件等。
插件的抽象接口
, 抽象接口是C++中的一类特殊组织方式,通过这种方式可以实现接口和程序实现部分的分离。 , 从语法上说,C++中的抽象接口就是一个只有纯虚函数的类,没有函数实现,没有成员变量,没有除
纯虚函数以外的任何东西。
, 由于抽象接口只有纯虚函数,所以抽象接口本身不能生成对象。
, 只有通过派生,在派生类中实现各个纯虚函数后,才能生成派生类的对象。
, 抽象接口规定了一组功能,而通过它的派生类对象来实现这些功能。
一个简单的抽象接口类如下所示:
class IAbstractInterfaceA{
public:
virtual ~IAbstractInterfaceA() {};
virtual void SomeFunction() = 0;
virtual bool IsDone() = 0;
}
, 为了创建基于抽象接口的程序,就要继承抽象接口,并提供其中所有函数的实现。 , 派生类继承了抽象接口,并实现了抽象接口中所有的纯虚函数。 如下所示:
// 在.h文件中
class MyImplementation : public IAbstractInterfaceA{ public:
virtual void SomeFunction();
virtual bool IsDone();};
// 在.cpp文件中
void MyImplementation::SomeFunction(){
// ……}
bool MyImplementation::IsDone(){
// ……}
如何更有效地定义和实现抽象接口
, 抽象接口中的所有函数都是虚函数
C++多态性规定:只有当基类的成员函数是虚函数时,指向基类的指针才可以在运行期正确地调用派生类对象的成员函数。
, 抽象接口中的所有函数都是纯虚函数
这样,只有派生类继承这个抽象接口,并实现了所有这些成员函数,编译器才允许创建相应的派生类对象。 , 可以从抽象接口中派生一个抽象类来提供部分成员函数的实现
6
这部分成员函数完成了一些通用的功能,然后从这个抽象类中派生出完成特定功能的具体类。
UML图
抽象接口的作用
, 充当程序的某个功能模块与另一个功能模块之间的连接管道
, 将抽象接口作为对象的标识
抽象接口作为功能模块之间的连接管道
两个功能模块仅仅通过抽象接口进行交互,除了抽象接口之外,两个模块的各种具体实现细节都互相屏蔽。例如,以下是一个游戏的图形渲染模块的抽象接口:
class IGraphicsRenderer{
public:
virtual ~IGraphicsRenderer() {};
// 渲染的功能函数1
virtual void SetWorldMatrix(const Matrix4d & mat) = 0;
// 渲染的功能函数2
virtual void RenderMesh(const Mesh & mes) = 0;
// ……};
, 在另一个模块(不妨称之为客户模块)中,如果要使用图形渲染模块的功能,可以通过这个抽象接口。
图形渲染模块所要做的就是创建特定的图形渲染器(也就是实现了这个抽象接口的派生类对象),并
把图像渲染器的指针传给客户模块。
, 例如,有一个图形渲染器用的是OpenGL库,另一个图形渲染器用的是Direct3D库,那么在图形渲
染模块可以这样生成派生类:
class GraphicsRendererOGL : public IGraphicsRenderer { public:
virtual void SetWorldMatrix(const Matrix4d & mat);
7
virtual void RenderMesh(const Mesh & mes);
// ……}
class GraphicsRendererD3D : public IGraphicsRenderer { public:
virtual void SetWorldMatrix(const Matrix4d & mat);
virtual void RenderMesh(const Mesh & mes);
// ……}
, 在图形渲染模块中,可以这样生成特定的图形渲染器(以使用OpenGL库为例): IGraphicsRenderer * g_pRenderer = new GraphicsRendererOGL();
, 然后把这个全局指针g_pRenderer传给客户模块,在客户模块中通过如下代码使用渲染器的功能:
g_pRenderer ->SetWorldMatrix(ObjectToWorld);
g_pRenderer ->RenderMesh(mesh);
可见,在客户模块中,不关心使用的是OpenGL的渲染器还是Direct3D的渲染器,它只要接收一个抽象接口的指针,然后使用抽象接口规定的功能就可以了。
, 综上所述,客户模块和渲染模块之间通过抽象接口进行交互,完成信息交换和功能调用。抽象接口起
着两个模块之间的连接管道的作用。
, 抽象接口规定了派生类所要实现的功能。抽象接口本身不能创建对象,它的成员函数是纯虚函数,只
能通过派生类来实现相应的功能。所以,抽象接口体现了‚功能与实现相分离?的特点,能够较好地减
少模块之间的耦合。
抽象接口作为对象的标识
, 如果对象实现了特定的抽象接口,那么表明对象具有特定的功能,所以可以把抽象接口看作是描述对
象功能的一种标识。
, 在程序里,不是固定地使用特定类的对象,而是在操作每个对象时,首先询问该对象是否实现了特定
的抽象接口。如果是,则调用抽象接口的功能函数,继续操作和处理这个对象;如果不是,则不再处
理这个对象。
, 下面来看3段伪码,理解抽象接口的作用。这3段伪码要完成的功能是:对场景中具有某些特性的对
象进行渲染。
第1段伪码列出所有符合条件的对象类型
void Renderworld()
{ // 检查每个对象,判断对象类型
for (each object in the world)
if object is enemy ||
object is environment object ||
object is terrain ||
…… // 更多的对象类型,所有这些对象类型都是可以执行渲染的
{
object.Render();
}
}
第2段伪码由对象自己判断是否符合条件
void Renderworld()
{ // 检查每个对象
8
for(each object in the world)
// 由对象的成员函数判断是否可以执行渲染
if (object.IsRenderable())
object.Render();
}
第3段伪码通过抽象接口判断对象是否符合条件
void Renderworld()
{ for(each object in the world) {
// 判断对象是否实现了指定抽象接口
if (object implements Renderable interface) {
// 使用抽象接口指针来访问渲染功能
IRenderable * pRend;
pRend = object.GetInterface(IRenderable);
pRend ->Render();
}
}
}
如何判断对象是否实现了某抽象接口,
我们可以通过一个抽象接口查询函数(QueryInterface)来完成这个功能。如果对象实现了抽象接口,则返回指向抽象接口的指针,否则返回空指针
void * GameEntity::QueryInterface(Interface interface) const
{
if (interface == IRENDERABLE) {
IRenderable * pRender = static_cast
(this);
return (void *)(pRender);
}
return NULL;
}
通过抽象接口判断对象是否符合条件
void RenderWorld() {
for (each object in the world) {
void * pInterface = object.QueryInterface(IRENDERABLE);
if (pInterface != NULL) {
IRenderable * pRend;
pRend = static_cast(pInterface);
pRend -> Render();
}
}
使用抽象接口的注意事项
, 使用抽象接口要考虑抽象接口是否会使程序的逻辑更合理。
, 抽象接口位于程序的不同层次可能会有不同的后果。
, 另外,还要考虑性能问题。
如果抽象接口放置在很底层,完成的功能是底层操作,那么一个框架会调用它很多次,这样会导致性能明显降低。而把抽象接口提高到一个稍微高点的层次,那么就可以大大减少对接口的调用,同时还不影
9
响整体性能。
插件的具体实现
, 插件结构是一种灵活的体系结构
在程序的主模块中提供了大部分用户都要使用的核心功能,而其余的功能则由称为插件的扩展部分提供。可以根据需要来选择安装和使用特定插件,如果不需要某个插件的功能,可以不使用甚至卸载这个插件。 , 插件结构是扩展现有软件的有效方法
有很多程序倾向于通过插件来实现功能的扩展。只要一个软件提供了插件扩展接口,就可以进行插件的开发,通过插件扩展软件的功能。需要做的就是实现一些抽象接口,然后在插件中提供扩展功能。
, 一旦使用了插件结构,最简单的方法就是为基本程序编写新的插件,而不是向基本程序中增加新的功
能。
, 要支持插件的使用,一个关键是基本程序要与插件模块相隔离。插件模块要良好封装,对基本程序隐
藏其内部实现细节。基本程序不依赖于插件的具体实现,插件遵循基本程序确定的接口。只有这样,
基本程序作为一个通用工具,才可以在不失一般性的情况下通过安装插件增加新的功能。 , 所有的插件结构都会围绕着抽象接口进行组织,这些抽象接口描述了插件的功能。抽象接口中包含了
程序中可以使用的操纵插件的所有函数。
插件对应的抽象接口的例子
这个插件完成的功能是导出数据。相应的抽象接口如下:
class IPluginExporter
{
public:
virtual ~IPluginExporter() {};
virtual bool Export(Data * pRoot) = 0;
virtual void About() = 0;
}
在这个抽象接口的基础上创建一个导出HTML格式数据的插件:
class PluginExporterHTML : IPluginExporter
{
public:
PluginExporterHTML(PluginMgr &mgr);
// IPlugin interface functions
virtual bool Export(Data *pRoot);
virtual void About();
private:
bool CreateHTMLFile();
void ParseData(Data *pRoot);
}
, 有些插件完成的可能不是导出数据而是导入数据的功能,或者是对数据进行处理的功能。对于导入数
据、处理数据的插件,不能从IPluginExporter接口派生,需要另外定义抽象接口。 , 对于不同类型的多种插件进行组织,最好的方法是使用继承。设置一个最基本的抽象接口,这个接口
包含对所有插件类型都适用的通用接口函数。其他抽象接口从这个基本抽象接口中派生。
可以创建一个基本的抽象接口如下:
10
class IPlugin
{
public:
virtual ~IPlugin() { };
virtual const std::string & GetName() = 0;
virtual const VersionInfo & GetVersion() = 0;
virtual void About() = 0;
}
这个基本抽象接口提供了不同类型的所有插件要实现的功能。
, 对于每一种插件类型,将创建一个从IPlugin导出的新的抽象接口,在新的抽象接口里添加新的功
能函数。例如,
class IPluginExporter : public IPlugin
{
public:
virtual ~IPluginExporter() { };
virtual bool Export(Data * pRoot) = 0;
}
class IPluginImporter : public IPlugin
{
public:
virtual ~IPluginImporter() { };
virtual bool Import(Data * pRoot) = 0;
}
class IPluginDataViewer : public IPlugin
{
public:
virtual ~IPluginDataViewer() { };
virtual bool Preprocess(Data * pRoot) = 0;
virtual bool View(Data * pData, HWND hwnd) = 0;
}
, 要创建特定的插件,就要继承特定的抽象接口。在插件的实现文件中,除了要实现基本抽象接口
IPlugin的功能函数,还要实现特定抽象接口的功能函数。
最终的继承关系树如下图所示。
11
插件的存在形式
, 我们已经对插件的抽象接口和具体实现进行了描述。但是,我们并没有涉及插件在程序中以何种形式
存在。我们也没有涉及何时采取何种方式装载插件以及卸载插件。
, 由于插件和程序的主模块是相对独立的,所以插件的编译应该和程序主模块的编译分开。另外,程序
主模块一直处于运行状态,插件在需要时被程序主模块动态加载,在不需要时从程序主模块动态卸载。 , 在程序运行中加载插件的最直接方法就是使用动态链接库(Dynamic Linking Library, DLL)。 静态链接
一个源程序在编程完成后要经过编译、链接才能得到二进制可执行文件。如果在链接时把所有目标文件都加入到可执行文件中,这称为‚静态链接?。
动态链接
如果把一部分目标文件不添加到可执行文件中,而是生成另外一个二进制文件,在可执行文件运行时根据需要将这个文件动态加载到内存,那么这称为‚动态链接?,生成的这个文件称为‚动态链接库?。
, 动态链接库能够被动态地装入应用程序的地址空间。应用程序能够在运行时根据要执行的操作,装入
相应的动态链接库。当显式装载动态链接库后,动态链接库中的二进制代码就可以被应用程序使用。 , 要创建一个动态链接库,就必须在编译器里进行特定的设置。通过设置指定产生的是动态链接库(.dll
文件),而不是可执行程序(.exe文件)或静态链接库(.lib文件)。
, 动态链接库通过显式的标记向使用该库的客户程序输出函数、类或者变量。一般来说,希望动态链接
库只输出必要信息,而不输出无关信息或冗余信息。
, 在插件结构中,输出的不是插件类本身,而是一个可以创建插件的全局工厂函数。这个工厂函数负责
创建实际的插件对象。
, 以下代码声明了创建插件对象的工厂函数:
extern ‚C? __declspec(dllexport) IPlugin *
CreatePlugin(PluginManager & mgr); , 其中的__declspec(dllexport)主要是为了表明向库外输出这个函数。extern ‚C?主要是通知编译
12
器按C语言的规则来编译这个函数,而不要象C++编译器那样给函数名加上额外的修饰符。这样,使
用这个动态链接库的程序可以用函数名本身来搜索输出函数,调用输出函数。
, 如前所述,插件所在的动态链接库的输出函数是创建插件对象的工厂函数。
, 工厂是C++软件设计与开发中的一个重要概念,正如我们日常生活中的工厂是用来生产产品的,在软
件开发中,工厂是用来创建对象的。
, 我们知道,对象是C++程序的基础,对象的生命期有创建、使用和销毁三大环节,其中创建是基础。
对于不同的对象有不同的创建方法。从安全起见,我们一般不让用户随意地创建对象,而是把创建对
象的工作放到专门的一个类或函数中,用户只需要调用这个类或函数即可。这就要用到工厂类或工厂
方法。
, 关于工厂这个概念,在‚设计模式?这门课程中会有详细的介绍,我们这里只需要知道,对于一个设计
良好的软件来说,应该将创建对象的工作都放到工厂中完成。
, 在每个包含插件的动态链接库中,除了插件的类声明代码(也就是.h文件)和插件的实现代码(也就
是.cpp文件),还需要提供一个工厂函数的实现,这个工厂函数也就是上面的例子中输出给外部的这
个函数。
, 在工厂函数的函数体中,我们可以用普通的创建对象的方法来创建插件对象。工厂函数的实现如下:
__declspec(dllexport) IPlugin * CreatePlugin(PluginManager & mgr)
{ // 创建一个输出HTML格式数据的插件
return new PluginExporterHTML(mgr);
}
, 总之,插件以动态链接库的形式存在,通过动态链接库的输出工厂函数来创建插件。 , 请注意,虽然插件是动态链接库,但是不一定要用dll作为扩展名,可以根据软件#设计#,采用其
他的反映插件类型的后缀。例如,可以为导出插件赋予后缀.exp,为导入插件赋予后缀.imp
插件的管理使用
, 管理和使用插件属于插件管理器的工作。插件管理器向插件的客户程序屏蔽了插件的实现细节,使得
插件客户程序可以透明地使用插件(不管插件的具体细节,直接使用插件的功能)。 , 通过插件管理器,可以减少插件和基本程序之间的耦合。
, 对于插件的客户程序(也就是使用插件的程序),通过插件管理器,可以决定什么时候装载插件,什么
时候卸载插件,以及如何使用插件。
动态装载插件
, 插件管理器动态装载插件所在的动态链接库。
, 在Win32平台中,使用如下函数:
HMODULE hDll = ::LoadLibrary(filename.c_str());
这里,LoadLibrary是一个全局的Windows API函数,其功能是根据文件名动态装载插件所在的动态链接库,同时返回一个句柄。这个句柄要保存起来,在释放动态链接库的时候要使用。 , 如果动态链接库加载失败,那么LoadLibrary函数会返回空指针(NULL指针)。当检查到函数的返回
值是空指针时,表明动态链接库没有加载成功。
创建插件对象
, 插件所在的动态链接库装载成功后,可以创建插件对象。
, 首先,要在装载的动态链接库中寻找创建插件对象的工厂函数。
13
, 在Win32平台中,使用如下语句:
CREATEPLUGIN pFunc=(CREATEPLUGIN)::GetProcAddress(hDll,
_T(‚CreatePlugin?));
CREATEPLUGIN 是预先定义的函数指针类型,其声明如下:
typedef IPlugin * (* CREATEPLUGIN)(PluginManager& mgr);
GetProcAddress是一个全局的Windows API函数,其功能是从特定的动态链接库的输出函数列表中寻找指定函数。这个语句将返回CreatePlugin函数的函数指针,也就是函数代码块的首地址。
, 当从装载的动态链接库找到创建插件对象的工厂函数的函数指针时,就可以使用这个工厂函数指针创
建插件对象。
, 使用工厂函数的函数指针创建插件对象的语句为:
IPlugin * pPlugin = pFunc(*this); 其中,pFunc是上一条语句中GetProcAddress函数返回的工厂函数的函数指针,也就是指向CreatePlugin函数的函数指针。
由于所有这些代码都是在插件管理器中,所以this指针指向的是插件管理器对象,*this就是插件管理器。这条语句完成的就是‚通过CreatePlugin函数和传入的插件管理器对象创建指向插件的指针。? 这条语句等同于 IPlugin *pPlugin = CreatePlugin(plugin_manager);
使用插件功能
当创建插件对象后,就会获得指向插件对象的指针: IPlugin * pPlugin,可以通过这个指针来调用插件中的成员函数,从而使用插件对象提供的各个功能。
卸载插件对象
当插件使用完毕,不再使用时,为了节省内存资源,我们需要卸载插件。卸载插件可以简单地通过卸载插件所在的动态链接库来完成:
::FreeLibrary(hDll);
使用保存的动态链接库的句柄,卸载这个动态链接库,从而完成插件的卸载。
替换插件对象
有时在软件开发中,需要用一个新的插件来代替当前的插件,由于没有直接替换插件的API函数,所以用两个步骤来完成:
, 首先卸载当前插件,
, 再装载新的插件。
这样做比强迫关闭程序然后再重新启动程序要快许多。
插件如何调用应用程序的功能,
, 到目前为止,介绍的都是应用程序调用插件的功能。有的时候,插件需要调用应用程序的功能 , 要保证插件对程序其它部分的访问,最清晰的方法是通过插件管理器来进行。因为在插件创建时,把
插件管理器作为一个参数传给了创建函数CreatePlugin,所以所有的插件都能访问插件管理器。这
样就可以把插件管理器作为插件访问程序其它部分的通路。
插件对于程序其它部分应该具有什么级别的访问能力,
, 这要辩证地看待。
, 对程序其它部分的访问限制越多,插件和程序之间的依赖性越少,插件和程序就可以相对独立地变化
而互不影响,但是插件能完成的功能也越少。
14
, 对程序其它部分的访问限制越少,插件可以自由支配的资源越多,能完成的功能越多,但是插件与程
序其它部分的耦合越紧密,越容易互相影响,而且容易出错。
, 一个比较好的原则是:在允许插件完成需要功能操作的前提下,尽量对其施加更多的限制条件。
, 最为安全和严格的方式,是通过插件管理器来完成任何操作。
, 插件管理器预计出所有插件的需要,为每个插件所要完成的操作提供相应的支持函数。 , 因为插件管理器是和应用程序的其它部分一起编译的,所以插件管理器对程序的其它部分有更多的了
解。这样,通过插件管理器访问程序要比通过插件直接访问程序更不易出现问题。 , 另外,随着程序的改变以及程序新版本的出现,插件管理器可以进行必要的改变,而插件不需改变。 , 如果插件直接访问程序,一旦程序发生改变,那么必须改变当前插件,否则插件就无效了。
插件管理器的安全性
插件管理器可以提供很多安全函数,这些安全函数给插件提供了访问程序的通道。插件使用这些安全函数可以完成相关的功能。通过提供更多的安全函数,可以给插件提供更充分的条件,实现更多的功能。 关于游戏的扩展性
一般来讲,许多游戏是可以由用户扩展的,但是扩展的方式通常是以新资源或新脚本的方式,而不是新代码方式,这主要是从安全性上来考虑的。但是在游戏开发中,在公司内部的程序代码可以采取插件结构,只是插件的抽象接口不对外公开,并且调用插件时要做必要的身份验证。
CMake使用
, cmake是kitware公司以及一些开源开发者在, 高效率,构建速度快
开发几个工具套件(VTK)的过程中衍生, 可扩展,可以为cmake编写特定功能的模
品,最终形成体系,成为一个独立的开放块,扩充cmake功能。
源代码项目。项目的诞生时间是2001年。 , CMake的缺点
, 其官方网站是www.cmake.org,可以通过访问, Cmake仍然比较复杂
官方网站获得更多关于cmake的信息 , cmake编写的过程实际上是编程的过程,, cmake的特点 需要编写的是CMakeLists.txt(每个目
, 开放源代码 录一个),使用的是?cmake语言和语法?
, 跨平台,可以生成本地编译配置文件
, 能够管理大型项目
, 使用方便,简化编译构建过程和编译过程
使用CMake的步骤如下:
, 运行CMake
, 在‚Where is the source code?文本框中,输入或者‚Browse Source...?到源代码的根目
录
, 在‚Where to build the binaries? 文本框中,输入或者‚Browse Build...?一个文件夹,
这个文件夹会被用于输出构建结果(lib文件,头文件,dll文件以及执行程序)
, 单击屏幕下方的‘Configure’按键。
, 选择目标编译平台,例如"Visual Studio 2005‚
, 当系统提示是否创建构建目录的时候选择'Ok‘
, 等待Configure执行结束。
, 屏幕上现在会有一些配置设置,并用红色标记(这是用来指出你是第一次看到他们)
, 只需要再次选择‘Configure’。
15
, 等运行结束了选择‚Generate?按键。
, 构建文件将会在你之前选择的文件夹下面产生,构建的结果是工程文件和项目文件 , 以OrgeShrewMouse为例演示CMake使用方法
, Ogre --- Object-oriented Graphics Rendering Engine 面向对象图形渲染引擎 , Ogre的特点:
, 开源软件,最先采用GPL授权,后来改为LGPL和MIT授权
, 是图形引擎,而不是游戏引擎
, 跨平台,不仅实现了跨OpenGL和D3D两个底层3D API,也实现了对Windows、Linux和Mac OS
三个操作系统的支持
, 引擎具有强大的插件系统,可以采用插件方式来扩展功能
, 坚持‚好的结构重于更多特性?的原则,注重优良的结构
, Ogre主要体系结构图
, Ogre重要部分介绍
, Root
, 根(Root)对象是OGRE系统的入口,该对象在程序一开始时创建,最后结束时销毁。
, 根(Root)对象帮助你配置系统,比如,通过showConfigDialog方法,OGRE会对渲
染系统(Render System)的所有选项进行检测,并且提供一个对话框,供用户对这些
选项(比如色彩深度、是否全屏显示)进行修改。
, 根(Root)对象还可以帮助你获得系统中其它对象的指针,比如:场景管理器
(SceneManager),渲染系统(RenderSystem),还有其它的资源管理器(Resource
Managers)等等。
, 场景管理器(SceneManager)
16
, 是应用程序中最常用的对象
, 场景管理器控制着由OGRE引擎渲染的场景中的所有内容
, 负责创建、组织、管理所有的摄像机(Cameras)、可移动的实体(Entities)、光线
(Lights)和材质(Materials)
, 管理场景中静态的部分,比如:整个场景地形
, 实体(Entity)
, 实体是场景中可移动对象的实例,它可以是一辆车、一个人、一条狗等等。实体(Entity)
在世界(World)场景中不一定非要一个固定的坐标位置。
, 实体(Entities)是以网格(Meshes)作为自身基础的,网格(Mesh)对象包括了一
整套用来描述自身模型的数据,多个实体(Entities)可以共用一种网格。 , 可以通过调用SceneManager::createEntity方法创建一个实体,然后,给该实体
(Entity)命名,并且将与其相关的网格(Mesh)对象关联起来。 , 通过关联不同的实体(Entities)到不同的场景结点(SceneNodes),可以在实体
(Entities)之间创建出复杂的位置及方向关系。
, 资源组管理器(ResourceGroupManager)
, 是提供加载纹理(Textures)、网格(Meshes)等可重用资源的‚集线器(Hub)?。使
用它,可以将要用到的资源进行分组。
, 资源组管理器(ResourceGroupManager)中包括了大量的资源管理器
(ResourceManager),每个资源管理器分管某类资源,比如纹理管理器
(TextureManager)、网格管理器(MeshManager)。
, 资源管理器(ResourceManager)能保证某类资源只被加载一次,它能较好地管理这些
资源占用的内存,还可以在不同的位置搜寻需要的资源。
, 多数情况下,你不需要直接与资源管理器(Resource Managers)打交道,资源管理器
(Resource Managers)能在OGRE系统需要其资源的情况下自动调用。
, 渲染系统(Render System)
, 定义了3D API的接口。它负责设置所有的渲染参数并且传送渲染指令给底层的API , 是抽象类,因为其实现依赖于某个具体的渲染系统的API接口(比如Direct3D的
D3DRenderSystem)
, 场景管理器(SceneManager)会在适当的时候调用渲染系统(RenderSystem)对象。 , 一个通常意义上的应用程序不会直接操作渲染系统(RenderSystem)对象,你需要渲染
的所有对象和场景设定都可以由场景管理器(SceneManager)、材质(Material)或其
它面向场景的类(Scene-oriented Classes)来完成。
, 只有当你想要创建多个渲染窗口,或者想要访问其它高级特性时,你才需要直接访问渲
染系统对象。
, 插件系统(Plugin System)
, OGRE在设计时就考虑了其扩展性,而使用插件则是一种最为常见的扩展方式。 , 许多OGRE中的类能被继承及扩展。比如,可以自定义一个改变场景组织方式的场景管
理器插件;再比如,开发一个从网络加载资源的资源管理器插件。
, 在加入新的功能插件的时候不用重新编译生成Ogre程序库。只要简单放入符合Ogre规
范的插件文件,Ogre就能通过自身的一套机制在初始化或者运行期间动态载入插件。
设计模式
, 设计模式(Design pattern)是一套被开发人员反复使用的软件设计经验的。使用设计模式
17
的目的是提高代码的可复用性、可扩展性、可维护性和可靠性。
, 经典书籍:《设计模式——可复用面向对象软件的基础》 1995年英文版 GoF(四人组)
, Façade模式
, 目的:为系统内部的相互关联的众多接口提供一致高层接口,通过这个高层接口统一处理对整个
系统的访问请求。采用Façade模式可以隐藏系统内部复杂细节,使系统更容易使用。
, 适用范围:
, 如果需要为一个复杂的系统提供一致的高层访问接口,降低访问系统的复杂度
, 减少系统与其它系统的依赖性
, 如果创建一个层次结构的系统,通过Façade模式来定义系统的入口点 , Façade体系结构图
, Ogre程序基本框架分析
, 启动VS2005,打开教材第1周的sln文件
, Orge程序基本框架的构成主要是依靠两个类:ExampleApplication类和ExampleFrameListener
类
, 教材P11图2-1给出了Ogre框架和实际应用的关系,如下:
, 在第1周的解决方案中,除了Day1以外,Day2 --- Day6都有一个main.cpp文件。(在Day1中其
实是把这个文件合并到EnvMapping.cpp中了)
, 打开这个main.cpp文件,发现其核心代码为:
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE,
LPSTR strCmdLine, INT )
#else
int main(int argc, char **argv)
18
#endif
{
// Create application object
ShrewMouseApplication app;
app.go();
}
, int PASCAL WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
if (InitApp(hInstance, nCmdShow) != DD_OK)
return FALSE;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
, ExampleApplication类中的go成员函数
, ExampleApplication类中的setup成员函数
, ExampleFrameListener类
, ExampleFrameListener的主要作用是监听系统输入,并根据监听到的事件对场景做出控制。
, ExampleApplication类中有一个数据成员mFrameListener,它是ExampleFrameListener
类的指针。
, 从ExampleFrameListener.h的代码中可以看到ExampleFrameListener类派生自
FrameListener类。FrameListener中有两个函数frameStarted和frameEnded。
, 在渲染每一帧之前,Ogre引擎会调用frameStarted函数,在渲染每一帧之后,Ogre引擎会调
用frameEnded函数,通常我们只需要把我们的控制逻辑代码放到我们派生的监听器类(如
ShrewMouseFrameListener)的frameStarted函数中,即可完成对场景的控制。 , ExampleFrameListener类
, 这个类涉及到Orge的事件处理模型
, OGRE事件处理采用的是publish subscribe mode, 即?发布和订阅?模型。
, 对象A试图在对象B产生C类型事件的时候执行指定的动作,于是,对象A告知对象B当产生C
类型事件的时候给自己发出一个通知。整个过程涉及到以下部分:
, 事件源(Event Source)。通常是GUI对象或用户输入或对象内部事件。Root对象 就
是一个事件源,它从内部产生FrameEvent事件
, 事件处理对象(Event Handler)。用户定义的事件处理器。也就是
ExampleFrameListener对象。
, 注册或订阅(registration or subscription)。这是将一个事件处理对象向事件源对
象注册的过程,这样当 事件处理器对象感兴趣的事件发生的时候可以立即收到通知
, 通知函数。通常会强制要求事件处理对象实现一个名称和参数已定的特殊方法。例如
ExampleFrameListener的frameStarted和frameEnded方法。
19
, ExampleFrameListener类
, ExampleFrameListener类中的注册和订阅
, 在ExampleApplication的createFrameListener函数里
// 产生事件处理对象
mFrameListener= new ExampleFrameListener(mWindow, mCamera);
...
// 将事件处理对象向事件源对象注册
mRoot->addFrameListener(mFrameListener);
, 当Root产生FrameEvent时ExampleFrameListener就会得到通知,就会调用
ExampleFrameListener的frameStarted和frameEnd方法。这样给了我们一个基于
帧的触发事件处理方法。
, 总结
, ExampleApplication和ExampleFrameListener两个类相互协同,组成Orge的基本程序框架。
要在这两个类的基础上进行派生,开发出自己的Orge程序。
, 在ExampleApplication的派生类中,要实现createScene函数,在这个函数中,完成场景的
建立和各个活动物体的创建。
, 在ExampleFrameListener的派生类中,要实现frameStarted函数,将对场景物体的控制逻辑
代码放在这个函数中。
动画
, Ogre引擎提供了多种不同类型的动画,包括在模型中定义的骨骼动画和顶点动画,以及操作场景节点
的节点轨迹动画。
, Day4的任务:在Day3的基础上为地鼠添加动画效果
, 关于动画的说明:
, 动画是由若干静态画面,快速交替显示而成。因人的眼睛会产生视觉暂留,就会有动的感觉。
Ogre引擎可以通过自动或半自动的方式实时产生并播放动画图片
, 可以通过离线工具来生成具体的动画文件,也可以通过实时的运算产生动画,比如可以实时
产生摄像机的运行轨迹。
, 通常的情况下,Ogre有两种不同的操作动画对象的方法,一种是通过关键帧,另外一种是通
过控制器。
, 用通俗的说法,关键帧是‚在某一时间对物体所有位置、方向、缩放的采样?。依赖于不同的
动画类型,Ogre系统中也支持多种的关键帧类型。
泛型编程
, 面向对象编程
, 以类和对象的概念为基础,通过设计类来封装数据、通过类继承来建立类之间的纵向联系、通过
多态性来使类之间的关联更灵活,具有以上特征的编程方式可以称为面向对象编程。
, C++是典型的面向对象的编程语言。它通过访问权限控制来实现封装性,通过类继承来实现继承
性,通过虚函数来实现多态性。
, 优点:与面向过程编程相比,更容易适应软件需求变化。易于修改、易于扩展、易于维护。
, 缺点:对象的类型是确定的。对于不同类型的对象,尽管某个程序的算法是相同的,但是仍然需
要为不同对象类型重复实现算法,从而造成代码重复(代码冗余)。
, 泛型编程
, 泛型编程(Generic Programming)就是编写完全一般化(不针对特定数据类型)并可重复使用的
20
算法,其效率与针对某特定数据类型而设计的算法相同。
, 所谓泛型(Genericity),是指在多种数据类型上皆可操作。
, 泛型程序设计方法不是面向对象程序设计方法的一部分,它有着与面向对象方法不同的设计技术。
在某些情况下,良好的泛型程序设计方法的规则可能违背良好的面向对象的程序设计方法的规则,
反之亦然。但是两种程序设计方法并不是水火不容,而是相得益彰。巧妙地结合这两种方法,可
以更有效地解决问题。
, 泛型编程的基础是模板技术
, 模板
, 所谓“模板”就是将类或函数中用到的某些数据类型参数化。一旦程序确定了数据类型,编译器
就自动针对这些数据类型产生出一份代码实例(这里实例是指函数或类),这个过程称为“模板的
实例化”。
, ‚模板?是C++支持参数化程序设计的工具,通过它可以实现参数化多态性。 , 所谓“参数化多态性”,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种
不同类型的对象。
, 模板是实现程序代码复用的有力工具。
, 模板包括两种
, 类模板
, 函数模板
, 模板
, 类模板例子
template // T 是商店中某项数据的类型
class Store
{private:
T item; // 某项数据,比如账单数据、顾客数据等
...
public:
T GetElem();
void PutElem(T x);
...
}
, 函数模板例子
template
T max(T a, T b) // 这是一个适用于多种类型的通用函数
{ return (a,即: #include
using namespace boost;
, 我们这里介绍教材中用到的两个智能指针: shared_ptr和scoped_ptr , shared_ptr
, 包装了new操作符在堆上动态分配的对象
, 实现的是引用计数型的智能指针
, 可以被自由地赋值和拷贝,在任意的地方共享它
23
, 当没有代码使用这个指针时,会自动删除被包装的动态分配的对象
, 可以安全地放到标准容器中,弥补了std::auto_ptr因为对象拥有权的转移而不能作
为STL容器元素的缺陷
, 其行为最接近C语言定义的原始指针,使用范围更广,完全消除了delete的使用和内
存泄漏
, 使用举例:share_ptr p(new int(100));
, 包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以
被正确地删除。
, 对包装对象的所有权更加严格,不能转让,一旦scoped_ptr获取了对象的所有权,你
就无法再从它那里取回来。
, scoped_ptr同时把拷贝构造函数和赋值操作符都声明为私有的,禁止对智能指针的复
制操作。
, scoped_ptr不允许拷贝、赋值,只能在scoped_ptr被声明的作用域内使用。
, 不再需要delete操作,在不被代码使用时会自动释放资源
, 使用举例: scope_ptr sp(new string(‚text?))
几个设计模式
, 什么是设计模式
, 是一套被反复使用的、广为人知的、经过分类编目的代码设计经验的总结。使用设计模
式是为了可重用代码,让代码更容易被别人理解,保证代码的可靠性。
, MVC模式
, M – Model (模型) 一般存放程序逻辑(企业数据和业务规则)
V – View(视图) 一般显示界面(与用户交互)
C – Controller(控制器) 一般用于建立程序框架(分发、处理请求)
, 目的是实现一种动态的程序设计,使后续对程序的修改和扩展工作简化,并且增强程序的重用能
力。除此之外,此模式可以简化复杂设计,让程序结构更加直观。
, 教材提供的3D游戏开发框架orz是一个控制器层,Orge作为视图层,游戏逻辑放在模型层。 , 几个设计模式
, Factory Method模式
, 工厂方法模式的目的是为了创建了另外的类的对象。
, 由一个父类规定创建所需的工厂方法,由父类的子类创建
具体类的对象。
, 当一个类不知道要创建的是哪一个类的对象时,类本身只定义创建接口(也就是工厂方法函数),
具体要创建哪一个对象交给子类来决定。
, UML图:
, Singleton模式
, 保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。
, 当类只能有一个实例,且客户可以从一个众所周知的访问点访问它时,可以使用Singleton模式。 , 在OGRE和orz中,这种模式主要使用在一些全局管理器中。
, UML图为:
, Builder模式
, 当一个复杂对象的创建需要多个步骤,可以用一个创建类包含这多个创建步骤。可以通过改变步
骤的具体实现来改变创建类实际完成的创建工作,但是使用创建类的客户程序感觉不到这种改变。
24
, 多用于被创建的对象有多种创建方式时,我们可以用一个创建类来封装不同的创建方式。父类提
供创建接口,子类完成具体创建。
, UML图:
, 观察者模式
, 定义对象间的一对多的依赖关系,当一个对象的状态发生改变时所有依赖于它的对象都得到通知
并自动被更新。
, 多个对象依赖于一个对象S,当被依赖的对象S的状态发生改变的同时需要改变依赖对象,但S
不知道有多少个对象需要改变,这时可以使用观察者模式。
, OGRE是一个开源的3D图形引擎,其性能优良,但是这还不够,要开发游戏还需要一套游戏开发框架。 , Orz是作者邸锐(免费打工仔)和Ogre 3D中文社区开发的一款开源游戏开发框架。这个框架可以简化
复杂的游戏开发过程。
, Orz框架集成了其他开源的游戏底层功能型引擎,并为用户提供了一套统一的逻辑接口。这些功能型引
擎包括OGRE 3D(图形引擎)、Fmod(音频引擎)、Newton(物理引擎,以插件方式提供)以及OIS(输
入处理)。
, 本周的内容将介绍Orz框架的使用方法,通过本周的学习同学们将了解到如何在Orz框架的基础上快
速开发程序原型。同学们还可以在本周代码基础上进行二次开发,完成简易的游戏。 , OrzSpaceInvaders Day1
, 如何使用Orz开发框架
, Orz框架是一款遵循MVC(Model – View – Controller)设计模式的游戏开发框架。
, 在Orz的视图层(View)中,包含了Fmod(音频引擎)、ODE(物理引擎)及OIS(输入处理)等功能引
擎,负责大部分硬件的封装,用于游戏效果的展示。通过代码或脚本写的游戏逻辑在这个结构下
被视作模型层(Model),用来定义游戏的内容。Orz借助一系列工具把逻辑和功能引擎有机且低耦
合地结合起来,Orz本身是MVC模式中的控制层(Controller)。
, 游戏开发人员编写逻辑(Model),通过Orz框架(Controller)定义的方法来调用Ogre 3D等功能
引擎(View)。
“碰撞检测”说明:
, Orz框架中是用Ogre的碰撞检测方法来检测实体之间的碰撞。当两个实体的包围盒发生了重叠,
就有可能是碰撞,但是仅仅依据包围盒发生重叠,还不能判定发生了碰撞。
, 由于在Orge场景中一般有很多物体,虽然从空间上说,物体之间确实存在位置重叠的情况,但
从逻辑上说,物体之间并不是都存在碰撞关系。所以碰撞检测要考虑空间和逻辑这两个方面。
, Ogre通过掩码为实体之间建立逻辑联系,将实体分组。对于场景中的每一个实体可以用设置
QueryFlags的方法,为它附加一个掩码,用于标记不同的碰撞组。
, 碰撞检测涉及一个查询器和一个带有掩码的实体。当查询器的掩码与实体的掩码相与不为0时,
那么查询器就可以检测是否与该实体发生碰撞。通过查询器执行查询,就可以返回碰撞检测结果
Orz框架介绍
, Orz是在Orge 3D图形引擎基础上开发的开源游戏开发框架,它目前主要支持单机版的程序。 , Orz的目标是:在未来开发出一个虚拟世界,这个虚拟世界由很多人参与,这个世界本身是由参与者共
同构建的,就象搭积木一样组织在一起。每个参与者开发出一个属于自己的模块,然后存在于一
个统一的世界体系之下。
, Orz框架的特点是:注重组件之间的离散化,降低开发人员之间的协同工作。
, 在传统的软件开发中,由于程序模块之间的耦合程度较高,在开发时,不同开发人员互相依赖。存在的
25
主要问题是:随着开发人员数目的增加,项目的复杂程度和通信成本按平方增加。
, 按照开源软件的经验,如果有一个大型项目,需要足够多的人合作,则唯一的方法就是尽量降低开发人
员的依赖性。
, Orz框架的开发思想是:定义一个公共知识库,其中定义了交互的规则。所有的开发人员按照提供的约
定完成自己的代码,完全没有和其他开发人员直接交互。这样增加开发人员数量不会过度增加开
发人员的通信成本。
, 使用Orz框架开发的游戏是按照MVC模式构建起来的。Model部分是游戏的逻辑,View部分是游戏逻
辑的展示,Controller部分就是Orz框架。
, 我们来看Orz框架的源代码:
, 在读Orz源代码时,最好首先用CMake将源代码生成Visual C++ (2005或以上)的工程文件。
这样可以在VC的集成环境中来读源代码,效率会提高很多。
, Orz的源代码主要包括8个源代码文件夹,它们是Controller_Base、Controller_Win32、
Framework_Base、Model_Base、Toolkit_Base、View_Fmod、View_OGRE3D和View_OIS。 , 其中,Controller_Base和Controller_Win32只包含main和WinMain等程序入口文件,对
整个框架的功能没有影响;
, Model_Base是一个插件,它是动态链接库的形式,Model_Base建立了一个简单的游戏逻辑,这
个游戏逻辑通过插件的方式可以加载到程序中来;
, View_Fmod、View_OGRE3D和View_OIS这三个文件夹是orz框架对音频、图形引擎和输入输出
函数的封装或适配,提供其他模块所需要的相关功能;
, Orz源代码中最核心的文件夹是Framework_Base和Toolkit_Base。前者提供了Orz框架提供
了核心逻辑,后者提供了Orz框架所需要的各种工具
, 在VC集成环境中,我们可看到: (除了controller_Base、Controller_Win32和Model_Base
外)源代码的每个文件夹中有lib和orz两个子文件夹,但是,在资源管理器中,orz文件夹并
不在每个项目文件夹的下方,而是单独一个文件夹。这是作者特意这样组织的,这样在集成环境
中可以把与同一个项目相关的所有文件放在一起。
, orz文件夹的作用是将要提供给外界游戏程序使用的所有orz框架的头文件放在一起,便于用户
在程序中包含这些头文件。orz文件夹中的头文件也可以看成是游戏程序使用orz框架的“接口”,
也就是说,外界游戏程序只能使用这些头文件中提供的类、函数或变量,orz其它的部分对于外
界游戏程序是隐藏的。所以,我们在读orz框架的源代码时,只需多读这些头文件,就可以理解
orz框架向外界提供了什么功能。
, 在VC集成环境中读orz源码,大家会发现,.h文件很多,且比较分散。如果一个个头文件地读,
会很耗时,收效不好。而.cpp文件比较少,所以可以以读cpp文件为主,在需要时再去读.h文
件。
, 我们前面说过,在orz框架中,比较核心的文件夹是Toolkit_Base和Framework_Base,其中,
Toolkit_Base下orz子文件夹中有5个子文件夹,比较复杂需要仔细去读的是DyLibManager
和EventSystem这两个子文件夹,它们分别处理插件管理和消息系统;Framework_Base下orz
子文件夹中有3个子文件夹,比较复杂需要仔细去读的是Logic和System两个子文件夹。 , 总之,深入理解Orz框架源代码对于使用Orz开发游戏程序十分重要。
A*寻径算法
26
, 1968年在人工智能领域开始应用
, 在游戏中普遍使用A*算法实现从开始位置到目的位置的路径寻找
, A*算法概述
, A*算法在人工智能中是一种典型的启发式搜索算法。所谓启发式搜索是将人的经验转化
为代价函数,通过代价函数控制在状态空间的搜索,使搜索次数大幅度降低,从而较快
地获得搜索结果。
, 如果在搜索的状态空间中存在由初始状态到达目标状态的多条路径,则使用A*算法必能
找到最优的路径。这里“最优”是相对于代价函数而言的,是指代价函数值最大或最小。
, 在游戏中,如果要使物体从某一个初始位置运动到指定位置,需要进行路径寻找。此时,
状态空间的状态对应着游戏的各个空间位置,可以使用A*算法来解决。游戏的地图中常
常有障碍物或者不同类型的地形,我们可以把这些作为A*算法要用到的代价函数。可以
根据游戏策划的需要,通过设置不同的代价函数得到不同的最优移动路径。 , A*算法是图搜索算法的一种。它是从初始节点开始到达目标节点。在A*算法中,要用到两个列表,
一个称为open表,主要用于存放未扩展节点;一个称为closed表,主要用于存放已扩展节点。 , 在从初始节点到一个中间节点n,代价函数f(n)=g(n)+h(n)。其中,g(n)就是起始节点到中间
节点n这段路径的实际代价;h(n)是从中间节点n到目标节点的最小代价的估计值。A*算法完成
的工作是:从初始节点开始,通过一系列的中间节点到达目标节点,使初始节点到目标节点的代
价函数值最小。
, 在从初始节点开始的每一个节点中,总是选择f值最小的节点作为扩展节点,将当前节点的所有
后继节点放入open表,对open表进行排序,选择f最小的后继节点作为扩展节点进行扩展。这
样,每次进行扩展时,总是对最优可能性最大的节点优先进行扩展。
, A*算法步骤
, 把起始节点放入open表中,记f=h,令closed表为空表
, 重复下列过程,直到找到目标节点为止。若open表为空,则失败,退出。
, 选取open表中未设置过的具有最小 f 值的节点为最佳节点bestnode,并把它放入
closed表。
, 若bestnode为一目标节点,则成功求得一解。
, 若bestnode不是目标节点,则扩展之,产生后继节点successor
, 对每个successor,进行下列过程:
? 建立从successor返回bestnode的指针
? 计算g(successor)=g(bestnode)+g(bestnode,successor)
? 如果successor节点在open表中,则称此节点为old,并把它添加至bestnode
的后继节点表中
, 比较新旧路径代价。如果g(successor) _epool; 这就是消息池。
, 消息系统在具体实现时解决了以下问题:
, 消息的分发效率问题(续)
第二,采用频道概念来控制消息分发。在向游戏世界中多个对象广播消息时,需
要确定哪些对象要接收这个消息,哪些对象要忽略这个消息。为了快速判断,采用频道
来分配消息。所谓频道(EventChannel)是对32位或64位整型的封装,消息和消息处
理对象都有自己的频道。只有消息和消息处理对象的频道进行“与”运算的结果不为0
时,消息处理对象才接收这个消息。消息频道在EventChannel.h和EventChannel.cpp
文件中定义,主要是由EventChannel类来实现。
, 多个游戏开发者如何定制消息
如果允许多个游戏开发者合作,可以分别定制消息,那么如何确保不同的游戏开
发者定制的消息互不影响,
在Orz开发框架中,引入了消息空间(EventSpace)的概念。每个消息空间在注
册时都设置一个唯一的字符串名称,并分配一个偏移量,系统保证不同名称的消息空间
中的消息不会冲突。
, 消息系统在具体实现时解决了以下问题:
, 多个游戏开发者如何定制消息(续)
在orz的源代码中,EventSpace.h和EventSpaceRegister.h两个文件说明了
消息空间是如何实现的。EventSapece类和EventSpaceRegister类实现了在消息空
间中创建消息等功能。特别是在EventSpaceRegister.h文件中定义了宏
DEF_EVENT_BEGIN、 DEF_EVENT和DEF_EVENT_END,以及 DEF_EVENT_CPP,借助这
些宏可以方便地声明不同名称的消息空间以及消息空间中的消息,还可以方便地实现这
些消息空间。通过消息空间,使得不同开发者定制的消息不会发生冲突和混淆。 , 如何实现消息的异步处理
目前,EventSystem是单线程的框架,消息分发和处理都要尽可能快速,这样
才不会阻塞渲染速度。但是存在一个问题:如果消息在被接收的同时立刻处理,很可能
会触发新的消息,新消息又会在发送方产生消息,这样就会产生一个消息循环触发,影
响渲染速度。
, 消息系统在具体实现时解决了以下问题:
29
, 如何实现消息的异步处理(续)
为了解决消息循环触发影响渲染速度的问题,在orz游戏框架中规定:在当前帧
触发的事件,在下一帧中处理。在EventWorld类中准备了两个队列_nowEvtList和
_nextEvtList,分别存放当前帧接收到的事件列表和上一帧接收到的事件列表,当前帧
应该处理的是上一阵接收到的事件列表。通过异步处理,避免的事件的循环触发造成的
性能下降。
, 关于对多线程的支持
orz框架支持消息驱动的多线程结构,但是多线程要是要求和渲染同步,那么就
必须要区分系统的渲染动作和逻辑代码引发的动作,这样才能使用多线程处理逻辑部分。
目前这部分还未实现,将来可能会使用Intel的Threading Building Blocks库来实
现多线程的功能。
, 消息系统的主要组件
, EventWorld --- 消息世界,是消息系统的核心管理组件
, 消息系统的主要组件
, EventWorld类的update方法需要放在固定的帧数下调用,在实际使用中通过Clock
(时钟)和TimerManager(计时器管理器)来保证在固定的帧速率下更新世界。
, Event --- 消息类,这个类有一些函数,可以设置和获得消息所属的频道、设置和获得
消息传递的数据、设置和获得消息的类型、设置和获得消息的发送者ID和接收者ID等
功能。
, EventChannel --- 消息频道类,这个类的函数可以完成添加和移除用户频道、系统频
道的功能,填写用户频道和系统频道的功能,创建频道对象的功能,以及其它相关功能。
, EventHandler --- 消息处理器类,这个类有增加和移除处理频道的功能,还有进入消
息管理世界时要调用的doEnable函数,离开消息管理世界时要调用的doDisable函数,
以及其它与消息处理有关的功能。
, EventSpace --- 消息空间类,这个类有创建消息空间内的消息、获得消息在本消息空
间中的消息值、判断消息空间是否有相同消息等功能。
, 消息系统的主要组件(续)
, EventSpaceRegister --- 用于消息空间注册的模板类。在这个类中定义了多个宏,用
于声明和实现消息空间。前面已经有所介绍,这里不再重复。
, EventFactory --- 消息工厂类,用于创建消息。包含创建消息、销毁消息、注册消息
空间、通过名称得到消息空间、搜索消息所在空间等多项功能函数。
, 总之,在Orz框架的源代码目录的EventSystem子目录下的各个文件共同实现了完善的消息系
统,为稳定的事件驱动机制提供了保证。
, 模板元编程
, ‚模板元编程?是C++语言的一种高阶特性,它是建立在“模板”这个概念基础之上的。关于“模板元编程”的详细内容,大家可以参考两本书:一本是《C++模板元编程》(荣耀译,机械工业出版社出版);一本是《C++ Template中文版》(陈伟柱译,人民邮电出版社出版)。我们在这里只介绍模板元编程的基本概念。
, 所谓“模板元编程”,英文原术语是metaprogramming,它是指“对一个程序进行编程”。也就是说,编译系统将会编译我们所写的代码,来生成新的代码,而这些新代码才真正实现了我们所期望的功能。 , ‚模板元编程?的最大特点在于:某些用户自定义的计算可以在程序编译器进行。而这通常能够在性能(因为在程序编译器进行的计算通常可以被编译器优化)或者接口简单性(一个模板元程序的代码通常比它扩展后的代码要简短)带来好处。
30
, 我们来看一个模板元编程的例子,这个例子是计算给定整数的指定次方 //原始摸板
template
class XY
{ public:
enum { result_ = Base * XY::result_ };
};
//用于终结递归的局部特化版
template
class XY
{
public:
enum { result_ = 1 };
};
, 让我们看看使用此模板来计算5^4 (通过实例化XY<5, 4>)时发生了什么:
// xytest.cpp
#include
#include "xy.h"
int main()
{
std::cout << "X^Y<5, 4>::result_ = " << XY<5, 4>::result_;
}
首先,编译器实例化XY<5, 4>,它的result_为5 * XY<5, 3>::result_,如此一来,又需要针对<5, 3>实例化同样的模板,后者又实例化XY<5, 2>…… 当实例化到XY<5, 0>的时候,result_的值被计算为1,至此递归结束。然后,将XY<5,0>代到递归计算式中,可以求出 XY<5,1>;将XY<5,1>代到递归计算式中,可以求出 XY<5,2>;… 这样一直回代下去,最终可以计算出XY<5, 4> , 可以想象,在前面那个例子中,如果我们以非常大的Y值来实例化类模板XY,那肯定会占用大量的编译器资源甚至会迅速耗尽可用资源(在计算结果溢出之前),因此,在实践中我们应该有节制地使用模板元编程技术。
, 虽然 C++标准建议的最小实例化深度只有17层,然而大多数编译器都能够处理至少几十层,有些编译器允许实例化至数百层,更有一些可达数千层,直至资源耗尽。
, 模板元编程技术并非都是优点,比方说,模板元程序编译耗时,带有模板元程序的程序生成的代码尺寸要比普通程序的大,而且通常这种程序调试起来也比常规程序困难得多。另外,对于一些程序员来说,以类模板的方式描述算法也许有点抽象。
, 编译耗时的代价换来的是卓越的运行期性能。通常来说,一个有意义的程序的运行次数(或服役时间)总是远远超过编译次数(或编译时间)。为程序的用户带来更好的体验,或者为性能要求严格的数值计算换取更高的性能,值得程序员付出这样的代价。
, 模板元程序几乎总是应该被封装在一个程序库的内部。对于库的用户来说,它应该是透明的。模板元程序可以(也应该)用作常规模板代码的内核,为关键的算法实现更好的性能,或者为特别的目的实现特别的效果。
, 目前常用的支持模板元编程的库有boost::mpl库。 boost::mpl库提供了一个通用、高层次的编程框架,其中包括了序列(Sequence)、迭代器(Iterator)、算法(Algorithm)、元函数(Metafunction)等多种组件,具有高度的可重用性,不但提高了模板元编程的效率,而且使模板元编程的应用范围得到相当的扩展。详细信息可以参考以下网址的系列文章:
31
, 逻辑层和系统层
, Orz系统框架是一个按照MVC(Model—View—Controller)设计模式开发的游戏框架。使用Orz框架开发的游戏由Model层、View层和Controller层组成。
, 其中,Controller层主要负责游戏程序的运行控制,在不同的游戏中这部分差别不大。 , Model层对应的是游戏的概念模型,也就是游戏的运行逻辑,对于不同的游戏程序而言,这部分有较大差别。在教材把这部分也称为“逻辑层”。
, View层主要是展示游戏的外在表现,包括Ogre图形引擎、音频引擎、物理引擎等可以让游戏输出内容的部分。所有这些功能引擎都可以划分到View层,在教材中把这些引擎以及它们提供的服务称为“系统层”。
, 我们先来看“逻辑层”
, 逻辑层负责游戏程序逻辑的设置。在基于orz框架的游戏开发中,orz框架已经为我们建立了游戏各部分的联系,我们只需要设计和实现游戏相关的逻辑即可完成整个游戏。
, 在orz游戏框架中存在4种不同概念的实体,也就是Theater(大厅)、Director(导演)、Scene(场景)和Actor(角色)。
, 其中,Director代表一个完整的游戏逻辑,负责调度Scene和Actor。Scene代表场景以及与场景相关的逻辑,Actor代表游戏中独立的个体以及与个体相关的逻辑。所以在设计游戏程序时,要把游戏逻辑合理分配到Director类、Scene类和Actor类中。
, 还有一个实体是Theater,如果一个游戏包含多个子游戏,用户可以选择子游戏来运行,那么需要一个对子游戏进行调度的实体,Theater就是完成子游戏调度功能的实体。一般情况下,我们开发的游戏只有一个子游戏,这时可以使用TheaterBase类,这个类提供默认的子游戏调度功能。 , 上一页PPT已经讲到,逻辑层存在4种不同概念的实体,实体对应的是Entity类。我们通过阅读orz框架的源码可以看到,Entity类是从EventHandler类和IDManager类派生而来。这说明每一个Entity类的对象都可以接收、发送和处理消息;同时,该对象还具有一个ID号。
, 在orz框架中,通过消息的发送代替了复杂的函数调用,简化了逻辑;通过ID号代替了原始的指针,提高了程序的安全性。
, orz框架还支持插件系统。通过配置插件可以实现逻辑功能的动态加载、使用和卸载。使用插件系统使得用户可以动态地扩展游戏的功能。插件系统和脚本相结合可以提供对游戏逻辑功能的灵活配置。 , 为了在orz框架下完成一个游戏逻辑,需要一个编程人员做如下工作:
1. 配置LogicFacade相关信息。LogicFacade是将Façade模式应用于逻辑层。Façade模式是为
模块提供一个易于使用的接口,所以LogicFacade是为用户方便地使用逻辑层提供的接口;可以通
过运行参数、XML脚本和使用程序语句定制这三种途径来灵活地对游戏逻辑进行配置。
2. 根据需要实现并提供插件。有一些逻辑功能可以封装在插件中,在运行时进行动态加载。
3. 创建Theater、Director、Scene、Actor等逻辑实体以及相应的工厂,供编程人员构建特定的
逻辑。
4. 在系统中注册这些创建实体的工厂,这样系统就可以使用这些工厂了。
5. 通过有限状态机实现系统内部逻辑的转换,通过消息系统发送消息,实现实体相互之间的交互。
, orz框架使用Ogre3D提供的图形渲染服务、FMod库提供的音频处理服务、OIS库提供的配置输入系统的服务。orz框架的系统层就是通过统一的方式创建并协调这些底层服务。
, 系统层是在游戏开发之前由一些决策者(如系统分析师、系统设计师)确立,并统一实现。之后,将系统层程序交给游戏的逻辑开发人员,逻辑开发人员在系统层的基础上开发各自的游戏逻辑。逻辑开发人员使用系统层提供的底层服务,但一般不修改系统层的程序代码。
, 在orz程序框架中,采用了模板元的方法,把系统层包含的各个子系统在编译期间确定。它的优点是只要把各个子系统确定了,在系统运行时就不能再改动这些子系统,提高了安全性。当然,这同时也是一
32
个缺点,在系统运行的时候不能对系统层的各个子系统进行动态配置。不过,在编译器确定子系统可以使系统层有更高的运行效率,因为省掉了运行时配置子系统的相关操作。
, orz框架的系统层为外界提供的接口是Framework_Base项目中orz文件夹下System子文件夹的SystemInterface类。
, 我们以OrzFighterClub Day5的解决方案为例,在FCController项目的FCController.cpp文件中定义了程序的入口函数main函数。main函数的第1条语句是: SystemPtr system(new
SystemList >()); 这是用模板元的方法把多个子系统管理器联系在一起,并以此创建一个SystemInterface类的智能指针system。 , 可以看到,这里声明的子系统管理器包括日志管理器、计时器管理器、Ogre图形管理器、GUI管理器、输入管理器、音频管理器、插件管理器。这些子系统管理器可以向编程人员提供底层的服务。为了便于使用这些子系统提供的服务,可以使用“适配”的方法另外定义一些简单的接口函数。
, SystemInterface是系统层与外部的接口类。
, SystemInterface类主要有以下常用函数:
void SystemInterface::run(void) 启动系统循环,系统进入运行状态
void SystemInterface::exit(void) 退出系统循环,系统终止
bool SystemInterface::running(void) const 检查系统是否在运行
virtual bool SystemInterface::init(void) 依次初始化每个子系统
virtual void SystemInterface::shutdown(void) 依次关闭每个子系统
virtual bool SystemInterface::update(TimeType interval) 依次更新所有子系统,更新的时间间隔为interval
, 总结: 在一个良好的游戏框架中,逻辑层和系统层应该完全分离。逻辑层负责游戏的高层逻辑,而系统层负责提供底层的各项服务。系统层一旦确定,一般不要轻易变动。逻辑层则是根据游戏的要求,可以使用各种各样、不同类型、不同复杂度的逻辑。逻辑层的配置可以通过参数、XML文件和程序代码三种方式;系统层的配置是在程序代码中使用模板元将各个子系统连接起来。逻辑层可以在运行时动态配置,系统则在编译期配置,一旦配置好,运行期不再变动。
33