• 沒有找到結果。

三维游戏引擎设计技术及其应用 - 万水书苑-出版资源网

N/A
N/A
Protected

Academic year: 2021

Share "三维游戏引擎设计技术及其应用 - 万水书苑-出版资源网"

Copied!
48
0
0

加載中.... (立即查看全文)

全文

(1)第3章 游戏引擎编程中的高级面向对象技术. 主要内容:在游戏引擎设计中,面向对象技术的使用程度是游戏 引擎封装好坏的重要因素,但是面向对象将涉及软件工程中的许多重 要内容,本章主要讲述面向对象中的两个重要技术:设计模式和泛型 编程,这两种编程都属于面向对象编程中的高级内容,也是在实际游 戏引擎开发中使用最广泛的内容。 本章重点: 设计模式的基本概念.  . 设计模式的方法. . 游戏引擎中所用的设计模式. . 标准模板库. . 容器的使用. . 迭代器的使用.

(2) 3.1. 设计模式. 设计面向对象软件比较困难,而设计可复用的面向对象软件就更加困难。必须找到相关的对 象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系。设计应 该对手头的问题有针对性,同时对将来的问题和需求也要有足够的通用性。大家都希望避免重复 设计或尽可能少做重复设计。有经验的面向对象设计者会告诉你,要一下子就得到复用性和灵活 性好的设计,即使不是不可能的,至少也是非常困难的。一个设计在最终完成之前常要被复用好 几次,而且每一次都有所修改。 有经验的面向对象设计者的确能做出良好的设计,而新手则会面对众多选择无从下手,总是 求助于以前使用过的非面向对象技术。新手需要花费较长时间领会良好的面向对象设计是怎么回 事儿。有经验的设计者显然知道一些新手所不知道的东西,这又是什么呢? 内行的设计者知道,不是解决任何问题都要从头做起。他们更愿意复用以前使用过的解决方 案。当找到一个好的解决方案,他们会一遍又一遍地使用。这些经验是他们成为内行的部分原因。. 第. 因此,会在许多面向对象系统中看到类和相互通信的对象(Communicating Object)的重复模式。. 3 章. 这些模式解决特定的设计问题,使面向对象设计更灵活优雅,最终复用性更好。它们帮助设计者 将新的设计建立在以往工作的基础上,复用以往成功的设计方案。一个熟悉这些模式的设计者不 需要再去发现它们,而能够立即将它们应用于设计问题中。 以下类比可以帮助说明这一点。小说家和剧本作家很少从头开始设计剧情。他们总是沿 袭一些业已存在的模式,像“悲剧性英雄”模式(《麦克白》、 《哈姆雷特》等)或“浪漫小说” 模式(存在着无数浪漫小说)。同样地,面向对象设计员也沿袭一些模式,像“用对象表示状 态”和“修饰对象以便于能容易地添加/删除属性”等。一旦懂得了模式,许多设计决策自然 而然就产生了。 设计模式使人们可以更加简单方便地复用成功的设计和体系结构。将已证实的技术表述成设 计模式也会使新系统开发者更加容易理解其设计思路。设计模式帮助你做出有利于系统复用的选 择,避免设计损害了系统复用性。通过提供一个显示类和对象作用关系以及它们之间潜在联系的 说明规范,设计模式甚至能够提高已有系统的文档管理和系统维护的有效性。简而言之,设计模 式可以帮助设计者更快更好地完成系统设计。 3.1.1 设计模式概述 一般而言,一个模式有以下 4 个基本要素: (1)模式名称(Pattern Name)。一个助记名,它用一两个词来描述模式的问题、解决方案 和效果。命名一个新的模式增加了我们的设计词汇。设计模式允许在较高的抽象层次上进行设计。 基于一个模式词汇表,我们自己以及同事之间就可以讨论模式并在编写文档时使用它们。模式名 可以帮助我们思考,便于我们与其他人交流设计思想及设计结果。找到恰当的模式名也是设计模 式编目工作的难点之一。 (2)问题(Problem)。描述了应该在何时使用模式。它解释了设计问题和问题存在的前因 后果,它可能描述了特定的设计问题,如怎样用对象表示算法等。也可能描述了导致不灵活设计 的类或对象结构。有时候,问题部分会包括使用模式必须满足的一系列先决条件。. 43. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(3) (3)解决方案(Solution)。描述了设计的组成成分、它们之间的相互关系及各自的职责和 协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而 具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或 对象组合)来解决这个问题。 (4)效果(Consequences)。描述了模式应用的效果及使用模式应权衡的问题。尽管描述设 计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重 要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面 向对象设计的要素之一,所以模式效果包括它对系统的灵活性、扩充性或可移植性的影响,显式 地列出这些效果对理解和评价这些模式很有帮助。 出发点的不同会产生对什么是模式和什么不是模式的理解不同。一个人的模式对另一个人来 说可能只是基本构造部件。本书中将在一定的抽象层次上讨论模式。“设计模式”并不描述链表 和 hash 表那样的设计,尽管它们可以用类来封装,也可复用;也不包括那些复杂的、特定领域内 的对整个应用或子系统的设计。本书中的设计模式是对被用来在特定场景下解决一般设计问题的 三维游戏. 类和相互通信的对象的描述。 一个设计模式命名、抽象和确定了一个通用设计结构的主要方面,这些设计结构能被用来构 造可复用的面向对象设计。设计模式确定了所包含的类和实例,它们的角色、协作方式以及职责 分配。每一个设计模式都集中于一个特定的面向对象设计问题或设计要点,描述了什么时候使用. 44. 引 擎 设 计 技 术 及 其 应 用. 它、在另一些设计约束条件下是否还能使用,以及使用的效果和如何取舍。 3.1.2 类的接口 设计模式采用多种方法解决面向对象设计者经常碰到的问题。这里给出几个问题以及使用设 计模式解决它们的方法。 1.寻找合适的对象 面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常称为方法或操 作。对象在收到客户的请求(或消息)后,执行相应的操作。 客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。由于这些 限制,对象的内部状态是被封装的,它不能被直接访问,它的表示对于对象外部是不可见的。 面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、 依赖关系、灵活性、性能、演化、复用等,它们都影响着系统的分解,并且这些因素通常还是互 相冲突的。 面向对象设计方法学支持许多设计方法。可以写出一个问题描述,挑出名词和动词,进而创 建相应的类和操作;或者,可以关注于系统的协作和职责关系;或者,可以对现实世界建模,再 将分析时发现的对象转化至设计中。至于哪一种方法最好,并无定论。 设计的许多对象来源于现实世界的分析模型。但是,设计结果所得到的类通常在现实世界中 并不存在,有些是像数组之类的低层类,而另一些则层次较高。例如,Composition 模式引入了 统一对待现实世界中并不存在的对象的抽象方法。严格反映当前现实世界的模型并不能产生也能 反映将来世界的系统。设计中的抽象对于产生灵活的设计是至关重要的。 设计模式帮助你确定并不明显地抽象和描述这些抽象的对象。例如,描述过程或算法的对象 现实中并不存在,但它们却是设计的关键部分。Strategy 模式描述了怎样实现可互换的算法族。.

(4) State 模式将实体的每一个状态描述为一个对象。这些对象在分析阶段,甚至在设计阶段的早期都 并不存在,后来为使设计更灵活、复用性更好才将它们发掘出来。 2.决定对象的粒度 对象在大小和数目上变化极大。它们能表示下至硬件或上至整个应用的任何事物。那么怎样 决定一个对象应该是什么呢? 设计模式很好地解决了这个问题。Facade 模式描述了怎样用对象表示完整的子系统, Flyweight 模式描述了如何支持大量的最小粒度的对象。其他一些设计模式描述了将一个对象分解 成许多小对象的特定方法。Abstract Factory 和 Builder 产生那些专门负责生成其他对象的对象。 Visitor 和 Command 生成的对象专门负责实现对其他对象或对象组的请求。 3.指定对象接口 对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构 (Signature) 。对象操作所定义的所有操作型构的集合被称为该对象的接口(Interface) 。对象接口描 述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象。 类型(Type)是用来标识特定接口的一个名字。如果一个对象接受 Window 接口所定义的所. 第. 有操作请求,那么就说该对象具有 Window 类型。一个对象可以有许多类型,并且不同的对象可. 3 章. 以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分则可以用其他类型刻 画。两个类型相同的对象只需要共享它们的部分接口。接口可以包含其他接口作为子集。当一个 类型的接口包含另一个类型的接口时,则说它是另一个类型的子类型(Subtype)的超类型. 45. (Supertype)。我们常说子类型继承了它的超类型的接口。. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术. 在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如 果不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。对象接口与其功 能实现是分离的,不同的对象可以对请求做不同的实现,也就是说,两个有相同接口的对象可以 有完全不同的实现。 当给对象发送请求时,所引起的具体操作既与请求本身有关又与接收对象有关。支持相同请 求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时 刻的连接就称为动态绑定(Dynamic Binding)。 动态绑定是指发送的请求直到运行时刻才受具体的实现的约束。因而,在知道任何有正确接 口的对象都将接受此请求时,可以写一个一般的程序,它期待着那些具有该特定接口的对象。进 一步讲,动态绑定允许在运行时刻彼此替换有相同接口的对象。这种可替换性就称为多态 (Polymorphism),它是面向对象系统中的核心概念之一。多态允许客户对象仅要求其他对象支 持特定接口,除此之外对其假设几近于无。多态简化了客户的定义,使得对象间彼此独立,并可 以在运行时刻动态改变它们相互的关系。 设计模式通过确定接口的主要组成成分及经接口发送的数据类型来帮助定义接口。设计模式 也许还会告诉你接口中不应包括哪些东西。Memento 模式是一个很好的例子,它描述了怎样封装 和保存对象内部的状态,以便一段时间后对象能恢复到这一状态。它规定了 Memento 对象必须定 义两个接口:一个是允许客户保持和复制 Memento 的限制接口,一个是只有原对象才能使用的用 来存储和提取 Memento 中状态的特权接口。 设计模式也指定了接口之间的关系。特别地,它们经常要求一些类具有相似的接口;或它们 对一些类的接口做了限制。例如,Decorator 和 Proxy 模式要求 Decorator 和 Proxy 对象的接口与.

(5) 被修饰的对象和受委托的对象一致。而 Visitor 模式中,Visitor 接口必须反映出 Visitor 能访问的 对象的所有类。 4.对象的实现 至此,我们很少提及到实际上怎样去定义一个对象。对象的实现是由它的类决定的,类指定 了对象的内部数据和表示,也定义了对象所能完成的操作。 我们基于 OMT 的表示法,将类描述成一个矩形,其中的类名以黑体表示。操作在类名下面, 以常规字体表示。类所定义的任何数据都在操作的下面。类名与操作之间以及操作与数据之间用 横线分隔。 返回类型和实例变量类型是可选的,因为并未假设一定要用具有静态类型的实现语言。 对象通过实例化类来创建,此对象被称为该类的实例。当实例化类时,要给对象的内部数据 (由实例变量组成)分配存储空间,并将操作与这些数据联系起来。对象的许多类似实例是由实 例化同一个类来创建的。 新的类可以由已存在的类通过类继承(Class Inheritance)来定义。当子类(Subclass)继承 三维游戏. 父类(Parent Class)时,子类包含了父类定义的所有数据和操作。子类的实例对象包含所有子类 和父类定义的数据,且它们能完成子类和父类定义的所有操作。以竖线和三角表示子类关系。 抽象类(Abstract Class)的主要目的是为它的子类定义公共接口。一个抽象类将把它的部分 或全部操作的实现延迟到子类中,因此一个抽象类不能被实例化。在抽象类中定义却没有实现的. 46. 引 擎 设 计 技 术 及 其 应 用. 操作被称为抽象操作(Abstract Operation)。非抽象类称为具体类(Concrete Class)。 子类能够改进和重新定义它们父类的操作。更具体地说,类能够重定义(Override)父类定 义的操作,重定义使得子类能接管父类对请求的处理操作。类继承允许只简单地扩展其他类即可 定义新类,从而可以很容易地定义具有相近功能的对象族。 抽象类的类名以斜体表示,以与具体类相区别。抽象操作也用斜体表示。图中可以包括实现 操作的伪代码,如果这样,则代码将出现在带有褶角的框中,并用虚线将该褶角框与代码所实现 的操作相连。 混入类(Mixin Class)是给其他类提供可选择的接口或功能的类。它与抽象类一样不能实例 化。混入类要求多继承。 类继承与接口继承的比较对理解对象的类(Class)与对象的类型(Type)之间的差别非常重 要。一个对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。 但是对象的类型只与它的接口有关,接口即对象能响应的请求的集合。一个对象可以有多个 类型,不同类的对象可以有相同的类型。 当然,对象的类和类型是有紧密关系的。因为类定义了对象所能执行的操作,也定义了对象 的类型。当我们说一个对象是一个类的实例时,即指该对象支持类所定义的接口。 类继承是一个通过复用父类功能而扩展应用功能的基本机制。它允许根据旧对象快速定义新 对象,允许从已存在的类中继承所需要的绝大部分功能,从而几乎无需任何代价就可以获得新的 实现。 然而,实现的复用只是成功的一半,继承所拥有的定义具有相同接口的对象族的能力也是很 重要的(通常可以从抽象类来继承)。为什么呢?因为多态依赖于这种能力。 当继承被恰当地使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅 添加或重定义操作,而没有隐藏父类的操作。这时,所有的子类都能响应抽象类接口中的请求,.

(6) 从而子类的类型都是抽象类的子类型。 只根据抽象类中定义的接口来操纵对象有以下两个好处: (1)客户无须知道他们使用对象的特定类型,只需要对象有客户所期望的接口。 (2)客户无须知道他们使用的对象是用什么类来实现的,只需要知道定义接口的抽象类。 这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原 则:针对接口编程,而不是针对实现编程。 不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。这是本 书设计模式的一个常见主题。 理解对象、接口、类和继承之类的概念对大多数人来说并不难,问题的关键在于如何运用它 们写出灵活的、可复用的软件。设计模式将告诉你怎样去做。 1.继承和组合的比较 面向对象系统中功能复用的两种最常用技术是类继承和对象组合(Object Composition)。正 如我们已经解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类 的复用通常被称为白箱复用(White-box Reuse)。术语“白箱”是相对可视性而言的:在继承方. 第. 式中,父类的内部细节对子类可见。. 3 章. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获 得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(Black-box Reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语 言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部 操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。 但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻 改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类 揭示了其父类的实现细节,所以继承常被认为“破坏了封装性”。子类中的实现与它的父类有如 此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。 当需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新 的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复 用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。 对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的 接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。 这还会产生良好的结果:因为对象只能通过接口访问,所以并不破坏封装性;只要类型一致,运 行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以 实现上存在较少的依赖关系。 对象组合对系统设计还有另一个作用,即优先使用对象组合有助于保持每个类被封装,并被 集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然 大物。另一方面,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖 于对象间的关系而不是被定义在某个类中。 这导出了面向对象设计的第二个原则:优先使用对象组合,而不是类继承。 理想情况下,不应为获得复用而去创建新的构件。应该能够只使用对象组合技术,通过. 47. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(7) 组装已有的构件来获得需要的功能。但是事实很少如此,因为可用构件的集合实际上并不足 够丰富。使用继承的复用使得创建新的构件要比组装旧的构件来得容易。这样,继承和对象 组合常一起使用。 然而,经验表明:设计者往往过度使用了继承这种复用技术。但依赖于对象组合技术的设计 却有更好的复用性(或更简单) 。你将会看到设计模式中一再使用对象组合技术。 2.委托 委托(Delegation)是一种组合方法,它使组合具有与继承同样的复用能力。在委托方式下, 有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者(Delegate)。这类似于 子类将请求交给它的父类处理。使用继承时,被继承的操作总能引用接受请求的对象,C++中通 过 this 成员变量,Smalltalk 中则通过 self。委托方式为了得到同样的效果,接受请求的对象将自 己传给被委托者(代理人),使被委托的操作可以引用接受请求的对象。 举例来说,可以在窗口类中保存一个矩形类的实例变量来代理矩形类的特定操作,这样窗口 类可以复用矩形类的操作,而不必像继承时那样定义成矩形类的子类。也就是说,一个窗口拥有 三维游戏. 一个矩形,而不是一个窗口就是一个矩形。窗口现在必须显式地将请求转发给它的矩形实例,而 不是像以前它必须继承矩形的操作。 引用名是可选的。委托的主要优点在于它便于运行时刻组合对象操作以及改变这些操作的组 合方式。假定矩形对象和圆对象有相同的类型,只需简单地用圆对象替换矩形对象,得到的窗口. 48. 引 擎 设 计 技 术 及 其 应 用. 就是圆形的。 委托与那些通过对象组合以取得软件灵活性的技术一样,具有如下不足之处:动态的、高度 参数化的软件比静态软件更难以理解。还有运行低效问题,不过从长远来看人的低效才是更主要 的。只有当委托使设计比较简单而不是更复杂时,它才是好的选择。要给出一个能确切告诉你什 么时候可以使用委托的规则是很困难的。因为委托可以得到的效率是与上下文有关的,并且还依 赖于你的经验。委托最适用于符合特定程式的情形,即标准模式的情形。 有一些模式使用了委托。在 State 模式中,一个对象将请求委托给一个描述当前状态的 State 对象来处理。在 Strategy 模式中,一个对象将一个特定的请求委托给一个描述请求执行策略的对 象,一个对象只会有一个状态,但它对不同的请求可以有许多策略。这两个模式的目的都是通过 改变受托对象来改变委托对象的行为。在 Visitor 中,对象结构的每个元素上的操作总是被委托到 Visitor 对象。 其他模式则没有这么多地用到委托。Mediator 引进了一个中介其他对象间通信的对象。有时, Mediator 对象只是简单地将请求转发给其他对象;有时,它沿着指向自己的引用来传递请求,使 用真正意义的委托。Chain of Responsibility 通过将请求沿着对象链传递来处理请求,有时,这个 请求本身带有一个接受请求对象的引用,这时该模式就使用了委托。Bridge 将实现和抽象分离开, 如果抽象和一个特定实现非常匹配,那么这个实现可以代理抽象的操作。委托是对象组合的特例。 它告诉你对象组合作为一个代码复用机制可以替代继承。 3.继承和参数化类型的比较 另一种功能复用技术(并非严格的面向对象技术)是参数化类型,也就是类属或模板。它允 许在定义一个类型时并不指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形 式提供。例如,一个列表类能够以它所包含元素的类型来进行参数化。如果想声明一个 Integer 列表,只需将 Integer 类型作为列表参数化类型的参数值;声明一个 String 列表,只需提供 String.

(8) 类型作为参数值。语言的实现将会为各种元素类型创建相应的列表类模板的定制版本。 参数化类型提供了除类继承和对象组合之外的第三种方法来组合面向对象系统中的行为。许 多设计可以使用这三种技术中的任何一种来实现。实现一个以元素比较操作为可变元的排序例 程,可有如下方法: (1)通过子类实现该操作(Template Method 的一个应用)。 (2)实现为传给排序例程的对象的职责。 (3)作为 C + +模板类的参数,以指定元素比较操作的名称。 这些技术存在着极大的不同之处。对象组合技术允许在运行时刻改变被组合的行为,但是它 存在间接性,比较低效。继承允许提供操作的默认实现,并通过子类重定义这些操作。参数化类 型允许改变类所用到的类型。但是继承和参数化类型都不能在运行时刻改变。哪一种方法最佳, 取决于你设计和实现的约束条件。 参数化类型在像 Smalltalk 那样的编译时刻不进行类型检查的语言中是完全不必要的。 一个面向对象程序运行时刻的结构通常与它的代码结构相差较大。代码结构在编译时刻就被 确定下来了,它由继承关系固定的类组成。而程序的运行时刻结构是由快速变化的通信对象网络. 第. 组成的。事实上两个结构是彼此独立的,试图由一个去理解另一个就好像试图从静态的动植物分. 3 章. 类去理解活生生的生态系统的动态性;反之亦然。 考虑对象聚合和相识的差别以及它们在编译和运行时刻的表示是多么的不同。聚合意味着一 个对象拥有另一个对象或对另一个对象负责,一般我们称一个对象包含另一个对象或者是另一个. 49. 对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期。. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术. 相识意味着一个对象仅仅知道另一个对象。有时相识也被称为“关联”或“引用”关系。相 识的对象可能请求彼此的操作,但是它们不为对方负责。相识是一种比聚合要弱的关系,它只标 识了对象间较松散的耦合关系。可以用普通的箭头线表示相识,尾部带有菱形的箭头线表示聚合, 实例聚合和相识很容易混淆,因为它们通常以相同的方法实现。C++中,聚合可以通过定义表示 真正实例的成员变量来实现,但更通常的是将这些成员变量定义为实例指针或引用;相识也是以 指针或引用来实现的。 从根本上讲,是聚合还是相识是由你的意图而不是由显示的语言机制决定的。尽管它们之间 的区别在编译时刻的结构中很难看出来,但这些区别还是很大的。聚合关系使用较少且比相识关 系更持久;而相识关系则出现频率较高,但有时只存在于一个操作期间,相识也更具动态性,使 得它在源代码中更难被辨别出来。 程序的运行时刻结构和编译时刻结构存在这么大的差别,很明显代码不可能揭示关于系统如 何工作的全部。系统的运行时刻结构更多地受到设计者的影响,而不是编程语言。对象和它们的 类型之间的关系必须更加仔细地设计,因为它们决定了运行时刻程序结构的好坏。 许多设计模式(特别是那些属于对象范围的)显式地记述了编译时刻和运行时刻结构的差别。 Composite 和 Decorator 对于构造复杂的运行时刻结构特别有用。 Observer 也与运行时刻结构有关, 但这些结构对于不了解该模式的人来说是很难理解的。Chain of Responsibility 也产生了继承所无 法展现的通信模式。总之,只有理解了模式,才能清楚代码中的运行时刻结构。 获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求你的系统设计 要能够相应地改进。 为了设计适应这种变化且具有健壮性的系统,必须考虑系统在它的生命周期内会发生怎样的.

(9) 变化。一个不考虑系统变化的设计在将来就有可能需要重新设计。这些变化可能是类的重新定义 和实现,修改客户和重新测试。重新设计会影响软件系统的许多方面,并且未曾料到的变化总是 代价巨大的。 设计模式可以确保系统能以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允 许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某一种特殊变化将更健壮。 下面列出一些导致重新设计的一般原因,以及解决这些问题的设计模式: (1)通过显式地指定一个类来创建对象。在创建对象时指定类名将使你受特定实现的约束 而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。 (2)对特殊操作的依赖。当你为请求指定一个特殊的操作时,完成该请求的方式就固定 下来了。为避免把请求代码写死,你将可以在编译时刻或运行时刻很方便地改变响应请求的 方法。 (3)对硬件和软件平台的依赖。外部的操作系统接口和应用编程接口在不同的软硬件平台 上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新。 三维游戏. 所以设计系统时限制其平台相关性就很重要了。 (4)对对象表示或实现的依赖。知道对象怎样表示、保存、定位或实现的客户在对象发生 变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。 (5)算法依赖。算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对. 50. 引 擎 设 计 技 术 及 其 应 用. 象在算法发生变化时不得不变化,因此有可能发生变化的算法应该被孤立起来。 (6)紧耦合。紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的 系统,要改变或删掉一个类,必须理解和改变其他许多类。这样的系统是一个很难学习、移植和 维护的密集体。 松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设 计模式使用抽象耦合和分层技术来提高系统的松散耦合性。 (7)通过生成子类来扩充功能。通常很难通过定义子类来定制对象。每一个新类都有固定 的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。如重定义一个操作 可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会 导致类爆炸,因为即使对于一个简单的扩充,也不得不引入许多新的子类。 一般的对象组合技术和具体的委托技术是继承之外组合对象行为的另一种灵活方法。新的功 能可以通过以新的方式组合已有对象而不是通过定义已存在类的子类的方式加到应用中去。另一 方面,过多地使用对象组合会使设计难以理解。许多设计模式产生的设计中,可以定义一个子类, 且将它的实例和已存在的实例进行组合来引入定制的功能。 (8)不能方便地对类进行修改。有时不得不改变一个难以修改的类。也许你需要源代码而 又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他 子类。设计模式提供在这些情况下对类进行修改的方法。 这些例子反映了使用设计模式有助于增强软件的灵活性。这种灵活性所具有的重要程度取决 于你将要建造的软件系统。下面来看一看设计模式在开发如下 3 类主要软件中所起的作用:应用 程序、工具箱和框架。 (1)应用程序。 如果将要建造像文档编辑器或电子制表软件这样的应用程序(Application Program),那么它.

(10) 的内部复用性、可维护性和可扩充性是要优先考虑的。内部复用性确保你不会做多余的设计和实 现。设计模式通过减少依赖性来提高内部复用性。松散耦合也增强了一类对象与其他多个对象协 作的可能性。例如,通过孤立和封装每一个操作来消除对特定操作的依赖,可使在不同上下文中 复用一个操作变得更简单。消除对算法和表示的依赖可达到同样的效果。 当设计模式被用来对系统分层和限制对平台的依赖性时,它们还会使一个应用更具可维护 性。通过显示怎样扩展类层次结构和怎样使用对象复用,它们可增强系统的易扩充性。同时,耦 合程度的降低也会增强可扩充性。如果一个类不过多地依赖其他类,扩充这个孤立的类还是很容 易的。 (2)工具箱。 一个应用经常会使用来自一个或多个被称为工具箱的预定义类库中的类。工具箱是一组相关 的、可复用的类的集合,这些类提供了通用的功能。工具箱的一个典型例子就是列表、关联表单、 堆栈等类的集合,C++的 I/O 流库是另一个例子。工具箱并不强制应用采用某个特定的设计,它 们只是为你的应用提供功能上的帮助。工具箱强调的是代码复用,它们是面向对象环境下的“子 程序库”。 工具箱的设计比应用设计要难得多,因为它要求对许多应用是可用的和有效的。再者,工具. 第. 3 章. 箱的设计者并不知道什么应用使用该工具箱及它们有什么特殊需求。这样,避免假设和依赖就变 得很重要,否则会限制工具箱的灵活性,进而影响它的适用性和效率。 (3)框架。. 51. 框架是构成一类特定软件可复用设计的一组相互协作的类。例如,一个框架能帮助建立适合. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术. 不同领域的图形编辑器,如艺术绘画、音乐作曲和机械。另一个框架也许能帮助你建立针对不同 程序设计语言和目标机器的编译器。而再一个也许能帮助你建立财务建模应用。你可以定义框架 抽象类的应用相关的子类,从而将一个框架定制为特定应用。 框架规定了你的应用的体系结构。它定义了整体结构、类和对象的分割、各部分的主要责任、 类和对象怎么协作,以及控制流程。框架预定义了这些设计参数,以便于应用设计者或实现者能 集中精力于应用本身的特定细节。框架记录了其应用领域共同的设计决策,因而框架更强调设计 复用,尽管框架常包括具体的立即可用的子类。 这个层次的复用导致了应用和它所基于的软件之间的反向控制(Inversion of Control)。当使 用工具箱(或传统的子程序库)时,需要写应用软件的主体并且调用想复用的代码。而当使用框 架时,应该复用应用的主体,写主体调用的代码。不得不以特定的名字和调用约定来写操作的实 现,但这会减少需要做出的设计决策。 不仅可以更快地建立应用,而且应用还具有相似的结构。它们很容易维护,且用户看来也更 一致。另一方面,你也失去了一些表现创造性的自由,因为许多设计决策无须你来作出。 如果说应用程序难以设计,那么工具箱就更难了,而框架则是最难的。框架设计者必须冒险 决定一个要适应于该领域的所有应用的体系结构。任何对框架设计的实质性修改都会大大降低框 架所带来的好处,因为框架对应用的最主要贡献在于它所定义的体系结构。因此设计的框架必须 尽可能地灵活、可扩充。 更进一步讲,因为应用的设计如此依赖于框架,所以应用对框架接口的变化是极其敏感的。 当框架演化时,应用不得不随之演化。这使得松散耦合更加重要,否则框架的一个细微变化都将 引起强烈反应。.

(11) 刚才讨论的主要设计问题对框架设计而言最具重要性。一个使用设计模式的框架比不用设计 模式的框架更可能获得高层次的设计复用和代码复用。成熟的框架通常使用了多种设计模式。设 计模式有助于获得无须重新设计即可适用于多种应用的框架体系结构。 当框架和它所使用的设计模式一起写入文档时,我们可以得到另外一个好处。了解设计模式 的人能较快地洞悉框架,甚至不了解设计模式的人也可以从产生框架文档的结构中受益。加强文 档工作对于所有软件而言都是重要的,但对于框架其重要性显得尤为突出。学会使用框架常常是 一个必须克服很多困难的过程。设计模式虽然无法彻底克服这些困难,但它通过对框架设计的主 要元素做更显式的说明可以降低框架学习的难度。 因为模式和框架有些类似,人们常常对它们有怎样的区别和它们是否有区别感到疑惑。它们 最主要的不同在于如下 3 个方面: (1)设计模式比框架更抽象。框架能够用代码表示,而设计模式只有其实例才能表示为代 码。框架的威力在于它们能够使用程序设计语言写出来,它们不仅能被学习,也能被直接执行和 复用。而本书中的设计模式在每一次被复用时,都需要被实现。设计模式还解释了它的意图、权 三维游戏. 衡和设计效果。 (2)设计模式是比框架更小的体系结构元素。一个典型的框架包括了多个设计模式,而反 之绝非如此。 (3)框架比设计模式更加特例化。框架总是针对一个特定的应用领域。一个图形编辑器框. 52. 引 擎 设 计 技 术 及 其 应 用. 架可能被用于一个工厂模拟,但它不会被错认为是一个模拟框架。而本书收录的设计模式几乎能 被用于任何应用。当然比我们的模式更特殊的设计模式也是可能的(如分布式系统和并发程序的 设计模式),尽管这些模式不会像框架那样描述应用的体系结构。 框架变得越来越普遍和重要。它们是面向对象系统获得最大复用的方式。较大的面向对象应用 将会由多层彼此合作的框架组成。应用的大部分设计和代码将来自于它所使用的框架或受其影响。 3.1.3 游戏引擎中所用的设计模式 RPG 游戏框架采用到常用的单件、对象工厂等模式。这些模式主要体现在游戏框架层次接口 规范、对象创建、对象管理以及相关功能调用上。早期的 RPG 游戏采用面向过程的方法为主要 设计和实现办法。当游戏规模达到一定级别后,游戏框架的各个模块就会出现命名重复、接口功 能难以管理、全局对象过多等致命的问题。采用面向对象的设计方法并辅助采用部分简单的设计 模式可以基本解决这类问题。 1.单件模式 对于整个游戏中只有一个实例存在的情况,使用单件模式,管理者就是这种模式的产物。该 单件模式图描述了单件类(Singlelon)的组成,其中包括实例接口 histance、单件操作接口 singletonOperationo 和单件、数据提取接口 GetsingletonData,实例句柄存储如 queInstance 和单件 的数据集合 singletonData,任何对单件的应用都是先通过 Instance 获实例后再进行类本身的方法 调用。singleton 模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。 这就提出了一个问题:如何绕过常规的构造器提供一种机制来保证一个类只有一个实例?客 户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是 类设计者的责任,而不是类使用者的责任。从另一个角度来说,singleton 模式其实也是一种职责 型模式。因为创建了一个对象,这个对象扮演了独一无二的角色,在这个单独的对象实例中,它.

(12) 集中了它所属类的所有权力,同时它也肩负了行使这种权力的职责。 2.对象工厂模式 对象工厂用于灵活的对象层次的实例创建。想要创建的对象类型,它包含了自己的操作, MyDocument 是从 Document 中派生的类型,My Application 是用户自己的对象创建器,而 Application 则负有创建 Document 的责任,它便是对象工厂,通过对 createDocument 的调用,它 创建了 MyDocument 的实例,而每一个 MyApplication 必须注册自己的对象类型的创建函数到 Application 中去。 在游戏系统中,经常面临着“一系列相互依赖的对象”的创建工作,同时由于需求的变 化,往往存在着更多系列对象的创建工作。如何应对这种变化?如何绕过常规的对象的创建 方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的 紧耦合? 在以下情况下应当考虑使用对象工厂模式:游戏系统不应当依赖于产品类实例如何被创建、 组合和表达的细节,这对于所有形态的工厂模式都是重要的。游戏系统有多于一个的产品族,而 游戏系统只消费其中某一产品族。同属于同一个产品族的产品是在一起使用的,这一约束必须在. 第. 系统的设计中体现出来。游戏系统提供一个产品类的库,所有的产品以同样的接口出现,从而使. 3 章. 客户端不依赖于实例。 3.观察者模式 观测者模式用于对象之间的信息通知。在逻辑游戏世界构建过程中,需要为某些对象建立一. 53. 种“通知依赖关系”,—个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象). 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术. 都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。使用观察者模式, 可以将这种依赖关系弱化,并形成一种稳定的依赖关系,从而实现软件体系结构的松耦合。 适用性:当一个抽象模型有两个方面,其中一个方面依赖于另一方面,将这二者封装在独立 的对象中以使它们可以各自独立地改变和复用;当对一个对象的改变需要同时改变其他对象,而 不知道具体有多少对象有待改变;当一个对象必须通知其他对象,而它又不能假定其他对象是谁。 换言之,不希望这些对象是紧密耦合的。 通过观察者模式,把一对多对象之间的通知依赖关系变得更为松散,大大地提高了程序的可 维护性和可扩展性,也很好地符合了开放—封闭原则。 4.命令模式 命令模式提供很好的过程框架。Command 定义一个 Excute 的方法框架,派生自 Command 的具体的 PasteCommand 实现 Excute 方法,实际的执行是通过对命令的执行方法进行动态绑定而 执行 PasteCommand 的方法。 在主程序中,是通过 Command 的对象指针调用 Execute,所以 PasteCommand 无法绕过这一 限制,这就体现了过程框架的意义。 在游戏软件系统中, “行为请求者”与“行为实现者”通常呈现一种“紧耦合” 。但在某些场 合,比如要对行为进行“记录、撤消/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适 的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦,将一组行为抽象为对象,可 以实现二者之间的松耦合?这就需要命令模式。 将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录请 求日志,以及支持可撤消的操作。.

(13) 效果及实现要点:Command 模式的根本目的在于将“行为请求者”与“行为实现者”解耦, 在面向对象语言中,常见的实现手段是“将行为抽象为对象”,实现命令接口的具体命令对象有 时候根据需要可能会保存一些额外的状态信息。 通过使用 composite 模式,可以将多个命令封装为一个“复合命令”(Macrocommand)。 Command 模式与 C#中的 Delegate 有些类似。但两者定义行为接口的规范有所区别:命令以面向 对象中的“接口—实现”来定义行为接口规范,更严格、更符合抽象原则;Delegate 以函数签名 来定义行为接口规范,更灵活,但抽象能力比较弱。 使用命令模式会导致某些系统有过多的具体命令类。某些系统可能需要几十个、几百个,甚 至几千个具体命令类,这会使命令模式在这样的系统里变得不实际。 适用性:使用命令模式作为在面向对象系统中的替代。call back 讲的便是先将一个函数登记 上,然后在以后调用此函数。需要在不同的时间指定请求、将请求排队。一个命令对象和原先的 请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身 仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以 三维游戏. 在串形化之后传送到另外一台机器上去。系统需要支持命令的撤消(Undo)。命令对象可以把状 态存储起来,等到客户端需要撤消命令所产生的效果时,可以调用 Undo 方法把命令所产生的效 果撤消掉。命令对象还可以提供 Redo 方法,以供客户端在需要时再重新实施命令效果。如果一 个系统要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志读回所有的数据. 54. 引 擎 设 计 技 术 及 其 应 用. 更新命令,重新调用 Execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更 新。 5.模板方法(TemplateMethod) TemplateMethod 模式是比较简单的设计模式之一,但它却是代码复用的一项基本的技术,在 类库中尤其重要。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 实现要点:TemplateMethod 模式是一种非常基础性的设计模式,在面向对象系统中有着大量 的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代 码复用方面的基本实现结构。除了可以灵活应对子步骤的变化外, “不用调用我,让我来调用你” 的反向控制结构是 TemplateMethod 的典型应用。在具体实现方面,被 TemplateMethod 调用的虚 方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为 Protected 方法。 适用性:采用模板方法模式可以一次性实现一个算法的不变的部分,并将可变的行为留给子 类来实现。各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是 Opdyke 和 Johnson 所描述过的“重分解以一般化”的一个很好的例子。首先识别现有代码中的不 同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这 些不同的代码。模板方法只在特定点调用 Hok 操作,这样就只允许在这些点进行扩展。 6.迭代器模式(Iterator) 在面向对象的游戏软件设计中,经常会遇到一类集合对象,这类集合对象的内部结构可能有 着各种各样的实现,但是归结起来有两点需要我们去关心: (1)集合内部的数据存储结构。 (2)遍历集合内部的数据。面向对象设计原则中有一条是类的单一职责原则,所以要尽可.

(14) 能地去分解这些职责,用不同的类去承担不同的职责。Iterator 模式就是分离了集合对象的遍历行 为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明 地访问集合内部的数据。 意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内 部表示。 效果及实现要点:迭代抽象,访问一个聚合对象的内容而无需暴露它的内部表示。迭代多态, 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操 作。迭代器的健壮性考虑,遍历的同时更改迭代器所在的集合结构会导致问题。 适用性:访问一个聚合对象的内容而无需暴露它的内部表示。支持对聚合对象的多种遍历。 为遍历不同的聚合结构提供一个统一的接口,即支持多态迭代。 Iterator 模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到 不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。. 3.2. STL 使用基础. 第. 3 章 C++标准模板库(Standard Template Library,STL)是 C++语言的重要组成部分,提供了多 个方面的特性,为游戏开发者提供了很多方便,所以本节将抛砖引玉地讲述 STL 的使用基础,概 述 STL 最基础、最常用的使用方法。如果读者对 STL 感兴趣并希望深入了解的话,可以参考相. 55. 关的书,可以是像《C++ primer》这样权威的 C++参考书,也可以是专门讲述 STL 的书籍。在游. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术. 戏开发中,一个提高开发效率的最有效的方法就是使用模板库。 3.2.1 标准模板库简介 C++标准模板库存在名字空间 std 中,用一组头文件的方式呈现。这些文件表明了这个库的 重要组成部分。在 C++标准模板库中主要有以下几个类型的库: (1)容器。能保存其他对象。许多计算机都涉及到建立各种对象的汇集,以及对这些汇集 的操作。逐个字符地读进一个字符串、逐个字符串地将该串打印出来都是这种工作的实际例子。 一个以保存一批对象为主要用途的类称为容器。C++标准库中将包括如下容器:  <vector>:T 的一维数组。 . <list>:T 的双向链表。. . <deque>:T 的双端队列。. . <stack>:T 的堆栈。. . <map>:T 的关联数组。. . <set>:T 的集合。. . <multimap>:关联数组。. . <multiset>:集合,值可以重复出现。. (2)迭代器。提供一种机制,使标准算法能通用于标准容器和类型。迭代器是一种处于容 器中的元素序列的非常一般而有用的概念。C++标准库中包括迭代器:<iterator>,即迭代器及迭 代器支持。 (3)算法。标准常用的算法,主要包括非修改性的序列操作、修改性的序列操作、序列操.

(15) 作、序列排序、集合算法、堆操作、最大和最小算法、排列等算法。主要存在以下算法库:  <algorithm>:通用算法 . <stdlib>:bsearch()和 qsort()。. 对于以上模板库的内容,考虑到本书不是主要讲述 C++的书籍,所以本书主要讲述容器和 迭代器的使用方法。 3.2.2 容器和迭代器使用基础 在本书中容器方面的使用讲述以下 4 个库:<vector>、<list>、<deque>、<stack>,因为这是 游戏编程中最常用的库。 1.Vector(向量) 标准的 vector 是定义在名字空间 std 中的一个模板,由<vector>给出。该模板是一个动态的数 组,将提供快速访问数组中的任何一个元素,并可以实现排序功能。其定义如下: template < class Type, class Allocator = allocator<Type> > class vector 三维游戏 56. 引 擎 设 计 技 术 及 其 应 用. 变量的定义说明如下: Type:存储在向量中的数据。 Allocator:存储分配器对象,用以了解向量的分配和释放内存情况。这个变量是可选的,其 默认值是 allocator<Type>。 该函数将可以提供在数组末端插入或删除元素的功能。这个容器善于在数组的开始和末端删 除元素。 其包括如下操作方法: (1)构造函数:进行模板类的构造和初始化。由于 vector 具有多个构造函数,其声明如下: vector(); explicit vector( const Allocator& _Al ); explicit vector( size_type _Count ); vector( size_type _Count, const Type& _Val ); vector( size_type _Count, const Type& _Val, const Allocator& _Al ); vector( const _vector& _Right ); template<class InputIterator> vector( InputIterator _First,.

(16) InputIterator _Last ); template<class InputIterator> vector( InputIterator _First, InputIterator _Last, const Allocator& _Al );. 其变量的定义为: _Al:分配器用于该对象。get_allocator 将返回分配器对象。 _Count:向量的初始长度。 _Val:向量数组的初始值。 _Right:通过拷贝构造向量。 _First:拷贝的第一个值的位置。 _Last:拷贝的最后一个值的位置。 现给出该函数应用的一个实例: #include <vector> #include <iostream> int main() { using namespace std; vector <int>::iterator v1_Iter, v2_Iter, v3_Iter, v4_Iter, v5_Iter; // 创建一个空向量 v0 vector <int> v0; // 创建一个有 3 个元素的向量 v1,其元素的默认值为 0 vector <int> v1( 3 ); //创建一个有 5 个值的向量 v2,其初始值为 2 vector <int> v2( 5, 2); //创建一个有 3 个值的向量 v3,其初始值为 1 和从分配器中获得向量 v2 vector <int> v3( 3, 1, v2.get_allocator() ); // 拷贝向量 v2 到 v4 vector <int> v4( v2 ); //拷贝向量 v4 的第一个元素到第三个元素到向量 v5 vector <int> v5( v4.begin() + 1, v4.begin() + 3 ); cout << "v1 =" ; for ( v1_Iter = v1.begin() ; v1_Iter != v1.end() ; v1_Iter++ ) cout << " " << *v1_Iter; cout << endl; cout << "v2 =" ;. 第. 3 章. 57. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(17) for ( v2_Iter = v2.begin() ; v2_Iter != v2.end() ; v2_Iter++ ) cout << " " << *v2_Iter; cout << endl; cout << "v3 =" ; for ( v3_Iter = v3.begin() ; v3_Iter != v3.end() ; v3_Iter++ ) cout << " " << *v3_Iter; cout << endl; cout << "v4 =" ; for ( v4_Iter = v4.begin() ; v4_Iter != v4.end() ; v4_Iter++ ) cout << " " << *v4_Iter; cout << endl;. 三维游戏 58. 引 擎 设 计 技 术 及 其 应 用. cout << "v5 ="; for ( v5_Iter = v5.begin() ; v5_Iter != v5.end() ; v5_Iter++ ) cout << " " << *v5_Iter; cout << endl; }. 最后输出结果为: v1 v2 v3 v4 v5. = = = = =. 0 2 1 2 2. 0 2 1 2 2. 0 2 2 2 1 2 2 2. 分配函数:进行向量的原有值的擦除,并分配现有值,其声明如下: void assign( size_type _Count, const Type& _Val ); template<class InputIterator> void assign( InputIterator _First, InputIterator _Last );. 其变量的定义为: _First:第一个要拷贝的元素的位置。 _Last:最后一个要拷贝的元素的位置。 _Count:插入这个向量的元素个数。 _Val:插入这个向量的元素的值。 现给出该函数应用的一个实例: #include <vector> #include <iostream> int main() { using namespace std;.

(18) vector <int> v1; vector <int>::iterator Iter; v1.push_back( v1.push_back( v1.push_back( v1.push_back( v1.push_back(. 10 20 30 40 50. ); ); ); ); );. cout << "v1 = " ; for ( Iter = v1.begin(); Iter != v1.end(); Iter++ ) cout << *Iter << " "; cout << endl; v1.assign( v1.begin() + 1, v1.begin() + 3 ); cout << "v1 = "; for ( Iter = v1.begin() ; Iter != v1.end() ; Iter++ ) cout << *Iter << " "; cout << endl; v1.assign( 7, 4 ) ; cout << "v1 = "; for ( Iter = v1.begin() ; Iter != v1.end() ; Iter++ ) cout << *Iter << " "; cout << endl; }. 该程序输出结果为: v1 = 10 20 30 40 50 v1 = 20 30 v1 = 4 4 4 4 4 4 4. 擦除函数:在指定位置删除元素,其声明如下: iterator erase( iterator _Where ); iterator erase( iterator _First, iterator _Last );. 其变量的定义为: _Where:指定被删除元素的位置。 _First:第一个删除元素之前的那个元素的位置。 _Last:最后一个被删除元素的位置。 现给出该函数应用的一个实例: #include <vector> #include <iostream> int main(). 第. 3 章. 59. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(19) { using namespace std; vector <int> v1; vector <int>::iterator Iter; v1.push_back( 10 ); v1.push_back( 20 ); v1.push_back( 30 ); v1.push_back( 40 ); v1.push_back( 50 ); cout << "v1 =" ; for ( Iter = v1.begin() ; Iter != v1.end() ; Iter++ ) cout << " " << *Iter; cout << endl;. 三维游戏. v1.erase( v1.begin() ); cout << "v1 ="; for ( Iter = v1.begin() ; Iter != v1.end() ; Iter++ ) cout << " " << *Iter; cout << endl; v1.erase( v1.begin() + 1, v1.begin() + 3 ); cout << "v1 ="; for ( Iter = v1.begin() ; Iter != v1.end() ; Iter++ ) cout << " " << *Iter; cout << endl;. 60. 引 擎 设 计 技 术 及 其 应 用. }. 输出结果为: v1 = 10 20 30 40 50 v1 = 20 30 40 50 v1 = 20 50. 通过上面的实例对向量的使用基本已经有一个相当的认识,表 3-1 给出向量使用的方法函数。 表 3-1 向量的方法函数 方法名. 含义. assign. 擦除向量,并指定元素到空向量. at. 返回向量指定位置的地址的值. back. 返回向量末端的地址的值. begin. 返回指向第一个元素随机进行入迭代器. capacity. 向量现有占用的内存. clear. 擦除向量中的所有元素. empty. 测试向量容器是否为空. end. 返回指向最后一个元素随机进行入迭代器. erase. 在指定位置删除元素. front. 返回向量首端的地址的值. get_allocator. 返回一个分配器类的对象. insert. 在指定位置插入一个元素或一定长度的元素.

(20) 续表 方法名. 含义. max_size. 返回向量的最大长度. pop_back. 删除向量的末尾的元素. push_back. 在向量的末端添加元素. rbegin. 返回指向翻转向量的第一个元素的迭代器. rend. 返回指向翻转向量的最后一个元素的迭代器. resize. 指明新向量的值. reserve. 向量对象的最小内存的长度. size. 返回向量元素的个数. swap. 交换两个向量的元素. vector. 向量的构造函数. 2.链表(list) STL 链表类是一个容器模板类,可以有效地实现任意位置的元素的插入和删除。该容器是以 双向链表数据结构存储元素,其定义如下: template < class Type, class Allocator=allocator<Type> > class list. 变量定义如下: Type:存储在链表中的数据的类型。 Allocator:存储分配器对象,用以了解链表的分配和释放内存情况。这个变量是可选的,其 默认值是 allocator<Type>。 构造函数:进行模板类的构造和初始化,由于 list 具有多个构造函数,其声明如下: list(); explicit list( const Allocator& _Al ); explicit list( size_type _Count ); list( size_type _Count, const Type& _Val ); list( size_type _Count, const Type& _Val, const Allocator& _Al ); list( const _list& _Right. 第. 3 章. 61. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(21) ); template<class InputIterator> list( InputIterator _First, InputIterator _Last ); template<class InputIterator > list( InputIterator _First, InputIterator _Last, const Allocator& _Al );. 其变量的定义为: _Al:分配器用于该对象,get_allocator 将返回分配器对象。 _Count:链表的初始长度。 三维游戏. _Val:链表的初始值。 _Right:通过拷贝构造链表。 _First:拷贝的第一个值的位置。 _Last:拷贝的最后一个值的位置。. 62. 下面给出该函数的一个使用实例:. 引 擎 设 计 技 术 及 其 应 用. #include <list> #include <iostream> int main() { using namespace std; list <int>::iterator c1_Iter, c2_Iter, c3_Iter, c4_Iter, c5_Iter, c6_Iter; // 创建一个新链表 c0 list <int> c0; //创建一个新链表 c1,该链表有 3 个元素,并赋予其初始值为默认值 0 list <int> c1( 3 ); //创建一个新链表 c2,该链表有 5 个元素,并赋予其初始值为 2 list <int> c2( 5, 2 ); //创建一个新链表 c3,该链表有 3 个元素,并赋予其初始值为 1 和 c2 的分配器 list <int> c3( 3, 1, c2.get_allocator() ); // 创建 c2 的拷贝链表 c4 list <int> c4(c2); //创建 c4 的第一个元素到第二个元素的拷贝链表 c5 c4_Iter = c4.begin(); c4_Iter++; c4_Iter++;.

(22) list <int> c5( c4.begin(), c4_Iter ); // 创建 c4 的第一个元素到第三个元素的拷贝链表 c6,并使用相同的分配器 c4_Iter = c4.begin(); c4_Iter++; c4_Iter++; c4_Iter++; list <int> c6( c4.begin(), c4_Iter, c2.get_allocator() ); cout << "c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl; cout << "c2 ="; for ( c2_Iter = c2.begin(); c2_Iter != c2.end(); c2_Iter++ ) cout << " " << *c2_Iter; cout << endl;. 第. 3 章 cout << "c3 ="; for ( c3_Iter = c3.begin(); c3_Iter != c3.end(); c3_Iter++ ) cout << " " << *c3_Iter; cout << endl; cout << "c4 ="; for ( c4_Iter = c4.begin(); c4_Iter != c4.end(); c4_Iter++ ) cout << " " << *c4_Iter; cout << endl; cout << "c5 ="; for ( c5_Iter = c5.begin(); c5_Iter != c5.end(); c5_Iter++ ) cout << " " << *c5_Iter; cout << endl; cout << "c6 ="; for ( c6_Iter = c6.begin(); c6_Iter != c6.end(); c6_Iter++ ) cout << " " << *c6_Iter; cout << endl; }. 输出结果为: c1 c2 c3 c4 c5 c6. = = = = = =. 0 2 1 2 2 2. 0 2 1 2 2 2. 0 2 2 2 1 2 2 2 2. list::sort()函数:用以对链表里的元素进行排序,其函数声明如下: void sort();. 63. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(23) template<class Traits> void sort( Traits _Comp );. 变量_Comp 是用以比较的运算符。 下面是该函数使用的一个实例: #include <list> #include <iostream> int main() { using namespace std; list <int> c1; list <int>::iterator c1_Iter;. 三维游戏. c1.push_back( 20 ); c1.push_back( 10 ); c1.push_back( 30 ); cout << "Before sorting: c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl;. 64. 引 擎 设 计 技 术 及 其 应 用. c1.sort(); cout << "After sorting c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl; c1.sort( greater<int>() ); cout << "After sorting with 'greater than' operation, c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl; }. 最后的输出结果如下: Before sorting: c1 = 20 10 30 After sorting c1 = 10 20 30 After sorting with 'greater than' operation, c1 = 30 20 10. list::splice:移除链表中的元素,并插入另一个链表中,下面是其函数实现的实例: void splice( iterator _Where, list<Allocator>& _Right ); void splice( iterator _Where, list<Allocator>& _Right,.

(24) iterator _First ); void splice( iterator _Where, list<Allocator>& & _Right, iterator _First, iterator _Last );. 变量定义如下: _Where:指定被删除元素的位置。 _First:第一个删除元素之前的那个元素的位置。 _Last:最后一个被删除元素的位置。 _Where:目标链表需要插入元素的位置。 _Right:需要插入目标链表的源链表。 _First:确定要插入目标链表的左边的范围。 _Last:确定要插入目标链表的右边的范围。. 第. 3 章. 下面是该函数的实例: #include <list> #include <iostream>. 65 int main() { using namespace std; list <int> c1, c2, c3, c4; list <int>::iterator c1_Iter, c2_Iter, w_Iter, f_Iter, l_Iter; c1.push_back( c1.push_back( c2.push_back( c2.push_back( c2.push_back( c3.push_back( c3.push_back( c4.push_back( c4.push_back( c4.push_back(. 10 11 12 20 21 30 31 40 41 42. ); ); ); ); ); ); ); ); ); );. cout << "c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl; cout << "c2 ="; for ( c2_Iter = c2.begin(); c2_Iter != c2.end(); c2_Iter++ ) cout << " " << *c2_Iter; cout << endl;. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(25) w_Iter = c2.begin(); w_Iter++; c2.splice( w_Iter,c1 ); cout << "After splicing c1 into c2: c2 ="; for ( c2_Iter = c2.begin(); c2_Iter != c2.end(); c2_Iter++ ) cout << " " << *c2_Iter; cout << endl; f_Iter = c3.begin(); c2.splice( w_Iter,c3, f_Iter ); cout << "After splicing the first element of c3 into c2: c2 ="; for ( c2_Iter = c2.begin(); c2_Iter != c2.end(); c2_Iter++ ) cout << " " << *c2_Iter; cout << endl;. 三维游戏 66. 引 擎 设 计 技 术 及 其 应 用. f_Iter = c4.begin(); l_Iter = c4.end(); l_Iter--; c2.splice( w_Iter,c4, f_Iter, l_Iter ); cout << "After splicing a range of c4 into c2: c2 ="; for ( c2_Iter = c2.begin(); c2_Iter != c2.end(); c2_Iter++ ) cout << " " << *c2_Iter; cout << endl; cout << "c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl; }. 输出结果如下: c1 = 10 11 c2 = 12 20 21 After splicing c1 into c2: c2 = 12 10 11 20 21 After splicing the first element of c3 into c2: c2 = 12 10 11 30 20 21 After splicing a range of c4 into c2: c2 = 12 10 11 30 40 41 20 21 c1 =. list::unique 函数:可以移除链表中相邻的同一个元素或者满足其他谓词约束的元素,该函数 的声明如下: void unique(); template<class BinaryPredicate> void unique( BinaryPredicate _Pred );. 变量_Pred 为二值的谓词条件。 下面给出该函数的一个实例: #include <list> #include <iostream> int main().

(26) { using namespace std; list <int> c1; list <int>::iterator c1_Iter, c2_Iter,c3_Iter; not_equal_to<int> mypred; c1.push_back( c1.push_back( c1.push_back( c1.push_back( c1.push_back( c1.push_back(. -10 ); 10 ); 10 ); 20 ); 20 ); -10 );. cout << "The initial list is c1 ="; for ( c1_Iter = c1.begin(); c1_Iter != c1.end(); c1_Iter++ ) cout << " " << *c1_Iter; cout << endl; list <int> c2 = c1; c2.unique(); cout << "After removing successive duplicate elements, c2 ="; for ( c2_Iter = c2.begin(); c2_Iter != c2.end(); c2_Iter++ ) cout << " " << *c2_Iter; cout << endl; list <int> c3 = c2; c3.unique( mypred ); cout << "After removing successive unequal elements, c3 ="; for ( c3_Iter = c3.begin(); c3_Iter != c3.end(); c3_Iter++ ) cout << " " << *c3_Iter; cout << endl; }. 输出结果如下: The initial list is c1 = -10 10 10 20 20 -10 After removing successive duplicate elements, c2 = -10 10 20 -10 After removing successive unequal elements, c3 = -10 -10. 通过上面的介绍,应该对链表模板类有了一个了解,表 3-2 给出了其操作方法。 表 3-2 链表模板类的方法 方法名. 含义. assign. 擦除链表,并指定到元素到空链表. back. 返回链表末端的地址的值. begin. 返回指向链表第一个元素随机进行入迭代器. clear. 擦除链表中所有元素. empty. 测试链表容器是否为空. end. 返回指向链表最后一个元素随机进行入迭代器. erase. 在链表指定位置删除元素. 第. 3 章. 67. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(27) 续表 方法名. 三维游戏 68. 引 擎 设 计 技 术 及 其 应 用. 含义. front. 返回链表首端的地址的值. get_allocator. 返回一个分配器类的对象. insert. 在链表指定位置插入一个元素或一定长度的元素. list. 构造链表. max_size. 返回链表的最大长度. merge. 移除源链表中的元素,插入新链表,并以升序排列新链表中的数据. pop_back. 删除链表的末尾的元素. pop_front. 删除链表的第一个元素. push_back. 在链表的末端添加元素. push_front. 在链表的前端添加元素. rbegin. 返回指向翻转链表的第一个元素的迭代器. remove. 通过匹配一个指定的值擦除链表中的一个元素. remove_if. 通过一个指定的规则擦除链表中的元素. rend. 返回指向翻转链表的最后一个元素的迭代器. resize. 指明新链表的值. reserve. 链表对象的最小内存的长度. size. 返回链表元素的个数. sort. 按照指定的方式进行链表的排序,默认的方式是升序排列. splice. 移除链表中的元素,并插入另一个链表中. 3.双向队列(Deque) STL 双向队列类是一个容器模板类,可以有效地实现任意位置的元素的插入和删除。该容器 类似于向量,但是比向量更能支持任一地方的插入和删除。该类的定义如下: template < class Type, class Allocator=allocator<Type> > class deque. 变量定义如下: Type:存储在队列中的数据的类型。 Allocator:存储分配器对象,用以了解队列的分配和释放内存情况。这个变量是可选的,其 默认值是 allocator<Type>。 现将其操作方法及其含义列于表 3-3 之中。 表 3-3 链表模板类的方法 方法名. 含义. assign. 擦除队列,并指定元素到空队列. at. 返回队列指定位置的地址的值. back. 返回队列末端的地址的值.

(28) 续表 方法名. 含义. begin. 返回指向第一个元素随机进行入迭代器. clear. 擦除队列中的所有元素. deque. 队列的构造函数. empty. 测试队列容器是否为空. end. 返回指向最后一个元素随机进行入迭代器. erase. 在指定位置删除元素. front. 返回队列首端的地址的值. get_allocator. 返回一个分配器类的对象. insert. 在指定位置插入一个元素或一定长度的元素. max_size. 返回队列的最大长度. pop_back. 删除队列的末尾的元素. pop_front. 删除队列的前端的元素. 第. push_back. 在队列的末端添加元素. 3 章. push_front. 在队列之前添加元素. rbegin. 返回指向翻转队列的第一个元素的迭代器. rend. 返回指向翻转队列的最后一个元素的迭代器. resize. 指明新队列的值. size. 返回队列元素的个数. swap. 交换两个队列的元素. 本章小结 设计模式和 STL 都属于面向对象编程中的高级内容,涉及的方面也非常广,是非常深奥的技 术,本章只作一个简介性质的讲解,如果读者对这方面比较感兴趣,可以钻研更深奥的内容。在 讲完本章之后将讲述游戏中的几何基础。. 69. 游 戏 引 擎 编 程 中 的 高 级 面 向 对 象 技 术.

(29) 第4章 三维游戏引擎中的几何基础. 主要内容:本章主要介绍地形生成过程中所涉及的几何基础及相 关技术,首先介绍计算机图形学中所涉及的各种坐标系,然后再介绍 通用的几何变换、地形渲染流程和光照纹理等相关技术。 本章重点:  向量及其运算 . 矩阵及矩阵操作. . 3 种坐标系. . 几何变换. . 四元数.

(30) 4.1. 向量及其运算. 4.1.1 向量的定义 向量(Vector)也称为矢量,简单来说,就是在给定的坐标系(Coordinate System)中描述位 置或方向的一组数。在 3D 图形学中,这个坐标系往往是笛卡儿坐标系(Cartesian Coordinates), 比如,一个向量可以用(4,5,3)来表示,如图 4-1 所示。向量不同于标量,标量仅有大小,而向量 既有大小又有方向。具体在 3D 图形学中,向量可以用来表示粒子的速度、加速度和光线方向等 既有大小又有方向的量。总之,向量为在三维空间中表示方向提供了方便。 Y. Y (5,3). 第. (4,5,3) X. X. 2D 坐标系 中的向量 Z. 3D 坐标系 中的向量. 在 D3DX 库中,用 D3DXVECTOR3 来表示 3D 空间中的三维向量,它是从 D3DVECTOR 继 承过来的,D3DVECTOR 结构体定义如以下代码所示: typedef struct D3DVECTOR { float x, y, z; } D3DVECTOR, *LPD3DVECTOR;. D3DVECTOR 含有 3 个浮点型变量,分别表示三维向量的一个分量。D3DX 库中对 D3DVECTOR 的派生类 D3DXVECTOR3 的定义如下: typedef struct D3DXVECTOR3 : public D3DVECTOR { public: // 构造函数 D3DXVECTOR3() {}; D3DXVECTOR3( CONST FLOAT * ); D3DXVECTOR3( CONST D3DVECTOR& ); D3DXVECTOR3( CONST D3DXFLOAT16 * ); D3DXVECTOR3( FLOAT x, FLOAT y, FLOAT z ); // 类型转换函数 operator FLOAT* (); operator CONST FLOAT* () const; // 赋值操作符 D3DXVECTOR3& operator += ( CONST D3DXVECTOR3& ); D3DXVECTOR3& operator -= ( CONST D3DXVECTOR3& );. 71. 三维游戏引擎中的几何基础. 图 4-1 2D 向量和 3D 向量. 4 章.

參考文獻

相關文件

評定量表 (rating scale) :指用以評定等級的工具,按評定結果可以看出學生 在某種特質上的等級,當中有各種形式如數字評定量表 (numerical rating scal e) 、圖示評定量表

对于二元函数的自变量,我

通常把这种过去时期的,具有滞后作用的变量 叫做 滞后变量(Lagged Variable) ,含有滞后变量

[r]

衡量一个地区或一个国家水资源的丰歉 程度的指标:

微积分的创立是数学发展中的里程碑, 它的发展 和广泛应用开启了向近代数学过渡的新时期, 为研究 变量和函数提供了重要的方法和手段. 运动物体的瞬

业技术”模块是在“技术与设计 1” “技术与设计 2”必修模块学完之后的一 个选修模块,它包括“绿色食品” “种质资源的保护和引进” “无土栽培” “营 养与饲料”

本章我们又一次经历了用函数研究变化规律的过程 ,用反比例函数刻画具 有反比例关系的两个变量之间的对应关系 :在变量y 随变量x 的变化而变化的