游戏开发基础 游戏开发基础

lufan0820

贡献于2013-12-31

字数:0 关键词: 游戏开发

游戏开发基础(1) 第一节. 概述 随着计算机技术的发展,计算机从高技术的神坛上走下来,走进我们的家庭, 走进我们的娱乐生活中。在家用计算机的使用中,作为游戏机的使用率是很高的。 同时对于游戏迷来说现有的游戏中总有很多不足之处,让人想一试身手作出一套更 好的游戏(不过,这可不容易)。作为和亲爱的用户您一样的游戏迷,我也有相同 的想法。这样便产生了这套游戏编程基础的教学软件。 这套软件主要是面对有一定编程基础的用户,但考虑到很多游戏迷在 Windows 编程方面还是个新手,所以这套软件中还增添了一章“Windows 编程基 础”。在基于 Windows 的游戏大部分都是支持 Directx 的,故我用了较大篇副讨论 了 Directx 5.0,并使用基于 Directx 5.0 的例程来讲解游戏编程的思想和实现。由于, Directx 6.0 中的很多在功能只支持 VC,而且在诸多高级语言中 C 是最适合游戏编 程的,所以这套软件中的例程及讲解都是基于 VC 的。相信,用户通过这套软件的学 习将能有信心编出使自己满意的游戏来。 自己满意的游戏是否是大部分人都喜欢的呢?是否能成为市场上成功的游戏 呢?为此在本章的下面几节中我们将讨论一些非技术的关于游戏总体设计的话题。 第二节 电脑游戏的发展史 60 年代早期 使用最原始的大型计算机,一小部分程序员开发游戏而其他人认为他们在做 研究。在 MIT 的实验室里开发了 Space War 和其它早期的游戏。 1973 年到 1975 年 Nolan Bushnell 建立了 Syxygy。在市场上出售 SpaceWar 的“Arcade”版,但 失败了。在 1975 年下半年,推出了 Pong,并使之成为一个非常流行的游戏,之 后,他重新命名公司为 Atarh 他们始创了我们今天所熟知的游戏工业。 1960 年后期到 1970 年早期 更多的程序员使用大型计算机开发游戏;WilliamCrowther 开发了流行游戏 Adventure。 1976 年 Rushnell 以$2,600 万将 Atari 出售给 WarnerCommunications。 1980 年其它游戏系统依次登台,象 Pllillins 的 Oddessy 和 Mattel 的 Intellivision,目标都是希望取代 Atari。一些小的个人计算机开始出现,但视频游 戏仍处于领先位置。 1977 年 Atari 推出了 Atari2600VCS,家庭视频游戏行业兴起!一个名叫 Apple 的小公 司出售一种计算机 Apple l,但没能引起人们的注意。 1979 年 Atari 公司的一些项尖的程序开发员从对 Atari 的意见不一致发展到公开的不 满,成立了自己的公司 Activision,其它“第三方”开发公司相应出现。 1981 年 IBM 推出了 IBM PC。 1982 年 Atari 开始滑坡,视频游戏市场处于大萧条状态。Atari 的主席 Ray Kassar 宣 布销售已下跌了 50%。个人计算机应时出现,许多新型计算机游戏开发者纷纷出 现,其中有些成为了专业开发队伍,新出现的知名公司包括 Sierra On-Line 、 Broder-bund、Synapse、Sirius 及 Strategic Simulations。 Electronic Arts 成立,并成为现代游戏公司的楷模。 1984 年 Apple 推出新型计算机产品 Macintosh,但该系统缺点是启动馒,此时该系统 的购买者和软件开发者的主要目的不是开发游戏。 1983 年伟大的视频游戏跌至最低点,Mattel 宣布其 Intellivision 产品损失 $22,500 万。 1985 年 Nintendo 由于投入一种新的视频游戏产品—任天堂娱乐系统,而重新繁荣了 家庭视膝游戏市场。16 位的计算机的革命继续,Atari 的产品 Atari ST 投入市场是 对 CommodOre 一个沉重的打击。 1986 年 CommodOre 极力宣传并推出 Amiga 计算机,该计算机是由 Atari 硬件设计 师 Jay Miner 设计的,最初是想作为下一代的视频游戏系统,但由 Commodore 投 资的这种机型却成为机器 Commodore64 的继承者。不幸的是,巨大的市场打击和 其它的开发埋没了该系统的远大前程。 Sega 推出了 Sega Master 系统,在技术上要优于 Nintendo Enter。tainment 系统。由于 Sega 忽视第三方开发者,没有征集足够多的软件支持该系统,所以没 有取得足够大的市场支持而失败。 1988 年 计算机行业从 8 位计算机转向 16 位系统,新出现的视频游戏控制台引起了 新的合并,Cine。maware、Epyx 等公司处于困难时期。 1987 年 游戏变得更复杂了,更多的公司转向集体开发,Electronic Arts 推出了第一个 集体开发产品“Skate or Die1" IBM PC 系列,由于具有了好的图形适配器,开始成为可采用的计算机游戏平台。 1989 年 新的 16 位系统初次登台,最值得注意的是 Sega 的 Genesis。当 Sega 由于 广告宣传和大量的 EA 运动游戏,占据控制台市场的领先地位时,任天堂(Nintendo) 觉悟已晚,损失惨重。 1990 年 Amiga 和 ST 在市场上基本灭迹,PC 系列和控制台成为主要的游戏开发平台。 Electromc Arts 开始成为主要的游戏开发和发行者。 1992 年 PC 游戏开始流行,在它迅速发展的几年里,其它一些计算机游戏平台被淘汰, 把 PC 游戏市场推向一个新高度。 1991 年 Nintendo 的产品 Super 一 NES 首次推向市场,16 位之战的全面爆发促进了 许多控制台系统的销售。 1993 年 Pentium 芯片出现,Microsoft 预先展示了它的新产品 Windows 操作 系统,代码名为 Chicago。尽管 PC 迅速发展,Sega 和 Ninter1do 继续处于领先位 置,控制台系统占领了世界游戏市场的 80%。 1994 年 Panasonic 推出了 Real-3Do 游戏机,预示着 32 位控制台系统的出现。 Atati 推出 Jaguar 32 位游戏机。但这两个产品(尤其是 3DO)都不很成功。 Id Software 推出了 Doom,使人们意识到可以使用共享软件发行方法。 1995 年 Sega 生产了 Sega 32 位控制台系统。Sony 推出了 Sony Playstation 32 位控 制台系统。 Microsoft 推出了 Windows 95 和 windows Game SDK,使得大量的游戏开发 转向 Windows 乎台。 Internet 和 Word Wide Web 流行,大量的用户上网。 1996 年 Nintendo 推出了 UItra64。大范围的多人游戏已经出现。多媒体、3D 和虚拟 现实等取代 WWW 而成为热点,主要的新技术和产品包括 Java 1 ShockWare、 JavaScript、Netscape 2 等等。 1997 年至今 Iintel 推出 MMX 技术,并基于此推出 Pentium MMX 和 PentiumⅡ处理器。 AMD 和 Cyrix 也基于 MMX 技术推出了 K6 和 M2处理器。游戏开始大量使用 MMX 技术。PC 与其他的控制台系统在性能上已相差不大了。 AMD 推出 3D-NOW 技术,使其基于 3D-NOW 的处理器 K6-3D 在图象处理的 方面超过 PentiumⅡ。 第三节 游戏的组成要素 游戏设计涉及一个置于所有其它要素的中心的东西——这就是交互性,使游戏 从其它的创造性的媒介(如艺术、电影、音乐和书等)中分离出来的这一要素可以 在游戏者同游戏的交互中体现,对于游戏这种媒介的驱动力量来自于游戏者的决 策,换句括说,是游戏者的行为,而不是媒介本身。你不要仅仅看或仅仅听一个游 戏——你应该控制它。设计者必须创造一个诱使人们去玩的产品,同时还要提供故 事线索、情绪感染、真实的声音和其它创造性的媒介给予的特性。这是比较高的要 求,但是这就是使游戏开发如此有趣的一个原因。 玩游戏就意味着决策。因此,我们的游戏需要创造让游戏者不得不决定做什 么的情境,这样,他或者她才能执行所要求的行为。在一些情况下,决策越具有挑 战性,这个游戏就越具有感染力。此外,游戏者越能够影响游戏的结果,这个游戏 就越能够吸引他们参与。 当坐下来设计一个游戏的时候,要努力创造有趣的交互,还要努力在创设的 情坎中为游戏牙提供作出决策的容易的方式。然后,提供将进步引出新情境的有趣 结果。这个完整的过程要一进一遍地进行,直到取得最终的结果。 1.影响结果 交互性的一个最重要方面就是游戏者能够影响游戏的结果,并对结果具有最 终的控制权,一个游戏者坐下来玩游戏时希望能够在过程中有所进步,或者能够挽 救世界,或者能够达到最高的等级水平,因此,在设计一个游戏时,你必须一开始 就让游戏者明白,他或者她能够对结果改变到什么程度。当然,你不需要预先告诉 游戏者他们能够改变什么结果,或者他们怎样改变它;你只需让他们知道:他们的 确能够影响结果。游戏设计者用于创建好的游戏的一个重要概念叫做多重结果。多 年以前,大部分游戏只提供两种结果——成功和失败,游戏者结束了这个游戏就是 成功者,否则游戏者被消灭。然而,许多现在的游戏,尤其是交互式故事或者 RPGs, 就可以有几个完全不同的结果,这些不同的结果可能让游戏者进入完全不同的新的 冒险,或者进入新的交互式故事。例如,如果你正在玩一个战争游戏,而且,你在 某一场战斗中成功了,你将可能获得军衔的提升,到上尉、少将或将军。 注意:在设计游戏时,给予游戏者通过他控制游戏来决定的各种各样的结果是十分 关键的,只有这样才能让人们花时间去玩。 2.成就的角色。 玩任何游戏的根本目标都是获得什么东西,不管怎样设计,在任何游戏中成 就是一个基本要素,当然,成就有许多不同的形式。可以简单到是打败一个对手、 得高分或达到较高的等级水平。当你在游戏中设计成就因素时,可以在许多不同的 水平上进行,你可以提供多重的成就目标;你也可以提供渐进的成就水平,例如你 可以在每隔三个等级之后的等级结尾以不同的“主要怪物”作为特殊对手,在整个游 戏的结尾则以“最大的物”,作为对手。 不管你的游戏提供了什么形式的成就,成就是游戏过程最主要的转折点。当 然,成就并不一定意味着赢,还应该是一个游戏逐渐走向最终结果的自然的前进, 一些游戏提供了实际的结束作为成就,还有一些游戏成就是绝对的成功(特别是体 育游戏),如赢得一场比赛或获得一个冠军。但它们也可以有不同的成就等级。 重要的是游戏中成就的获得不应该有大的跳跃,而应该是进行一定的时间后 发生的渐进的过程。另外,我觉得一个游戏应该有 3/4 的潜在游戏者能够获得 100%的成就水平,毕竟,游戏不能因为太难而被认为违反一般潮流,游戏者喜欢 的挑战是那些可以征服的挑战。 人类的本性就是希望获得成就,如果人们去做一些不能获得成就感的事情, 这是不符合人类本性的。这就是说,不要设计一个游戏,而又故意过早发布提供线 索的资料。 3.失败的角色。 “游戏结束(Game over)”大概是计算机游戏带来的最不好的一个短语,在 这以前,基于竞争成绩或者简单成就的游戏都包含有各种各样的失败程度。 4.改变情景 许多游戏允许游戏者控制或改变玩的情景或者参数。游戏者经常喜欢或需要 修正游戏的可玩性,这不仅包括简单地改变他们能够拥有的“生命(lives)”的数量, 而且包括改变一个战争游戏中的所有数字、参数。 通过改变情景,设计者应该提供游戏者如下特性: a.修正游戏难度的能力 b. 改变玩的环境的能力 c. 修正等级水平或游戏角色的能力 5.问题解决。 我们已经花了一些时间讨论成就和失败,这些原则的具体例子可以在问题解 决中发现,而问题解决又经常与“智力难题类游戏(puzzles)”相联系,而且也是其 它类游戏包括冒险游戏、RPG、策略游戏中的关键要素。 问题解决是给游戏者清晰定义一些挑战,然后通过解决问题的方式来进行交 流。这佯,游戏设计的一个主要工作就是创建一些有趣的和富有挑战性的问题,这 些问题具有符合逻辑的解决方法,使游戏者能够最终通过玩游戏解决问题。如果方 法太简单,游戏者将迅速完成,然后再去玩其它的一些游戏(也许,这些游戏还是 你的主要竞争者)。另一方面,如果解决问题的方法太难,游戏者可能由于挫折而 放弃。 这听起来容易,但是,事实上许多游戏对于游戏者仍就具有一些不合逻辑的 方法,或者问题太难,游戏中间题解决直接与游戏的成功或失败相联系,人们不介 意失败的危险(这是挑战的一部分),但是,如果不可能获得成功或者根本就不可 行,这个问题就是无法解决的,也就不再成为问题,而是一个绊脚石。 在游戏中一个与呈现问题相关的有趣的事情是创设问题情境。你创设的问题应 该让游戏者把已有的知识运用到问题解决中来。许多游戏的问题解决知识仅仅限于 游戏本身的知识,对于大多数游戏来说这是不错的。 然而,还有许多游戏,尤其是冒险游戏或者“交互式故事”,你可以表现真实 世界中的智力难题,一个简单例子是这样一个古老的智力难题,一个拿着一块木板 和一块石头的人被陷在一个巨大的坑里,还有一股水流向坑里,然后流到排水管里。 游戏可以拿着石头来堵上排水管,当水填满坑以后就可以用木板漂上来。这是一个 非常简单的例子,但是当你设计游戏时,尤其是你想对那些并不是劲头十足的游戏 者构成挑战,就应该考虑运用这类智力难题。 不论什么时候,都要努力把游戏者变成问题解决过程所不可或缺的部分,这 样才难把游戏者吸引在你的游戏上面。这方面做得较好的一个例子是 slmCity。 角色扮演和进入其他世界 大概计算机和视频游戏的最基本的感染力就是能够逃避现实。大部分游戏是 角色扮演游戏,把游戏者置身于一个想象的世界或情境中。玩游戏的人具有与看电 影和看书的人一样的原因:人类具有想象,而且大部分的人需要给他们的想象以刺 激。电影和书这么做了,而游戏又更进一步,允许游戏者实际地参与。 以游戏设计为立足点来看,这就意味着你的一个重要的职责就是要给游戏者 传达一个思想,通过它游戏者能够进入另一个世界。在这方面游戏设计就需要创造 不同的气氛。不论它就仅枪战游戏那样简单,还是和 Broderbund 的新的法庭素材 的产品那样复杂,最为重要的是,其中的一个目标就是创造一个世界,游戏者被假 设为里面的一个角色。 6.幻觉状态(Suspension of Disbelief) 我们刚刚讨论了进入另一个世界和人的想象力的独特本质,这种思想的最终体 现就是在游戏界常听的一个术语“幻觉状态”。当游戏设计者谈到“幻觉状态”时,他 们描述的是一种“想象的状态”这是指游戏者的意识融人到游戏世界中,这样,他或 者她就不是在玩游戏,而实际上在体验另一个世界。 7.个人经验 游戏设计的另一个核心因素是游戏者通过游戏所获得的个人经验。通过试验, 我主要注意到游戏者在玩游戏时的三个核心的个人经验成分。 1.有趣 “有趣”是一种主观体验,只有试着玩了以后才能决定产品是不是真的有趣。而且, 游戏只是对于将要玩的人是有趣的,如果你专门为女士设计了一游戏,就不必要增 加一些对年轻男孩子来说有趣的因素。 2.学习 我想并不是所有的游戏都必须是:纯教育意义上的一个完整的学习经验。相反,我 想所有好的游戏都要求你去学习以征服这个游戏,包括在游戏中学习对手的弱点和 寻找智力难题解答案。在这两种倩况下都需要你有计划地设计好这些活动。如果想 让游戏者学习计算机的弱点,你就应该有意地计划这些弱点是什么。 3.探索 超越有趣的最重要的因素是探索:现实的个人经验。我们去看电影或读小说的一个 主要原因是:逃避现实。游戏通过把令人震惊的图形、人工智能、立体声音和狂野 的想象一起融合在游戏的交互要素中,最终给人们提供了逃避现实的机会。我认为 其中最重要的是探索原则。这就是游戏,当你玩它时,就逃离了你存在的世界,而 假设一个生命在探索新世界。探索是一个关键的要素。最为重要的是,当你建构一 个游戏时,在本质上你就在探索一个新世界。” 8。还有什么 除了这些基本概念,游戏设计还有更多的东西。游戏设计是非常主观的,到现在, 你仅仅了解了游戏设计思想的一部分。就像任了何好学生一样,你应该一直寻找和 探索以扩展你的知识。 此外,还应该懂得所有的游戏开发的材料并不仅仅来自于阅读有关游戏设计的书和 玩其它的游戏,前面我所提到的许多开发者都往往从其它方面吸收新东西。 游戏设计是信息时代的复兴艺术,一个真正的复兴艺术家是一些万能博士,他们从 不同的渠道获得大量的思想,超越了少量特定的游戏设计材料来寻找有关游戏的设 计思想和灵感。 第四节 游戏的细节设计 定是截然不同的。我所要求的那些内容可能与人们所的喜欢玩的内容不一致, 从而导致人们认为 这个产品没有娱乐价值。 当然,其中一些是市场决策,你必须尽早决定你想达到的市场方面的参数。并 且应该提前考虑技术和实现的问题。生产一个要让我喜欢的竞选游戏,需要先进行 大量的研究并整理大量的统计数据。 在设计你的游戏时,你必须预先确切地决定要用多少细节。一些游戏设计者把 细节放在最需要的地方。例如,考虑一个潜水艇模拟,如果你想使一个区域非常详 细,该是哪个区域呢?当然是“用鱼雷袭击的部分”,你可以把潜水艇在搜索攻击目 标时的运动设计得简单,但是,一旦你发现了船只,游戏应该能够允许你最大限度 地参与鱼雷攻击的过程,因为这正是潜水艇游戏的焦点。· 1.什么时候一个任务会变得象日常琐事? 在一些情况下,如果一个游戏变得相当真实,就可能导致游戏中的任务成为一些日 常琐事。我记得的有这么一个游戏,这是一个令人难以置信的模拟游戏,让你驾驶 F—16 战斗机。这个模拟是如此真实,甚至要求游戏者去完成一个真正的飞行员在 起飞之前要作的许多任务,这些任务是如此之烦琐,致使游戏的娱乐性大大减少。 这方面的缺点在一些 RPG 游戏中也可以发现,与他们在真实生活中的所作所为相 比,其中游戏者可能发现他或者她花更多的时间在为别人跑腿。当然这并不是说这 些游戏不好,上面谈到的游戏就是一个非常让人吃惊的精确的模拟,许多模拟游戏 迷们都喜欢这些精确的细节。但是,应该明白许多游戏在写实方面做得太过火了。 对我来说最大的问题是什么时候“任务”变成了日常琐事。尽管我能够看到这些细节 对游戏写实的贡献,但是,在我看来,它们更多地是玩游戏的障碍。所以说,这就 是一个娱乐性和写实主义之间的难问题。最优秀的游戏处理这个问题是通过增加一 些可控的设置来让人们控制这些写实主义的因素,设置的值越高,则游戏就变得越 真实。 在游戏开发的过程中测试模拟游戏是十分重要的,要看人们对于你设置的任务的反 应,如果你的任务太简单或者太复杂,你可以在测试阶段不断地进行调整。 2.“任务瓶颈” 在你的游戏中寻找“任务瓶颈”。任务瓶颈包括两种类型:必须的和隐藏的。必须的 任务瓶颈是最糟糕的一种类型,它们是这样一些活动:游戏要求你一遍又一遍地执 行以取得进步,这种类型的一个例子可以在冒险游戏中发现,其中增加了一些赌博 的因素作为一种让游戏者增加钱财的方式,在这种情况下,你可能花一整天去玩各 种各样的愚蠢的赌博比赛,其唯一目的就是赚钱。这确切他说还不是娱乐,因而对 于游戏的真正目廊来说就是一个瓶颈。 隐藏的任务瓶颈是那些允许游戏者花大量的时间重复地作些事情以获得策略性优 势的程序流程,甚至有时设计者并不是有意这么做。 最后一关是评测产品,看看在什么地方游戏者发现了他们不喜欢的重复性任务或者 发现其他各种各样的问题。大部分问题都比较好解决,以避免让游戏成为一系列“日 常琐事式的”重复性任务。 3.在游戏设计中运用市场研究的结果 许多情况下,你的设计需要进行适当的改变以满足市场走向。当你设计一个商品化 的游戏时,重要的是要记住你必须最大限度地满足市场,而不是为了取悦于你自己 或者参与游戏开发工作的其他人。你自己不去购买你自己的游戏,而是其它人购买, 他们对你的游戏有和你不同的期望和要求。 当把市场因素综合到你的设计中时,第一步是了解你面对的市场,这是一个本质问 题:“谁是这个游戏的一般购买者?”然后,努力确保你充分考虑他们的要求。例如, 让我们设计一个深海潜水游戏并考虑相关的市场因素: 游戏概要:深海潜水:这是一个关于深海潜水和去寻找埋葬的金银财宝的游戏。目 标是在水下的冤死鬼抓住你之前发现你能够找到的所有金钱。 市场:喜欢潜水的人;喜欢冒险游戏的人;喜欢海洋的人。 这里我作了一般性的假设,例如,这个游戏对于喜欢冒险游戏的人具有吸引力。这 样这个游戏需要一个详细的故事——一些冒险游戏的爱好者的基本要求。对于吸引 喜欢潜水的人,我们最好确保给他们一个有关深海潜水的令人心眼的模拟,因为既 然喜欢潜水,可能就是这方面的专家,不会轻易被愚弄的。 这里的意思是:在你设计游戏和推断你的潜在对象的大概情况时,要按照他们的要 求来调整你的思想和设计,这将不时地把你的游戏引向不同的方向。也许你最初的 设计不能算是一个故事,只是一个潜水的模拟,然而,冒险游戏的市场总体上却是 很大的,因此,你将发现你自己被迫增加了一些详细的神秘而有趣的故事。 在今天竞争激烈的游戏市场,许多游戏公司都在努力创作一些能够让更多的人们想 玩的游戏。他们肯定都正在把市场研究和市场驱动的设计整合进来,用以帮助创建 人们想要的软件。 5.满足“铁杆儿”游戏者——正面和反面的意见 在你设计游戏时,你应该特别考虑一个最重要的游戏群体——铁杆儿游戏者。高技 术市场学说告诉我们,一个最重要的规则就是满足那些游戏高手,他们也会反过来 产生帮助销售产品的热情,他们可能把产品推荐给其他游戏者,并促进他们的购买 决策。 当然也要明白一点:你为游戏高手制作的游戏很可能对于哪些偶然玩游戏的人或不 是高手的人来说很没意思。例如,人们可能不喜欢一个花 400 小时才能完成或者 拥有 100 多种坦克的战争游戏,相反,他们可能喜欢一个真正好的、线性定向的 游戏故事,或者一个关于人们之间友谊的游戏。当然也有折衷的办法,但是越来越 多的开发者却在作在铁杆儿游戏者要求以外的很多事情,实际大部分游戏都这么 做。 这里没有真正正确或错误的方法,在你设计一个游戏时,你应该描述谁是潜在对像, 对于这个问题从两方面考虑是很重要的。如果你与其他喜欢游戏制作的人一样,就 不太好办了,因为 99%的热爱制作游戏的人他们本身就是铁杆儿游戏迷。因此, 对于许多游戏设计者来说,必须有意识地考虑这些问题才不致于使他们按照习惯去 设计制作游戏。 如果你想真的突破铁杆儿游戏者所喜欢的游戏类型,就不要在游戏中加入一些会让 铁杆儿游戏者和评论者注意到的一些关键特征。当支持调制解调器(Modem)fo 网络的游戏在市场上屈指可数时,有较大影响的评论家和铁杆儿游戏者会特别注意 这类游戏的出现,这些人的挑剔和批评可能让你的游戏变得一钱不值,而如果不让 他们注意到,你的游戏说不定会流行。 总之,你应该懂得如何满足游戏专家所喜欢的基本要素和特征,然后,有目的地决 定在哪些地方实施这些特征。这里的关键是“有目的地”,不要猜想和假设,决定特 殊的需要是什么,然后你就尽你所能去创建能够满足铁杆儿游戏者的游戏,也许同 时能够吸引一些游戏高手之外的玩家。 第五节 对手智能设计 人工智能是个错误的名称,特别是与游戏联系起来时。我们真正的目的是要使用一 些技术在游戏中加入“人”或类似智能的特征。这种技术越聪明越自然,它就越会被 游戏者承认。我想对于这种技术更好的名字是“模拟智能” 大多数的游戏不需要非 常前沿的技术,如神经网络。(而少数使用此种技术的游戏也都是 AI 专家们感兴 趣的那类,如国际象棋等等。你从没有见到哪个打斗类游戏使用“神经网络”技术。) 对于游戏中的 AI 我比较喜欢这个定义: 一个非游戏者控制的对象在基于各种复杂因素时的决策行为就象时由真正的人作 出的,这是通过使用 一个决策算法来完成的,这个决策算法根据设计者确定的规 则和提供给程序的信息进行处理。 这定义中,使用了三个需要进一步解释的术语: 1.决策 一个“智能的”对象通过抉择来决定它的行动而不是随机地动作。你可以把随机性作 为强调某一个特定的决策的权重,但最终的决策是在至少两个可能的结果中抉择产 生的。对游戏者行为预测得越准确(包括更深层次的反应),对游戏者来说这个游 戏就越具有智能。因而重要的是决策必须模拟人的本能反应。它一般由可观察的信 息组成,这些信息包括外部、内部的信息,例如敌人的数量、角色的肚子是否饿了 等等。 2.多因素 作为人类,你知道对一件事作出决策需要考虑许多因素。举个例子来说,到哪儿去 吃饭就是一个颇费周折的问题,你是开车去还是走着去?是和别人一起去吗?想吃 什么菜?花多少钱合适?如果时间紧张,是否去吃快餐呢?要不要预订?…… 使一个物体具有智能也要考虑很多因素并能迅速作出决策。这些因素决定对不同的 情况有不同的反应。你的算法能支持(或处理)的因素越多,你创建一个真实的环 境的可能性就越大。例如,你要定义一个战争游戏中敌军将领的决策行为,你可以 让他们根据军中坦克、飞机、舰船的数量来做出生产军备力量的决策。另一方面, 你应该引进其它因素。例如,这些敌军应该考虑一些军事基地建在岛屿上,所以要 多生产舰船,少生产坦克,他们也要决定是进攻还是防守。你也应该加入一些感情 因素。例如,一个好战的将军会在处于劣势时仍旧进攻,并生产进攻型武器:飞机, 而实际上埋智的决策是应该防守并生产防守型武器,如坦克等,记住,一个“智能 的”决策没有必要是最好的、最具有理性的决策。 所以当你试图模拟智能时,先确定用于做决策的信息(多因素)。同样要考虑决策 者评估信息时所具有的个性。 3.规则 你也要确定在什么情况和规则下,一个对象可以获取作出决策所需要的信息。例如, 在战争游戏中,计算机控制的对手是否可以观察全局,或只能得到本地信息?如果 允许对手观察全局而不允许游戏者这做,那计算机就在作弊。但由于人类玩家实在 太聪明了,就扯平了。 19.1.3 重要的是表现在游戏者面前的是什么 当然,AI 的目的不是创建一个在“作弊”方面表现良好的系统。但在游戏中,用“作 弊”是使一个对手具有挑战性是最好的方法。不过这样做要小心,因为一旦游戏者 发现计算机在“作弊”,他很可能再也不玩这个游戏了。 对于大多数 AI 游戏开发者来说,他们的目的是创造出一个最好的计算机对手,这 样做的结果就是:他们创造出的敌人太强大了、太聪明了。这就引发了一些矛盾。 首先,游戏应该让大多数游戏者能成为胜昔。Al 开发者可能在努力创造一些能打 败游戏高手的游戏敌人,但是我作为一个游戏者时,我不喜欢这样的游戏。我只希 望我的计算机对手在我不认真玩的时候打败我,只要我全神贯注地玩,我就应该能 赢。 第二,一个太智能化、复杂化的游戏会显得不真实。例如,在战争游戏中,一个 AI 开发者会坚持把计算机对手做成世界上最伟大的将军,这就很不真实。让计算 机对手时不时犯点错误或者让它模拟现实存在的某位将军的风格才能使游戏更为 真实。AI 理论趋向于创造一个理想化的情景,而不是真实世界。 所以,你需要做的只是让计算机对手具有真实的外在形象和性格特怔,而不是创遣 一个世界上第一强大的对手。你可以用 AI 技术为游戏者表现游戏世界中的深度、 挑战性,模拟异类世界的情形。 不管你使用什么技术,最重要的是使用后的效果。如果有人对你说:“在你的游戏 中加点人工智能吧”,他们很可能是让你加一些真实画面和角色,而不是什么神经 网络之类的技术。 第六节 游戏的界面设计 著名的游戏开发者 BilI Volk,曾经对游戏设计写下了一个等式“界面+产品要素= 游戏”。这个等式与著名的程序设计语言的设计者 Nicholas Wirth 的经典等式“算法 十数据结构=程序”是相似的。很明显,BilI Volk 的目标不是说建构游戏就是简单 的加法,而是强调在游戏设计中界面的重要性。他的观点基本上是这样的:你的游 戏就是你的界面。 界面是游戏中所有交互的门户。不论你是用简单的游戏杆,还是运用具有多种输入 设备的全窗口化的界面,你的界面是联系产品要素和游戏者的纽带。 创建一个好的界面都需要什么素和技术呢?下面我们将讨论一些建构有效的游戏 界面时可能有用的一些基本问题。 1.内务管理问题 内务管理问题是指当一个人玩游戏时所必须的安装、准备等任务。这些问题包括初 始安装、调用和保存游戏、游戏设置和在线指导或者一般文档。 2.游戏者介人的问题 游戏者介入的问题大概是与界面有关的最重要的因素。从根本上说,它将影响游戏 者玩游戏的方法,是通过键盘、游戏杆、鼠标或者是这几种的结合?或者还运用其 它类型的输入设备? 在设计界面时,应该了解游戏输入设备的范围。还要充分考虑灵活性和能够能够运 用到你的游戏中的设备的类型。 3.让游戏者尽快开始游戏 什么时候当你拿到一个新游戏后,在开始游戏之前一页又一页地阅读用户手册?如 果你与大部分的游戏者一佯,你大概是直接开始玩,井努力去了解如何通过用户界 面进行交互,游戏者并不是典型的软件用户,他们没有兴趣学习大量新特征——他 们只是简单地想玩!所以,当你设计你的用户界面时,应该使它容易让人理解和接 受。你的第一个目标应该是让游戏及其界面尽可能地符合直觉。当然,你可以提供 一个手册,但是不要期望你的所有用户都去读它。 下面让我们看看帮助你设计界面,以便于让用户能够正确入门的一些基本原则: 1.降低计算机的影响 降低计算机的影响是交互性中比较抽像的一个概念,但又是“幻觉状态中的一个成 分。当然,在我们坐下来用计算机时,我们意识到我们正在运用计算机。然而在设 计一个游戏特别是设计界面时,应该尽量让游戏者忘记他们正在使用计算机,这样 会让他们感觉更好一些。尽量使你的游戏开始得又快又容易。游戏者进入一个游戏 花的时间越长,越会意识到这是个游戏。 2.在你的游戏中加入帮助 尽量把你的手册结合到游戏当中,避免把游戏者拉开屏幕让他们去看书面的文字口 这方面通过优秀的设计是可以解决的,如果需要的话,把文本世结合到游戏中。 例如,如果你有一幅让游戏者使用的地图,就不要让它成为文档的一部分,应该把 它做成屏幕上的图形。 3.避免运用标准的界面 对于大部分在 Windows 环境下设计的游戏都别去运用常规的 Windows 界面。如果 你这么做的话,你就又在提醒他们正在运用计算机。应该运用其他的对像作为按钮 井重新定制对话框,尽量避免菜单等可能提醒他们正在运用计算机的对象。 4,综合集成界面 界面上关键的信息要简化。因为对于许多产品,界面绝对是产品特征的门户。对于 游戏来说,目标就是要让界面越来越深入到游戏本身的结构中去。对于大量的游戏 者来说,其中只有少部分人具有计算机经验,因而,界面问题就更加重要。 第七节 基于可玩性的考虑之一 可玩性不仅是测量游戏有趣程度或是图像如何“酷”的量度,它也是一个游戏的有趣 程度,难易程度和坚固性等性能的总和,此外还包括音响效果、模拟图像等等。可 玩性的关钟是为你的游戏的每个方面努力奋斗。虽说只有开发完毕后才能看出游戏 的最终可玩性,而且开发期间的测试和调整也会起很大的作用。但是,每个细节的 优化都可能大大提高可玩性。 1.最终的可玩性不到最后不会表现出来。 在游戏完成之前你不会知道你的游戏如何好玩。首先,你要确定你能完成你的游戏, 除非测试游戏的人“都”说你的游戏真的差劲。在完成所有的声音、图像等工作以前, 你不会确切知道你的游戏到底怎样。其次,最终结果常常表明每个小细节都很重要。 游戏者会注意到游戏的方方面面,从简介中错位的像点到不合适的音效。每个小的 失误都会带来不良的影响,破坏了游戏者的整体游戏体验和游戏的可玩性。 2.早期测试和经常测试——中期调整 当你的想法还停留在纸面上的时候,它可能看起来很神奇,但一旦你开始实现它, 事情往往就变了,并且经常是变得很糟!因此,每个较大的游戏程序都需要很长的 测试过程。 当在屏幕上的某些东西不是你最初想象的那样,或是你自认为设计很好的动画速度 太慢或很难看。有时你的测试者会说:你的游戏太简单、太弱智、太烦人。所以即 使你花几个月的时间停留在纸面设计上,你也不必感到沮丧。 准备好根据意见反馈来改变你的游戏设计计划。有时批评会有帮助,他们可以给你 实际的建议并帮助你,使你的游戏更具可玩性。但有时他们也没什么帮助,你还得 自己作出判断。必须明白,你应该在开发过程中不断测试你的游戏的可玩性,而不 要等到开发完毕,那时就来不及作重大调整了。 3.设计内容:了解你的特定顾客 增加游戏可玩性的最好办法是:确定游戏是面向特定顾客而设计的。由于年龄、性 别和教育背景等原因,对某一个游戏者来说好玩的游戏不一定让另一个游戏者喜 欢。因此,在设计成功的游戏中,一个最重要的行动是调查研究需求和其它关于特 定顾客的需要。 第八节 基于可玩性的考虑之二 让我们来看一下,如何针对不同年龄、性别来设计游戏。 游戏的市场是非常广阔的。你可以为 2-4 岁的孩子、受过大学教育的成人或在此 两者之间的任何年龄层的人设计游戏。 1.年龄段: (1)学龄前早期阶段(2-4 岁):这一年龄段的孩子在控制鼠标、单独使用键盘 或其他复杂的输入设备方面存在一定困难。因此,成人使用的标准游戏设备不适用 于他们。他们不能打字也不能阅读文字。那么应如何为他们设计游戏呢了 学前儿童可以使用光标键。他们能在键盘上找出单个的字母键。如果你设计的热区 有较大的区域,那么使用鼠标也是可以的。事实上,热区应不小于屏幕的 1/12。 然而,语音最适合早期学前儿童游戏。下面是使用语音的一些规则: a.你的游戏应简单到不需要介绍,儿童立刻可以开始玩游戏。但是千万别用语音指 导替代简单易懂的界面,可以使用简单的短语指导儿童玩游戏。。 b.当游戏中有一段时间没有用户输入时,播放一段短语告知儿童应该做什么,如“点 一下猫”或“按 A 键”。 c.把语音作为奖励使用。在儿童完成一个任务后可以说:“很好”或“干得好”等。 d.讲故事采用播放语音的方式。 e.让你游戏中的人物说话。小孩子特别喜欢天真可爱的声音和单词。 f.小孩子喜欢明亮的颜色和简单的图形,但不幸的是,出版商喜欢细化的、高解析 度的图形。因为他们的成人口味与孩子很不相同。成年人趋向于认为产品越复杂, 质量就会越好。你作为一个游戏开发者应努力平衡这两方面的需求。 不要让界面显得太杂乱。学前儿童的大脑还没有那么多经验来体验复杂的详细图 片。使用醒目的提示方式如弹出式菜单、随机动画等等。记住你的游戏者是儿童。 尽量避免令人害怕的图像。这个年龄段的儿童还处于没有准备接受诸如伤害、死亡、 分裂、暴力、怪物、女巫和坏家伙等概念的阶段。事实上,大部分不友好的东西都 应该避免。 (2)学龄前阶段(4-6 岁):适用于早期学前儿童的定律同样适用于这个年龄组。 这些儿童能认出单词。他们中的一些人成为使用游戏俸的专家,并能更好地控制鼠 标。除游戏俸和鼠标之外,提供一个支持键盘的界面不失为一个好主意。 (3)小学早期阶段(5-8 岁):在这个年龄段,儿童可能喜欢怪物和其他坏家 伙, 只要这些敌人是“安全的”。在游戏中敌人应当是友好而幽默的。暴力、伤害和流血 仍然应该避免。 (4)小学/中学过渡阶段(7-11 岁):这些儿童已达到“理性的年龄”,他们开 始发展自己的“同辈文化(Peer CuIture)”,并开始思考他们自己。在这个年龄段, 孩子们开始注意到比他们大的群体。作为设计者,你一定小心不要用太“孩子气” 的材料。你的游戏表现的特点应比这个年龄层稍大一点。。 这个年龄层的儿童能阅读文章,尽管有时并不流利。在使用恰好在儿童记忆范围内 的词汇表还是范围稍大一点的词汇表方面是有争论的,你应该自己拿主意。 (5)青少年阶段(12 岁以上):这个年龄层的儿童被认为是最难以对付的顾客。 针对他们的父母的市场策略是,非传统的教育题材的游戏可能最有发展前途。 “酷”是这些孩子追求的终极目标。男孩子疯狂地玩像 Doom 和真人快打这样的游 戏,因为在那里他们满足自己的强烈欲望。女孩子更感兴趣的是社会活动,坐在计 算机面前并不总出现在她们的时间表里。这些十几岁的孩子从能力和脑力上讲已经 是成人了。实际上他们可能比大人更会玩计算机。包含合作/对抗玩法的游戏在这 里可能最有市场。没错,用怪物、鲜血和暴力你一定可以取得成功!想想看,要是 真人快打中没有这些它还会是什么?多人游戏和联机游戏同样也是很好的。 (6)成人阶段(17 岁以上):这些游戏者大多是受到良好教育的成人,他们那种 欣赏、处理复杂事物的能力允许你使用一些性、暴力成分较多的题材(在法律和道 德的范围以内)。 2.确定性别特征 (1)游戏应该让男性和女性都作主角,而不是只让男性表现得特殊。有时可以允 许游戏者选择不同性别的角色进行游戏。 (2)不要把鲜血和暴力特别表现出来。 (3)也不要特别表现打斗的场面。 (4)避免传统的性别习惯。男性不应总是领导者,女性也不应总是被描绘成漂亮 的陪衬角色。 (5)可以试试加点幽默,但可不是黄色的幽默。 第九节 基于商品性的考虑 这些当你设计游戏的时候,你需要考虑哪些主要因素呢?它们当中有许多是与市场 销售有关。例如: 对象是谁? 让你的游戏与众不同的话,需要做什么?” 你的游戏在哪儿和怎样被销售? 你的游戏属于哪一类?这是很重要的,因为它决定了你的游戏在零售渠道中怎样被 分 和销售。 竞争和需求的情况如?。 你需要用 3D 图形和交互技术(真实 3D 景像、虚拟现实等)来吸引顾客吗? 你的游戏价格将会是多少?你会不断地升级吗? 为了有可能甘展游戏市场,你为游戏增加了一些什么特点? 你能在你的游戏中设计一些附加的成分,如附加的等级水平、场景和故事吗(例如, 你创建了一个模拟类的游戏,如飞行模拟器,你可以创设一些其他的地形地貌,让 游戏者去探索。这些附加的内容可以单独包装销售。)? 你的游戏能不能成能够被移植到其它环境(尤其是没有键盘的控制台环境),而且 付出代价不大? 你能不能为你的游戏中独有的特点办理许可证或者进行名称认证? 你的游戏是不是具有国际性的感染力? 在设计游戏时,是否有办法让你的游戏保持尽可能长的生命力? 如果游戏销售状况良好的话,是不是可以有续集?。 这个游戏仅仅是一个娱乐性的产品还是可以作为教育的工具? 你的游戏是不是可以作为多人游戏?能够在网络上运行吗? 其中有些问题在后面的设计过程中将会被解决,但是你仍然需要一开始设计就考虑 这些问题,否则的话在后面你会后悔的。无论你自信你的游戏会多么火爆,这些最 终与市场相关的问题都是很重要的。毕竟,不管你的游戏设计得多么好,如果最终 没有卖出去的话,所有艰苦的劳动都是徒劳。。 你的问题清单应该包括上面提到的这些问题和一些与你的特定开发情境有关的问 题。当 然,你应该回答的问题还不止这些,但这些是我觉得应该强调的问题。有的开发者 认为最大的问题是:“我能筹集足够的资金制作这个产品吗?”,这个问题的答案应 该考虑以下三个主要方面: 生产这个游戏的成本,与之相关的还有开发这个游戏要花的时间。 游戏销售能力和销售利润。 开发者的能力,他们能否在进行主要投资之前对这些问题知道尽可能多的答案。 当你考虑这些问题以后,你将会发现这些问题可以分为两组: 强调成本和市场要素的问题。 强调创造性要素的问题。 下面让我们更详细地考虑一些更重要的与市场相关的问题。 1.你要设计什么类型的游戏? 这是你要回答的最为重要的问题,而且你要在创建设计文件以前回答这个问题。从 本质上说,可以归结为这个问题:“做完以后要得到什么结果?” 如果在一定程度上你能够把你的最终结果可视化,你就能更好地回答我在上面提到 的那些详细问题。 大部分开发者决定他们要开发什么类型的游戏主要从两方面来考虑,一方面是选择 他们感兴趣的,另一方面是选择销售量可能大的。如果你喜欢战争游戏,你会说: “我要开发一个战争游戏”。这种情况下,你的目标是最终期望的结果。下一步就要 求详细地考虑如何达到目标。 a.你的对像的年龄范围和性别特征是什么? 随着越来越多的人们购买计算机和控制台,就越来越要求游戏适合不同的年龄范 围。游戏工业也就不仅仅以男孩子为特定对像。最近大量的讨论是关于生产适合女 性购买和娱乐的游戏。你需要预先考虑和确认你的游戏的确切对像,千万不要落人 这样的思维陷井:“我的游戏是如此火爆,以致于每一个人都想买去玩”。尽管生产 一个具有较宽年龄范围的游戏是可能的,但是大量成功的游戏都是指向一个特定的 年龄段,这是很重要的,因为,如果你的游戏对于一个特定的年龄段太具挑战性(或 者没有什么挑战性),你的游戏都将不会被经常玩。 在这一方面,设计一个游戏和写一本书是没有区别的。如果你写一本关于在另一个 星系上生活的科幻小说,你最好对你的读者有一个清晰的概念。如果你是给青年人 写的书,就应该保证运用比较短的句予并详细解释在你的故丰中比较重要的技术概 念。 b.你的游戏允许玩多少小时? 如果你的游戏是一个消遣游戏,即能够在办公休息的间隙用 20 分钟就能很快结束 (我希望没有人在工作时间玩游戏被抓住。)?或者你的游戏是一个能够持续长达 100 小时的冒险游戏?或者你的游戏是一个技巧性游戏(Arcade)或体育游戏,即 要求一小时又一小时反复地玩? 玩的时间在某些方式上决定于消费者,首先,价值是要考虑的,也就是说每玩一小 时游戏投入的成本是多少?有些消费者不希望花太长的时间玩游戏。其次,你需要 决定的游戏在哪里中断。 c.你的游戏将在什么系统上运行? 你的游戏是一个单一平台的游戏,还是一个能移植到其他平台上的游戏?这个决定 在很大程度上将影响以后的技术决策。一个仅仅为 Windows 平台开发的游戏比设 计一个多平台的游戏更具技术上的自由度 d.你打算让你的游戏满足什么样的价格水平? 你采用的游戏平台和对价格水平的选择,对技术都有影响。当然,一个低价的产品 只需要较少的投资,这里最大利润不是问题,而什么样的投资对你选择的价格水平 能生产最好的游戏才是一个问题。价格是消费者考虑的主要问题,当他们看到游戏 的价格达到 70 或 80 美元时,许多人就变成“望价兴叹”者。如果你想销售量大,就 应该努力创建一些低成本的娱乐产品。这些事实将使你的游戏价位成为一个主要决 策,这当然会影响你的设计。 5.你对你的游戏将来用什么样的销售方式? 在游戏商业中,销售就是一切。如果没有充分考虑与游戏本身相适应的销售计划, 即使最好的产品化的游戏也将可能面对失败。应该预先尽你所能投入时间和资源, 以确保你计划开发的游戏能够充分利用手头的资源去销售。你应该主要考虑的三种 销售系统是:零售、共享和直销,或者它们的任意组合。 如果你计划运用共享的方式,你的游戏设计应该与主要是零售的产品不同。应该探 索不同的销售方式中产品之间的差异,并调整你的设计,以最大限度地满足人们的 需求。对于每一个寻找新鲜花佯的消费者,有许多不同的销售方式。 6.你的游戏具有足够的交互性吗? 这听起来似乎奇怪,但是,我确实看到大量的游戏只具有低水平的交互性。如果你 的游戏不能让游戏者与环境进行充分的交互,这是很麻烦的。例如,如果你在一个 有人的城市,而游戏者又不能与这些人交谈,这就限制了交互性。由此再进一步说, 如果,你有一个许多人居住的城市,但是,你只能与其中很少的几个人交谈,这实 际上仍然限制了交互性,因此,在你设计你的游戏和附加的游戏组件时,应该确保 它们能够有助于游戏交互的本质。你可能没有意识到,实际上在游戏中这种只具有 低水平交互性的例子是很多的,如果你在游戏设计开发的一些环节上没有很好地注 意这一向题的话,这就更不可避免了。 2.举一个例子 就像我前面所说的,你应该从具有最大创造性的问题出发:“我到底想做什么?” 然后充实一些你的思想。最后,你将面对成本和市场问题,这些问题将对你最终的 创造性决策产生影响。上我们再来看看这方面的问题。 例如,假设你将制作一个篮球游戏,你最初的目标可能是创造一个分场景的、五人 对五人的动作游戏,每一个场景包括一些漂亮的扣篮动作的视频片断。然后你应该 考虑成本和市场间的问题,对像是谁?你能否买得起或者获得这些视频信息的使用 许可权?你能否用得起“魔术师”Johnson 或者 Charles Barkley 配音?你将用什么 平台?要回答这些问题你应该进行一些创造性的思考,以决定它的成本是多少,要 花多少时间制作,有什么样的技术障碍等等。 对于这一产品进行了许多讨论以后,特别是考虑了市场和成本因素,你最后决定 Windows 平台,将游戏设计成三人对三人的比赛,而不是设计五人对五人(为了 节省时间和资金)。这样你就又设定了一些参数,进一步的创造性设计要考虑到这 些参数,这样你才能在这些参数的制约下规范你最初的思想。 设计和计划是共存的:它们互相依存。设计一个游戏,尤其是一个主要的商业化产 品,要求比较好的平衡。真正设计的创造性也正体现在一个由成本和其他技术参数 制约的现实的框架内,创立一个富有艺术性和感染力的游戏。创造性的设计并不是 不受任何制约性。 至此,我们已经讨论了各种各样的游戏设计的思想、困难和问题,也逐渐明白了应 用这些思想的背景条件。当然,这也并不是说在设计一个实际的游戏时都要运用这 些思想,设计一个具有 3000 个角色(而且每一个角色都有非常酷的个性和独有的 对话引擎)的 RPG 游戏听起来很好,但是你还要考虑实现技术、时间和投资的限 制。 游戏开发基础(2) 第二章 windows 编程基础 第一节 引言 为了跟上潮流,我们抛弃了已快被淘汰的 DOS 操作系统,所有的讲解和例程都是 基于微软的 Windows 操作系统的。考虑到很多的用户并没有 Windows 编程基础, 所以我们设置了这一专门讲述、讨论 Windows 的术语、概念的部分,以使这部分 用户能较快地理解和掌握我们所讲述、讨论的编程思想和编程方法。这一部分中主 要讲述的是 Windows 中十分基本的东西,所以用户应根据自己的情况有选择的进 行学习。好!现在就让我们进入艰苦而又精彩有趣的游戏编程之路吧! 第二节 windows 的介绍 Windows 应用程序可以采用面向过程的实现方法。也可以使用面向对象的结构。 所有的实现方法都集成了点击控制和弹出菜单,能够运行特别为 Windows 编写的 应用程序。 Windows 是一种基于图形界面的多任务操作系统。为这个环境开发的程序(那些专 门为 Windows 设计的)有着相同的外观和命令结构。对用户来说,这使得学习使用 Windows 应用程序变得容易了。为了帮助开发 Windows 应用程序,Windows 提供 了大量的内建函数以方便地使用弹出菜单、滚动条、对话框、图标和其他一些友好 的用户界面应该具有的特性。 Windows 运行应用程序以硬件无关的方式来处理视频显示、键盘、鼠标、打 印机、串行口以及系统时钟。 最值得注意的 Windows 特性就是其标准化的图形用户界面。统一的界面使用图片 或图标来代表磁盘驱动器、文件、子目录以及其它操作系统的命令和动作。 统一 的用户界面也为程序员带来了好处。例如,你可以很方便地使用常见菜单和对话框 的内建函数。所有的菜单都具有相同风格的键盘和鼠标接口,因为是 Windows 而 不是程序员在实现它。 Windows 的多任务环境允许用户在同一时刻运行多个应用程序或同一个应用程序 的多个实例。一个应用程序可能处于激活状态。激活的应用程序是指它正接收用户 的输入。因为每一个瞬间仅有一个程序能够被处理,因此同一时间也只能有一个应 用程序处于激活状态。但是,可以有任意个数的并行运行的任务。 第三节 windows 的基本概念 Windows 消息和面向对象编程 Windows 实现了一种仿 OOP(面向对象编程)环境。Windows 下的消息系统负 责在多任务环境中分解信息。从应用程序的角度来看,消息是关于发生的事件的通 知。用户可以通过按下或移动鼠标来产生这些事件,也可以是通过改变窗口大小或 选择一个菜单项等。这些事件也可以由应用程序本身产生。Windows 本身也能产 生消息。如“关闭 Windows”消息,Windows 通过这个消息来通知所有的应用程序, Windows 将被关闭。 内存管理 在 Windows 系统中系统内存是最重要的共享资源之一。当同一时刻有多个应 用程序在运行时,为了不耗尽系统资源,每个应用程序必须合作以共享内存。同时, 当启动新的程序和关闭老的程序时,内存会变得碎片化。通过移动内存中的代码和 数据块,Windows 能够使内存空闲空间连起来。在 Windows 下也有可能超量使用 内存。例如,应用程序可以比内存容量大。Windows 能够废弃当前不使用的代码, 在以后需要时再从应用程序中将之读入内存。Windows 应用程序可以共享可执行 文件中的例程。包含可共享的例程的文件称为动态链接库(DLL)。Windows 包括了 运行时将 DLL 例程链入程序的机制。 硬件无关性 Windows 同时提供了硬件或设备无关性,使你免于在生成程序的时候不得不 考虑所有可能使用的显示器、打印机或输入设备。在 Windows 下面,每种硬件设 备的驱动程序只编写一次。硬件无关性使编程对应用程序开发者来说更为简单。应 用程序与 Windows 而不是各种设备打交道。 动态键接库 动态键接库提供了更多的 Windows 功能。它们通过一个有力而灵活的图形用 户界面增强了基本的操作系统。动态键接库包括一些预定义的函数,它们可以在一 个应用程序被调入时与之键接(动态地),而不是在应用程序被创建时(静态地)。 动态键接库使用 DLL 后缀。函数库将每一个程序员从重复开发诸如读取字符或格 式化输出之类的通用例程中解放出来。程序员可以方便地构造它们自己的库以包含 更多的功能,比如改变字体或检验文本。把函数变为通用工具减少了冗余设计,这 是 OOP 的一个关键特性。 Windows 的库是被动态地键接的。或者说,键接器并不把函数拷贝到程序的可执 行文件中去。相反,当程序运行时,它产生对库函数的调用。自然,这样做节约了 内存。不管有多少应用程序在运行,在 RAM 中总是只有库的一份考贝,而这个库 可以被共享。 Windows 的可执行文件格式 Windows 具有一种新的可执行文件的格式,称为 New Excutable 格式。它包括新 型的文件头,能够保存有关 DLL 函数的信息。 第四节 windows 的窗口 Windows 的窗口 窗口看起来就是显示设备中的一个矩形区域,它的外观与特定的应用程序无 关,可是,对于一个应用程序来说,窗口是屏幕上应用程序能够直接控制的矩形区 域。应用程序能够创建并控制主窗口的一切,如大小和形状。当用户启动一个程序 时,一个窗口就被创建了。用户每次单击窗口,应用程序作出响应。关闭一个窗口 会使应用程序结束。多窗口带给用户 Windows 的多任务能力。通过将屏幕分为不 同的窗口,用户能够使用键盘或鼠标选择一个并行运行的应用程序,以此对多任务 环境中的一个特定程序进行输入,Windows 截取了用户的输入并分配必要的资源 (例如微处理器)。 Windows 的布局 所有的 Windows 应用程序都具有诸如边框、控制菜单、About 对话框之类的 共同特征。这些特征使得各个 Windows 应用程序非常类似。 边框 Windows 的窗口被边框所包围。边框由围出窗口的线条组成。对于新手而言,边 框看起来仅仅是为了将一个应用程序的屏幕视口与其它的区别开。但是,对于熟练 者,边框有着不同的作用。例如,如果将鼠标指针放在边框上并按下鼠标的左键, 用户就可以改变窗口的大小。 标题条 应用程序的名字显示在窗口顶部的标题条中。标题条总是在相关窗口顶部的中央。 标题条非常有用,它可以帮助你记住正在运行哪个应用程序。活动应用的标题条以 不同于非活动应用程序的颜色显示。 控制图标 控制图标是每个窗口左上方的小图片,每个应用程序都使用它。在控制图标上单击 鼠标键会使 Windows 显示系统菜单。 系统菜单 当用鼠标单击控制图标时就打开了控制菜单。它提供了诸如 Restore,Move,Size, Minimize,Maximize 以及 Close 这样的标准操作。 最小化图标 每个 Windows 95 或 Windows NT 应用程序都在窗口的右上角显示三个图标。最左 边的图标是一段短下划线,这就是最小化图标。它可以使用程序被最小化。 最大化图标 最大化图标是三个图标中中间的那一个,看起来象两个小窗口。使用最大化图标可 以使用应用程序占满整个屏幕。如果选择了这个图标,其它应用程序窗口都会被盖 住。 垂直滚动条 如果有必要,应用程序可以显示一个垂直滚动条。垂直流动条显示在应用程序窗口 的右边,在两端有两个方向相反的箭头。它还有一个着色的棒和一个透明的窗口块。 后者被用于显示当前显示内容与整个文档(着色的棒)的关系。你可以用滚动条来选 择显示哪一页。一般在任何一个箭头上单击一下会使显示内容移动一行。单击向上 箭头下方的窗口块并拖动它会使屏幕输出快速更新到应用程序屏幕输出的任意位 置。 水平滚动条 也可以显示一个水平滚动条。水平滚动条显示在窗口的底部,具有与垂直滚动条类 似的功能。你用它来选择要显示哪些列。一般在任何一个箭头上单击一个会使显示 内容移动一列。单击向左箭头右边的窗口块并拖动它会使屏幕输出快速更新到应用 程序屏幕输出的任意位置。 菜单条 一个可选择的菜单条可以显示在标题条的下方。通过菜单条来选择菜单和子菜单。 这种选择可以通过用鼠标单击,也可以用热键组合来实现。热键组合经常是 ALT 与命令中带下划线的字母的组合,比如 File 命令中的“F”。 用户区 通常用户区占据了窗口最大的部分。这是应用程序的基本输出区域。应当由应用程 序来复杂管理用户区。另外,应用程序可以输出到用户区。 第五节 windows 的类 窗口的基本组件有助于说明应用程序的外观。有的时候应用程序需要创建两个外观 和表现都相似的窗口。Windows 的 Paint 就是一个例子。借助于同时运行 Paint 的 两个实例(或拷贝),Paint 允许用户剪贴或拷贝图片的一部分。然后信息就可以从 一个实例拷贝到另一个实例。Paint 的每个运行实例的外观和表现都与其他的相同。 这就需要每个实例创建自己的外观和功能类似的窗口。 在这种情况下被创建的外观和功能都很类似的窗口被称为是属于同一个窗口类的。 但是,你创建的窗口可以有不同的特征。它们可以有不同的大小,不同的位置,不 同的颜色或不同的标题,也可以使用不同的光标。 每个被创建的窗都基于一个窗口类。在用 C 语言开发撕于的基于传统的函数调用 方式的应用程序中,一些窗口为在 Windows 应用程序初始化的进修注册。你的应 用程序可以注册属于自己的窗口类。为了能够使几个窗口在同一个窗口类的基础上 创建,Windows 定义了一些窗口特征,如 CreateWindows()的参数,另一些定义 的窗口类的结构。当你注册一个窗口类的时候,这个类可以被 Windows 下运行着 的任何程序所使用。对于使用 MFC 的应用程序来说,多数注册工作已经由预定义 的对象完成了。 具有相似的外观和表现的窗口可以被组合成一个类,以此来减少需要维护的信息。 因为每个窗口类都有自己的可共享的类结构,不需要复制不必要的窗口类参数。同 时,同类的两个窗口使用相同的函数以及相关的例程。这样可以节省时间和空间, 因为不存在代码复制。 第六节 windows 中的面向对象编程 在 Windows 下传统的 C 程序吸收了一些面向对象编程的特性。对象是一种包含数 据结构和对这些数据结构进行操作的函数的抽象数据类型。而且,对象接收会引起 它们不同动作的消息。 比如,一个 Windows 的图形对象是可以被作为一个实体来操纵的一些数据的集合, 对于用户它是可视界面的一部分。特别地,一个对象意味这数据和数据的功能。菜 单、标题条、控制块以及滚动条等都是图形对象的例子。下一部分描述一些影响应 用程序外观的新的图形对象。 图标 图标是用来使用记住特定操作、想法或产品的小图形对象。比如,一个电子表 格程序被最小化时可以显示一个很小的柱状图以提醒用户这个程序还在运行之中。 在柱状图上双击鼠标会使 Windows 激活这个应用程序。图标是非常有力的工具。 它很适合用来引起用户的注意,比如在发出错误警告或者是向用户提供选择时。 光标 光标是 Windows 用来跟踪指点设备的运动的图形符号。这种图形符号可以改 变形状以指明特定的 Windows 操作。比如,当标准的箭头光标变为沙漏光标时说 明 Windows 正在执行一个命令,需要暂停。 编辑光标 应用程序在窗口中显示编辑光标以告诉用户在哪儿输入。编辑光标与其他屏幕 符号显然不同,因为它是闪烁的。多数时候,鼠标输入与光标相连,而键盘输入与 编辑光标相连。但是,可以用鼠标来改变编辑光标的输入点。 消息框 消息框是另一类 Windows 图形对象。消息框是一种包含标题、图标和消息的 弹出式窗口。图(?)是关闭 Windows Notepad 程序时出现的一个标准的消息框。 ------------------------------------------------------------------------- | | ------------------------------------------------------------------------ Windows 的对话框 对话框与消息框相似的地方在于它也是一种弹出式窗口。但是对话框主要用于 接受用户输入而不仅仅是显示一些输出。对话框允许应用程序接受输入,每次一个 域或是一个框的内容,而不是每次一个字符。图(?)显示了一个典型的 Windows 对 话框。对知框的图形设计由 Windows 为你自动完成。对话框的布局通常用编译器 中的资源编辑器完成。 ----------------------------------------------------------------------- | | ----------------------------------------------------------------------- 字体 字体是一种图形对象或资源,它定义了完整的字符集合的字样。这些字符都有 一个特定的大小和风格,可以使文本具有不同的外观。字样是字符的一种基本属性, 它定义了字符的衬线和笔画宽度。 位图 位图是一种显示图片(按像素组织),存储于内存。当应用程序需要快速显示图片时 可以使用位图。因为位图直接从内存中传送,所以它比用程序重新画出图片要快得 多。位图有两个基本用途。首先,它可以在屏幕上显示图片。其次位图也用于创建 刷子。刷子使你可以在屏幕上画出并填充对象。 使用位图有两个缺点。首先,与其尺寸有关,位图会占据难以预估的大量内存。 每个被显示的像素都要在内存中占据相应的空间。在彩色显示器上显示一个像素会 比在单色显示器上占据更多的内存。在单色显示器上,只需一位(bit)就可以表示出 像素的状态。可是在可以显示 16 种颜色的彩色显示器上,需要四位才能表示一个 像素的特征。同样地,随着显示设备分辨率的增加,位图对内存的需求也增加了。 位图的另一个缺点是它只包括静态的图片。比如,如果用位图来代表一辆汽车,就 没有办法来访问图片的不同部分,如轮踏、顶盖、窗等。但是,如果汽车是有一系 列基本绘图例程来生成的,应用程序就可以改变向这些例程传送的数据从而改变图 片的不同部分。例如,应用程序可以修饰顶蓬线并把一辆轿车变为敞蓬车。 画笔 当 Windows 在屏幕上显示一个图形时,它使用当前画笔的信息。画笔用于画 出线条或轮廊。画笔具有三个基本特征:线宽、线型(虚线、短线、实线)以及颜 色。Windows 永远保留着用于画白线和黑线的画笔,任何应用程序可以使用它。 你也可以创建自己的画笔。 刷子 Windows 用刷子来画出颜色并以预定义的样式来填充一个区域。刷子至少有 8× 8 个像素大小。刷子有三个基本特征:样式和颜色。由于它们至少有 8× 8 的大 小,刷子被称作具有样式而不象画笔,称为线型。样式可以是纯的颜色,也可以是 阴影线、斜线或其它用户自定义的组合 第七节 windows 的消息 Windows 的消息 在 Windows 中,应用程序并不直接写屏幕、处理硬件中断或直接对打印机输 出。相反,应用程序使用合适的 Windows 函数或者等待一个适当的消息被发出。 Windows 消息系统负责在多任务环境中分派消息。从应用程序的角度来看, 消息可以看作是发生的事件的通知,有些需要作出特定的反应,有些就不需要。这 些事件可能由用户产生,比如按下了鼠标或移动了鼠标,改变了窗口的大小或者选 择了一个菜单。同时,这些事件也可能由应用程序本身所产生。 这个过程使你的应用程序必须完全面向消息处理。当接收到消息时,应用程序 必须能激活并决定正确的动作,完成这个动作之后回到等待状态。 通过检查消息的格式和来源,下一部分将更仔细地讨论消息系统。 消息的格式 消息通知一个应用程序发生了一个事件。从技术上来讲,消息不仅仅是与应用 程序相关,而且是与应用程序的某一特定窗口有关。因此,所有的消息都被发往窗 口。 在 Windows 下只有一个消息系统-即系统消息队列。但是,每个正在 Windows 下运行的应用程序都有它自己的消息队列。系统消息队列中的每个消息最终都要被 USER 模块传送到应用程序的消息队列中去。应用程序的消息队列中存储了程序的 所有窗口的全部消息。 不管消息具有什么类型,它们都有四个参数:一个窗口句柄,一个消息类型, 两个附加的 32 位参数。窗口消息中定义的第一个参数是消息所关联的窗口句柄。 在编写 Windows 应用程序的时候经常使用句柄。句柄是一个唯一的数字,它 被用于标识许多类型的对象,如菜单、图标、画笔和刷子、内存分配、输出设备甚 至窗口实例。在 Windows 95 和 Windows NT 下面,程序的每个运行着的拷贝叫做 实例。 因为 Windows 95 和 Windows NT 允许你同时运行一个程序的多个实例,操作 系统就有必要保持对这些实例的追踪。这是通过赋予每个运行实例一个唯一的实例 句柄来实现的。 实例句柄通常被用作一个内部维护着的表的索引。通过引用表中的元素而不是 实际的内存地址,Windows 95 和 Windows NT 可以动态地调整所有的资源,而只 需在此资源所对应的表格位置中插入一个新的地址。 根据一个应用程序的多个实例被处理的方式,内存资源由 Windows 95 和 Windows NT 保存。 应用程序的实例具有很重要的作用。应用程序的实例定义了程序的函数所需的 所有对象。这包括控件、菜单、对话框以及更多的新 Windows 类。 消息中的第二个参数是消息类型。这是在 Windows 独有的一些头文件中定义 的标识符。这些头文件可以通过 WINDOWS.H 来使用。在 Windows 下,每个消息 由两个字符的助记符开始,跟着是下划线,最后是一个描述符。 最后的两个参数提供了解释消息所需的附加信息。因此最后两个参数的内容依 赖于消息的类型。 产生消息 消息传送概念使 Windows 能够实现多任务。消息有四个基本来源。应用程序 可以从用户那儿接受消息,也可以是 Windows 本身,应用程序本身或者是其它应 用程序。 用户消息包括按键消息、鼠标移动、鼠标指点或单击、菜单选择、滚动条的定 位等。应用程序必须花费大量的时间来处理用户消息。用户产生的消息表明运行程 序的人希望改变应用程序的表现方式。 无论何时,如果状态发生改变,将会有一个消息被发往应用程序。一个例子是 用户单击了应用程序的图标,表明他们想要将此应用程序变为活动的应用程序。在 这种情况下,Windows 告诉应用程序它的主窗口被打开了,它的大小和位置被改 变了等等 Windows 产生的消息可以被处理,也可以被忽略,这跟应用程序当前的 状态有关。 相应消息 在传统的面向过程的 C 语言 Windows 应用程序中,对于遇到的每一种消息,它都 有一个相应的过程来处理这消息。不同的窗口对相同的消息会产生不同的响应。 Windows 把每个消息发送到应用程序的不同窗口,而不同的窗口对相同的消息会 有不同解释。不令应用程序需要不同的过程来处理每一种消息,每一个窗口也应有 不同的过程来处理不同的消息。窗口过程集合了应用程序的所有消息处理过程。 消息循环 所有 Windows 应用程序的一个基本组成就是消息处理循环。每一个 C 应用程序都 在内部执行这个操作。C 应用程序包含了创建并初始化窗口的过程,随后是消息处 理循环,最后是结束应用程序所需的一些代码。消息循环负责处理 Windows 发给 主程序的消息。在这儿,程序知道有了消息,并且要求 Windows 将消息发送到合 适的窗口过程以供处理。当消息被接受时,窗口过程就执行希望的动作。 第八节 windows 的函数 Windows 向应用程序开发人员提供了数以百计的函数。这些函数的例子包括 DispatchMes-sage(),PostMessage(),RegisterWindowMessage()以及 SetActiveWindow()。对于使用基础类库的 C++程序员,许多函数自动被运行。 在 16 位的 Windows 3.x 下的函数声明包括一个 pascal 修饰符,这在 DOS 下更为 有效 Windows95 和 Windows NT 下的 32 位应用程序不再使用这个修饰符。如你 所知,所有 Windows 函数的参数是通过系统来传递的。函数的参数从最右边的参 数开始向左压入栈,这是标准的 C 方式。在从函数返回之前,调用过程必须按原 来压入栈的字节数调整栈指针。 第九节 windows 应用程序框架 Windows 头文件:WINDOWS.H WINDOWS.H 头文件(以及其它相关文件)是所有程序的内在部分。传统上, WINDOWS.H 是所有 C 语言编写的 Windows 应用程序必需的一部分。当在 C++ 中使用基础类库时,WINDOWS.H 包括在 AFXWIN.H 头文件中。 Windows 应用程序的组成 在开发 Windows 应用程序的过程中有一些重要的步骤: *用 C 语言编写 WinMain()函数和相关的窗口函数,或者在 C++中使用基础类, 比如 CWinApp 等。 *创建菜单、对话框和其它资源并把它们放入资源描述文件。 *(可选)使用 Vinsual C++编译器中的企业编辑器来创建对话框。 *(可选)使用 Vinsual C++编译器中的企业编辑器来创建对话框。 *用项目文件来编译并链接所有的 C/C++源程序和资源文件 Windows 应用程序中的组成部分 1. WinMain()函数 Windows 95 和 Windows NT 需要一个 WinMain()函数。这是应用程序开始执行和 结束的地方。 从 Windows 向 WinMain()传递四个参数。下面的代码段演示了这些参数的使用: int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPreInst, LPSTR 1pszCmdLine, int nCmdShow) 第一个参数 hInst 包含了应用程序的实例句柄。当应用程序在 Windows 下运行时, 这个数字唯一标识了应用程序。 第二个参数 hPreInst 将始终是一个 NULL 值,表明没有这个应用程序的其它实例 正在运行,因为在 Windows 95 和 Windows NT 下每个应用程序都在它自己单独的 地址空间中运行。 第三个参数 1pszCmdLine 是指向一个以'/0'结尾的字符串的长指针,这个字符串代 表了应用程序的命令行参数。 WinMain()的第四个参数是 nCmdShow。在 nCmdShow 中存储的整数代表了 Windows 预定义的许多常量中的一个,它决定了窗口显示的方式。 2. WNDCLASS WinMain()负责注册应用程序的主窗口类。每个窗口类都建立在一些用户选择的风 格、字体、标题字、图标、大小、位置等的基础上。窗口类实际上是定义这些属性 的一个模板。 基本上,所有的 Windows 类定义都使用相同的标准 C/C++结构。下面的例子 是一个说明 WNDCLASSW 结构的 typedef 语句,WNDCLASS 是从这儿继承的: typedef struct tagWNDCLASSW UINT style; WNDPROC 1pfnWndProc; int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hCursor; HBR8USH hbrBackground; LPCWSTR 1pszMenuName; LPCWSTR 1pszClassName; WNDCLASSW,*PWNDCLASSW,NEAR*NPWNDCLASSW, FAR*LPWNDCLASSW; 下面的部分讨论了 WNDCLASS 结构中的不同的域。其中有些域可以被赋予 NULL, 告诉 Windows 使用缺省的预定义值。 style:style 域指明了类风格。 1pfnWndProc:接受一个指向窗口函数的指针,它将执行所有的窗口任务。 cbClsExtra:指定了必须在窗口类结构后面分配的字节数,它可以是 NULL。 cbWndExtra:指定了必须在窗口实例后面分配的字节数,它可以是 NULL。 hInstance:定义了注册窗口类的应用程序实例。它必须是一个实例句柄,不 得是 NULL。 hIconhIcon:划定利用窗口最小化时显示的图标。它可以是 NULL。 hCursorhCursor:定义了应用程序使用的光标。这个句柄可以是 NULL。 hbrBackground:提供了背景刷子的标识符。 1pszMenuName:是指向一个以空字符结尾的字符串的指针。这个字符串是菜单的 资源名。这一项可以为 NULL。 1pszClassName:是指向一个以空字符结尾的字符串的指针。这个字符串是窗口类 的名字。 3.WNDCLASSEX Windows 提供了一种扩展的 WNDCLASS 定义,名为 WNDCLASSEX,它允许应用 程序使用小图标。下面是 WNDCLASSEX 结构的定义: typedef struct WNDCLASSEX UINT style; WNDPROC 1pfnWndProc; int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hbrBackground; LPCTSTR 1pszMenuName; LPCTSTR 1pszClassName; HICON hIconSm; WNDCLASSEX; 你可以看到这两个结构是相同的,除了 WNDCLASSEX 包括了 hIconSm 成员,这 是与窗口类有关的小图标的句柄。 4.定义窗口类 应用程序可以定义它们自己的窗口类,只要先定义一个合适类型的结构,然后用窗 口类的信息来填充结构的域。 下面的代码示范了如何定义并初始化一个 WNDCLASS 结构。 char szProgName[]="ProgName"; . . . WNDCLASS wcApp; . . . wcApp.1pszClassName=szProgName; wcApp.hInstance=hInst; wcApp.1pfnWndProc=WndProc; wcApp.hCursor=LoadCursor(NULL,IDC-ARROW); wcApp.hIcon=NULL; wcApp.1pszMenuName=szAppIName; wcApp.hbrBackground=GetStockObject(WHITE-BRUSH); wcApp.style=CS-HREDRAW| CS-VREDRAW; wcApp.cbClsExtra=0; wcApp.cbWndExtra=0; if(!RegisterClass (&wcApp)) return 0; WNDCLASS 结构中的第二个域是 wcApp.hInstance,它被赋予了 WinMain()被激活 后返回的 hInst 的值。这指明了应用程序的当前实例。1pfnWndProc 被赋予执行所 有窗口任务的窗口函数的指针地址。对于大部分应用程序,这个函数叫做 WndProc()。 注意:WndProc()是一个用户定义而不是预定义的函数名。在赋值语句之前必 须给出函数原型。 wcApp.hCursor 域被赋予实例的光标句柄。 当 wcApp.1pszMenuName 被赋予 NULL 值的时候,Windows 就认为这个窗口类没 有菜单。 如果有,菜单必须有一个名字,它必须出现在引号里面。GetStockOject() 函数返回一个刷子句柄,用于在这个类创建的窗口用户区中画出背景色。 wcApp.style 窗口类风格被设为 CS-HREDRAW 或 CS-VREDRAW。 最后的两个域,weApp.cbClsExtra 以及 wcApp.cbWndExtra 经常被设为 0。这些域 可以被选用以指明窗口结构和窗口数据结构后面应该保留的附加字节数。 下面这段代码用于注册窗口类: if(!hpreInst) . . . if(! RegisterClass(&wcApp)) return FALSE; Windows 95 和 Windows NT 通过检查 hPreInst的值来确定多少个实例,而 hPreInst 总是 NULL,所以就注册窗口类. 5.创建窗口 窗口通过调用 CreateWindow()函数来创建。这个过程对所有版本的 Windows 都是 一样的。窗口类定义了窗口的一般特征,允许同一个窗口类被用于多个不同的窗 口,CreateWin-dow()函数的参数指明了关于窗口的更详细的信息。 CreateWindow()函数的参数信息包括以下内容:窗口类、窗口标题、窗口风格、 幕位置、窗口的父句柄、菜单句柄、实例句柄以及 32 位的附加信息。在大部分应 用程序中 ,这个函数会是下面这个样子: hWnd=CreateWindow(szProgName,"Simple Windows Program", WS-OVERLAPPEDWINDOW,CW-USEDEFAULT, CW-USEDEFAULT,CW-USEDEFAULT, CW-USEDEFAULT,(HWND)NULL,(HMENU)NULL, (HANDLE)hInst,(LPSTR)NULL); 第一个域 szProgName(已赋过值)定义了窗口的类,后面是窗口标题条上使用的标 题。窗口的风格是第三个参数 下面的六个参数代表了窗口的 x、y 坐标和 x、y 方向的大小,然后是父窗口句柄和 窗口菜单句柄。每个域都被赋予一个缺省值。hInst 域包含了程序的实例句柄,后 面是一个附加参数(NULL)。 显示和更新窗口 在 Windows 下,ShowWindow()函数被用来实际显示一个窗口。下面的代码 示范了这个函数: Show Window(hWnd,nCmdShow); 在调用 CreateWindow()时生成的窗口句柄被用作 hWnd 参数。ShowWindow()的 第二个参数是 nCmdShow,决定了窗口被如何显示。这个显示状态也被称为窗口 的可视状态。 显示窗口的最后一步是调用 Windows 的 Update Window()函数。 UpdateWindow(hWnd); 6.消息循环 一旦调用 Win-Main()函数并显示了窗口,应用程序就需要一个消息处理循环。最 常用的实现方法是使用一个标准的 while 循环: while (GetMessage (&lpMsg,NULL,0,0)) { TranslateMessage(&lpMsg); DispatchMessage(&lpMsg); } GETMESSAGE()函数:应用程序要处理的下一个消息可以通过调用 Windows 的 GetMessage()函数来取得。 NULL 参数指示函数取回这个应用程序的任何窗口的任何消息。最后两个参数 0 和 0 告诉 GetMessage()不要使用任何消息过滤器。消息过滤器能够将接收到的消息 限制在一个明确的范围之内,如键盘消息或鼠标消息等。 一般应用程序应该确认通向消息循环的所有步骤都已经正确地执行过了。这包括确 认每个窗口类都已经注册过,都已经被创建。否则,一旦进入了消息循环,只有一 个消息能够结束这个循环。无论何时处理了 WM-QUIT 消息,返回值是 FALSE。这 会引发主循环关闭例程。WM-QUIT 消息是应用程序退出消息循环的唯一途径。 TRANSLATEMESSAGE()函数:通过 TranslateMessage()函数,虚拟消息可以被转 换为字符消息。 DISPATCHMESSAGE()函数:Windows 通过 DispatchMessage()函数将当前的消息 发送到正确的窗口过程。 ******* 窗口函数 所有的应用程序都必须包括一个 WinMain()函数和一个回调窗口函数。因为一 Win-dows 应用程序从不直接访问任何窗口函数,每个应用程序都必须向 Windows 提出请求以执行规定的操作。 一个回调函数在 Windows 中注册,当 Windows 要对一个窗口进行操作时,它就被 调用。各个应用程序的回调函数的实际代码长度会大不相同。窗口函数本身可以非 常小,只处理一个或两个消息,也可以非常大而且复杂。 下面的代码段(不完整的应用程序说明语句)显示了在应用程序中的回调窗口函数 WndProc()的一个范例: LRESULT CALLBACK WndProc(HWND hWnd,UNIT messg,WPARAM wParam,LPARAM 1Param) HDC hdc; PAINTSTRUCT ps; switch(messg) case WM-PAINT: hdc=BeginPaint(hWnd,&ps); . . . ValidateRect(hWnd,NULL); EndPaint(hWnd,&ps); break; case WM-DESTROY: postQuitMessage(0); break; default: return(DefWindowProc(hWnd,messg,wParam,1param)); return(0); Windows 希望窗口类结构定义中 wcApp,1pfnWndProc 域的名字能够与回调函数 的名 字匹配。后面用这个窗口类创建的所有窗口的回调函数都应该用 WndProc()的名 字。 下面的代码段讨论一个窗口类结构中回调函数名的位置和赋值: . . . wcApp.1pszClassName=szProgName; wcApp.hInstance=hInst; wcApp.1pfnWndProc=WndProc; . . . Windows 有向百个消息可以发送给窗口函数。这些消息用“WM-”打头的标识符来 标识。 WndProc()的第一个参数是 hWnd。hWnd 包含了 Windows 发送消息的窗口句柄。 函数的第二个参数 messg 按 WINUSER.H 中的定义指明了即将被处理的实际消息。 最后的两个参数 wParam 以及 1Param,指明了处理每个消息所需的附加信息。 WndProc()函数继续定义了两个变量:hdc 指明了显示设备句柄,ps 指明了存储用 户区 信息所需的一个 PAINTSTRUCT 结构。 回调函数被用于检查将被处理的消息并选择执行适当的动作。这个选择过程通常在 一个标准的 C 语言的 switch 语句中完成。 模块定义文件 正如你在前面学到的,LINK 提供了所有 Windows 应用程序需要的模块定义文件在 命令行方式下的替代品。模块定义文件向链接器提供有关的定义和描述信息,这样 链接器就可以知道如何来组织 Windows 应用程序的可执行文件。这些信息已经成 为新执行文件格式的文件头的一部分。 注意:在 Windows 95 和 Windows NT 下面,你不太可能需要创建一个模块定义文 件。这些信息是为了完整性和向后兼容。 第十节 VC++提供的 windows 编程工具 Visual C++编译器包含几个资源编辑器。单独的编辑器可以通过编译器主菜单中的 Insert Resource 菜单来运行。图形对象都是资源,象图标、光标、消息框、对话 框、字体、位图、画笔、刷子等。资源代表应用程序的可执行文件中包含的数据。 资源编译器 RC.EXE 是一个 Windows 资源的编译器。。 资源以及附加的编译器的使用增加了应用程序开发的复杂性。但是它容易在项 目工具中使用。 项目文件 项目文件提供了概览资源和程序代码编译过程的手段,同时也可以使应用程序 的可执行版本保持最新。它们跟踪源文件的日期和时间以实现这些增强的功能。项 目文件包含了有关特定程序的编译链过程的信息。项目文件是在集成的 C 或 C++ 编辑环境中创建的。项目文件还支持增强的编译和链接。 资源 当你使用 VisualC++编译器提供的资源编辑器时,用自己的图标、指针和位图 来定制 Windows 应用程序非常容易。这些编辑器给你提供了一个开发图形资源的 完整环境。这些编辑器同时也能帮助你开发菜单和对话框-Windows 下数据输入 的基本手段。这些编辑器还能帮你操纵单独的位图、加速键和字符串。。 资源编辑器 每一种编辑器都在 VisualC++环境中提供,都是编译器的一个集成的部分。这 样,每种编辑器都是在 Windows 下运行的完全集成的资源开发工具。你可以通过 选择 Insert Resource 来启动每一种编辑器。 下面我们将通过教程演示资源编辑器的使用。请单击返回,然后运行教程。 ? 第十一节 MFC 的基本概念 基础类库为你提供了易于使用的对象。将 Windows 与 C++联系起来是很自然的, 这样就可以充分利用面向对象技术的优点。MFC 开发组实现了大量的 Windows 应 用程序编程接口(API)。这个 C++库在一些可重用的类中封装了最重要的数据结构 和 API 函数调用。 类似 MFC 这样的类库比起前面两章讨论的 C 程序员使用的函数库有很多优点。 下面列出了 C++类的一些优点,比如: *用类对数据和代码进行封装 *继承性 *消除函数和变量名的冲突 *类是语言的自然扩展 *通常,精心设计的库减少了代码量 利用基础类库,创建一个窗口所需的代码大约只占传统应用程序的三分之一。 这就可以使程序员只用花很少的时间与 Windows 打交道,把更多的精力集中在开 发自己的程序代码上。 22.2 MFC 的设计考虑 基础类库设计小组定义了严格的设计规则,在设计 MFC 库时必须遵循这些规 则。这些规则和方针如下: *利用 C++的威力,但不能把程序员吓倒 *使从标准 API 调用到类库的转换尽可能简单 *允许混合使用传统的函数调用和新的类库 *在设计类库的时候综合考虑功能和效率 *建成的类库必须能够方便地在不同平台间移植,如 Windows 95 和 Windows NT 设计小组感到要开发高质量的代码必须从 MFC 库本身开始。C++基础类库必须又 小又快。它的简单性使它易于使用,而执行速度与庞大的 C 函数库接近。 这些类的设计方式应该让熟练的 Windows 程序员不必重新学习各种函数的名 字。通过仔细的命名和设计可以实现这一点。Microsoft 认为这一点是 MFC 区别于 其它类库的一个特征。 MFC 小组还把基础类库设计为是允许以混合方式编程的。这就是说,在同一个源 文件里,既可以使用类也可以使用传统的函数调用。即使是在使用 MFC 时,类似 SetCursor()和 GetSystemMetrics()这样的函数还是需要直接调用。 Microsoft 也知道类库必须方便使用。其它厂商提供的一些类库设计得太抽象。按 Microsoft 的说法,这些笨重的类企图生成又大又慢的应用程序。MFC 库提供了合 理的抽象,保证代码很小。 开发小组将原始的 MFC 库设计为动态的而不是静态的。动态的结构是这些类可以 适应我们现在使用的 Windows 95 和 Windows NT 环境。 22.3 MFC 库的关键特性 从其它编译器厂商那儿也可以获得 Windows 类库,但 Microsoft 宣称他们的 MFC 类库具有许多真正的优点: *全面支持所有的 Windows 函数、控件、消息、GDI(图形设备接口)绘图原语、 菜单以及对话框。 *使用与 Windows API 相同的命名约定。因此,从名字上就可以直接知道类的 功能。 *消除了一个错误源,即大量的 switch/case 语句。所有的消息都被映射到类 的成员函数。这种消息-方法的映射方法应用于所有的消息。 *能够把对象的信息输出到文件,这提供了更好的诊断支持。同时还提供了验 证成员变量的能力。 *增强的例外处理设计,使得程序代码失败的可能性更小。能够解决“内存不足” 以及其它一些问题。 *可以在运行时决定数据对象的类型。这允许对类的域进行动态操纵。 *小而快速的代码。前面已经提到,MFC 库只添加了很少一些代码,执行起来 几乎与传统的 C 语言 Windows 应用程序一样快。 *对组件对象模型(COM)的支持。 有经验的 Windows 程序员会立刻喜欢上其中的两个特性:熟悉的命名约定和消息- 方法映射机制。如果你重新检查一下在第二十一章中开发的应用程序的源代码,你 会看到大量用于处理错误的 switch/case 语句。还应该注意这些应用程序调用了大 量的 API 函数。当你使用 MFC 库的时候,这两种现象都消失或减少了。 专业程序员肯定会欣赏在 MFC 库中实现的更好的诊断和很小的代码。现在程序员 就可以利用 MFC 库的好处而不必担心他们的应用程序的代码大小了。 最后,MFC 是唯一真正有用的类库。 22.4 一切从 CObject 类开始 类似 MFC 这样的类库通常都来自很少的几个基类。然后,另外的类就可以从这些 基类中继承而来。CObject 是在开发 Windows 应用程序时大量使用的一个基类。 在 MFC/INCLUDE 子目录下提供的 MFC 库头文件包括了许多类定义信息。 我们来简单地看一下,CObject,它在头文件 AFX。H 中有定义: /////////////// //class CObject is the root of all compliant objects class CObject public: //Object model(types,destruction,allocation) virtual CRuntimeClass*GetRuntimeClass () const; virtual~CObject();//virtual destructors are necessary //Diagnostic allocations void*PASCAL operator new(size-t nSize); void*pascal operator new(size-t,void*p); void PASCAL operator delete(void*p); #if defined(-DEBUG)&&!defined(-AFX-NO-DEBUG-CRT) //for file name/line number tracking using DEBUG-NEW void* PASCAL operator new(size-t nSize,LPCSTR 1pszFileName,int nLine); //Disable the copy constructor and assignment by default //so you will get compiler errors instead of unexpected //behavior if you pass objects by value or assign objects. protected: CObject(); private: CObject(const CObject& objectSrc);//no implementation void operator=(const CObject& objectSrc); //Attributes public: BOOL IsSerializable()const; BOOL IsKindOf(const CRuntimeClass*pClass)const; //Overridables virtual void Serialize (CArchive& ar); //Diagnostic Support virtual void AssertValid()const; virtual void Dump(CDumpContext& dc)const; //Implementation public: static const AFX-DATA CRuntimeClass classCObject; #ifdef-AFXDLL static CRuntimeClass*PASCAL-GetBaseClass(); #endif ; 为了清楚起见,对这段代码作了一些细微的改动。但和你在头文件 AFX.H 可以找 到的代码基本一样。 检查 CObject 的代码,注意构成这个类定义的成分。首先,CObject 被分为公有、 保护和私有三个部分。CObject 还提供了一般的和动态的类型检查以及串行化的功 能。回忆一下,动态类型检查使你可以在运行时确定对象的类型。借助于永久性的 概念,对象的状态可以被保存到存储介质中,比如磁盘。对象的永久性使对象成员 函数也可以是永久的,允许对象数据的恢复。 子类从基类继承而来。例如,CGdiObject 是一个从 CObject 类继承来的类。这儿 是 AFXWIN。H 中找到的 CGdiObject 类定义。同样,为了清楚起见,对其作了一 些改动。 ////////////////////// //CGdiObjet abstract class for CDC SelectObject class CGdiObject:public CObject DECLARE-DYNCREATE(CGdiObject) public: //Attributes HGDIOBJ m-hObject;//must be first data member operator HGDIOBJ()const; static CGdiObject*PASCAL FromHandle(HGDIOBJ hObject); static void PASCAL Delete TempMap(); BOOL Attach (HGDIOBJ hObject); HGDIOBJ Detach(); //Constructors CGdiobject();//must create a derived class object BOOL DeleteObject(); //Operations int GetObject (int nCount,LPVOID 1pObject)const; UINT GetObjectType()const; BOOL CreateStockObject(int nIndex); BOOL UnrealizeObject(); BOOL operator==(const CGdiObject& obj)const; BOOL operator!=(const CGdiObject& obj)const; //Implementation public: virtual~CGdiObject(); #ifdef-DEBUG virtual void Dump(CDumpContext& dc)const; virtual void AssertValid()const; #endif ; CGdiObject和它的成员函数允许在 Windows 应用程序中创建并使用绘画对象,如 自定义画笔、刷子和字体等。诸如 CPen 之类的类是进一步从 CGdiObject 类继承 而来的。 Microsoft 提供了 MFC 库的全部源代码,以尽可能地增加编程的灵活性。但是,对 于初学者,没有必要去了解不同的类是如何定义的。 例如,在传统的 C 语言 Windows 应用程序中,DeleteObject()函数按下面的语法 调用: DeleteObject(hBRUSH);/*hBRUSH is the brush handle*/ 在 C++中,利用 MFC 库,可以按下面的语法访问类成员函数以实现同样的目的: newbrush.DeleteObject();//new brush is current brush 正如你可以看到的,从 C 语言 Windows 函数调用转向类库对象是简单的。Microsoft 在开发所有 Windows 类的时候都使用这种方法,使得从传统函数调用到继承类库 对象的转移非常简单。 游戏开发基础(3) 第三章 DirectX SDK 简介 第一节 关于 DirectX SDK Microsoft DirectX 提供了一套非常优秀的应用程序接口,包含了设计高性能、实时 应用程序的源代码。DirectX 技术将帮助您建构下一代的电脑游戏和多媒体应用程 序。它的内容包括了 DirectDraw、DirectSound、DirectPlay、Direct3D 和 DirectInput 等部分,它们分别主要应用在图形程序、声音程序等方面。 由于 DirectX,使在 Windows 下运行应用程序的性能可以与在 DOS 或游戏平台下 运行的应用程序性能相媲美,甚至超过它们。它将为您的 Windows 游戏开发提供 一个具有鲁棒性的、标准化的操作环境。 DirectX 包括两部分:运行期部分(Runtime)和 SDK。在 DirectX 开发时,这两部 分都要用到,但在 DirectX 应用程序运行时只用运行期部分。在 Windows NT 4.0 及以上版本中含有 DirectX 运行期部分,Win95 则没有。但 Win95 可以很容易获得 DirectX 运行期部分。而 Windows NT 4.0 以前的版本不能运行 DirectX 程序。许多 基于 DirectX 的应用程序和游戏都包含了 DirectX 运行期部分。 它目前有五个版本:1、2、3、5 和 6(没有版本 4)。不同版本具有不同的运行 期部分,但新版本的运行期部分可与旧版本的应用程序配合,即向上兼容。当前大 部分流行的游戏都是基于版本 5 开发的。 第二节 DirectX5 SDK 的获得 DirectX SDK 包括开发 DirectX 应用程序所需要到的全部示例和帮助文件,但这些 都是可 选资源,必须的文件是头文件(.h文件)和库文件(.lib 文件)。获得 DirectX SDK 比获得运行期部分要困难一些。Windows NT 4.0 和 Win95 都不带 DirectX SDK,要 获得 SDK 可通过以下 3 种办法: * 购买 Visual c++5.0(包括 DirectX SDK) * 访问 Microsoft Web 站点的 DirectX 下载页 * 成为 MSDN(Microsoft 开发网络)用户 SDK 也可以从 Microsoft Web 站点上获得,下载量很大,尤其是在拨号连接时,有可 能需要一整夜的时间。 成为 MS DN 用户是获取 SDK 的好办法,除非您反对通过 Microsoft 付费的方式获得 升级及其操作系统的程序开发特权。SDK 由 MS DN level 2 及以上提供。 第三节 元件对象模型(COM) DirectX 根据 Microsoft 的 COM(Component Object Model,即元件对象模型)规格 得以实现。COM 设计成用来提供完全便携的、安全的、可升级的软件结构。COM 使用一个面向对象的模型,这要比像 C++等语言所用的模型更严格。例如,COM 对象经常通过成员函数进行存取,而且并不具备公共数据成员。COM 对继承性的 支持与 C++相比也是有限的。 很多 DirectX 中 API 是作为 COM 对象的实例来创建的。您可以这样看:对象就象 一个黑盒子,它代表了硬件,从而需要通过一个接口与应用程序进行联络。通过 COM 接口发送和接收的命令被称为“方式(method)”。例如,方式 IDirectDraw2::GetDisplayMode 是通过接口 IDirectDraw2 发送,从而能够从 DirectDraw 对象得到当前显示适配器的显示模式。 COM 对象提供实际函数性,而 COM 接口则提供存取函数性的方法。COM 对象不 能被直接存取,相反,所有的存取者通过接口来完成。这条规则是如此强有力的得 到遵守,以致于我们不能给任何 COM 对象以名称。我们只能给用来存取对象的接 口名称。 所有的 COM 接口都是从 IUnknown 接口中衍生出来的。“I”标志经常用于命名 COM 接口(它代表 Interface 即接口)。 IUnknown 接口提供 3 个成员函数,所有 COM 接口因而继承这些函数: ●AddRef():使对象内部引用值加一。例如,当您创建一个新的接口时,构造函数 将自动调用 AddRef()。 ●QueryInterface():让 COM 对象就它们是否支持特定接口进行查询。例如,升级 的 COM 对象提供附加的接口,而非现有接口的修改版。QueryInterface()函数可以 用来确定旧的接口,以决定新的接口是否被支持。如果被查询的对象不支持有问题 的接口,则更替接口的指针就返回。 ●Release():使对象内部引用值减一。您应该在接口的指针将要超出范围时或者要 通过使用接口指针来结束时使用这个函数。 对象的每一个 DirectX 接口都代表了一种设备,例如 IDirectDraw2、IDirectSound 和 IDirectPlay。DirectX 对象模型象征性地为每一个设备提供了一个主对象,其它 所支持的功能对象由主对象衍生而来。例如,DirectDraw 对象代表了显示适配器, 您可以由它创建 DirectDrawSurface 对象来代表显存。同样地,DirectSound 对象 代表声卡并可以创建 DirectSoundBuffer 对象代表声卡上的声音资料。 在 C 程序中可以调用任意的 COM 接口方式。下面的例子创建一个平面,使用 DirectDraw 对象的 IDirectDraw2::CreateSurface 方式: ret = lpDD->lpVtbl->CreateSurface (lpDD, &ddsd, &lpDDS, NULL); 这里 lpDD 表示与新建平面相关的 DirectDraw 对象。该方式填充一个平面类型的 结构(&ddsd)并返回一个指向平面的指针(&lpDDS)。 同样功能用 C++的实现为: ret = lpDD->CreateSurface(&ddsd, &lpDDS, NULL) 第四节 DirectDraw DirectDraw 是 DirectX SDK 中的一个组件,它允许用户直接地操作显存,支持硬件 覆盖、内存与显存反转。DirectDraw 在提供这些功能的同时并支持基于 Microsoft Windows 的应用程序和设备驱动。DirectDraw 作为一种软件接口,支持 Windows 的 GDI(图像设备接口),并提供直接操作显示设备的功能。它提供了与设备无关 的游戏、Windows 子系统等软件开发方式。 它还能够支持许多种显示硬件,包括从简单的 SVGA 显示器到高级硬件性能(支持 图像剪切、延伸和非 RGB 颜色格式)。接口的设计使您的应用程序能够知道所使 用硬件的特性,从而可以支持它们的硬件加速功能。 关于 DirectDraw,在以后还将有更加详细的讲解。 第五节 DirectSound Microsoft DirectSound 的 API 是 DirectX 平台软件开发工具(S DK)程序员指南的 声音组件。DirectSound 提供了硬件加速、对声音设备的直接操作。在提供这些功 能的同时,它还保持了与当前设备驱动的兼容性。 新版本的 DirectSound 能够在运行中查询硬件性能以确定最优的操作方式,还具有 抓获声音、低延迟混音等功能特性。它的新功能还包括支持属性集(Porperty Sets), 从而即使 DirectSound 不直接支持新硬件的特性,它也能够利用并受益。 DirectSound 是通过“硬件抽象层(HAL)”来操作声音硬件的,这是一个由声音设 备驱动来实现的接口。HAL 提供了以下功能: 要求和释放对声音硬件的控制; 描述声音硬件的特性; 在硬件允许的条件下实现特定的操作; 在硬件不允许的时候报告操作失败。 DirectSound 能够自动地利用硬件加速,包括硬件混音和硬件声音缓冲。您的应用 程序无须查询硬件和程序特性,而可以在运行期部分查询 DirectSound 来获得对声 音设备特性的充分描述,然后根据各个特性的存在或缺少来使用不同的方法进行优 化。您还可以指定接受硬件加速的声音缓冲区。 下图显示了 DirectSound 与系统中其它声音组件的关系: 关于 DirectSound,在以后还将有更加详细的讲解。 第六节 DirectPlay DirectPlay 的主要作用在于使应用程序间通讯方式得以简单化。DirectPlay 的技术 不仅提供了一个方案,实现了通讯与传送协议及在线服务的无关性,而且还实现了 与传输服务器和游戏服务器的无关性。万一所基于的网络不支持某一方式, DirectPlay 包含了必要的代码来仿效它。 DirectPlay 的 service provider 结构使应用程序与网络隔绝开来。应用程序通过查 询 DirectPlay 来获得所基于网络功能特性(例如延迟、带宽等),从而相应地调整 通讯方式。下面的图显示了 DirectPlay service provider 的结构: 使用 DirectPlay 的第一步是选择使用哪一种 service provider,它决定了将用于通 讯的网络或协议。协议可以是遍布 Internet 的 TCP/IP、局域网中的 IPX 或两台计 算机间的连接电缆。 DirectPlay 提供了两种很有用的连接管理方式: IDirectPlay3::EnumConnections 列举了应用程序可以获得的所有连接; IDirectPlay3::InitializeConnection 初始化一种特定的连接。 在确定了网络的连接后,为了方便理解下面的内容,先来看看这样几个概念: 首先从 session 讲起。DirectPlay session 是若干台计算机之间的通讯通道。一个应 用程序只有在成为了某个 session 的一部分后才能够开始与其它计算机进行通讯。 有两种方法可以实现:列举一个网络中所有存在的 session 然后加入其中的一个; 或新建一个 session 并等待其它计算机的加入。当应用程序成为 session 的一部分 后,它就可以创建一个 player 并与 session 中其它的 player 相互传递信息。 每一个 session 中都有一台计算机作为主机。主机是 session 的所有者并拥有唯一 的改动 session 属性的权力。 下图显示了一个 session 模型: Session 有两种类型:对等类型和客户/服务器类型。 Player 是 session中逻辑概念上的对象,能够发送和接收消息。在 DirectPlay Session 中没有对实体计算机的表示方法。Player 可以作为一个本地 player(在您自己的计 算机上),或作为一个远程 player(在其它计算机上)。只有在拥有了至少一个 player 后,计算机才能够发送和接收消息。每一台计算机可以拥有不止一个本地 player。 Group 是在逻辑概念上是一些 player 的集合。通过创建一个 group,一个应用程序 可以向它发送消息使其中所有的 player 都收到。DirectPlay 提供了管理 group 和成 员关系的方式。 下图显示了一个 session 内容的逻辑结构: 假如,一台计算机在完成了连接之后,就可以调用 IDirectPlay3::EnumSessions 列 举所有可以得到的 session。然后,IDirectPlay3::Open 根据 session cache 中的内 容加入一个 session,或新建一个 session。 接下来,可以创建 player 和 group。而在游戏中,更多的是通过 Message Managment 中的各个方式进行频繁地、大量的消息传送。 在不同类型的 session 中,消息的流向是不同的。在对等类型中,消息的传送是各 个 player 之间直接进行的。对于使用 multicast 向 group 中传送消息时,则要在 group 中指定一个 multicast server,通过它再将消息传送给 group 中其它的计算 机。 在客户/服务器类型的 session 中,所有的 player 只与服务器进行直接通讯,再由 它与其它各个客户机进行消息传送。下图表示了这样的关系: 第七节 DirectInput DirectInput 用以支持包括鼠标、键盘和游戏杆等在内的输入设备,甚至还有力度 反馈的高级输入/输出设备。它也是基于 COM 的。 DirectInput 对象由输入设备提供数据。每一个相应设备都有所谓“对象实例”,即 个别的控件,如按键、按纽和游戏杆的轴向等。键盘上的每一个按键都可以作为键 盘对象的一个对象实例。即使程序在后台运行,DirectInput 也能够对输入设备做 出响应。 一般来说,先要创建一个 DirectInput 对象(通过 DirectInputCreat 方式),表示 一个 DirectInput子系统。然后,列举出系统中可以得到的输入设备,再为这些个 别设备创建 DirectInputDevice 对象(通过 IDirectInput::CreateDevice 方式)。这 种对象的方式用于获取关于设备的信息,设置它们的属性并从它们那里得到数据。 下面给出一个使用鼠标的例子。您可以在光盘 Example 目录下的 Scrawl.c 文件中 找到这些代码。 首先创建 DirectInput 鼠标设备 // LPDIRECTINPUT g_pdi; // 已被初始化 LPDIRECTINPUTDEVICE g_pMouse; HRESULT hr; hr = g_pdi->CreateDevice(GUID_SysMouse, &g_pMouse, NULL); if (FAILED(hr)) { Complain(hwnd, hr, "CreateDevice(SysMouse)"); return FALSE; } CreateDevice 方式有三个参数:第一个是全局独有标志符 GUID_SysMouse,这里 表明为鼠标;第二个是 IDirectInputDevice 接口指针类型,如果这个调用成功,它 就指向可用的鼠标;一般不使用 COM 聚合体,第三个参数为 NULL。 然后,设置鼠标信息的格式: hr = g_pMouse->SetDataFormat(&c_dfDIMouse); if (FAILED(hr)) { Complain(hwnd, hr, "SetDataFormat(SysMouse, dfDIMouse)"); return FALSE; } 设置鼠标行为: hr = g_pMouse->SetCooperativeLevel(hwnd, DISCL_EXCLUSIVE | DISCL_FOREGROUND); //后面的两个标志符实现了前台运行时对输入设备的独占 if (FAILED(hr)) { Complain(hwnd, hr, "SetCooperativeLevel(SysMouse)"); return FALSE; } 要使用事件通告(event notification)来发觉鼠标的动作并将经过缓冲的鼠标输入 读进来,这都需要进行一些设置。首先,创建一个事件,将它与鼠标联系起来。当 发生硬件中断时,就可以告诉鼠标设备对象有新的数据来到。 // HANDLE g_hevtMouse; // 全局变量 g_hevtMouse = CreateEvent(0, 0, 0, 0); if (g_hevtMouse == NULL) { Complain(hwnd, GetLastError(), "CreateEvent"); return FALSE; } hr = g_pMouse->SetEventNotification(g_hevtMouse); if (FAILED(hr)) { Complain(hwnd, hr, "SetEventNotification(SysMouse)"); return FALSE; } 现在您需要设定缓冲区大小。在此之前要初始化一个 DIPROPDWORD 结构,这当 中很多数量是无关紧要的,关键是最后一个,dwData,它决定了缓冲区中可容纳 项目的数量。 #define DINPUT_BUFFERSIZE 16 DIPROPDWORD dipdw = { // the header { sizeof(DIPROPDWORD), // diph.dwSize sizeof(DIPROPHEADER), // diph.dwHeaderSize 0, // diph.dwObj DIPH_DEVICE, // diph.dwHow }, // the data DINPUT_BUFFERSIZE, // dwData }; 接着按照您希望改变的属性的标志符,将头地址传送给 IDirectInputDevice::SetProperty 方式: hr = g_pMouse->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph); if (FAILED(hr)) { Complain(hwnd, hr, "Set buffer size(SysMouse)"); return FALSE; } 至此,设置结束。然后开始根据程序的要求操作鼠标。 第八节 Direct3D Direct3D 提供了与硬件无关的的 3D 视频硬件操作方式。您可以在两种模式(立即 模式和保留模式)中选择其中之一使用。 立即模式较保留模式是一个低层的 3D API,后者建构于前者之上。立即模式适合 于开发高品质的多媒体应用程序,它在底层与加速硬件进行通讯。保留模式应用于 对 3D 场景进行实时的操纵。 对 3D 场景的操作是基于矩阵和多边形的运算,这是创建和管理 3D 的基础知识, 在很多资料中均可获得。 使用立即模式可以选择通过调用 DrawPrimitive 方式或利用执行缓冲区。对于初学 者一般从调用 DrawPrimitive 方式。不过,千万不要认为两者有优劣之分。在熟悉 了 Direct3D 后,使用哪个方法取决于您应用程序的要求。前者的方法与其它 COM 雷同;在利用执行缓冲区的方法时,要创建 DirectDraw 和 Direct3D 对象,设置渲 染状态,填充执行缓冲区等。 无论哪种模式,应用程序与硬件的通讯都很类似。正如下图所示。由于 Direct3D 相当于 DirectDraw 对象的一个接口,这里的 HAL 被表示为 DirectDraw/Direct3D HAL。 对保留模式的操作是通过使用一些对象来实现的。Direct3D 和 DirectDraw 是紧密 联系在一起的。一个 DirectDraw 对象将 DirectDraw 和 Direct3D 状态封装起来, 通过 IDirectDraw::QueryInterface 方式将 IDirect3D 接口转换为 DirectDraw 对象。 有一个很重要的概念是 z 缓冲区。它决定了将很多显示内容如何覆盖和裁剪。如果 没有 z 缓冲区,保留模式无法给覆盖层排序。没有指定 z 顺序的覆盖层被缺省设定 为 0,处于最底层。一共可以有四十亿个覆盖层(应该够用了吧!),z 顺序为 2 的层将可能遮掩了 z 顺序为 1 的层的某些内容。记住,不能有两个层的 z 顺序相同。 关于 3D 场景,还有诸如材资、光源等概念,在此不再一一聱述。 第九节 Vc++中引入 Direct SDK 一旦安装了 S DK,就得马上通知 Visual C++ SDK 的位置。默认状态下,SDK 安装 在 dxsdk 目录下。头文件放在 dxsdk/inc 目录下,库文件放在 dxsdk/lib 目录下。 可利用下述两种方法之一通知 visual C++ SDK 的位置。一种方法是在使用文件时 给出完整的文件路径;另一种法是将这些目录加到 Visual C++的搜索路径中。第 二种方法更好一些,可以通过 Tools[Options]Directories 对话框实现。 增加 dxsdk/lib 目录的方法大体上同增加 dxsdk/inc 目录的方法相同。 如果你获得了一个含有比 Visual C++的 DirectX SDK 新的版本,您需要将 DirectX SDK 目录置于常规的 Visual C++目录上面。否则就得使用旧版本。(Visual C++从 上至下查找目录) 根据我们已经讨论过的内容,你应该能够编辑 DirectX 程序了。然而还有最后一个 潜在的障碍。除非 INITGUID 符号已经被定义,否则在 DirectX GUIDs 下调用 Query-Interface()函数的程序同 DirectX2 SDK 的链接会失败。INITGUID 符号只能 由一个源文件定义,并且必须出现在#include 语句之前,如下列代码所示: #define INITGUID #include〈ddraw.h〉 //…other includes… 对于 DirectX3 及以上版本,这种解决方法都是有效的,但还有一个更好的方法, 即将 dxguid.lib 文件链接到你的工程上(Build[Settings]Link 对话框),以替代 INITGUID 符号的定义。 游戏开发基础(4) 第四章 diectxdarw 基础篇 第一节 DirectDraw 简介 Grubers 的一个观点是 DirectDraw“只是一个 bltting 发动机”。这是相当准确的,但 却太简化了。更准确地讲,DirectDraw 是一个可以提供软件仿真测试的独立于硬 件设备的 bltting 发动机。DirectDraw 的主要用途是尽可能快、尽可能可靠并且尽 可能连续地将图形考贝到视频显示设备上。 另外一个定义 DirectDraw 的方式是把它作为一个视频存储器管理器,同常规 的存储器管理器一样,DirectDraw 发放存储器信息包,跟踪每一个信息包的状态。 信息包可以随意地创建、复制、修改或破坏,同时这些操作的细节被程序员隐含起 来,这样讲是过于简单了。此外,DirectDraw 是能够使用系统 RAM 和视频 RAM 的。存储器管理器也经常被设计成和主要目标一样强健,而不只是追求性能。对于 DirectDraw,性能只是设计目标之一。 从技术角度讲,DirectDraw 是随同设备驱动器集合的便携式 API。DirectDraw 设 计成完全避开传统意义上的 Windows 图形机构(GDI,或称图形设备接口)。GDI 由于性能低而名声不好,所以 DirectDraw 的设备独立性在提供最佳性能方面是至 关重要的。 第二节 DirectDraw 基本概念 1. 显示模式 显示模式是由允许将要显示的图形输出的显示硬件支持的可视配置。最常用的显示 模式属性是分辨率。Windows 使用的显示模式的默认值是 640× 480 的分辨率。这 意味着,水平方向有 640 个像素,垂直方向有 480 个像素。其他一些常见的显示 模式分辨率有 800× 600,1024× 768。一些显示卡支持 Mode X 显示模式。一个典 型的 Mode X 显示模式的分辨率为 320× 200。 显示模式也随像素深度的变化而变化。像素深度决定着每一个像素所容纳的多 少不同的值,因而也就可以显示多少种颜色。例如对于 8 位像素深度的显示模式, 每个像素能够再现 256 种颜色的一种。像素深度为 16 位的显示模式支持 65536 种颜色(即 2 的 n 次方),典型的像素深度为 8、16、24 和 32 位。 显示模式由安装在机器中的显示设备或视频卡支持。显示设备有自己的 RAM, 从计算机的 RAM 中分离出来。我们把位于显示设备中的存储器称为显示 RAM,而 把常规存储器称为系统 RAM。 支持一个给定的显示模式的 RAM 的容量取决于显示模式的分辨率和像素深度。例 如,640× 480× 8(640× 480 像素,深度为 8 位)的显示模式需要 307200 字节。 1024× 768× 16 的显示模式需要 1572864 字节。支持显示模式的存储器必须是显示 RAM。一个给定显示设备所支持的显示模式因此也被可以利用的显示 RAM 的容量 所限制。例如,1024× 768× 16 的显示模式因为需要一兆字节以上的内存,所以就 不能被只有一兆字节 RAM 的显示设备所支持。 DirectDraw 的一个主要特征是显示模式切换。这允许一个 DirectDraw 应用程序检 测和激活所安装的显示设备所支持的任何显示模式。我们将在第 4 章中讨论显示 模式切换的细节。 2. 硬件加速 DirectDraw 具有最优性能的最重要的原因是,它尽可能地使用硬件加速来进行设 计。硬件加速发生在当显示设备能够用建立在显示设备之中的处理功能执行操作 时。硬件加速具有两个优点,首先,当硬件加速出现的时候,硬件按指定要求设计 成支持图形操作,这提供了执行给定任务的最快的方法:其次,硬件加速使得计算 主处理器从执行操作中解放出来,这使得主处理器可以执行其他任务。 3. 表面 表面是存储器的一个矩形部分的 DirectDraw 术语,通常包括图像数据。该存储器 通常用来表示一个存在于显示 RAM 或系统 RAM 中的表面。驻留在显示 RAM 中的 表面享有超高性能,因为绝大多数显示硬件不能对系统 RAM 直接存取。 表面分为向大类,最简单的类型是脱离屏幕表面。脱离屏幕表面可以驻留在显示 RAM 中或系统 RAM 中,但却不能被显示。这类表面一般用于存储子画面和背景。 另一方面,一个主表面是可在屏幕上看到的视频 RAM 的一部分部分。所有的 DirectDraw 程序(可以提供视频输出)都拥有主表面。主表面必须驻留在显示 RAM 中。 主表面通常很复杂,或是可翻转的。可翻转表面允许页面翻转,这是一项整个表面 的内容可以通过一个硬件操作而瞬时可见的技术。页面翻转用于许多基于 DirectDraw 或其他的图形应用程序中,因为它可以产生相当平滑、不闪烁的动画。 一个可翻转的主表面实际上是两个表面,一个可见,另一个不可见。不可见的表面 称为后备缓冲区。当发生页面翻转时,以前是后备缓冲区的表面就成为可见的,而 以前可见的表面则成为后备缓冲区。 离屏表面和主表面都有两类:调色板的和无调色板的。在 DirectDraw 中,只 有 8 位表面是调色板表面。调色板表面并不包含色彩数据,但是却引入一个色彩 表。该表称为调色板。像素深度为 16、24 或 32 位的表面是无调色板表面。无调 色板表面存储实际色彩值,而不引入调色板。 因为在无调色板表面中的每一个像素都存储色彩数据,所以知道表面的像素格式是 很重要的。像素格式描述了存储于像素中的红色、绿色和蓝色(RGB)元件的方式。 像素格式随像素深度、显示模式和硬件设计的不同而不同,在第 5 章中可以了解 所有的像素格式。 4. Bltting Bltting 是用于复制的图形语言。典型的 blt 操作是将离屏表面的内容拷贝到一个后 备缓冲区中。当 Bltting 通过硬件完成的时候,执行速度相当快。如果无法得到硬 件加速,DirectDraw 将使用一个软件操作来仿真 blt。这种仿真操作虽然也能够完 成任务,但却比硬件慢得多.一般只有驻留在显示 RAM 中的表面能够通过使用显示 硬件来完成 blt。 blt 操作调用一个源表面和一个目标表面,源表面的内容被拷贝到目标表面中。 源表面中的内容在操作中不会改变,只有目标表面受 blt 的影响。blt 操作也并不需 要使用全部的源表面或目标表面。源表面中的任何矩形区域可以被放置于目标表面 中的任何位置。 不规则形状表面的 bltting(例如典型的子画面)是以透明方式完成的。透明性是通过 指定表面中某个不被 blt 操作拷贝的像素而获得的。像素值通过使用色彩键码给以 标志。 色彩键码可以附加到源表面或目标表面上。源色彩键码是很普遍的。源色彩键码允 许透明性,因为源表面中的像素值并未被考贝。至于目标色彩,只有目标表面中通 过色彩所指定的像素值能够被源表面的内容所覆盖。 DirectDraw 也支持一些特定的操作,包括拉伸、压缩、镜像映射,以及混合等。 这些功能的实现往往取决于显示硬件。DirectDraw 能够仿真其中的某些操作,但 是跟性能相比,价格往往是昂贵的。 DirectDraw 也有不能仿真的功能(例如目标色彩键码)。使用这些功能是冒险的,除 非该功能为所安装的显示硬件支持,否则使用该功能的操作将失败。这给 DirectDraw 的开发者带来两种基本选择:要么放弃使用这些功能:要么往应用程 序中增加定制软件。 5. 调色板 使用 8 位显示模式的应用程序需要提供调色板。调色板就是任何时候都可以 使用的色彩表。如果 8 位显示模式不需要调色板,应用程序将被迫使用 256 种颜 色的固定设置。调色板允许用户定义将要使用的 256 种颜色之一。 当你使用调色板显示模式时,必须保证在应用程序中的图像也使用同一调色 板。如果没有做到这一点,所显示的一些或全部图像中将出现错误的颜色。调色板 也会带来麻烦,尤其是用一个调色板来显示大量图像的时候。调色板也有一些优势。 正如前面提到的,调色板允许在一个有限色彩的场合使用最多的色彩。调色板也允 许调色板动画。 调色板动画是动画通过改变调色板项目,而不是改变像素值来执行的技术,这 就使得一个屏幕上的很多像素可以瞬时改变颜色。对于一些有限的应用程序,诸如 分配的、重复的动画,调色板动画很有用处。 6. 剪裁 理想状态下,一个 blt 操作就是整个表面被 blt 成为另一个表面。通常源表面被 blt 成为目标表面的边,或者目标表面被另一个表面或窗口遮蔽。像这样的情况就需要 进行剪裁。剪裁只允许一部分或一个表面的一部分被 blt。 在编写窗口 DirectDraw 应用程序时经常用到剪裁,因为这些应用程序必须遵守 Windows 桌面的规则。我们将在本章后面讨论窗口应用程序。 DirectDraw 提供全矩形剪裁支持。也有这种情况,就是付费提供定制剪裁例程, 我们将在第 3 章中研究定制剪裁解决方案。 7. 其他表面 离屏表面和主表面(具有任选的后备缓冲区)是绝大多数 DirectDraw 应用程序的主 干。然而一些其他的表面就有不同,包括重叠表面、alpha 通道表面、Z-缓冲区以 及 3D 设备表面等。 重叠表面是硬件单色画面,因而也就在仅在支持重叠的显示硬件上获得。和软件单 色画面不同,它可以被移动而不需要背景图像被恢复。 alpha 通道表面用来执行 alpha 调配。Alpha 调配是透明的高级形式。允许表面以 透明度或半透明方式来拷贝。alpha 通道表面可用来控制每一像素的透明度设置。 alpha 通道表面的深度有 1、2、4、8 位。1 位深度 alpha 通道表面仅支持两种透 明设置,不透明(非透明)或不可见(全透明)。另一方面,8 位 alpha 通道表面允许 256 种不同的透明度设置。Alpha 调配是不被 DirectDraw 仿真的功能的一个例子。 为了使用 alpha 调配,因而就需要有支持它的显示硬件或建立在应用程序之中的定 制调配方案。 Z-缓冲区和 3D 设备表面用于 3D 应用程序中。这些类型的表面已被特别地加入到 DirectDraw 之中,以支持 Direct3D。Z-缓冲区用于景象绘制时期,以跟踪景象中 离浏览者最近的对象,从而该对象可以在其他对象的前面出现。3D 设备表面可以 用来作为 Direct3D 绘制目标的表面。本书并不包括 Z-缓冲区或 3D 设备。 第三节 元件对象模型(COM) 1.Microsoft 的 COM 规格 DirectDraw 根据 Microsoft 的 COM(Component Object Model)规格得以实现。COM 设计成用来提供完全便携的、安全的、可升级的软件结构,COM 是一个大的项目, 但是它并不是本软件讨论的对象。我们讨论 COM 只是为了方便使用 DirectDraw 进 行编程。 COM 使用一个面向对象的模型,这要比像 C++等语言所用的模型更严格。例如, COM 对象经常通过成员函数进行存取,而且并不具备公共数据成员。COM 对继承 性的支持与 C++相比也是有限的。 2. 对象和接口的比较 COM 在对象和接口之间具有很大的区别。COM 对象提供实际函数性,而 COM 接 口则提供存取函数性的方法。COM 对象不能被直接存取,相反,所有的存取者通 过接口来完成。这条规则是如此强有力的得到遵守,以致于我们不能给任何 COM 对象以名称。我们只能给用来存取对象的接口名称。因为我们无法存取 COM 对象, 所以这里绝大多数时候是根据接口来讲的。 一个 COM 对象能够支持多个接口。这听起来像个特例,但是它经常出现,因为根 据 COM 规格,一个 COM 接口一旦定义之后,就不能再被改变或增加。这样做是 为保证旧程序在一个 COM 对象升级的时候不会被停止使用。这个初始接口始终存 在,一个新的、替换的接口在提供存取对象的新的函数性的时候才被提供。 3. IUnknown 接口 所有的 COM 接口都是从 IUnknown 接口中衍生出来的。“I”标志经常用于命名 COM 接口(它代表 Interface 即界面)。DirectDraw 接口总以“I”开头。但是在文献中经常 看不到这个标志。以后提到接口时也将省略“I”标志。 IUnknown 接口将提供 3 个成员函数,所有 COM 接口因而继承这些函数: ●AddRef() ●Release() ●QueryInterface() AddRef()和 Release()成员函数为称为生命期封装(lifetime encapsulation)的 COM 功能提供支持。生命期封装是一个将每一个对象根据它自己的结构放置的协议。 生命期封装通过引用值来实现。每一个对象拥有一个可以跟踪对象的指针数,或者 引用的内部值。当对象创建之后,该值为 1。如果附加的接口或接口的指针被创建, 则该值递增。与此类似,如果接口的指针被破坏,则该值递减。当它的引用数到 0 的时候,该对象自行破坏。 AddRef()函数用来使对象的内部引用值递增。绝大部分时间里,该函数通过 DirectDraw API 被用户调用。例如,当你使用 DirectDrawaw API 创建一个新的接 口时,创建函数就自动调用 AddRef()。 Release()函数用来给对象的内部引用值递减。用户应该在接口的指针将要超出范 围时或者要通过使用接口指针来结束时使用这个函数。AddRef()和 Release()函数 都返回一个值,表示对象新的引用值。 QueryInterface()函数允许 COM 对象就它们是否支持特定接口进行查询。例如,升 级的 COM 对象提供附加的接口,而非现有接口的修改版。QueryInterface()函数可 以用来确定旧的接口,以决定新的接口是否被支持。如果被查询的对象不支持有问 题的接口,则更替接口的指针就返回。 4. GUID 为了查询一个对象是否支持使用 QueryInterface()函数的指定接口,就有秘要识别 有问题的接口。这通过接口的 GUID(Globally Unique IDentifier)来实现。一个 GUID 是一个 128 位的值,也就是说,对于所有意图和目的是唯一的。所有 DirectDraw 接口的 GUIDs 都包含在 DirectX 头文件中。 上述对于 COM 的简单介绍,就是为有效使用 DirectDraw API 所需要的全部内容。 以后当我们再讨论 DirectDraw API 时,你会发现这些内容是有联系的。 第四节 DirectDraw 接口函数 1.关于 DirectDraw API 衡量 API 的一个方法就是看它的大小。一个庞大复杂的 API 可能就是计划不周的 结果。另一方面,一个庞大的 API 有时就意味着每一种情况都有可能出现。一个 小的 API 就是一个新的、缺乏功能的软件包的证据。它也意味着,一个 API 只能 做它所需要做的,而不能多做一点。 DirectDraw API 是比较小的,因此本章中所讨论的每一个函数不致于使本章看起来 像一本参考手册。DirectDraw 提供很少的方便,也很少有限制。 DirectDraw 由个 COM 对象构成,每个对象可以通过一个或多个接口存取。这些接 口包括: ●DirectDraw ●DirectDraw2 ●DirectDrawSurface ●DirectDrawSurface2 ●DirectDrawSurface3 ●DirectDrawPalette ●DirectDrawClipper 我们将讨论每一个接口,并随后讨论它们的成员函数。但我们并不讨论每个函数的 细节,因为我们并不是向您提供一份参考手册。相反,我们将讨论每个函数是干什 么的,为什么这样使用,以及你有可能如何去使用它。 当 DirectX 首次推出的时候(早先它被称作 Games SDK),DirectDraw 核心函数性以 DirectDraw 接口表示。当 DirectX2 推出的时候,DirectDraw 也已经被升级了。 DirectDraw 遵守 COM 规格而未被改变。新的函数性可能通过 DirectDraw2 接口存 取。 特别要注意的是,DirectDraw2 接口是 DirectDraw 接口的超级设置。DirectDraw2 接口可提供 DirectDraw 接口的所有函数,另外还增加了一些新的函数。如果你正 在使用 DirectX 或更高版高,那么你可以随意选用 DirectDraw 接口或 DirectDraw2 接口。但是,由于 DirectDraw2 接口较 DirectDraw 接口的功能更强,所以没有必 要使用 DirectDraw 接口。同样,Microsoft 并不主张使用这些无组织的、网络可变 的接口。因此,在本书以后的程序中我们只使用 DirectDraw2 接口。 DirectDraw 和 DirectDraw2 接口提供的成员函数如下(按字母顺序排列): ●Compact() ●CreateClipper() ●CreatePalette() ●CreateSurface() ●DuplicateSurface() ●EnumDisplayModes() ●EnumSurfaces() ●FlipToGDISurface() ●GetAvailableVidMem() ●GetCaps() ●GetDisplayMode() ●GetFourCCCodes() ●GetGDISurface() ●GetMonitorFrequency() ●GetScanline() ●GetVerticalBlankStatus() ●RestoreDisplayMode() ●SetCooperativeLevel() ●SetDisplayMode() ●WaitForVerticalBlank() 接下来我们讨论 DirectDraw 接口函数。注意,在本章以后的内容中,DirectDraw 接口既表示 DirectDraw 接口,也表示 DirectDraw2 接口。只有在区分 DirectDraw 接口和 DirectDraw2 接口的函数时,才加以区别。 1. 接口创建函数 DirectDraw 接口表示 DirectDraw 本身。该接口在被用于创建其他 DirectDraw 接口 实例时,是一个主接口。DirectDraw 接口提供三个这样的接口实例创建函数: ●CreateClipper() ●CreatePalette() ●CreateSurface() CreateClipper()函数用于创建 DirectDrawClipper 接口实例。并非所有的 DirectDraw 应用程序都用到剪裁器,所以该函数并不是在所有的程序中都出现。我们将很快讨 论 DirectDrawClipper 的细节。 CreatePalette()函数用于创建 DirectDrawPalette 接口实例。同 DirectDrawClipper 一样,并非所有的 DirectDraw 应用程序都用到调色板。比如,应用程序使用 16 位显示模式时,就不用调色板。但是,当应用程序使用 8 位显示模式时,就必须 创建至少一个 DirectDrawPalette 实例。 CreateSurface()函数用于创建 DirectDrawSurface 接口实例。任何一个 DirectDraw 应用程序都要用表面来生成图像数据,因此经常要用到这一函数。 DirectDraw 接口自己的实例是由 DirectDrawCreate()函数创建的。 DirectDrawCreate()是 DirectDraw 函数中少有的几个常规函数之一,但并不是 COM 接口成员函数。 2. GetCaps()函数 DirectDraw 接口允许准确确定软硬件都支持的特征。GetCaps()函数可以对两个 DDCAP 结构实例进行初始化。一个结构表明哪些特征由显示硬件直接支持,另一 个结构表明哪些特征由软件仿真支持。最好是用 GetCaps()函数来决定你将用到的 特征是否被支持。 提示:DirectX 浏览器 DirectX SKD 是与 DXVIEW 程序同时推出的。DXVIEW 说明了 DirectX 组件的功能, 包括 DirectDraw。大多数系统中,有两个 DirectDraw 项目:主显示驱动器和硬件 仿真层。第一项说明了显示硬件的功能。第二项说明了在缺乏硬件支持的情况下, DirectDraw 将要仿真的一些特征。在具有两个以上的 DirectDraw 支持的显示卡的 计算机中,DXVIEW 会显示卡的功能。 3. SetCooperativeLevel()函数 SetCooperativeLevel()函数用于指定应用程序所要求的对显示硬件的控制程度。比 如,一个正常合作度意味着应用程序既改变不了当前显示模式,也不能指定整个系 统调色板的内容。而一个专有的合作度允许显示模式切换,并能完全控制调色板。 不管你决定使用哪种合作度,都必须调用 SetCooperativeLevel()函数。 4. 显示模式函数 DirectDraw 接口提供 4 种显示模式操作函数。它们是: ●EnumDisplayModes() ●GetDisplayMode() ●RestoreDisplayMode() ●SetDisplayMode() EnumDisplayModes()函数可用于查询 DirectDraw 使用何种显示模式。通过设置 EnumDisplayModes()函数默认值可以得到所有的显示模式,而且可以通过显示模 式描述消除那些不感兴趣的模式。进行显示模式切换的过程中最好使用 EnumDisplayModes()函数。现在市场上有各种各样的显示设备,每种显示设备都 有自己的特征和局限。除了默认的 640× 480× 8 窗口显示模式,最好不要依靠任何 给定的显示模式的支持。 GetDisplayMode()函数可以检索到有关当前显示模式的信息,并在 DDSURFACEDESC 结构实例中显示当前显示模式的宽度、高度、像素深度以及像 素格式等信息。还有别的途径可以检索到同样的信息(比如检索主表面描述),因此 该函数并不出现在所有的程序中。 SetDisplayMode()函数用于激活所支持的显示模式。SetDisplayMode()函数的 DirectDraw2 版本还允许设定显示模式的刷新率。而 DirectDraw 接口版本的 SetDisplayMode()函数只能进行显示模式宽度、高度和像素深度的设置。任何一个 要进行显示模式切换的程序都要用到 SetDisplayMode()函数。 RestoreDisplayMode()函数用于存储调用 SetDisplayMode()函数之前的显示模式。 SetDisplayMode()和 RestoreDisplayMode()函数都要求优先使用 SetCooperativeLevel()函数得到的专有合作存取。 5. 表面支持函数 除了 CreateSurface()函数之外,DirectDraw 接口还提供了以下向个表面相关函数: ●DuplicateSurface() ●EnumSurfaces() ●FlipToGDISurface() ●GetGDISurface() ●GetAvailableVidMem() ●Compact() DuplicateSurface()函数用于考贝当前表面。该函数只复制表面接口,不复制内存。 被复制的表面与源表面共享内存,因此改变内存的内容就同时改变了两个表面的图 像。 EnumSurfaces()函数可用于迭代所有满足指定标准的表面。如果没有指定标准, 那么所有当前表面都被枚举。 FlipToGDISurface()函数的作用是在终止页面翻转应用程序前确保主表面得以正确 存储。取消页面翻转时,有两个表面交替显示。这就是说,在终止应用程序之前有 可能没有保存最初的可见表面。这种情况下,Windows 通过绘制一个不可见表面 来恢复。利用 FlipToGDISurface()函数就可以轻而易举地避免发生这种情况。 GetGDISurface()函数可以向只被 GDI 认可的表面返回一个提针。GDI 表面是 Windows 用于输出的表面。在进行屏幕捕捉时,这个函数非常有用,DirectDraw 可以捕捉到 Windows 桌面的任一部分。 GetAvailableVidMem()函数用于检索正在使用中的视频存储器(显示 RAM)的数量。 这一函数由 DirectDraw2 接口提供,而不是由 DirectDraw 接口提供。该函数用于 确定应用程序利用显示 RAM 可创建表面的数量。 Compact()函数不是通过 DirectX5 实现的,但它可以为视频存储器提供碎片整理技 巧。在基于显示 RAM 的表面被不断创建或受到破坏的时候,可以释放大量内存。 6. 监视器刷新函数 DirectDraw 接口提供了 4 种适于计算机显示设备或监视器的函数,但这些函数不 适于显示卡,它们是: ●GetMonitorFrequency() ●GetScanLine() ●GetVerticalBlankStatus() ●WaitForVerticalBlank() 这些函数尤其与监视器的刷新机制紧密机连。这在确保生成动画时尽可能不产生闪 烁和图像撕裂现象时是至关重要的。但必须注意,并非所有的显示卡/监视器组合 都支持这些函数。 GetMonitorFrequency()函数用于检索监视器当前的刷新率。刷新率通常用赫兹表 示,缩写为 Hz。例如,60Hz 的刷新率表示屏幕每秒更新 60 次。 GetScanLine()函数用于向监视器返回当前正在被刷新的扫描行(水平像素行)。不是 所有的显示设备/监视器组合都支持该函数。如果这一功能得不到支持,该函数将 返回 DDERR-UNSUPPORTED。 对于高性能图形应用程序来说,通常要求利用垂直刷新同步地更新屏幕。尤其是, 当显示器刚完成屏幕刷新时,最好能够更新主表面。否则,屏幕的一部分显示新的 图像数据,而另一部分仍显示旧的图像数据,这种现象就是所谓的图像撕裂。 DirectDraw 默认利用垂直刷新同步更新屏幕。如果不是这样还可以利用 GetVerticalBlankStatus()和 WaitForVerticalBlank()函数实现同步刷新。 7. GetFourCCCodes()函数 DirectDraw 接口提供的最后一个函数是 GetFourCCCodes()函数。该函数用于返回 显示卡所支持的 FourCC 代码。FourCC 代码用于描述非 RGB 或 YUV 表面。我们不 在此讨论 YOV 表面,它们已超出本书的范围。 第五节 DirectDrawSurface 接口函数 同 DirectDraw 接口一样,DirectDrawSurface 接口也遵守 COM 规格.最初,表面支 持是由 DirectDrawSurface 接口提供的。DirectX2 介绍了 DirectDrawSurface2 接口 的新的函数性,DirectX5 介绍了 DirectDrawSurface3 接口。 尽管本软件中讨论的是 DirectDraw2 接口,而不是 DirectDraw 接口,但我们仍忠 于最初的 DirectDrawSurface 接口,因为 DirectDrawSurface2 和 DirectDrawSurface3 接口新增的函数并不十分重要。在以后的内容里,我们将用 DirectDrawSurface 接口表示这 3 种接口,除非特别注明。 DirectDrawSurface 是最大的 DirectDraw 接口,它允许表面内容的拷贝、清除以及 被调用程序直接存取。DirectDrawSurawSurface 接口总共提供 36 个成员函数,按 字母顺序排列如下: ●AddAttachedSurface() ●AddOverlayDirtyRect() ●Blt() ●BltBatch() ●BltFast() ●DeleteAttachedSurface() ●EnumAttachedSurfaces() ●EnumOverlayZOrders() ●Flip() ●GetAttachedSurface() ●GetBltstatus() ●GetCaps() ●GetClipper() ●GetColorKey() ●GetDC() ●GetDDInterface() ●GetFlipStatus() ●GetOverlayPosition() ●GetPalette() ●GetPixelFormat() ●GetSurfaceDesc() ●IsLost() ●Lock() ●PageLock() ●PageUnlock() ●ReleaseDC() ●Restore() ●SetClipper() ●SetColorKey() ●SetOverlayPosition() ●SetPalette() ●SetSurfaceDesc() ●Unlock() ●UpdateOverlay() ●UpdateOverlayDisplay() ●UpdateOverlayZOrder() 1. 表面描述函数 我们首先讨论的个可用于检索表面自身信息的函数,它们是: ●GetCaps() ●GetPixelFormat() ●GetSurfaceDesc() ●SetSurfaceDesc() 同 DirectDraw 接口提供的 GetCaps()函数一样,DirectDrawSurface 接口提供的 GetCaps()函数用于输出表征哪些特征可被表面支持的数据。该信息包括:表面是 主表面还是离屏表面;表面使用的存储器定位于显示 RAM 还是系统 RAM。 GetPixelFormat()函数在用于高彩和真彩表面时是非常重要的,这是由于像素格式 因显示卡的不同而不同。该函数返回表征码,这些表征码表明每一种颜色部件是如 何存储的。 GetSurfaceDesc()函数返回一个表面描述。该信息包括表面的宽度、高度和深度。 表面像素格式(同样被 GetPixelFormat()函数检索)也包含在其中。 SetSurfaceDesc()函数(对于 DirectX5)来讲是新增的,只由 DirectDrawSurface3 接 口提供)允许设定某些表面属性。该函数可用于指定表面使用的内存。这一点在设 计定制表面存储器管理器策略时非常有用。 2。 表面 Blt 函数 DirectDrawSurface 接口提供 3 个支持 blt 操作的函数: ●Blt() ●BltBatch() ●BltFast() Blt()函数是一个主要函数。Blt()函数能够进行常规的 blting(无特殊影响的简单的表 面到表面的 blt),同时支持延伸、旋转、镜像和颜色填充的操作。当用于同剪裁器 关联的表面时,Blt()可进行剪裁 blt 操作。 BltBatch()函数不是在 DirectX3 下实现的(你可以调用该函数,但什么也不会发生)。 执行 BltBatch()函数时,如果可能,它可同时进行多 blt 操作。 BltFast()函数是 Blt()函数的优化版本。BltFast()函数的效率提高了,但性能却下降 了。BltFast()函数不能进行一些特殊的 blt 操作,而 Blt()函数可以。而且,BltFast() 函数不能用于剪裁。但是 BltFast()函数支持源和目标色彩键码 blt 的操作。在遵循 定制剪裁例程的情况下,BltFast()函数可进行 DirectDraw 能够提供的最快捷、灵 活的 blt 操作。在下章中我们将执行一个定制剪裁例程。 以上 3 个 blt 函数均将源表面和目标表面作为变量。其他的数据,例如 blt 在目标 表面上的理想定位,是通过指定理想 blt 操作的确切属性来提供的。一旦可能,这 3 个函数将进行硬件加速 blt。 3. Flip()函数 Flip()函数用于页面翻转操作。调用 Flip()函数可隐藏屏幕上先前可见的表面,并使 一个后备缓冲区显现。只有被明确地创建为翻转表面的表面,才响应该函数的调用。 必须牢记,真正的翻转操作不可能总是成功。页面翻转要求有足够的显示 RAM 容 纳两整屏有效数据。如果满足不了这一要求,系统 RAM 中将创建一个后备缓冲区。 这时调用 Flip()函数进行的是 blt 操作而不是页面翻转。基于系统 RAM 的后备缓冲 区中的内容被拷贝到主表面上。这样会严重影响性能,但是,在真正的页面翻转中 如果没有足够的显示 RAM,又不退出程序,也只能如此了。如果你的应用程序要 求最佳性能,就得设法避免激活不能进行真正页面翻转的显示模式。 4. 表面状态函数 下面讨论两个能检索有关操作和翻转操作信息的函数,它们是: ●GetBltStatus() ●GetFlipStatus() GetBltStatus()函数用于确定当前是否进行 blt 操作。这一点很重要,因为正被 blt 的表面不能进行其他操作。该函数表明,给定的表面是否正是一个进行 blt 操作的 源表面或目标表面。 同样地,GetBltStatus()函数表明是否正在进行翻转操作。即使 DirectDraw 通过 blt 操作仿真页面翻转,该函数而不 GetBltStatus()也必须用于由 Flip()函数初始化的监 视器页面翻转。 5. 色彩键码函数 DirectDrawSurface 接口提供了以下两个函数,来设置和检查表面色彩键码或色彩 键码的范围,它们是: ●GetColorKey() ●SetColoKey() 默认状态下,表面没有色彩键码。一个色彩键码只对应一种颜色,但某些硬件支持 色彩键码范围。色彩键码和色彩键码范围是 DDCOLORKEY 结构定义的。 GetColorKey()和 SetColoKey()函数都将该结构的指针作为变量。在要求表面的一 部分透明时或需要进行目标色彩键码操作时,可以使用这两个函数。 6. Lock 和 Unlock()函数 DirectDraw 的一个主要特点,就是能够提供对图像数据的直接存取。直接存取可 以提供最佳性能和更好的灵活性,因为没有中间 API 影响运行速度,并且开发人 员呆任意使用图像数据。对表面存储器的中间存取通过以下出众个函数实现: ●Unlock() ●Lock() Lock()函数向组成表面的存储器返回一个指针,不管表面存储器位于显示 RAM 还 是系统 RAM。存储器一般按线性风格排列,以便能简单地进行图像 数据存取。 Unolock()函数在完成表面存储器的存取之后指定给 DirectDraw。 对图像数据的直接存取必须付出代价。为了支持这种存取方式,DirectDraw 在表 面锁定的时候必须关闭基本的 Windows 机构。在 Windows95 状态下,如果忘记 解锁表面,必定会损坏机器。 因此,表面锁定的时间应尽量缩短。测试前应仔细检查 Lock()和 Unlock()函数之间 的程序调用。因为这一程序无法用传统的调试程序进行调试。 锁定表面不能被 blt 和翻转,因此试图保持表面处于锁定状态没有任何有益之处。 而且,一旦表面解锁,由 Lock()函数检索的指针就失效了。 表面锁定后不能再次被锁定。在表面锁定时也就无法调用 Lock()函数。 7. GetDC()ReleaseDC()函数 对表面的直接存取占用很大内存,有时候把表面作为一个常规的 Windows 设备会 更好。在此,DirectDrawSurface 接口提供以下两个函数: ●GetDC() ●ReleaseDC() GetDC()函数提供了一个 DC(设备环境),可以用常规的 Win32 函数写到表面上。 例如,DC 可以用 Win32 的 TextOut()函数在表面上绘制文本。用完 DC 后必须马 上调用 ReleaseDC()函数。 同 Lock()和 Unlock()函数使用一样,在调用完 GetDC()函数后必须马上调用 ReleaseDC()函数。这是因为 GetDC()函数内部调用 Lock 函数,而 ReleaseDC()函 数内部调用 Unlock()函数。 8. PageLock()和 PageUnlock()函数 接下来,我们讨论另外两个与 Lock()函数和 Unlock()函数看上去非常相像的函数: ●PageLock() ●PageUnlock() 尽管这两个函数看上去很像 Lock()和 Unlock()函数,但它们却有完全不同的作用。 PageLock()和 PageUnlock()函数用于控制 Windows 对基于系统 RAM 的表面的处理 方式。这两个函数由 DirectDrawSurface2 接口提供,而不是由 DirectDrawSurface 接口提供。 当 Windows 认为当前正在运行的其他应用程序或进程更适于使用内存时, Windows 会向硬盘释放部分内存。这种缓冲对于整个系统内存都起作用,因此驻 留在系统内存中的 DirectDraw 表面有可能被存到硬盘上。如果要用到这样的表面, Windows 需要花费一定的时间从硬盘上读取表面数据。 PageLock()函数提示 Windows 哪些给定的表面不应该释放到硬盘上。这样,在使 用表面时就不用耗费时间进行硬盘存取了。相反地,PageUnlock()函数用于告知 Windows 哪些表面内存可被释放。 过程调用 PageLock()函数会减少缓冲内存的总量,从而导致 Windows 的速度大大 降低。至于这种情况何时发生,取决于页面锁定系统内存量及机器提供的系统内存 量。 PageLock()和 PageUnlock()函数主要是由 DirectDraw 提供而非 DirectDraw 应用程 序。举个例子来说,DirectDraw 自动使用 PageLock()函数,以确保运行 blt 操作时, 基于系统 RAM 的表面不被释放到硬盘。 PageLock()函数可以被同一个表面多次调用。DirectDraw 用参考计数法记录 PageLock()函数被调用的次数,因此多次调用 PageUnlock()函数就必须避免多次 调用 PageLock()函数。 PageLock()和 PageUnlock()函数对于驻留在显示 RAM 中的表面不起作用。 9. IsLost()的 Restore()函数 现在讨论两个与使用驻留在显示 RAM 中的表面有关的函数: ●IsLost() ●Restore() 让我们来看一看下面这种情况。应用程序正在运行时,尽量把表面分配到显示 RAM 中,剩下的创建到系统 RAM 中。应用程序在运行一段时间之后,用户执行或切换 到另一个应用程序。该应用程序是任意的,可以是一个常规的 Windows 程序,如 Windows 开发程序或记事本。它也可以是另外的 DirectDraw 应用程序,该程序也 试图将尽可能多地分配显示 RAM。如果 DirectDraw 不接受显示 RAM,那么新的应 用程序就很可能根本运行不了。相反的情况就意味着,应用程序不允许分配到任何 显示 RAM 中。 因此,DirectDraw 可以随意将任何一个或者所有的基于显示 RAM 的表面从非激活 应用程序中移走。这种情况就是所谓的表面丢失。从技术上讲,程序仍具有表面, 但它们不再同任何内存相关。要使用丢失的表面会导致 DDERR-SURFACELOST 错 误。IsLost()函数可用于确定一个表面是否丢失了内存。 表面内存丢失后可通过 Restore()函数恢复,但只能在应用程序被重新激活之后才 可恢复。这会导致应用程序无法将处于最小化状态的所有表面复原。 Restore()函数可恢复附属于表面的任一内存,但并不恢复内存的内容。表面被复 原后,应用程序就可以恢复表面内容了。 注意,这种用法不适合利用系统 RAM 创建的表面。如果需要用到基于系统 RAM 的 表面所占内存,那么 Windows 会立即将这些表面释放到硬盘上。Windows 自动地 处理存储和恢复,包括恢复表面的内容。 10. GetDDInterface()函数 GetDDInterface()函数可检索用于创建给定表面的 DirectDraw 接口的指针。由于 程序中大多数情况下只有一个 DirectDraw 接口实例,所以 GetDDInterface()函数 并不常用。但是一个应用程序中有可能使用多个 DirectDraw 接口,在这种情况下, GetDDInterface()函数会起到重要作用。 11. 表面连接函数 DirectDrawSurface 接口提供以下 4 个函数,用来维持表面间的连接: ●AddAttachedSurface() ●DeleteAttachedSurface() ●EnumAttachedSurfaces() ●GetAttachedSurface() DirectDraw 支持大量的用于表面间的连接情况。最常见的情况就是页面翻转。进 行页面翻转时,两个或两个以上的表面连接成环状,每次调用 Flip()函数时,都会 使连成环状的表面中的下一个表面显现。 表面连接函数用于创建、检查或消除表面间的连接,但这些函数并非必不可少的。 DirectDraw 往往是自动创建连接表面。比如,当创建一个主翻转表面时,可以指 定用于连接表面的后备缓冲区的数量。DirectDraw 就会创建这些表面,并将它们 连接起来。 12. 重叠函数 DirectDrawSurface 接口用于支持重叠的函数如下: ●AddOverlayDirtyRect() ●EnumOverlayZOrders() ●GetOverlayPosition() ●SetOverlayPosition() ●UpdateOverlay() ●UpdateOverlayDisplay() ●UpdateOverlayZOrder() GetOverlayPosition()和 SetOverlayPosition()函数用于控制重叠的位置。 UpdateOverlay()函数用于更新大量的重叠设置,包括重叠是否可见,以及重叠是 以色彩键码还是用 alpha 混合到背景表面上。 UpdateOverlayDisplay()函数用于更新显示设置。该函数用于更新整个重叠显示, 或者只更新由 AddOverlayDirtyRect()函数指定的矩形重叠部分。 EnumOverlayZOrders()函数可根据重叠的 Z 值(Z 值控制哪一个重叠位于最上面) 重复重叠。重叠可按从前到后或从后到前的顺序枚举。 13. 剪裁器函数 DirectDraw 支持的剪裁是将 DirectDrawClipper 接口(该接口我们尚未讨论)的一个 实例连接到表面。一旦连接完毕,剪裁器对象就会有规律地 blt 到表面。剪裁器/ 表面的连接由以下两个 DirectDrawSurface 函数控制: ●GetClipper() ●SetClipper() SetClipper()函数用来将剪裁器对象连接到表面。GetClipper()函数用于向前一个连 接的剪裁器对象返回一个指针。SetClipper()函数还可用于解除表面同剪裁器的连 接,具体的做法是通过指定 NULL 来替代 DirctDrawClipper 接口指针。 14。 调色板函数 像剪裁器一样,调色板也可连接到表面。DirctDrawSurface 接口为此提供以下两个 函数: ●GetPalette() ●SetPalette() SetPalette()函数用来将 DirctDrawPalette 接口(该接口我们接下来就要讨论)的一 个实例连接到表面。GetPalette()函数用于检索前一个连接调色板的指针。 调色板可被连接到任何表面,但只有连接到主表面时,调色板才起作用。当与主表 面连接时,调色板决定显示硬件调色板的设置。 第六节 DirectDrawPlette 接口函数 DirctDraw 提供 DirctDrawPalette 接口用于调色板显示模式和表面。尽管 Windows 支持几种低于 8 位像素深度的显示模式,但 DirctDraw 所支持的唯一的调色板显示 模式是 8 位模式。 DirctDrawPalette 接口实例由 DirctDraw CreatePalette()函数创建。CreatePalette() 函数用大量的标志来定义调色板属性。 DirctDrawPalette 接口只提供以下 3 个函数: ●GetCaps() ●GetEntries() ●SetEntries() GetCaps()函数用于检索有关调色板的信息,包括调色板项目数量,调色板是否支 持同步垂直刷新,以及在 8 位调色板状态下是否所有的 256 个调色板项目都能被 设定。 SetEntries()函数允许在程序中设置调色板的色彩值。该数据从文件中读取。而这 些项目在运行过程中可被计算和设定。GetEntries()函数用于检索先前设定的调色 板项目。 DirctDrawPalette()接口实例可利用 DirctDrawSurface SetPalette()函数连接到表 面。将不同调色板连接到主表面或利用 SetEntries()函数改变调色板项目都可激活 调色板。 第七节 DirectDrawClipper 接口函数 DirctDrawClipper 接口支持剪裁。将剪裁器对象连接到表面并在 blt 操作中将其当 作目标表面就可以进行剪裁。 directDrawClipper 实例由 DirectDraw CreateClipper()函数创建。DirectDrawClipper 接口支持以下 5 个函数: ●SetHWnd() ●GetHWnd() ●IsClipListChanged() ●SetClipList() ●GetClipList() 剪裁器对象一般用于出现在窗口中的 DirctDraw 应用程序必需的剪裁。要求剪裁器 必须确保在 blt 操作过程中考虑到桌面上其他的窗口。比如,当应用程序的全部或 一部分被另一个窗口遮蔽,剪裁就必须确保被遮蔽的窗口不被 DirctDraw 应用程序 破坏。 桌面剪裁由 SetWnd()函数完成。SetHWnd()函数将剪裁器对象连接到一个窗口句 柄。这样就初始化了 Windows 和剪裁器对象之间的通讯。当桌面上的任何一个窗 口发生变化时,剪裁器对象就会得到通知,并作出反应。GetHWnd()函数用于决定 剪裁器同哪一个窗口句柄连接。IsClipListChanged()函数用于决定内部剪裁清单是 否因桌面的改变而被更新。 SetClipList()和 GetClipList()函数为 DirectDrawClipper 接口提供便利的定制使用。 SetClipList()函数用于定义一些矩形区域,这些矩形区域用来定义 blt 操作的合法区 域。GetClipList()函数用于检索剪裁器的内部剪裁数据。 一旦被连接到表面,Blt(),BltBatch()以及 UpdateOverlay()函数所进行的 blt 操作 将根据 DirctDrawCliper 接口中包含的数据被自动地剪裁。注意,Blt Fast()函数在 此被忽略。BltFast()函数不支持剪裁。 第八节 附加 DirectDraw 接口 DirctDraw 还提供了另外个我们没有讨论的接口,它们是: ●DDVideoPortContainer ●DirectDrawColorControl ●DirectDrawVideoport 这些接口由 DirectX5 介绍,它们提供低水平视频端口控制。这些接口还向 DirctDraw 表面提供流活动视频的方法。尽管利用这些接口可以为 DirctDraw 应用程序增加视 频支持,但除非高水平视频 APIs 不能满足需要,否则最好不用这一方法。 第九节 DirectDraw 结构 我们已讨论过 DirctDraw 接口及其成员函数了,接下来再看看 DirctDraw 定义的结 构。DirctDraw 总共定义了 8 全结构: ●DDBLTFX ●DDCAPS ●DDOVERLAYFX ●DDPIXELFORMAT ●DDSURFACEDESC ●DDSCAPS ●DDBLTBATCH ●DDCOLORKEY 我们已经见过其中的一些结构了,比如在讨论 DirctDrawSurface SetColorKey()函 数时我们就接触过 DDCOLORKEY 结构。在此,我们不详细讨论每一个结构的细节, 但必须指出,DirctDraw quirk 被忘记时会导致失败。 以上所列结构中的前 5 个有一个称作 dwSize 的字段。该字段用于存储结构的大小, 设定该字段的工作由你来做。另外,该字段如果没有被正确设定的话,那么任何一 个将这 5 个结构作为变量的 DirctDraw 函数都会失效。 以 DDSURFACEDESC 结构为例,使用结构的代码如下: DDSURFACEDESC surfdesc; surfdesc.dwSize=sizeof(surfdesc); surf->GetSurfaceDesc(&surfdesc); 首先声明结构,然后用 sizeof()关键字设定 dwSize 字段。最后结构传递给 DirctDrawSurface GetSurfaceDesc()函数。忘记设定 dwSize 字段将导致这段代码 失效。 到底为什么 DirctDraw 坚持要求给出它所定义的结构的大小?原因在于这 5 个包含 dwSize 字段的结构将来有可能会改变。DirctDraw 会检查结构的大小,以便确定正 在使用的版本。DirctDraw 坚持要求给出一个有效的大小值,是为了让开发者提供 有效的结构大小。这样做是有好处的,因为 DirctDraw 的新版本可以正确运用旧版 本的 DirctDraw 程序。 在使用结构之前,最好将结构初始化为零。这样,前面的代码就变成: DDSURFACEDESC surfdesc; ZeroMemory (&surfdesc,sizeof(surfdesc)); surfdesc.dwSize=sizeof(surfdesc); surf->GetSurfaceDesc(&surfdesc); ZeroMemory()函数是一个 Win32 函数,它将作为第一个参数的存储器设定为 零.ZeroMemory()函数的第二个参数表明有多少存储器应被初始化。这一做法的好 处是,通过 GetSurfaceDesc()函数调用可以知道结构的哪些字段被更新了。如果没 有对结构进行初始化,就有可能将结构字段中不可预测的值当作 DirectDraw 的设 定值。 第十节 窗口应用程序 DirctDraw 应用程序主要有两种型式:窗口的和全屏的。窗口 DirctDraw 应用程序 看起来就像一个常规的 Windows 程序。我们很快将讨论到全屏应用程序。 窗口应用程序包括窗口边界、标题框以及菜单,这些都是传统的 Windows 应用程 序中常见的部分。由于窗口应用程序同其他窗口一起出现在桌面上,因此它们被迫 使用 Windows 当前所使用的分辨率和比特深度。 窗口程序有一个主表面,但只在进行真实页面翻转时才显现。而且,主表面并不代 表窗口的客户区域(该区域在窗口边界内)。主表面还代表整个桌面。这就是说,你 的程序必须追踪窗口的位置和大小,以便在窗口内正确显示可见的输出。换言之, 利用窗口化的应用程序中可以在整个桌面上进行绘图。 如果不允许页面翻转,那么图像就必须从离屏缓冲区 blt 到主表面上。这就增加了 图像撕裂的可能性,因为 blt 比页面翻转速度慢。为了避免图像撕裂,blt 操作可以 与监视的刷新率保持同步。 如果与窗口客户区域同样大小的离屏缓冲区被创建到显示 RAM 中,窗口应用程序 就可以很好地运行。这样,窗口的内容可利用离屏表面合成。然后离屏表面可以通 过硬件加速很快地到主表面上。 由于显示存储器的缺乏而不得不将离屏缓冲区创建到系统 RAM 中时,会严重影响 性能。不幸的是,这种情况常常发生,尤其是在只有 2MB 显示卡的时候,这是因 为人们总希望为自己 Windows 的桌面设置高分辨的显示模式。例如,采用 1024× 768× 16 显示模式的主表面自己就要占用 2MB 的 RAM。在一个 2MB 显示卡 上,几乎没有显示 RAM 留给离屏表面。 窗口应用程序的另一个问题是剪裁。一个性能良好的应用程序必须有一个连接到主 表面的剪裁对象。这是有损性能的,原因在于为了检查剪裁器的剪裁清单内容, blt 操作只能在一个小的矩形部分内进行。而且,不能使用优化的 BltFast()函数。 Bltting 必须用较慢的,(而且更笨重的)Blt()函数。 最后要讲的是,窗口应用程序不允许全调色板控制。由于 Windows 保留了20 个 调色板项,所以在 256 种颜色只有 236 种颜色可被设定。被 Windows 保留的颜色 只用系统调色板的前 10 个项和后 10 个项。因此在给图像上色时,只能使用中间 236 个调色板项。 第十一节全屏应用程序 包含对显示设备进行专有存取的 DirctDraw 应用程序就是全屏应用程序。这种应用 程序可以任意选取显示卡所支持的显示模式,并享有全调色板控制。另外,全屏应 用程序还可进行页面翻转。因此同窗口应用程序相比,全屏应用程序速度更快,灵 活性更好。 典型的全屏应用程序首先检查所支持的显示模式,并激活其中一个。然后创建具有 一个或更多后备缓冲区的可翻转主表面,剩下的显示 RAM 用于创建离屏表面。当 显示 RAM 耗尽时,就启用系统 RAM。屏幕被在后备缓冲区中合成的第一个场景更 新,然后进行页面翻转。即使主表面占用了所有的可用的显示 RAM,全屏应用程 序还可输出执行其窗口化的副本,这是因为全屏应用程序可进行真实页面翻转。 由于全屏应用程序不一定非得使用 Windows 的当前显示模式,所以显示 RAM 的可 用与否不存在多少问题。如果只检测到 2MB 的显示 RAM,就可使用低分辨率显示 模式来保留内存。如果检测到 4MB 的显示 RAM,应用程序就可使用要求的显示模 式并仍有保持良好性能。 全调色板控制也是个有利因素。这样可以使用所有 256 个调色板项而无需根据 Windows 保留的 20 中颜色重新分配位图。 第十二节混合应用程序 混合应用程序既可以在全屏方式下运行也可在窗口方式下运行。混合应用程序内部 非常复杂,但却可以提供良好的性能。用户可在窗口方式下运行,如果太慢,则可 切换到全屏方式下运行。 编写混合应用程序最好的方法是编写一个定制库,该库包括那些与应用程序使用全 屏方式还是窗口方式无关的函数。 游戏开发基础(5) 第五章 diectxdarw 基础篇 第一节 DirectDraw 简介 Grubers 的一个观点是 DirectDraw“只是一个 bltting 发动机”。这是相 当准确的,但却太简化了。更准确地讲,DirectDraw 是一个可以提供软件 仿真测试的独立于硬件设备的 bltting 发动机。DirectDraw 的主要用途是 尽可能快、尽可能可靠并且尽可能连续地将图形考贝到视频显示设备上。 另外一个定义 DirectDraw 的方式是把它作为一个视频存储器管理器, 同常规的存储器管理器一样,DirectDraw 发放存储器信息包,跟踪每一个 信息包的状态。信息包可以随意地创建、复制、修改或破坏,同时这些操 作的细节被程序员隐含起来,这样讲是过于简单了。此外,DirectDraw 是 能够使用系统 RAM 和视频 RAM 的。存储器管理器也经常被设计成和主要目 标一样强健,而不只是追求性能。对于 DirectDraw,性能只是设计目标之 一。 从技术角度讲,DirectDraw 是随同设备驱动器集合的便携式 API。 DirectDraw 设计成完全避开传统意义上的 Windows 图形机构(GDI,或称图 形设备接口)。GDI 由于性能低而名声不好,所以 DirectDraw 的设备独立性 在提供最佳性能方面是至关重要的。 第二节 DirectDraw 基本概念 1. 显示模式 显示模式是由允许将要显示的图形输出的显示硬件支持的可视配置。最常 用的显示模式属性是分辨率。Windows使用的显示模式的默认值是640×480 的分辨率。这意味着,水平方向有 640 个像素,垂直方向有 480 个像素。 其他一些常见的显示模式分辨率有 800×600,1024×768。一些显示卡支持 Mode X 显示模式。一个典型的 Mode X 显示模式的分辨率为 320×200。 显示模式也随像素深度的变化而变化。像素深度决定着每一个像素所 容纳的多少不同的值,因而也就可以显示多少种颜色。例如对于 8 位像素 深度的显示模式,每个像素能够再现 256 种颜色的一种。像素深度为 16 位 的显示模式支持 65536 种颜色(即 2 的 n 次方),典型的像素深度为 8、16、 24 和 32 位。 显示模式由安装在机器中的显示设备或视频卡支持。显示设备有自己 的 RAM,从计算机的 RAM 中分离出来。我们把位于显示设备中的存储器称为 显示 RAM,而把常规存储器称为系统 RAM。 支持一个给定的显示模式的 RAM 的容量取决于显示模式的分辨率和像素深 度。例如,640×480×8(640×480 像素,深度为8 位)的显示模式需要307200 字节。1024×768×16 的显示模式需要 1572864 字节。支持显示模式的存储 器必须是显示 RAM。一个给定显示设备所支持的显示模式因此也被可以利用 的显示 RAM 的容量所限制。例如,1024×768×16 的显示模式因为需要一兆 字节以上的内存,所以就不能被只有一兆字节 RAM 的显示设备所支持。 DirectDraw 的一个主要特征是显示模式切换。这允许一个 DirectDraw 应用 程序检测和激活所安装的显示设备所支持的任何显示模式。我们将在第 4 章中讨论显示模式切换的细节。 2. 硬件加速 DirectDraw 具有最优性能的最重要的原因是,它尽可能地使用硬件加速来 进行设计。硬件加速发生在当显示设备能够用建立在显示设备之中的处理 功能执行操作时。硬件加速具有两个优点,首先,当硬件加速出现的时候, 硬件按指定要求设计成支持图形操作,这提供了执行给定任务的最快的方 法:其次,硬件加速使得计算主处理器从执行操作中解放出来,这使得主 处理器可以执行其他任务。 3. 表面 表面是存储器的一个矩形部分的 DirectDraw 术语,通常包括图像数据。该 存储器通常用来表示一个存在于显示 RAM 或系统 RAM 中的表面。驻留在显 示 RAM 中的表面享有超高性能,因为绝大多数显示硬件不能对系统 RAM 直 接存取。 表面分为向大类,最简单的类型是脱离屏幕表面。脱离屏幕表面可以驻留 在显示 RAM 中或系统 RAM 中,但却不能被显示。这类表面一般用于存储子 画面和背景。 另一方面,一个主表面是可在屏幕上看到的视频 RAM 的一部分部分。所有 的 DirectDraw 程序(可以提供视频输出)都拥有主表面。主表面必须驻留在 显示 RAM 中。 主表面通常很复杂,或是可翻转的。可翻转表面允许页面翻转,这是一项 整个表面的内容可以通过一个硬件操作而瞬时可见的技术。页面翻转用于 许多基于DirectDraw 或其他的图形应用程序中,因为它可以产生相当平滑、 不闪烁的动画。一个可翻转的主表面实际上是两个表面,一个可见,另一 个不可见。不可见的表面称为后备缓冲区。当发生页面翻转时,以前是后 备缓冲区的表面就成为可见的,而以前可见的表面则成为后备缓冲区。 离屏表面和主表面都有两类:调色板的和无调色板的。在 DirectDraw 中,只有 8 位表面是调色板表面。调色板表面并不包含色彩数据,但是却 引入一个色彩表。该表称为调色板。像素深度为 16、24 或 32 位的表面是 无调色板表面。无调色板表面存储实际色彩值,而不引入调色板。 因为在无调色板表面中的每一个像素都存储色彩数据,所以知道表面的像 素格式是很重要的。像素格式描述了存储于像素中的红色、绿色和蓝色 (RGB)元件的方式。像素格式随像素深度、显示模式和硬件设计的不同而不 同,在第 5 章中可以了解所有的像素格式。 4. Bltting Bltting 是用于复制的图形语言。典型的 blt 操作是将离屏表面的内容拷贝 到一个后备缓冲区中。当 Bltting 通过硬件完成的时候,执行速度相当快。 如果无法得到硬件加速,DirectDraw 将使用一个软件操作来仿真 blt。这 种仿真操作虽然也能够完成任务,但却比硬件慢得多.一般只有驻留在显示 RAM 中的表面能够通过使用显示硬件来完成 blt。 blt 操作调用一个源表面和一个目标表面,源表面的内容被拷贝到目标 表面中。源表面中的内容在操作中不会改变,只有目标表面受 blt 的影响。 blt 操作也并不需要使用全部的源表面或目标表面。源表面中的任何矩形区 域可以被放置于目标表面中的任何位置。 不规则形状表面的 bltting(例如典型的子画面)是以透明方式完成的。透明 性是通过指定表面中某个不被 blt 操作拷贝的像素而获得的。像素值通过 使用色彩键码给以标志。 色彩键码可以附加到源表面或目标表面上。源色彩键码是很普遍的。源色 彩键码允许透明性,因为源表面中的像素值并未被考贝。至于目标色彩, 只有目标表面中通过色彩所指定的像素值能够被源表面的内容所覆盖。 DirectDraw 也支持一些特定的操作,包括拉伸、压缩、镜像映射,以及混 合等。这些功能的实现往往取决于显示硬件。DirectDraw 能够仿真其中的 某些操作,但是跟性能相比,价格往往是昂贵的。 DirectDraw 也有不能仿真的功能(例如目标色彩键码)。使用这些功能是冒 险的,除非该功能为所安装的显示硬件支持,否则使用该功能的操作将失 败。这给 DirectDraw 的开发者带来两种基本选择:要么放弃使用这些功能: 要么往应用程序中增加定制软件。 5. 调色板 使用 8 位显示模式的应用程序需要提供调色板。调色板就是任何时候 都可以使用的色彩表。如果 8 位显示模式不需要调色板,应用程序将被迫 使用 256 种颜色的固定设置。调色板允许用户定义将要使用的 256 种颜色 之一。 当你使用调色板显示模式时,必须保证在应用程序中的图像也使用同 一调色板。如果没有做到这一点,所显示的一些或全部图像中将出现错误 的颜色。调色板也会带来麻烦,尤其是用一个调色板来显示大量图像的时 候。调色板也有一些优势。正如前面提到的,调色板允许在一个有限色彩 的场合使用最多的色彩。调色板也允许调色板动画。 调色板动画是动画通过改变调色板项目,而不是改变像素值来执行的 技术,这就使得一个屏幕上的很多像素可以瞬时改变颜色。对于一些有限 的应用程序,诸如分配的、重复的动画,调色板动画很有用处。 6. 剪裁 理想状态下,一个 blt 操作就是整个表面被 blt 成为另一个表面。通常源 表面被 blt 成为目标表面的边,或者目标表面被另一个表面或窗口遮蔽。 像这样的情况就需要进行剪裁。剪裁只允许一部分或一个表面的一部分被 blt。 在编写窗口 DirectDraw 应用程序时经常用到剪裁,因为这些应用程序必须 遵守 Windows 桌面的规则。我们将在本章后面讨论窗口应用程序。 DirectDraw 提供全矩形剪裁支持。也有这种情况,就是付费提供定制剪裁 例程,我们将在第 3 章中研究定制剪裁解决方案。 7. 其他表面 离屏表面和主表面(具有任选的后备缓冲区)是绝大多数DirectDraw应用程 序的主干。然而一些其他的表面就有不同,包括重叠表面、alpha 通道表面、 Z-缓冲区以及 3D 设备表面等。 重叠表面是硬件单色画面,因而也就在仅在支持重叠的显示硬件上获得。 和软件单色画面不同,它可以被移动而不需要背景图像被恢复。 alpha 通道表面用来执行 alpha 调配。Alpha 调配是透明的高级形式。允许 表面以透明度或半透明方式来拷贝。alpha 通道表面可用来控制每一像素的 透明度设置。alpha 通道表面的深度有 1、2、4、8 位。1 位深度 alpha 通 道表面仅支持两种透明设置,不透明(非透明)或不可见(全透明)。另一方 面,8 位 alpha 通道表面允许 256 种不同的透明度设置。Alpha 调配是不被 DirectDraw 仿真的功能的一个例子。为了使用 alpha 调配,因而就需要有 支持它的显示硬件或建立在应用程序之中的定制调配方案。 Z-缓冲区和 3D 设备表面用于 3D 应用程序中。这些类型的表面已被特别地 加入到 DirectDraw 之中,以支持 Direct3D。Z-缓冲区用于景象绘制时期, 以跟踪景象中离浏览者最近的对象,从而该对象可以在其他对象的前面出 现。3D 设备表面可以用来作为 Direct3D 绘制目标的表面。本书并不包括 Z-缓冲区或 3D 设备。 第三节 元件对象模型(COM) 1.Microsoft 的 COM 规格 DirectDraw 根据 Microsoft 的 COM(Component Object Model)规格得以实 现。COM 设计成用来提供完全便携的、安全的、可升级的软件结构,COM 是 一个大的项目,但是它并不是本软件讨论的对象。我们讨论 COM 只是为了 方便使用 DirectDraw 进行编程。 COM 使用一个面向对象的模型,这要比像 C++等语言所用的模型更严格。例 如,COM 对象经常通过成员函数进行存取,而且并不具备公共数据成员。COM 对继承性的支持与 C++相比也是有限的。 2. 对象和接口的比较 COM 在对象和接口之间具有很大的区别。COM 对象提供实际函数性,而 COM 接口则提供存取函数性的方法。COM 对象不能被直接存取,相反,所有的存 取者通过接口来完成。这条规则是如此强有力的得到遵守,以致于我们不 能给任何 COM 对象以名称。我们只能给用来存取对象的接口名称。因为我 们无法存取 COM 对象,所以这里绝大多数时候是根据接口来讲的。 一个 COM 对象能够支持多个接口。这听起来像个特例,但是它经常出现, 因为根据 COM 规格,一个 COM 接口一旦定义之后,就不能再被改变或增加。 这样做是为保证旧程序在一个 COM 对象升级的时候不会被停止使用。这个 初始接口始终存在,一个新的、替换的接口在提供存取对象的新的函数性 的时候才被提供。 3. IUnknown 接口 所有的 COM 接口都是从 IUnknown 接口中衍生出来的。“I”标志经常用于 命名 COM 接口(它代表 Interface 即界面)。DirectDraw 接口总以“I”开头。 但是在文献中经常看不到这个标志。以后提到接口时也将省略“I”标志。 IUnknown 接口将提供 3 个成员函数,所有 COM 接口因而继承这些函数: ●AddRef() ●Release() ●QueryInterface() AddRef()和 Release()成员函数为称为生命期封装(lifetime encapsulation)的 COM 功能提供支持。生命期封装是一个将每一个对象根 据它自己的结构放置的协议。 生命期封装通过引用值来实现。每一个对象拥有一个可以跟踪对象的指针 数,或者引用的内部值。当对象创建之后,该值为 1。如果附加的接口或接 口的指针被创建,则该值递增。与此类似,如果接口的指针被破坏,则该 值递减。当它的引用数到 0 的时候,该对象自行破坏。 AddRef()函数用来使对象的内部引用值递增。绝大部分时间里,该函数通 过 DirectDraw API 被用户调用。例如,当你使用 DirectDrawaw API 创建 一个新的接口时,创建函数就自动调用 AddRef()。 Release()函数用来给对象的内部引用值递减。用户应该在接口的指针将要 超出范围时或者要通过使用接口指针来结束时使用这个函数。AddRef()和 Release()函数都返回一个值,表示对象新的引用值。 QueryInterface()函数允许 COM 对象就它们是否支持特定接口进行查询。 例如,升级的 COM 对象提供附加的接口,而非现有接口的修改版。 QueryInterface()函数可以用来确定旧的接口,以决定新的接口是否被支 持。如果被查询的对象不支持有问题的接口,则更替接口的指针就返回。 4. GUID 为了查询一个对象是否支持使用 QueryInterface()函数的指定接口,就有 秘要识别有问题的接口。这通过接口的 GUID(Globally Unique IDentifier) 来实现。一个 GUID 是一个 128 位的值,也就是说,对于所有意图和目的是 唯一的。所有 DirectDraw 接口的 GUIDs 都包含在 DirectX 头文件中。 上述对于 COM 的简单介绍,就是为有效使用 DirectDraw API 所需要的全部 内容。以后当我们再讨论 DirectDraw API 时,你会发现这些内容是有联系 的。 第四节 DirectDraw 接口函数 1.关于 DirectDraw API 衡量 API 的一个方法就是看它的大小。一个庞大复杂的 API 可能就是计划 不周的结果。另一方面,一个庞大的 API 有时就意味着每一种情况都有可 能出现。一个小的 API 就是一个新的、缺乏功能的软件包的证据。它也意 味着,一个 API 只能做它所需要做的,而不能多做一点。 DirectDraw API 是比较小的,因此本章中所讨论的每一个函数不致于使本 章看起来像一本参考手册。DirectDraw 提供很少的方便,也很少有限制。 DirectDraw 由个 COM 对象构成,每个对象可以通过一个或多个接口存取。 这些接口包括: ●DirectDraw ●DirectDraw2 ●DirectDrawSurface ●DirectDrawSurface2 ●DirectDrawSurface3 ●DirectDrawPalette ●DirectDrawClipper 我们将讨论每一个接口,并随后讨论它们的成员函数。但我们并不讨论每 个函数的细节,因为我们并不是向您提供一份参考手册。相反,我们将讨 论每个函数是干什么的,为什么这样使用,以及你有可能如何去使用它。 当 DirectX 首次推出的时候(早先它被称作 Games SDK),DirectDraw 核心 函数性以 DirectDraw 接口表示。当 DirectX2 推出的时候,DirectDraw 也 已经被升级了。DirectDraw 遵守 COM 规格而未被改变。新的函数性可能通 过 DirectDraw2 接口存取。 特别要注意的是,DirectDraw2 接口是 DirectDraw 接口的超级设置。 DirectDraw2 接口可提供 DirectDraw 接口的所有函数,另外还增加了一些 新的函数。如果你正在使用 DirectX 或更高版高,那么你可以随意选用 DirectDraw 接口或 DirectDraw2 接口。但是,由于 DirectDraw2 接口较 DirectDraw 接口的功能更强,所以没有必要使用 DirectDraw 接口。同样, Microsoft 并不主张使用这些无组织的、网络可变的接口。因此,在本书以 后的程序中我们只使用 DirectDraw2 接口。 DirectDraw 和 DirectDraw2 接口提供的成员函数如下(按字母顺序排列): ●Compact() ●CreateClipper() ●CreatePalette() ●CreateSurface() ●DuplicateSurface() ●EnumDisplayModes() ●EnumSurfaces() ●FlipToGDISurface() ●GetAvailableVidMem() ●GetCaps() ●GetDisplayMode() ●GetFourCCCodes() ●GetGDISurface() ●GetMonitorFrequency() ●GetScanline() ●GetVerticalBlankStatus() ●RestoreDisplayMode() ●SetCooperativeLevel() ●SetDisplayMode() ●WaitForVerticalBlank() 接下来我们讨论 DirectDraw 接口函数。注意,在本章以后的内容中, DirectDraw 接口既表示 DirectDraw 接口,也表示 DirectDraw2 接口。只有 在区分 DirectDraw 接口和 DirectDraw2 接口的函数时,才加以区别。 1. 接口创建函数 DirectDraw 接口表示 DirectDraw 本身。该接口在被用于创建其他 DirectDraw 接口实例时,是一个主接口。DirectDraw 接口提供三个这样的 接口实例创建函数: ●CreateClipper() ●CreatePalette() ●CreateSurface() CreateClipper()函数用于创建 DirectDrawClipper 接口实例。并非所有的 DirectDraw 应用程序都用到剪裁器,所以该函数并不是在所有的程序中都 出现。我们将很快讨论 DirectDrawClipper 的细节。 CreatePalette()函数用于创建 DirectDrawPalette 接口实例。同 DirectDrawClipper 一样,并非所有的 DirectDraw 应用程序都用到调色板。 比如,应用程序使用 16 位显示模式时,就不用调色板。但是,当应用程序 使用 8 位显示模式时,就必须创建至少一个 DirectDrawPalette 实例。 CreateSurface()函数用于创建 DirectDrawSurface 接口实例。任何一个 DirectDraw 应用程序都要用表面来生成图像数据,因此经常要用到这一函 数。 DirectDraw 接口自己的实例是由 DirectDrawCreate()函数创建的。 DirectDrawCreate()是 DirectDraw 函数中少有的几个常规函数之一,但并 不是 COM 接口成员函数。 2. GetCaps()函数 DirectDraw 接口允许准确确定软硬件都支持的特征。GetCaps()函数可以对 两个 DDCAP 结构实例进行初始化。一个结构表明哪些特征由显示硬件直接 支持,另一个结构表明哪些特征由软件仿真支持。最好是用 GetCaps()函数 来决定你将用到的特征是否被支持。 提示:DirectX 浏览器 DirectX SKD 是与 DXVIEW 程序同时推出的。DXVIEW 说明了 DirectX 组件的 功能,包括 DirectDraw。大多数系统中,有两个 DirectDraw 项目:主显示 驱动器和硬件仿真层。第一项说明了显示硬件的功能。第二项说明了在缺 乏硬件支持的情况下,DirectDraw 将要仿真的一些特征。在具有两个以上 的 DirectDraw 支持的显示卡的计算机中,DXVIEW 会显示卡的功能。 3. SetCooperativeLevel()函数 SetCooperativeLevel()函数用于指定应用程序所要求的对显示硬件的控 制程度。比如,一个正常合作度意味着应用程序既改变不了当前显示模式, 也不能指定整个系统调色板的内容。而一个专有的合作度允许显示模式切 换,并能完全控制调色板。不管你决定使用哪种合作度,都必须调用 SetCooperativeLevel()函数。 4. 显示模式函数 DirectDraw 接口提供 4 种显示模式操作函数。它们是: ●EnumDisplayModes() ●GetDisplayMode() ●RestoreDisplayMode() ●SetDisplayMode() EnumDisplayModes()函数可用于查询 DirectDraw 使用何种显示模式。通过 设置 EnumDisplayModes()函数默认值可以得到所有的显示模式,而且可以 通过显示模式描述消除那些不感兴趣的模式。进行显示模式切换的过程中 最好使用 EnumDisplayModes()函数。现在市场上有各种各样的显示设备, 每种显示设备都有自己的特征和局限。除了默认的 640×480×8 窗口显示 模式,最好不要依靠任何给定的显示模式的支持。 GetDisplayMode()函数可以检索到有关当前显示模式的信息,并在 DDSURFACEDESC 结构实例中显示当前显示模式的宽度、高度、像素深度以及 像素格式等信息。还有别的途径可以检索到同样的信息(比如检索主表面描 述),因此该函数并不出现在所有的程序中。 SetDisplayMode()函数用于激活所支持的显示模式。 SetDisplayMode()函数的DirectDraw2 版本还允许设定显示模式的刷新率。 而 DirectDraw 接口版本的 SetDisplayMode()函数只能进行显示模式宽度、 高度和像素深度的设置。任何一个要进行显示模式切换的程序都要用到 SetDisplayMode()函数。 RestoreDisplayMode()函数用于存储调用 SetDisplayMode()函数之前的显 示模式。SetDisplayMode()和 RestoreDisplayMode()函数都要求优先使用 SetCooperativeLevel()函数得到的专有合作存取。 5. 表面支持函数 除了 CreateSurface()函数之外,DirectDraw 接口还提供了以下向个表面 相关函数: ●DuplicateSurface() ●EnumSurfaces() ●FlipToGDISurface() ●GetGDISurface() ●GetAvailableVidMem() ●Compact() DuplicateSurface()函数用于考贝当前表面。该函数只复制表面接口,不 复制内存。被复制的表面与源表面共享内存,因此改变内存的内容就同时 改变了两个表面的图像。 EnumSurfaces()函数可用于迭代所有满足指定标准的表面。如果没有指定 标准,那么所有当前表面都被枚举。 FlipToGDISurface()函数的作用是在终止页面翻转应用程序前确保主表面 得以正确存储。取消页面翻转时,有两个表面交替显示。这就是说,在终 止应用程序之前有可能没有保存最初的可见表面。这种情况下,Windows 通过绘制一个不可见表面来恢复。利用 FlipToGDISurface()函数就可以轻 而易举地避免发生这种情况。 GetGDISurface()函数可以向只被 GDI 认可的表面返回一个提针。GDI 表面 是 Windows 用于输出的表面。在进行屏幕捕捉时,这个函数非常有用, DirectDraw 可以捕捉到 Windows 桌面的任一部分。 GetAvailableVidMem()函数用于检索正在使用中的视频存储器(显示 RAM) 的数量。这一函数由 DirectDraw2 接口提供,而不是由 DirectDraw 接口提 供。该函数用于确定应用程序利用显示 RAM 可创建表面的数量。 Compact()函数不是通过 DirectX5 实现的,但它可以为视频存储器提供碎 片整理技巧。在基于显示 RAM 的表面被不断创建或受到破坏的时候,可以 释放大量内存。 6. 监视器刷新函数 DirectDraw 接口提供了 4 种适于计算机显示设备或监视器的函数,但这些 函数不适于显示卡,它们是: ●GetMonitorFrequency() ●GetScanLine() ●GetVerticalBlankStatus() ●WaitForVerticalBlank() 这些函数尤其与监视器的刷新机制紧密机连。这在确保生成动画时尽可能 不产生闪烁和图像撕裂现象时是至关重要的。但必须注意,并非所有的显 示卡/监视器组合都支持这些函数。 GetMonitorFrequency()函数用于检索监视器当前的刷新率。刷新率通常用 赫兹表示,缩写为 Hz。例如,60Hz 的刷新率表示屏幕每秒更新 60 次。 GetScanLine()函数用于向监视器返回当前正在被刷新的扫描行(水平像素 行)。不是所有的显示设备/监视器组合都支持该函数。如果这一功能得不 到支持,该函数将返回 DDERR-UNSUPPORTED。 对于高性能图形应用程序来说,通常要求利用垂直刷新同步地更新屏幕。 尤其是,当显示器刚完成屏幕刷新时,最好能够更新主表面。否则,屏幕 的一部分显示新的图像数据,而另一部分仍显示旧的图像数据,这种现象 就是所谓的图像撕裂。DirectDraw 默认利用垂直刷新同步更新屏幕。如果 不是这样还可以利用 GetVerticalBlankStatus()和 WaitForVerticalBlank()函数实现同步刷新。 7. GetFourCCCodes()函数 DirectDraw 接口提供的最后一个函数是 GetFourCCCodes()函数。该函数用 于返回显示卡所支持的 FourCC 代码。FourCC 代码用于描述非 RGB 或 YUV 表面。我们不在此讨论 YOV 表面,它们已超出本书的范围。 第五节 DirectDrawSurface 接口函数 同 DirectDraw 接口一样,DirectDrawSurface 接口也遵守 COM 规格.最初, 表面支持是由 DirectDrawSurface 接口提供的。DirectX2 介绍了 DirectDrawSurface2 接口的新的函数性,DirectX5 介绍了 DirectDrawSurface3 接口。 尽管本软件中讨论的是 DirectDraw2 接口,而不是 DirectDraw 接口,但我 们仍忠于最初的 DirectDrawSurface 接口,因为 DirectDrawSurface2 和 DirectDrawSurface3 接口新增的函数并不十分重要。在以后的内容里,我 们将用 DirectDrawSurface 接口表示这 3 种接口,除非特别注明。 DirectDrawSurface 是最大的 DirectDraw 接口,它允许表面内容的拷贝、 清除以及被调用程序直接存取。DirectDrawSurawSurface 接口总共提供 36 个成员函数,按字母顺序排列如下: ●AddAttachedSurface() ●AddOverlayDirtyRect() ●Blt() ●BltBatch() ●BltFast() ●DeleteAttachedSurface() ●EnumAttachedSurfaces() ●EnumOverlayZOrders() ●Flip() ●GetAttachedSurface() ●GetBltstatus() ●GetCaps() ●GetClipper() ●GetColorKey() ●GetDC() ●GetDDInterface() ●GetFlipStatus() ●GetOverlayPosition() ●GetPalette() ●GetPixelFormat() ●GetSurfaceDesc() ●IsLost() ●Lock() ●PageLock() ●PageUnlock() ●ReleaseDC() ●Restore() ●SetClipper() ●SetColorKey() ●SetOverlayPosition() ●SetPalette() ●SetSurfaceDesc() ●Unlock() ●UpdateOverlay() ●UpdateOverlayDisplay() ●UpdateOverlayZOrder() 1. 表面描述函数 我们首先讨论的个可用于检索表面自身信息的函数,它们是: ●GetCaps() ●GetPixelFormat() ●GetSurfaceDesc() ●SetSurfaceDesc() 同 DirectDraw 接口提供的 GetCaps()函数一样,DirectDrawSurface 接口 提供的 GetCaps()函数用于输出表征哪些特征可被表面支持的数据。该信息 包括:表面是主表面还是离屏表面;表面使用的存储器定位于显示 RAM 还 是系统 RAM。 GetPixelFormat()函数在用于高彩和真彩表面时是非常重要的,这是由于 像素格式因显示卡的不同而不同。该函数返回表征码,这些表征码表明每 一种颜色部件是如何存储的。 GetSurfaceDesc()函数返回一个表面描述。该信息包括表面的宽度、高度 和深度。表面像素格式(同样被 GetPixelFormat()函数检索)也包含在其中。 SetSurfaceDesc()函数(对于 DirectX5)来讲是新增的,只由 DirectDrawSurface3 接口提供)允许设定某些表面属性。该函数可用于指定 表面使用的内存。这一点在设计定制表面存储器管理器策略时非常有用。 2。 表面 Blt 函数 DirectDrawSurface 接口提供 3 个支持 blt 操作的函数: ●Blt() ●BltBatch() ●BltFast() Blt()函数是一个主要函数。Blt()函数能够进行常规的 blting(无特殊影响 的简单的表面到表面的 blt),同时支持延伸、旋转、镜像和颜色填充的操 作。当用于同剪裁器关联的表面时,Blt()可进行剪裁 blt 操作。 BltBatch()函数不是在 DirectX3 下实现的(你可以调用该函数,但什么也 不会发生)。执行 BltBatch()函数时,如果可能,它可同时进行多 blt 操作。 BltFast()函数是 Blt()函数的优化版本。BltFast()函数的效率提高了,但 性能却下降了。BltFast()函数不能进行一些特殊的 blt 操作,而 Blt()函 数可以。而且,BltFast()函数不能用于剪裁。但是 BltFast()函数支持源 和目标色彩键码 blt 的操作。在遵循定制剪裁例程的情况下,BltFast()函 数可进行 DirectDraw 能够提供的最快捷、灵活的 blt 操作。在下章中我们 将执行一个定制剪裁例程。 以上 3 个 blt 函数均将源表面和目标表面作为变量。其他的数据,例如 blt 在目标表面上的理想定位,是通过指定理想 blt 操作的确切属性来提供的。 一旦可能,这 3 个函数将进行硬件加速 blt。 3. Flip()函数 Flip()函数用于页面翻转操作。调用 Flip()函数可隐藏屏幕上先前可见的 表面,并使一个后备缓冲区显现。只有被明确地创建为翻转表面的表面, 才响应该函数的调用。 必须牢记,真正的翻转操作不可能总是成功。页面翻转要求有足够的显示 RAM 容纳两整屏有效数据。如果满足不了这一要求,系统 RAM 中将创建一个 后备缓冲区。这时调用 Flip()函数进行的是 blt 操作而不是页面翻转。基 于系统 RAM 的后备缓冲区中的内容被拷贝到主表面上。这样会严重影响性 能,但是,在真正的页面翻转中如果没有足够的显示 RAM,又不退出程序, 也只能如此了。如果你的应用程序要求最佳性能,就得设法避免激活不能 进行真正页面翻转的显示模式。 4. 表面状态函数 下面讨论两个能检索有关操作和翻转操作信息的函数,它们是: ●GetBltStatus() ●GetFlipStatus() GetBltStatus()函数用于确定当前是否进行 blt 操作。这一点很重要,因 为正被 blt 的表面不能进行其他操作。该函数表明,给定的表面是否正是 一个进行 blt 操作的源表面或目标表面。 同样地,GetBltStatus()函数表明是否正在进行翻转操作。即使 DirectDraw 通过 blt 操作仿真页面翻转,该函数而不 GetBltStatus()也必须用于由 Flip()函数初始化的监视器页面翻转。 5. 色彩键码函数 DirectDrawSurface 接口提供了以下两个函数,来设置和检查表面色彩键码 或色彩键码的范围,它们是: ●GetColorKey() ●SetColoKey() 默认状态下,表面没有色彩键码。一个色彩键码只对应一种颜色,但某些 硬件支持色彩键码范围。色彩键码和色彩键码范围是 DDCOLORKEY 结构定义 的。GetColorKey()和 SetColoKey()函数都将该结构的指针作为变量。在要 求表面的一部分透明时或需要进行目标色彩键码操作时,可以使用这两个 函数。 6. Lock 和 Unlock()函数 DirectDraw 的一个主要特点,就是能够提供对图像数据的直接存取。直接 存取可以提供最佳性能和更好的灵活性,因为没有中间 API 影响运行速度, 并且开发人员呆任意使用图像数据。对表面存储器的中间存取通过以下出 众个函数实现: ●Unlock() ●Lock() Lock()函数向组成表面的存储器返回一个指针,不管表面存储器位于显示 RAM 还是系统 RAM。存储器一般按线性风格排列,以便能简单地进行图像 数 据存取。Unolock()函数在完成表面存储器的存取之后指定给 DirectDraw。 对图像数据的直接存取必须付出代价。为了支持这种存取方式,DirectDraw 在表面锁定的时候必须关闭基本的 Windows 机构。在 Windows95 状态下, 如果忘记解锁表面,必定会损坏机器。 因此,表面锁定的时间应尽量缩短。测试前应仔细检查 Lock()和 Unlock() 函数之间的程序调用。因为这一程序无法用传统的调试程序进行调试。 锁定表面不能被 blt 和翻转,因此试图保持表面处于锁定状态没有任何有 益之处。而且,一旦表面解锁,由 Lock()函数检索的指针就失效了。 表面锁定后不能再次被锁定。在表面锁定时也就无法调用 Lock()函数。 7. GetDC()ReleaseDC()函数 对表面的直接存取占用很大内存,有时候把表面作为一个常规的 Windows 设备会更好。在此,DirectDrawSurface 接口提供以下两个函数: ●GetDC() ●ReleaseDC() GetDC()函数提供了一个 DC(设备环境),可以用常规的 Win32 函数写到表面 上。例如,DC 可以用 Win32 的 TextOut()函数在表面上绘制文本。用完 DC 后必须马上调用 ReleaseDC()函数。 同 Lock()和 Unlock()函数使用一样,在调用完 GetDC()函数后必须马上调 用 ReleaseDC()函数。这是因为 GetDC()函数内部调用 Lock 函数,而 ReleaseDC()函数内部调用 Unlock()函数。 8. PageLock()和 PageUnlock()函数 接下来,我们讨论另外两个与 Lock()函数和 Unlock()函数看上去非常相像 的函数: ●PageLock() ●PageUnlock() 尽管这两个函数看上去很像 Lock()和 Unlock()函数,但它们却有完全不同 的作用。PageLock()和 PageUnlock()函数用于控制 Windows 对基于系统RAM 的表面的处理方式。这两个函数由 DirectDrawSurface2 接口提供,而不是 由 DirectDrawSurface 接口提供。 当 Windows 认为当前正在运行的其他应用程序或进程更适于使用内存时, Windows 会向硬盘释放部分内存。这种缓冲对于整个系统内存都起作用,因 此驻留在系统内存中的 DirectDraw 表面有可能被存到硬盘上。如果要用到 这样的表面,Windows 需要花费一定的时间从硬盘上读取表面数据。 PageLock()函数提示 Windows 哪些给定的表面不应该释放到硬盘上。这样, 在使用表面时就不用耗费时间进行硬盘存取了。相反地,PageUnlock()函 数用于告知 Windows 哪些表面内存可被释放。 过程调用 PageLock()函数会减少缓冲内存的总量,从而导致 Windows 的速 度大大降低。至于这种情况何时发生,取决于页面锁定系统内存量及机器 提供的系统内存量。 PageLock()和 PageUnlock()函数主要是由 DirectDraw 提供而非 DirectDraw 应用程序。举个例子来说,DirectDraw 自动使用 PageLock() 函数,以确保运行 blt 操作时,基于系统 RAM 的表面不被释放到硬盘。 PageLock()函数可以被同一个表面多次调用。DirectDraw 用参考计数法记 录 PageLock()函数被调用的次数,因此多次调用 PageUnlock()函数就必须 避免多次调用 PageLock()函数。 PageLock()和 PageUnlock()函数对于驻留在显示 RAM 中的表面不起作用。 9. IsLost()的 Restore()函数 现在讨论两个与使用驻留在显示 RAM 中的表面有关的函数: ●IsLost() ●Restore() 让我们来看一看下面这种情况。应用程序正在运行时,尽量把表面分配到 显示 RAM 中,剩下的创建到系统 RAM 中。应用程序在运行一段时间之后, 用户执行或切换到另一个应用程序。该应用程序是任意的,可以是一个常 规的 Windows 程序,如 Windows 开发程序或记事本。它也可以是另外的 DirectDraw 应用程序,该程序也试图将尽可能多地分配显示 RAM。如果 DirectDraw 不接受显示 RAM,那么新的应用程序就很可能根本运行不了。 相反的情况就意味着,应用程序不允许分配到任何显示 RAM 中。 因此,DirectDraw 可以随意将任何一个或者所有的基于显示 RAM 的表面从 非激活应用程序中移走。这种情况就是所谓的表面丢失。从技术上讲,程 序仍具有表面,但它们不再同任何内存相关。要使用丢失的表面会导致 DDERR-SURFACELOST 错误。IsLost()函数可用于确定一个表面是否丢失了内 存。 表面内存丢失后可通过 Restore()函数恢复,但只能在应用程序被重新激活 之后才可恢复。这会导致应用程序无法将处于最小化状态的所有表面复原。 Restore()函数可恢复附属于表面的任一内存,但并不恢复内存的内容。表 面被复原后,应用程序就可以恢复表面内容了。 注意,这种用法不适合利用系统 RAM 创建的表面。如果需要用到基于系统 RAM 的表面所占内存,那么 Windows 会立即将这些表面释放到硬盘上。 Windows 自动地处理存储和恢复,包括恢复表面的内容。 10. GetDDInterface()函数 GetDDInterface()函数可检索用于创建给定表面的 DirectDraw 接口的指 针。由于程序中大多数情况下只有一个 DirectDraw 接口实例,所以 GetDDInterface()函数并不常用。但是一个应用程序中有可能使用多个 DirectDraw 接口,在这种情况下,GetDDInterface()函数会起到重要作用。 11. 表面连接函数 DirectDrawSurface 接口提供以下 4 个函数,用来维持表面间的连接: ●AddAttachedSurface() ●DeleteAttachedSurface() ●EnumAttachedSurfaces() ●GetAttachedSurface() DirectDraw 支持大量的用于表面间的连接情况。最常见的情况就是页面翻 转。进行页面翻转时,两个或两个以上的表面连接成环状,每次调用 Flip() 函数时,都会使连成环状的表面中的下一个表面显现。 表面连接函数用于创建、检查或消除表面间的连接,但这些函数并非必不 可少的。DirectDraw 往往是自动创建连接表面。比如,当创建一个主翻转 表面时,可以指定用于连接表面的后备缓冲区的数量。DirectDraw 就会创 建这些表面,并将它们连接起来。 12. 重叠函数 DirectDrawSurface 接口用于支持重叠的函数如下: ●AddOverlayDirtyRect() ●EnumOverlayZOrders() ●GetOverlayPosition() ●SetOverlayPosition() ●UpdateOverlay() ●UpdateOverlayDisplay() ●UpdateOverlayZOrder() GetOverlayPosition()和 SetOverlayPosition()函数用于控制重叠的位 置。UpdateOverlay()函数用于更新大量的重叠设置,包括重叠是否可见, 以及重叠是以色彩键码还是用 alpha 混合到背景表面上。 UpdateOverlayDisplay()函数用于更新显示设置。该函数用于更新整个重 叠显示,或者只更新由 AddOverlayDirtyRect()函数指定的矩形重叠部分。 EnumOverlayZOrders()函数可根据重叠的 Z 值(Z 值控制哪一个重叠位于最 上面)重复重叠。重叠可按从前到后或从后到前的顺序枚举。 13. 剪裁器函数 DirectDraw 支持的剪裁是将 DirectDrawClipper 接口(该接口我们尚未讨 论)的一个实例连接到表面。一旦连接完毕,剪裁器对象就会有规律地 blt 到表面。剪裁器/表面的连接由以下两个 DirectDrawSurface 函数控制: ●GetClipper() ●SetClipper() SetClipper()函数用来将剪裁器对象连接到表面。GetClipper()函数用于 向前一个连接的剪裁器对象返回一个指针。SetClipper()函数还可用于解 除表面同剪裁器的连接,具体的做法是通过指定 NULL 来替代 DirctDrawClipper 接口指针。 14。 调色板函数 像剪裁器一样,调色板也可连接到表面。DirctDrawSurface 接口为此提供 以下两个函数: ●GetPalette() ●SetPalette() SetPalette()函数用来将 DirctDrawPalette 接口(该接口我们接下来就要 讨论)的一个实例连接到表面。GetPalette()函数用于检索前一个连接调色 板的指针。 调色板可被连接到任何表面,但只有连接到主表面时,调色板才起作用。 当与主表面连接时,调色板决定显示硬件调色板的设置。 第六节 DirectDrawPlette 接口函数 DirctDraw 提供 DirctDrawPalette 接口用于调色板显示模式和表面。尽管 Windows 支持几种低于 8 位像素深度的显示模式,但 DirctDraw 所支持的唯 一的调色板显示模式是 8 位模式。 DirctDrawPalette 接口实例由 DirctDraw CreatePalette()函数创建。 CreatePalette()函数用大量的标志来定义调色板属性。 DirctDrawPalette 接口只提供以下 3 个函数: ●GetCaps() ●GetEntries() ●SetEntries() GetCaps()函数用于检索有关调色板的信息,包括调色板项目数量,调色板 是否支持同步垂直刷新,以及在 8 位调色板状态下是否所有的 256 个调色 板项目都能被设定。 SetEntries()函数允许在程序中设置调色板的色彩值。该数据从文件中读 取。而这些项目在运行过程中可被计算和设定。GetEntries()函数用于检 索先前设定的调色板项目。 DirctDrawPalette()接口实例可利用 DirctDrawSurface SetPalette()函 数连接到表面。将不同调色板连接到主表面或利用 SetEntries()函数改变 调色板项目都可激活调色板。 第七节 DirectDrawClipper 接口函数 DirctDrawClipper 接口支持剪裁。将剪裁器对象连接到表面并在 blt 操作 中将其当作目标表面就可以进行剪裁。 directDrawClipper 实例由 DirectDraw CreateClipper()函数创建。 DirectDrawClipper 接口支持以下 5 个函数: ●SetHWnd() ●GetHWnd() ●IsClipListChanged() ●SetClipList() ●GetClipList() 剪裁器对象一般用于出现在窗口中的 DirctDraw 应用程序必需的剪裁。要 求剪裁器必须确保在 blt 操作过程中考虑到桌面上其他的窗口。比如,当 应用程序的全部或一部分被另一个窗口遮蔽,剪裁就必须确保被遮蔽的窗 口不被 DirctDraw 应用程序破坏。 桌面剪裁由 SetWnd()函数完成。SetHWnd()函数将剪裁器对象连接到一个窗 口句柄。这样就初始化了 Windows 和剪裁器对象之间的通讯。当桌面上的 任何一个窗口发生变化时,剪裁器对象就会得到通知,并作出反应。 GetHWnd()函数用于决定剪裁器同哪一个窗口句柄连接。 IsClipListChanged()函数用于决定内部剪裁清单是否因桌面的改变而被 更新。 SetClipList()和 GetClipList()函数为 DirectDrawClipper 接口提供便利 的定制使用。SetClipList()函数用于定义一些矩形区域,这些矩形区域用 来定义 blt 操作的合法区域。GetClipList()函数用于检索剪裁器的内部剪 裁数据。 一旦被连接到表面,Blt(),BltBatch()以及 UpdateOverlay()函数所进行 的 blt 操作将根据 DirctDrawCliper 接口中包含的数据被自动地剪裁。注 意,Blt Fast()函数在此被忽略。BltFast()函数不支持剪裁。 第八节 附加 DirectDraw 接口 DirctDraw 还提供了另外个我们没有讨论的接口,它们是: ●DDVideoPortContainer ●DirectDrawColorControl ●DirectDrawVideoport 这些接口由 DirectX5 介绍,它们提供低水平视频端口控制。这些接口还向 DirctDraw 表面提供流活动视频的方法。尽管利用这些接口可以为 DirctDraw 应用程序增加视频支持,但除非高水平视频 APIs 不能满足需要, 否则最好不用这一方法。 第九节 DirectDraw 结构 我们已讨论过 DirctDraw 接口及其成员函数了,接下来再看看 DirctDraw 定义的结构。DirctDraw 总共定义了 8 全结构: ●DDBLTFX ●DDCAPS ●DDOVERLAYFX ●DDPIXELFORMAT ●DDSURFACEDESC ●DDSCAPS ●DDBLTBATCH ●DDCOLORKEY 我们已经见过其中的一些结构了,比如在讨论 DirctDrawSurface SetColorKey()函数时我们就接触过 DDCOLORKEY 结构。在此,我们不详细 讨论每一个结构的细节,但必须指出,DirctDraw quirk 被忘记时会导致失 败。 以上所列结构中的前 5 个有一个称作 dwSize 的字段。该字段用于存储结构 的大小,设定该字段的工作由你来做。另外,该字段如果没有被正确设定 的话,那么任何一个将这 5 个结构作为变量的 DirctDraw 函数都会失效。 以 DDSURFACEDESC 结构为例,使用结构的代码如下: DDSURFACEDESC surfdesc; surfdesc.dwSize=sizeof(surfdesc); surf->GetSurfaceDesc(&surfdesc); 首先声明结构,然后用 sizeof()关键字设定 dwSize 字段。最后结构传递给 DirctDrawSurface GetSurfaceDesc()函数。忘记设定 dwSize 字段将导致 这段代码失效。 到底为什么 DirctDraw 坚持要求给出它所定义的结构的大小?原因在于这 5 个包含 dwSize 字段的结构将来有可能会改变。DirctDraw 会检查结构的大 小,以便确定正在使用的版本。DirctDraw 坚持要求给出一个有效的大小值, 是为了让开发者提供有效的结构大小。这样做是有好处的,因为 DirctDraw 的新版本可以正确运用旧版本的 DirctDraw 程序。 在使用结构之前,最好将结构初始化为零。这样,前面的代码就变成: DDSURFACEDESC surfdesc; ZeroMemory (&surfdesc,sizeof(surfdesc)); surfdesc.dwSize=sizeof(surfdesc); surf->GetSurfaceDesc(&surfdesc); ZeroMemory()函数是一个 Win32 函数,它将作为第一个参数的存储器设定 为零.ZeroMemory()函数的第二个参数表明有多少存储器应被初始化。这一 做法的好处是,通过 GetSurfaceDesc()函数调用可以知道结构的哪些字段 被更新了。如果没有对结构进行初始化,就有可能将结构字段中不可预测 的值当作 DirectDraw 的设定值。 第十节 窗口应用程序 DirctDraw 应用程序主要有两种型式:窗口的和全屏的。窗口 DirctDraw 应用程序看起来就像一个常规的 Windows 程序。我们很快将讨论到全屏应 用程序。 窗口应用程序包括窗口边界、标题框以及菜单,这些都是传统的 Windows 应用程序中常见的部分。由于窗口应用程序同其他窗口一起出现在桌面上, 因此它们被迫使用 Windows 当前所使用的分辨率和比特深度。 窗口程序有一个主表面,但只在进行真实页面翻转时才显现。而且,主表 面并不代表窗口的客户区域(该区域在窗口边界内)。主表面还代表整个桌 面。这就是说,你的程序必须追踪窗口的位置和大小,以便在窗口内正确 显示可见的输出。换言之,利用窗口化的应用程序中可以在整个桌面上进 行绘图。 如果不允许页面翻转,那么图像就必须从离屏缓冲区 blt 到主表面上。这 就增加了图像撕裂的可能性,因为 blt 比页面翻转速度慢。为了避免图像 撕裂,blt 操作可以与监视的刷新率保持同步。 如果与窗口客户区域同样大小的离屏缓冲区被创建到显示 RAM 中,窗口应 用程序就可以很好地运行。这样,窗口的内容可利用离屏表面合成。然后 离屏表面可以通过硬件加速很快地到主表面上。 由于显示存储器的缺乏而不得不将离屏缓冲区创建到系统 RAM 中时,会严 重影响性能。不幸的是,这种情况常常发生,尤其是在只有 2MB 显示卡的 时候,这是因为人们总希望为自己 Windows 的桌面设置高分辨的显示模式。 例如,采用 1024×768×16 显示模式的主表面自己就要占用 2MB 的 RAM。在 一个 2MB 显示卡上,几乎没有显示 RAM 留给离屏表面。 窗口应用程序的另一个问题是剪裁。一个性能良好的应用程序必须有一个 连接到主表面的剪裁对象。这是有损性能的,原因在于为了检查剪裁器的 剪裁清单内容,blt 操作只能在一个小的矩形部分内进行。而且,不能使用 优化的 BltFast()函数。Bltting 必须用较慢的,(而且更笨重的)Blt()函 数。 最后要讲的是,窗口应用程序不允许全调色板控制。由于 Windows 保留了 20 个调色板项,所以在 256 种颜色只有 236 种颜色可被设定。被 Windows 保留的颜色只用系统调色板的前 10 个项和后 10 个项。因此在给图像上色 时,只能使用中间 236 个调色板项。 第十一节全屏应用程序 包含对显示设备进行专有存取的 DirctDraw 应用程序就是全屏应用程序。 这种应用程序可以任意选取显示卡所支持的显示模式,并享有全调色板控 制。另外,全屏应用程序还可进行页面翻转。因此同窗口应用程序相比, 全屏应用程序速度更快,灵活性更好。 典型的全屏应用程序首先检查所支持的显示模式,并激活其中一个。然后 创建具有一个或更多后备缓冲区的可翻转主表面,剩下的显示 RAM 用于创 建离屏表面。当显示 RAM 耗尽时,就启用系统 RAM。屏幕被在后备缓冲区中 合成的第一个场景更新,然后进行页面翻转。即使主表面占用了所有的可 用的显示 RAM,全屏应用程序还可输出执行其窗口化的副本,这是因为全屏 应用程序可进行真实页面翻转。 由于全屏应用程序不一定非得使用 Windows 的当前显示模式,所以显示 RAM 的可用与否不存在多少问题。如果只检测到 2MB 的显示 RAM,就可使用低分 辨率显示模式来保留内存。如果检测到 4MB 的显示 RAM,应用程序就可使用 要求的显示模式并仍有保持良好性能。 全调色板控制也是个有利因素。这样可以使用所有 256 个调色板项而无需 根据 Windows 保留的 20 中颜色重新分配位图。 第十二节混合应用程序 混合应用程序既可以在全屏方式下运行也可在窗口方式下运行。混合应用 程序内部非常复杂,但却可以提供良好的性能。用户可在窗口方式下运行, 如果太慢,则可切换到全屏方式下运行。 编写混合应用程序最好的方法是编写一个定制库,该库包括那些与应用程 序使用全屏方式还是窗口方式无关的函数。 游戏开发基础(6) DirectSound 第一节 关于声音 声音是空气的一系列振荡,称为声波,一般可以用二维的波形图来表示。数字音频 是指使用某种设备将声波记录下来并保存为一种数字化的文件。播放相应的文件就 可以产生某种声音效果。数字音频的音质随着采样频率及所使用的位数不同而有很 大的差异。因此,了解所使用音频文件格式的有关标准是很有必要的。例如,CD 中的音频是 16 位,采样频率达到 44.1MHz 的立体声数字音频。 在所有声音文件的格式中,WAV 是最普遍的。这是 Windows 平台上最常见的格式, 由微软公司创造。支持 8 位和 16 位的音质、多样本、对立体声和单声道音频均可 播放。它还支持多种音频压缩算法。 要在游戏中取得好的声音效果,例如,使用 3D音效,可以有两种方法来实现:一 是使用一定的工具软件对声音文件进行处理,生成播放效果足够好的文件,然后在 游戏程序中直接将这样的文件播放。显然,这样比较简单,但是不灵活。如果需要 音效随着游戏场景的变化而不断改变,且不受所具有声音文件数量的限制,就需要 进行实时混音了。 第二节 DirectSound 结构 DirectSound 的功能模块包括播放、声音缓冲区、三维音效、音频抓获、属性集等。 DirectSound playback 建构于 IDirectSound COM 接口之上。IDirectSoundBuffer, IDirectSound3DBuffer 和 IDirectSound3DListener 接口则用以实现对声音缓冲区和三维音效的操作。 DirectSound capture 建构于 IDirectSoundCapture 和 IDirectSoundCaptureBuffer COM 接口之上。 其它的 COM 接口,如 IKsPropertySet,使应用程序能够从声卡的扩展功能中最大 地受益。 最后,IDirectSoundNotify 接口用于在播放或音频抓获达到一定地方时向产生一个 事件。 第三节 播放功能概述 DirectSound 缓冲区对象表示一个包含声音数据的缓冲区,这些数据以 PCM 格式 被存储。该对象不仅可以用于开始、停止或暂停声音的播放,还能够设置声音数据 中诸如频率和格式等属性。 缓冲区分为主缓冲区和副缓冲区。主缓冲区中是听者将要听到的音频信号,一般是 将副缓冲区中信号混音后的结果。而副缓冲区中存放着许多单独的声音信号,有的 可以直接播放,有的要混音,有的循环播放。主缓冲区由 DirectSound 自动创建, 而副缓冲区需由应用程序来创建。DirectSound 将副缓冲区中的声音混合后,存入 主缓冲区,再输出到相应播放设备。 DirectSound 中没有解析声音文件的功能,需要您自己在应用程序中将不同格式的 声音信号改变过来(PCM)。 缓冲区可以在主板的 RAM、波表存储器、DMA 通道或虚拟存储器中。 多个应用程序可以用同一声音设备来创建 DirectSound 对象。当输入焦点在应用程 序中发生变化时,音频输出将自动在各个应用程序的流之间切换。于是,应用程序 不用在输入焦点改变中反复地播放和停止它们的缓冲区。 通过 IDirectSoundNotify 接口,当播放到了一个用户指定的地方,或播放结束时, DirectSound 将动态地通知拥护这一事件。 第四节 音频抓获概述 DirectSoundCapture 对象可以查询音频抓获设备的性能,并为从输入源抓获音频 而创建缓冲区。 其实,在 Win32 中早已经有了抓获音频的功能,而目前的(版本 5) DirectSoundCapture 与只比较并没有什么新的功能。不过,DirectSoundCapture API 使您能够编写使用相同接口的播放和音频抓获程序,而且,这也为将来可能出 现的 API 改进提供了原始模型,使您可以从中受益。 DirectSoundCapture 还能够抓获压缩格式的音频。 DirectSoundCaptureBuffer 对象表示一个用于抓获音频的缓冲区。它可以循环利 用,也就是说,当输入指针达到缓冲区的最后时,它会回到开始的地方。 DirectSoundCaptureBuffer 对象的各种方式使您能够设定缓冲区的属性、开始或停 止操作、锁定某部分存储器(这样就可以安全地将这些数据保存或用于其它目的)。 与播放类似,IDirectSoundNotify 接口使在输入指针到达一定地方时通知用户。 第五节 初始化 对于一些简单的操作,可以使用缺省的首选设备。不过,在游戏的制作中,我们可 能还是需要知道一些特定的声音设备。于是,您应该先列举出可用的声音设备。 在此之前,您需要先设定一个回收函数,在每一次 DirectSound 发现新设备后调用 该函数。函数中您可以做任何事情,但您必须将它定义得与 DSEnumCallback 形式 相同。如果希望列举继续,函数应返回真,否则返回假。 下面的例程来自光盘 Example 目录下的 Dsenum.c 文件。它列举可用的设备并在 一个列表框中增加一条相应的信息。首先是他的回收函数: BOOL CALLBACK DSEnumProc(LPGUID lpGUID, LPCTSTR lpszDesc, LPCTSTR lpszDrvName, LPVOID lpContext ) { HWND hCombo = *(HWND *)lpContext; LPGUID lpTemp = NULL; if( lpGUID != NULL ) { if(( lpTemp = LocalAlloc( LPTR, sizeof(GUID))) == NULL ) return( TRUE ); memcpy( lpTemp, lpGUID, sizeof(GUID)); } ComboBox_AddString( hCombo, lpszDesc ); ComboBox_SetItemData( hCombo, ComboBox_FindString( hCombo, 0, lpszDesc ), lpTemp ); return( TRUE ); } 当包含了列表框的对话框被初始化后,列举开始: if (DirectSoundEnumerate((LPDSENUMCALLBACK)DSEnumProc, &hCombo) != DS_OK ) { EndDialog( hDlg, TRUE ); return( TRUE ); } 创建 DirectSound 对象最简单的方法是使用 DirectSoundCreate 函数。其中的第一 个参数为相应设备的全局独有标志符(GUID)。您可以通过列举声音设备得到 GUID,或使用 NULL 来为缺省设备创建对象。 LPDIRECTSOUND lpDirectSound; HRESULT hr; hr = DirectSoundCreate(NULL, &lpDirectSound, NULL)); 创建 DirectSound 对象后,应设置合作层。这是为了确定各个 DirectSound 应用程 序被允许操作声音设备的范围,防止它们在错误的时间或通过错误的方式操作设 备。 所使用的方式为 IDirectSound::SetCooperativeLevel。这里 hwnd 参数是应用程序 窗口的句柄: HRESULT hr = lpDirectSound->lpVtbl->SetCooperativeLevel( lpDirectSound, hwnd, DSSCL_NORMAL); 这里确定的合作层为 normal,这样使用声卡的应用程序可以顺序地进行切换。合 作层包括 Normal、Priority、Exclusive 和 Write-primary,级别依次增加。 正如在前面提到过,DirectSound 可以充分发挥硬件的增强功能,因此,它需要先 设法了解设备的特性。我们可以通过 IDirectSound::GetCaps 方式来达到这个要求。 如下所示: DSCAPS dscaps; dscaps.dwSize = sizeof(DSCAPS); HRESULT hr = lpDirectSound->lpVtbl->GetCaps(lpDirectSound, &dscaps); DSCAPS 结构接收关于声音设备性能和资源的信息。注意,初始化该结构中 dwSize 成员是调用它之前所必须的。 除此之外,您还可以查询和设定扬声器的设置,以及整理声音存储器使尽量获得最 大的备用空间。 第六节 如何播放 初始化完成后,DirectSound 将自动创建主缓冲区用于混音并传送至输出设备。而 副缓冲区则需要您自己来创建了。 下面的例程演示了用 IDirectSound::CreateSoundBuffer 方式创建一个基本的副缓 冲区: BOOL AppCreateBasicBuffer( LPDIRECTSOUND lpDirectSound, LPDIRECTSOUNDBUFFER *lplpDsb) { PCMWAVEFORMAT pcmwf; DSBUFFERDESC dsbdesc; HRESULT hr; // 设定声波格式结构 memset(&pcmwf, 0, sizeof(PCMWAVEFORMAT)); pcmwf.wf.wFormatTag = WAVE_FORMAT_PCM; pcmwf.wf.nChannels = 2; pcmwf.wf.nSamplesPerSec = 22050; pcmwf.wf.nBlockAlign = 4; pcmwf.wf.nAvgBytesPerSec = pcmwf.wf.nSamplesPerSec * pcmwf.wf.nBlockAlign; pcmwf.wBitsPerSample = 16; // 设置 DSBUFFERDESC 结构,用以设定缓冲区控制选项 memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); dsbdesc.dwSize = sizeof(DSBUFFERDESC); // 要求缺省的控制 dsbdesc.dwFlags = DSBCAPS_CTRLDEFAULT; // 3 秒的缓冲区 dsbdesc.dwBufferBytes = 3 * pcmwf.wf.nAvgBytesPerSec; dsbdesc.lpwfxFormat = (LPWAVEFORMATEX)&pcmwf; // 创建缓冲区 hr = lpDirectSound->lpVtbl->CreateSoundBuffer(lpDirectSound, &dsbdesc, lplpDsb, NULL); if(DS_OK == hr) { // 成功,获得的接口在*lplpDsb 当中 return TRUE; } else { // 失败 *lplpDsb = NULL; return FALSE; } } 您必须设定缓冲区的控制选项。这是使用 DS BUFFERDESC 结构中的 dwFlags 成员, 具体细节请参见 DirectX 5 的帮助。 副缓冲区不支持混音等特效,因此,您需要能够直接操作主缓冲区。不过,当您获 权写主缓冲区时,其它特性将失去作用,从而硬件加速混音失效。所以,大部分应 用程序几少直接操作主缓冲区。 如果要求操作主缓冲区,可以在调用 IDirectSound::CreateSoundBuffer 方式时设 定 DSBUFFERDESC 结构中的 DS BCAPS_PRIMARYBUFFER 标志符,而且,合作层 必须是 Write-primary。 下面的例程演示了如何得到对主缓冲区的写操作能力: BOOL AppCreateWritePrimaryBuffer( LPDIRECTSOUND lpDirectSound, LPDIRECTSOUNDBUFFER *lplpDsb, LPDWORD lpdwBufferSize, HWND hwnd) { DSBUFFERDESC dsbdesc; DSBCAPS dsbcaps; HRESULT hr; // 设置声波格式结构 memset(&pcmwf, 0, sizeof(PCMWAVEFORMAT)); pcmwf.wf.wFormatTag = WAVE_FORMAT_PCM; pcmwf.wf.nChannels = 2; pcmwf.wf.nSamplesPerSec = 22050; pcmwf.wf.nBlockAlign = 4; pcmwf.wf.nAvgBytesPerSec = pcmwf.wf.nSamplesPerSec * pcmwf.wf.nBlockAlign; pcmwf.wBitsPerSample = 16; // 设置 DSBUFFERDESC 结构 memset(&lplpDsb, 0, sizeof(DSBUFFERDESC)); dsbdesc.dwSize = sizeof(DSBUFFERDESC); dsbdesc.dwFlags = DSBCAPS_PRIMARYBUFFER; // 缓冲区大小由声音硬件决定 dsbdesc.dwBufferBytes = 0; dsbdesc.lpwfxFormat = NULL; // 对主缓冲区必须设为 NULL // 获得 write-primary 合作层 hr = lpDirectSound->lpVtbl->SetCooperativeLevel(lpDirectSound, hwnd, DSSCL_WRITEPRIMARY); if (DS_OK == hr) { // 成功,试图创建缓冲区 hr = lpDirectSound->lpVtbl->CreateSoundBuffer(lpDirectSound, &dsbdesc, lplpDsb, NULL); if (DS_OK == hr) { // 成功,设定主缓冲区为 desired 格式 hr = (*lplpDsb)->lpVtbl->SetFormat(*lplpDsb, &pcmwf); if (DS_OK == hr) { // 如果希望得知缓冲区大小,调用 GetCaps dsbcaps.dwSize = sizeof(DSBCAPS); (*lplpDsb)->lpVtbl->GetCaps(*lplpDsb, &dsbcaps); *lpdwBufferSize = dsbcaps.dwBufferBytes; return TRUE; } } } // 设定合作层失败 // 创建缓冲区,或设定结构 *lplpDsb = NULL; *lpdwBufferSize = 0; return FALSE; } 播放一段声音的过程包括以下四个步骤: 1 锁定(IDirectSoundBuffer::Lock)副缓冲区的一部分。由您设定的偏移量决定 下一步写操作的起始点; 2 写数据; 3 解锁(IDirectSoundBuffer::Unlock); 4 将声音传送给主缓冲区,并由那里输出(IDirectSoundBuffer::Play)。 下面的 C 程序向缓冲区中写入数据,由 dwOffset 指定开始时的偏移量: BOOL AppWriteDataToBuffer( LPDIRECTSOUNDBUFFER lpDsb, // DirectSound 缓 冲区 DWORD dwOffset, // 自己的写标记位置 LPBYTE lpbSoundData, // 数据的起点 DWORD dwSoundBytes) // 拷贝块的大小 { LPVOID lpvPtr1; DWORD dwBytes1; LPVOID lpvPtr2; DWORD dwBytes2; HRESULT hr; // 得到被写块的地址 hr = lpDsb->lpVtbl->Lock(lpDsb, dwOffset, dwSoundBytes, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0); // 如果返回 DSERR_BUFFERLOST,释放并重试锁定 if(DSERR_BUFFERLOST == hr) { lpDsb->lpVtbl->Restore(lpDsb); hr = lpDsb->lpVtbl->Lock(lpDsb, dwOffset, dwSoundBytes, &lpvPtr1, &dwAudio1, &lpvPtr2, &dwAudio2, 0); } if(DS_OK == hr) { // 写到指针 CopyMemory(lpvPtr1, lpbSoundData, dwBytes1); if(NULL != lpvPtr2) { CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2); } // 释放 hr = lpDsb->lpVtbl->Unlock(lpDsb, lpvPtr1, dwBytes1, lpvPtr2, dwBytes2); if(DS_OK == hr) { // 成功 return TRUE; } } // 某步骤失败 return FALSE; } 游戏开发基础(7) 第七章 游戏编程的特点 第一节 概述: 电脑游戏在计算机发展使用中可以说扮演了一个极为有趣的角色,一方面不为很多 人所赞同,认为是一种浪费;而另一方面电脑游戏却是推动计算机的各项技术迅速 发展的最有力的力量之一。 这一点,可以从 3d 类游戏对硬件无止境的需求,游戏迷对游戏图像的质量、游戏 的交互性、人机界面的友好性等方面的需求体现出来(当然游戏迷对游戏的的构思、 创意的要求也是苛刻且无止境的,但这一点只有靠您自己的想象力,我们是爱莫能 助了)。 从游戏近期的发展中,我们从 3d 游戏的发展,可以看到从 Doom 到现在的古墓丽 影 2、雷神之锤 2,3d游戏的画面从生硬单调的多边形到今天柔和复杂几进乱真的 场景、道具、怪物,敌人从只会疯狂向你冲来到今天会偷袭、会审时度势地采取合 适的方式方法向你进攻;游戏无论从硬件支持还是编程技术方面都有突飞猛进的进 展。在游戏发展的过程中,很多技术也随之发展起来了,例如各种图形加速卡的出 现和发展,directx 的出现,和各个成功游戏中采用的各种优化技术都推动了计算 机技术的发展。 游戏可以说是集合了每个时期计算机行业中最先进的硬件技术和最新的编程思想, 比如近期的游戏都是采用了面向对象的编程思想的基于 Windows 的软件,大部分 图象要求高的游戏都要求或支持图形加速卡。同时游戏编程中也有自己基本的方式 方法、结构和理论,在这一章的学习中我们将讨论这些问题。 在这一章中我们将讨论下面几个问题: 程序入口 即是游戏获取外部操作的讯息,得到下次刷新所需的新参数的手段。如 同一般的 SDK Windows 应用程序一样,程序的入口为 WINMAIN()。 游戏初始化 包括创建标准的 WINDOWS 程序所需的初始化程序以及游戏内部的 初始化程序,例如游戏系统初始化、游戏图形的装入、游戏声音的装入等。 游戏内部循环: 游戏的循环入口是 WINDOWS 消息循环内部的一个函数调用,游 戏内部循环包括刷新游戏单位、画游戏单位两部分。 刷新游戏单位: 用于每一帧刷新游戏单位的状态,例如改变游戏单位的状态、改变 游戏单位的位置、获取外部信息等。 画游戏单位: 用于每一帧往屏幕上画游戏单位的图象,并进行特殊处理以提高速 度。 计算机人工智能: 主要用于受计算机处理的游戏单位的行为控制算法,程序部分位 于刷新计算机游戏单位部分中。 游戏内存管理: 这一部分对于优质高效的游戏软件是十分重要的,内存管理不当会 导致游戏性能的降低,甚至引起死机。 游戏交互设计: 交互设计是游戏可玩性的关键,友好的交互界面和交互方式可以使 游戏增色不少。 游戏图象底层设计: 游戏软件的主要处理时间花在处理图象和画图象上,所以游戏 图象底层的设计对于游戏的最终效果是十分重要的。 游戏多媒体设计: 主要包括图形界面设计、游戏音乐音效设计、游戏动画设计、游 戏影象设计的几个方面,更广泛的说还包括游戏所有运行过程的功能设计。 第二节 程序入口 这个标题看起来似乎很难理解,它的意思就是当游戏被启动时,计算机从什么地方 开始运行程序的。在 Windows 的应用程序上,Winmain()函数一般就是程序入 口。游戏开始后,就调用 Winmain()函数,然后再按语句的顺序或所接受到的消 息调用相应的函数。 从第三章 Windows 编程基础中我们了解到 Winmain()函数的的结构、运行过程, 现在我们就游戏编程的角度来讨论 Winmain()函数的编制。 int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { MSG msg; while( lpCmdLine[0] == '-' || lpCmdLine[0] == '/') { lpCmdLine++; switch (*lpCmdLine++) { case 'e': bUseEmulation = TRUE; break; case 'w': bFullscreen = FALSE; break; case 'f': bFullscreen = TRUE; break; case '1': CmdLineBufferCount = 1; break; case '2': case 'd': CmdLineBufferCount = 2; break; case '3': CmdLineBufferCount = 3; break; case 's': bStretch = TRUE; break; case 'S': bWantSound = FALSE; break; case 'x': bStress= TRUE; break; case '?': bHelp= TRUE; bFullscreen= FALSE; // give help in windowed mode break; } while( IS_SPACE(*lpCmdLine) ) { lpCmdLine++; } } GameMode.cx = getint(&lpCmdLine, 640); GameMode.cy = getint(&lpCmdLine, 480); GameBPP = getint(&lpCmdLine, 8); /* * create window and other windows things */ if( !initApplication(hInstance, nCmdShow) ) { return FALSE; } /* * Give user help if asked for * * This is ugly for now because the whole screen is black * except for the popup box. This could be fixed with some * work to get the window size right when it was created instead * of delaying that work. see ddraw.c * */ if( bHelp ) { MessageBox(hWndMain, "F12 - Quit\n" "NUMPAD 2 - crouch\n" "NUMPAD 3 - apple\n" "NUMPAD 4 - right\n" "NUMPAD 5 - stop\n" "NUMPAD 6 - left\n" "NUMPAD 7 - jump\n" "\n" "Command line parameters\n" "\n" "-e Use emulator\n" "-S No Sound\n" "-1 No backbuffer\n" "-2 One backbuffer\n" "-4 Three backbuffers\n" "-s Use stretch\n" "-x Demo or stress mode\n", OUR_APP_NAME, MB_OK ); } /* * initialize for game play */ if( !InitGame() ) { return FALSE; } dwFrameTime = timeGetTime(); while( 1 ) { if (PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE)) { if (!GetMessage( &msg, NULL, 0, 0)) { break; } TranslateMessage(&msg); DispatchMessage(&msg); } else if (!bPaused && (bIsActive || !bFullscreen)) { ProcessFox(lastInput); lastInput=0; } else { WaitMessage(); } } if (AveFrameRateCount) { AveFrameRate = AveFrameRate / AveFrameRateCount; Msg("Average frame rate: %d", AveFrameRate); } return msg.wParam; } /* WinMain */ 我们知道在消息循环之中只有一个消息----WM_QUIT 可以结束这个循环,退出 WINDOWS。所以我们要在消息循环之前完成所有的工作即所有的初始化。关于初 始化这个概念在下一节我们将详细讨论。在我们提供的例程(在光盘的 sample 目 录中)中的 foxbear.c 里的 WMain()中我们可以看到在消息循环之前先运行 DDint ()函数对 DirectDraw 进行初始化,检测命令行参数并对相关的参数进行赋值和 确定显示模式,进行窗口的初始化,检测 bhelp 的值以确定是否显示帮助对话框, 进行游戏的初始化。 在一个游戏的消息循环中除了包含一般 Windows 应用程序的消息循环所应包含的 部分外还应有调用有关检测游戏单位状态位置、刷新游戏单位和重画新图以及有关 人工智能的函数的部分。在例程中的消息循环部分包含了一个关于检测游戏单位状 态位置、刷新游戏单位和重画新图函数的调用。 在这些调用中一般有两种方法: 1.在消息循环中直接调用有关函数。比如说在一个 RPG 的游戏中每个循环都检测 主角的的位置是否发生改变,若改变了则在新位置上重画主角的图。 2.通过检测 WM_TIMER 消息,以决定是否调用有关函数。即是每隔一段时间(若 干个时钟周期),检测一次,然后决定函数的调用与否。 在上面的两种方法里,第一种是现在较常用的,它的缺点是 CPU 资源占用相对较 多,但对不同的机型适应性较强,较稳定。第二种在一些较老的游戏或对速度要求 不高的游戏中较常见,与第一种相比它的 CPU 资源占用相对较少,但在不同的机 型中表现差异极大。 在谈 WinMain()的编制时,窗口函数(WINPROC)的编制是必须说的。窗口函 数可以说是把功能不同的函数通过 Switch-Case 结构连起来组成一个复杂程序的 线索。它的基本编写方法在 Windows 编程基础中我们就已经谈到了。仔细阅读例 程中的 MainWndProc()函数相信对您是有相当大的帮助的。 /* * MainWndProc * * Callback for all Windows messages */ long FAR PASCAL MainWndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) { PAINTSTRUCT ps; HDC hdc; switch( message ) { case WM_SIZE: case WM_MOVE: if (IsIconic(hWnd)) { Msg("FoxBear is minimized, pausing"); PauseGame(); } if (bFullscreen) { SetRect(&rcWindow, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)); } else { GetClientRect(hWnd, &rcWindow); ClientToScreen(hWnd, (LPPOINT)&rcWindow); ClientToScreen(hWnd, (LPPOINT)&rcWindow+1); } Msg("WINDOW RECT: [%d,%d,%d,%d]", rcWindow.left, rcWindow.top, rcWindow.right, rcWindow.bottom); break; case WM_ACTIVATEAPP: bIsActive = (BOOL)wParam && GetForegroundWindow() == hWnd; if (bIsActive) Msg("FoxBear is active"); else Msg("FoxBear is not active"); // // while we were not-active something bad happened that caused us // to pause, like a surface restore failing or we got a palette // changed, now that we are active try to fix things // if (bPaused && bIsActive) { if (RestoreGame()) { UnPauseGame(); } else { if (GetForegroundWindow() == hWnd) { // // we are unable to restore, this can happen when // the screen resolution or bitdepth has changed // we just reload all the art again and re-create // the front and back buffers. this is a little // overkill we could handle a screen res change by // just recreating the front and back buffers we dont // need to redo the art, but this is way easier. // if (InitGame()) { UnPauseGame(); } } } } break; case WM_QUERYNEWPALETTE: // // we are getting the palette focus, select our palette // if (!bFullscreen && lpPalette && lpFrontBuffer) { HRESULT ddrval; ddrval = IDirectDrawSurface_SetPalette(lpFrontBuffer,lpPalette); if( ddrval == DDERR_SURFACELOST ) { IDirectDrawSurface_Restore( lpFrontBuffer ); ddrval= IDirectDrawSurface_SetPalette(lpFrontBuffer,lpPalette); if( ddrval == DDERR_SURFACELOST ) { Msg(" Failed to restore palette after second try"); } } // // Restore normal title if palette is ours // if( ddrval == DD_OK ) { SetWindowText( hWnd, OUR_APP_NAME ); } } break; case WM_PALETTECHANGED: // // if another app changed the palette we dont have full control // of the palette. NOTE this only applies for FoxBear in a window // when we are fullscreen we get all the palette all of the time. // if ((HWND)wParam != hWnd) { if( !bFullscreen ) { if( !bStress ) { Msg("***** PALETTE CHANGED, PAUSING GAME"); PauseGame(); } else { Msg("Lost palette but continuing"); SetWindowText( hWnd, OUR_APP_NAME " - palette changed COLORS PROBABLY WRONG" ); } } } break; case WM_DISPLAYCHANGE: break; case WM_CREATE: break; case WM_SETCURSOR: if (bFullscreen && bIsActive) { SetCursor(NULL); return TRUE; } break; case WM_SYSKEYUP: switch( wParam ) { // handle ALT+ENTER (fullscreen) case VK_RETURN: bFullscreen = !bFullscreen; ExitGame(); DDDisable(TRUE); // destroy DirectDraw object GameMode.cx = 320; GameMode.cy = 200; InitGame(); break; } break; case WM_KEYDOWN: switch( wParam ) { case VK_NUMPAD5: lastInput=KEY_STOP; break; case VK_DOWN: case VK_NUMPAD2: lastInput=KEY_DOWN; break; case VK_LEFT: case VK_NUMPAD4: lastInput=KEY_LEFT; break; case VK_RIGHT: case VK_NUMPAD6: lastInput=KEY_RIGHT; break; case VK_UP: case VK_NUMPAD8: lastInput=KEY_UP; break; case VK_HOME: case VK_NUMPAD7: lastInput=KEY_JUMP; break; case VK_NUMPAD3: lastInput=KEY_THROW; break; case VK_F5: bShowFrameCount = !bShowFrameCount; if( bShowFrameCount ) { dwFrameCount = 0; dwFrameTime = timeGetTime(); } break; case VK_F6: { static i; // // find our current mode in the mode list // if(bFullscreen) { for (i=0; i= NumModes) { i = 0; } Msg("ModeList %d %d",i,NumModes); GameMode.cx = ModeList[i].w; GameMode.cy = ModeList[i].h; GameBPP = ModeList[i].bpp; bStretch = FALSE; InitGame(); } break; case VK_F7: GameBPP = GameBPP == 8 ? 16 : 8; InitGame(); break; case VK_F8: if (bFullscreen) { bStretch = !bStretch; InitGame(); } else { RECT rc; GetClientRect(hWnd, &rc); bStretch = (rc.right != GameSize.cx) || (rc.bottom != GameSize.cy); if (bStretch = !bStretch) SetRect(&rc, 0, 0, GameMode.cx*2, GameMode.cy*2); else SetRect(&rc, 0, 0, GameMode.cx, GameMode.cy); AdjustWindowRectEx(&rc, GetWindowStyle(hWnd), GetMenu(hWnd) != NULL, GetWindowExStyle(hWnd)); SetWindowPos(hWnd, NULL, 0, 0, rc.right-rc.left, rc.bottom-rc.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); } break; case VK_F9: DevIndex ++; bUseEmulation = FALSE; if (DevIndex >= MaxDevIndex) DevIndex = 0; ExitGame(); DDDisable(TRUE); // destroy DirectDraw object InitGame(); break; case VK_F4: // treat F4 like ALT+ENTER (fullscreen) PostMessage(hWnd, WM_SYSKEYUP, VK_RETURN, 0); break; case VK_F3: bPaused = !bPaused; break; case VK_ESCAPE: case VK_F12: PostMessage(hWnd, WM_CLOSE, 0, 0); return 0; } break; case WM_PAINT: hdc = BeginPaint( hWnd, &ps ); if (bPaused) { char *sz = "Game is paused, this is not a bug."; TextOut(ps.hdc, 0, 0, sz, lstrlen(sz)); } EndPaint( hWnd, &ps ); return 1; case WM_DESTROY: hWndMain = NULL; lastInput=0; DestroyGame(); // end of game DDDisable(TRUE); // destroy DirectDraw object PostQuitMessage( 0 ); break; } return DefWindowProc(hWnd, message, wParam, lParam); } /* MainWndProc */ 第三节 游戏初始化 游戏初始化包括三部分: 1.Windows 的初始化。 2.游戏工具的初始化。 3.游戏的初始化。 在这三部分中 Windows 的初始化,也就是对窗口的注册、定义和初始化。我们在 Win- dows 编程基础中已经谈过,这里就不再详述了。 游戏工具的初始化,是指对游戏程序中用到的工具进行初始化。对于一个游戏而言 我们需要针对游戏的需要使用一些对图形或声音管理绘制或播放的以及其他功能 的系统,这些系统就是我们所说的游戏工具(有时人们也称之为游戏引擎)。这些 工具有时是由一些游戏公司提供的,比如 MICROSOFT 的 DirectX5 SDK,有时是 自己针对游戏需要编制的或使用上一部作品中用过的系统。在本例程中是指对 Directdraw 和 DirectSound 进行初始化,您可以通过阅读 DDinit()和 InitSound ()以及 InitGame()函数的一部分的原代码以及阅读我们提供有关 Directdraw 和 DirectSound 的章节来理解。 DDinit()和 InitSound()以及 InitGame()函数的代码: /* * InitGame * * Initializing current game */ BOOL InitGame( void ) { ExitGame(); GameSize = GameMode; /* * initialize sound */ InitSound( hWndMain ); /* * init DirectDraw, set mode, ... * NOTE GameMode might be set to 640x480 if we cant get the asked for mode. */ if( !PreInitializeGame() ) { return FALSE; } if (bStretch && bFullscreen) { GameSize.cx = GameMode.cx / 2; GameSize.cy = GameMode.cy / 2; GameRect.left = GameMode.cx - GameSize.cx; GameRect.top = GameMode.cy - GameSize.cy; GameRect.right = GameMode.cx; GameRect.bottom = GameMode.cy; if (lpStretchBuffer) Msg("Stretching using a system-memory stretch buffer"); else Msg("Stretching using a VRAM->VRAM blt"); } else { GameRect.left = (GameMode.cx - GameSize.cx) / 2; GameRect.top = (GameMode.cy - GameSize.cy) / 2; GameRect.right = GameRect.left + GameSize.cx; GameRect.bottom = GameRect.top + GameSize.cy; } /* * setup our palette */ if( GameBPP == 8 ) { lpPalette = ReadPalFile( NULL ); // create a 332 palette if( lpPalette == NULL ) { Msg( "Palette create failed" ); return FALSE; } IDirectDrawSurface_SetPalette( lpFrontBuffer, lpPalette ); } /* * load all the art and things. */ if( !InitializeGame() ) { return FALSE; } /* * init our code to draw the FPS */ makeFontStuff(); /* * spew some stats */ { DDCAPS ddcaps; ddcaps.dwSize = sizeof( ddcaps ); IDirectDraw_GetCaps( lpDD, &ddcaps, NULL ); Msg( "Total=%ld, Free VRAM=%ld", ddcaps.dwVidMemTotal, ddcaps.dwVidMemFree ); Msg( "Used = %ld", ddcaps.dwVidMemTotal- ddcaps.dwVidMemFree ); } return TRUE; } /* InitGame */ /* * InitSound * * Sets up the DirectSound object and loads all sounds into secondary * DirectSound buffers. Returns FALSE on error, or TRUE if successful */ BOOL InitSound( HWND hwndOwner ) { int idx; DSBUFFERDESC dsBD; IDirectSoundBuffer *lpPrimary; DSEnable(hwndOwner); if (lpDS == NULL) return TRUE; /* * Load all sounds -- any that can't load for some reason will have NULL * pointers instead of valid SOUNDEFFECT data, and we will know not to * play them later on. */ for( idx = 0; idx < NUM_SOUND_EFFECTS; idx++ ) { if (SoundLoadEffect((EFFECT)idx)) { DSBCAPS caps; caps.dwSize = sizeof(caps); IDirectSoundBuffer_GetCaps(lpSoundEffects[idx], &caps); if (caps.dwFlags & DSBCAPS_LOCHARDWARE) Msg( "Sound effect %s in hardware", szSoundEffects[idx]); else Msg( "Sound effect %s in software", szSoundEffects[idx]); } else { Msg( "cant load sound effect %s", szSoundEffects[idx]); } } /* * get the primary buffer and start it playing * * by playing the primary buffer, DirectSound knows to keep the * mixer active, even though we are not making any noise. */ ZeroMemory( &dsBD, sizeof(DSBUFFERDESC) ); dsBD.dwSize = sizeof(dsBD); dsBD.dwFlags = DSBCAPS_PRIMARYBUFFER; if (SUCCEEDED(IDirectSound_CreateSoundBuffer(lpDS, &dsBD, &lpPrimary, NULL))) { if (!SUCCEEDED(IDirectSoundBuffer_Play(lpPrimary, 0, 0, DS BPLAY_LOOPING))) { Msg("Unable to play Primary sound buffer"); } IDirectSoundBuffer_Release(lpPrimary); } else { Msg("Unable to create Primary sound buffer"); } return TRUE; } /* InitSound */ /* * DDInit */ BOOL DDInit( void ) { DirectDrawEnumerate(&DDEnumCallback,NULL); DDEnumCallback((GUID *)DDCREATE_EMULATIONONLY, "Hardware Emulation Layer", "", NULL); return TRUE; } 游戏的初始化是指调入游戏中的图象、声音等资源和游戏中的角色、道具的属性、 初始位置、状态等并画出初始画面的图象以及游戏的系统、操作方法的定义、游戏 的规则等。比如说在一个 RPG 游戏之中,在游戏开始时内存中就应装入主角的图 象组(比如走时的几幅图,状态对话框中的图)、状态(级别、HP、MP、DP 等 等)、属性(性别、职业等)等,描述整个游戏世界的图,NPC 的各种属性、游 戏的规则(各种攻击方式的效果、升级所需的经验值等)等等,总之要装入您所设 计的游戏世界的一切。在例程的 InitGame()中调用的函数 InitializeGame()就完 成了这个任务。 InitializeGame()的代码: /* * InitializeGame */ BOOL InitializeGame ( void ) { Splash(); hBitmapList = LoadBitmaps(); if( hBitmapList == NULL ) { return FALSE; } InitTiles( &hTileList, hBitmapList, C_TILETOTAL ); InitPlane( &hForeground, &hForePosList, "FORELIST", C_FORE_W, C_FORE_H, C_FORE_DENOM ); TilePlane( hForeground, hTileList, hForePosList ); InitPlane( &hMidground, &hMidPosList, "MIDLIST", C_MID_W, C_MID_H, C_MID_DENOM ); TilePlane( hMidground, hTileList, hMidPosList ); InitPlane( &hBackground, &hBackPosList, "BACKLIST", C_BACK_W, C_BACK_H, C_BACK_DENOM ); TilePlane( hBackground, hTileList, hBackPosList ); InitSurface( &hSurfaceList, "SURFLIST", C_FORE_W, C_FORE_H ); SurfacePlane( hForeground, hSurfaceList ); InitFox( &hFox, hBitmapList ); InitBear( &hBear, hBitmapList ); InitApple( &hApple, hBitmapList ); DDClear(); // clear all the backbuffers. return TRUE; } /* InitializeGame */ 在现在的大部分游戏中游戏世界中的每个组成部分通常是用结构或类分别定义储 存的。比如一个即时战略游戏中,各种建筑物放在一个类中,而每个建筑物的属性 就放在该类的一个子类中;各种武器放在一个类中,每种武器放在该类的一个子类 中。 class Weapon { WEAPON_TYPE Type; char Name; DWORD Id; WORD Defend; WORD Attack; ... }; 第四节 游戏内部循环 游戏内部循环包括刷新游戏单位、画游戏单位两部分。它的实现过程是这样的:检 测状态,作出判断,绘出新图。看起来这并不是一个循环,对吗?是的,游戏内部 循环并不是一个真正的循环,它实际上是由消息循环完成循环作用的。让我们从例 程中看看这是如何实现的吧! 在消息循环中的第一个 else if 语句是这样的 else if (!bPaused && (bIsActive || !bFullscreen)) { ProcessFox(lastInput); lastInput=0; } if 后的表达式的含义是:当游戏没有被暂停时(bPause 为 FLASE)或以窗口模式 显示(bFullscreen 为 FLASE)且窗口处于活动状态(bIsActive 为 TRUE)时执行 { ProcessFox(lastInput); lastInput=0; } 语句段。而函数 ProcessFox(lastInput)通过调用 ProcessInput()和 NewGameFrame( )达成刷新游戏单元和重画新图的功能。(这三个函数的原代码 见例程 foxbear.c 和 gameproc.c 两文件)。 ProcessFox(lastInput): /* * ProcessFox */ BOOL ProcessFox(SHORT sInput) { if ((lpFrontBuffer && IDirectDrawSurface_IsLost(lpFrontBuffer) == DDERR_SURFACELOST) || (lpBackBuffer && IDirectDrawSurface_IsLost(lpBackBuffer) == DDERR_SURFACELOST)) { if (!RestoreGame()) { PauseGame(); return FALSE; } } ProcessInput(sInput); NewGameFrame(); return TRUE; } /* ProcessFox */ static HFONT hFont; DWORD dwFrameCount; DWORD dwFrameTime; DWORD dwFrames; DWORD dwFramesLast; SIZE sizeFPS; SIZE sizeINFO; int FrameRateX; char szFPS[] = "FPS %02d"; char szINFO[] = "%dx%dx%d%s F6=mode F8=x2 ALT+ENTER=Window"; char szINFOW[] = "%dx%dx%d%s F6=mode F8=x2 ALT+ENTER=Fullscreen"; char szFrameRate[128]; char szInfo[128]; COLORREF InfoColor = RGB(0,152,245); COLORREF FrameRateColor = RGB(255,255,0); COLORREF BackColor = RGB(255,255,255); /* * initNumSurface */ void initNumSurface( void ) { HDC hdc; RECT rc; int len; dwFramesLast = 0; len = wsprintf(szFrameRate, szFPS, 0, 0); if( lpFrameRate && IDirectDrawSurface_GetDC(lpFrameRate, &hdc ) == DD_OK ) { SelectObject(hdc, hFont); SetTextColor(hdc, FrameRateColor); SetBkColor(hdc, BackColor); SetBkMode(hdc, OPAQUE); SetRect(&rc, 0, 0, 10000, 10000); ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rc, szFrameRate, len, NULL); GetTextExtentPoint(hdc, szFrameRate, 4, &sizeFPS); FrameRateX = sizeFPS.cx; GetTextExtentPoint(hdc, szFrameRate, len, &sizeFPS); IDirectDrawSurface_ReleaseDC(lpFrameRate, hdc); } if (bFullscreen) len = wsprintf(szInfo, szINFO, GameSize.cx, GameSize.cy, GameBPP,bStretch ? " x2" : ""); else len = wsprintf(szInfo, szINFOW, GameSize.cx, GameSize.cy, GameBPP,bStretch ? " x2" : ""); if( lpInfo && IDirectDrawSurface_GetDC(lpInfo, &hdc ) == DD_OK ) { SelectObject(hdc, hFont); SetTextColor(hdc, InfoColor); SetBkColor(hdc, BackColor); SetBkMode(hdc, OPAQUE); SetRect(&rc, 0, 0, 10000, 10000); ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rc, szInfo, len, NULL); GetTextExtentPoint(hdc, szInfo, len, &sizeINFO); IDirectDrawSurface_ReleaseDC(lpInfo, hdc); } } /* initNumSurface */ NewGameFrame( ): /* * NewGameFrame */ int NewGameFrame( void ) { SetSpriteX( hFox, 0, P_AUTOMATIC ); SetSpriteY( hFox, 0, P_AUTOMATIC ); SetPlaneVelX( hBackground, GetSpriteVelX(hFox), P_ABSOLUTE ); SetPlaneVelX( hMidground, GetSpriteVelX(hFox), P_ABSOLUTE ); SetPlaneVelX( hForeground, GetSpriteVelX(hFox), P_ABSOLUTE ); SetPlaneX( hBackground, 0, P_AUTOMATIC ); SetPlaneX( hMidground, 0, P_AUTOMATIC ); SetPlaneX( hForeground, 0, P_AUTOMATIC ); SetSpriteX( hBear, 0, P_AUTOMATIC ); SetSpriteX( hApple, 0, P_AUTOMATIC ); SetSpriteY( hApple, 0, P_AUTOMATIC ); /* * once all sprites are processed, display them * * If we are using destination transparency instead of source * transparency, we need to paint the background with the color key * and then paint our sprites and planes in reverse order. * * Since destination transparency will allow you to only write pixels * on the destination if the transparent color is present, reversing * the order (so that the topmost bitmaps are drawn first instead of * list) causes everything to come out ok. */ if( bTransDest ) { gfxFillBack( dwColorKey ); DisplayFrameRate(); DisplaySprite( hBuffer, hApple, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hBear, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hFox, GetPlaneX(hForeground) ); DisplayPlane( hBuffer, hForeground ); DisplayPlane( hBuffer, hMidground ); DisplayPlane( hBuffer, hBackground ); } else { DisplayPlane( hBuffer, hBackground ); DisplayPlane( hBuffer, hMidground ); DisplayPlane( hBuffer, hForeground ); DisplaySprite( hBuffer, hFox, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hBear, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hApple, GetPlaneX(hForeground) ); DisplayFrameRate(); } gfxSwapBuffers(); return 0; } /* NewGameFrame */ 第五节 刷新游戏单元 刷新游戏单位的作用是在每一桢刷新游戏单位的状态。请您先阅读一下下面的 ProcessInput()函数的部分代码,然后再看看下面这两个例子。 ProcessInput()函数的部分代码: /* * ProcessInput */ BOOL ProcessInput( SHORT input ) { static BOOL fBearPlaying = FALSE; LONG foxSpeedX; LONG foxSpeedY; LONG foxX; LONG foxY; LONG bearX; LONG bearY; LONG appleX; LONG appleY; ACTION foxAction; DIRECTION foxDir; BOOL cont = TRUE; foxSpeedX = GetSpriteVelX( hFox ); foxAction = GetSpriteAction( hFox ); foxDir = GetSpriteDirection( hFox ); if( (GetSpriteActive(hFox) == FALSE) && (input != 4209) ) { input = 0; } switch( input ) { case KEY_DOWN: if( foxAction == STOP ) { break; } else if( foxAction == STILL ) { SetSpriteAction( hFox, CROUCH, SAME ); } else if( foxAction == WALK ) { SetSpriteAction( hFox, CROUCHWALK, SAME ); } break; case KEY_LEFT: if( foxAction == STOP ) { break; } else if( foxSpeedX == 0 ) { if( foxAction == STILL ) { if( foxDir == RIGHT ) { ChangeSpriteDirection( hFox ); SetPlaneSlideX( hForeground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneSlideX( hMidground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneSlideX( hBackground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneIncremX( hForeground, C_BOUNDINCREM, P_ABSOLUTE ); SetPlaneIncremX( hBackground, C_BOUNDINCREM, P_ABSOLUTE ); SetPlaneIncremX( hMidground, C_BOUNDINCREM, P_ABSOLUTE ); } else { SetSpriteAction( hFox, WALK, LEFT ); SetSpriteSwitch( hFox, C_FOX_WALKSWITCH, P_ABSOLUTE ); SetSpriteVelX( hFox, -C_FOX_XMOVE, P_RELATIVE ); } } else if( foxAction == CROUCH ) { if( foxDir == RIGHT ) { ChangeSpriteDirection( hFox ); SetPlaneSlideX( hForeground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneSlideX( hMidground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneSlideX( hBackground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneIncremX( hForeground, C_BOUNDINCREM, P_ABSOLUTE ); SetPlaneIncremX( hBackground, C_BOUNDINCREM, P_ABSOLUTE ); SetPlaneIncremX( hMidground, C_BOUNDINCREM, P_ABSOLUTE ); } else { SetSpriteAction( hFox, CROUCHWALK, LEFT ); SetSpriteSwitch( hFox, C_FOX_WALKSWITCH, P_ABSOLUTE ); SetSpriteVelX( hFox, -C_FOX_XMOVE, P_RELATIVE ); } } else { SetSpriteVelX( hFox, -C_FOX_XMOVE, P_RELATIVE ); } } else { SetSpriteVelX( hFox, -C_FOX_XMOVE, P_RELATIVE ); } break; case KEY_RIGHT: . . . case KEY_STOP: if( foxAction == STOP ) { break; } else if( (foxAction == RUN) || (foxAction == BLURR) ) { SetSpriteAction( hFox, STOP, SAME ); SetSpriteAccX( hFox, -foxSpeedX / 25, P_ABSOLUTE ); SoundPlayEffect( SOUND_STOP ); } else { SetSpriteVelX( hFox, 0, P_ABSOLUTE ); } break; case KEY_UP: if( foxAction == STOP ) { break; } else if( foxAction == CROUCH ) { SetSpriteAction( hFox, STILL, SAME ); } else if( foxAction == CROUCHWALK ) { SetSpriteAction( hFox, WALK, SAME ); } break; case KEY_JUMP: if( foxAction == STOP ) { break; } else if( (foxAction == STILL) || (foxAction == WALK) || (foxAction == RUN) || (foxAction == CROUCH) || (foxAction == CROUCHWALK) ) { SetSpriteAction( hFox, JUMP, SAME ); SetSpriteSwitchType( hFox, TIME ); SetSpriteSwitch( hFox, C_FOX_JUMPSWITCH, P_ABSOLUTE ); SetSpriteVelY( hFox, -C_FOX_JUMPMOVE, P_ABSOLUTE ); SetSpriteAccY( hFox, C_UNIT / 2, P_ABSOLUTE ); SoundPlayEffect( SOUND_JUMP ); } break; case KEY_THROW: if( foxAction == STOP ) { break; } else if( (foxAction == STILL) || (foxAction == WALK) || (foxAction == RUN) || (foxAction == CROUCH) || (foxAction == CROUCHWALK) ) { SetSpriteAction( hFox, THROW, SAME ); SetSpriteSwitch( hFox, C_FOX_THROWSWITCH, P_ABSOLUTE ); SetSpriteVelX( hFox, 0, P_ABSOLUTE ); SetSpriteSwitchType( hFox, TIME ); } else if( foxAction == JUMP ) { SetSpriteAccY( hFox, 0, P_ABSOLUTE ); SetSpriteSwitch( hFox, C_FOX_THROWSWITCH, P_ABSOLUTE ); SetSpriteAction( hFox, JUMPTHROW, SAME ); SetSpriteVelY( hFox, 0, P_ABSOLUTE ); SetSpriteSwitchDone( hFox, FALSE ); SetSpriteSwitchForward( hFox, TRUE ); } break; default: break; } /* * Fox actions follow... */ if( GetSpriteActive(hFox) == FALSE ) { goto bearActions; } if( abs(GetSpriteVelX( hFox )) < C_FOX_XMOVE ) { SetSpriteVelX( hFox, 0, P_ABSOLUTE ); } foxAction = GetSpriteAction( hFox ); if( GetSpriteVelY(hFox) == 0 ) { if( GetSurface( hForeground, hFox ) == FALSE ) { if( (foxAction == WALK) || (foxAction == RUN) || (foxAction == CROUCHWALK) ) { SetSpriteAccY( hFox, C_UNIT / 2, P_ABSOLUTE ); } else if( foxAction == STOP ) { SetSpriteAccY( hFox, C_UNIT / 2, P_ABSOLUTE ); SetSpriteAccX( hFox, 0, P_ABSOLUTE ); } } } else if( GetSpriteVelY(hFox) > 2 * C_UNIT ) { if( (foxAction == WALK) || (foxAction == RUN) || (foxAction == CROUCHWALK) ) { SetSpriteSwitchForward( hFox, FALSE ); SetSpriteAction( hFox, JUMP, SAME ); SetSpriteSwitchType( hFox, TIME ); SetSpriteSwitch( hFox, C_FOX_JUMPSWITCH, P_ABSOLUTE ); } if( foxAction == STOP ) { SetSpriteAction( hFox, STUNNED, SAME ); SetSpriteAccX( hFox, -GetSpriteVelX(hFox) / 25, P_ABSOLUTE ); SoundPlayEffect( SOUND_STUNNED ); } } foxSpeedX = GetSpriteVelX( hFox ); foxSpeedY = GetSpriteVelY( hFox ); foxAction = GetSpriteAction( hFox ); foxDir = GetSpriteDirection( hFox ); switch( foxAction ) { case STUNNED: if( (GetSpriteVelY(hFox) >= 0) && (!GetSurface( hForeground, hFox ) == FALSE) ) { SetSpriteAccY( hFox, 0, P_ABSOLUTE ); SetSpriteAction( hFox, STOP, SAME ); SetSpriteVelY( hFox, 0, P_ABSOLUTE ); SetSpriteAccX( hFox, -foxSpeedX / 25, P_ABSOLUTE ); // SetSurface( hForeground, hFox ); SoundPlayEffect( SOUND_STOP ); } break; case CROUCHWALK: if( foxSpeedX == 0 ) { SetSpriteAction( hFox, CROUCH, SAME ); } else if( foxSpeedX > C_FOX_WALKMOVE ) { SetSpriteVelX( hFox, C_FOX_WALKMOVE, P_ABSOLUTE ); } else if( foxSpeedX < -C_FOX_WALKMOVE ) { SetSpriteVelX( hFox, -C_FOX_WALKMOVE, P_ABSOLUTE ); } break; case STOP: if( foxSpeedX == 0 ) { SetSpriteAction( hFox, STILL, SAME ); SetSpriteAccX( hFox, 0, P_ABSOLUTE ); } break; case RUN: if( (foxSpeedX < C_FOX_WALKTORUN ) && (foxSpeedX > 0) ) { SetSpriteAction( hFox, WALK, RIGHT ); SetSpriteSwitch( hFox, C_FOX_WALKSWITCH, P_ABSOLUTE ); } else if( foxSpeedX > C_FOX_RUNTOBLURR ) { SetSpriteAction( hFox, BLURR, RIGHT ); SetSpriteSwitch( hFox, C_FOX_BLURRSWITCH, P_ABSOLUTE ); } else if( (foxSpeedX > -C_FOX_WALKTORUN ) && (foxSpeedX < 0) ) { SetSpriteAction( hFox, WALK, LEFT ); SetSpriteSwitch( hFox, C_FOX_WALKSWITCH, P_ABSOLUTE ); } else if( foxSpeedX < -C_FOX_RUNTOBLURR ) { SetSpriteAction( hFox, BLURR, LEFT ); SetSpriteSwitch( hFox, C_FOX_BLURRSWITCH, P_ABSOLUTE ); } break; case WALK: if( foxSpeedX == 0 ) { SetSpriteAction( hFox, STILL, SAME ); } else if( foxSpeedX > C_FOX_WALKTORUN ) { SetSpriteAction( hFox, RUN, RIGHT ); SetSpriteSwitch( hFox, C_FOX_RUNSWITCH, P_ABSOLUTE ); } else if( foxSpeedX < -C_FOX_WALKTORUN ) { SetSpriteAction( hFox, RUN, LEFT ); SetSpriteSwitch( hFox, C_FOX_RUNSWITCH, P_ABSOLUTE ); } break; case BLURR: if( (foxSpeedX < C_FOX_RUNTOBLURR ) && (foxSpeedX > C_FOX_WALKTORUN) ) { SetSpriteAction( hFox, RUN, RIGHT ); SetSpriteSwitch( hFox, C_FOX_RUNSWITCH, P_ABSOLUTE ); } else if( (foxSpeedX > -C_FOX_RUNTOBLURR ) && (foxSpeedX < -C_FOX_WALKTORUN) ) { SetSpriteAction( hFox, RUN, LEFT ); SetSpriteSwitch( hFox, C_FOX_RUNSWITCH, P_ABSOLUTE ); } break; case JUMPTHROW: if( !GetSpriteSwitchDone(hFox) == FALSE ) { SetSpriteSwitchForward( hFox, FALSE ); SetSpriteAction( hFox, JUMP, SAME ); SetSpriteSwitch( hFox, C_FOX_JUMPSWITCH, P_ABSOLUTE ); SetSpriteSwitchDone( hFox, FALSE ); SetSpriteAccY( hFox, C_UNIT / 2, P_ABSOLUTE ); SoundPlayEffect( SOUND_THROW ); } else if( (GetSpriteBitmap(hFox) == 1) && (GetSpriteDirection(hFox) == RIGHT) ) { SetSpriteActive( hApple, TRUE ); SetSpriteX( hApple, GetSpriteX(hFox) + 60 * C_UNIT, P_ABSOLUTE ); SetSpriteY( hApple, GetSpriteY(hFox) + 30 * C_UNIT, P_ABSOLUTE ); SetSpriteVelX( hApple, 8 * C_UNIT, P_ABSOLUTE ); SetSpriteVelY( hApple, -4 * C_UNIT, P_ABSOLUTE ); SetSpriteAccX( hApple, 0, P_ABSOLUTE ); SetSpriteAccY( hApple, C_UNIT / 4, P_ABSOLUTE ); } else if( (GetSpriteBitmap(hFox) == 1) && (GetSpriteDirection(hFox) == LEFT) ) { SetSpriteActive( hApple, TRUE ); SetSpriteX( hApple, GetSpriteX(hFox) + 15 * C_UNIT, P_ABSOLUTE ); SetSpriteY( hApple, GetSpriteY(hFox) + 30 * C_UNIT, P_ABSOLUTE ); SetSpriteVelX( hApple, -8 * C_UNIT, P_ABSOLUTE ); SetSpriteVelY( hApple, -4 * C_UNIT, P_ABSOLUTE ); SetSpriteAccX( hApple, 0, P_ABSOLUTE ); SetSpriteAccY( hApple, C_UNIT / 4, P_ABSOLUTE ); } break; case THROW: if( !GetSpriteSwitchDone(hFox) == FALSE ) { SetSpriteAction( hFox, STILL, SAME ); SetSpriteSwitchType( hFox, HOR ); SetSpriteSwitch( hFox, 0, P_ABSOLUTE ); SetSpriteSwitchDone( hFox, FALSE ); SoundPlayEffect( SOUND_THROW ); } else if( (GetSpriteBitmap(hFox) == 1) && (GetSpriteDirection(hFox) == RIGHT) ) { SetSpriteActive( hApple, TRUE ); SetSpriteX( hApple, GetSpriteX(hFox) + 60 * C_UNIT, P_ABSOLUTE ); SetSpriteY( hApple, GetSpriteY(hFox) + 50 * C_UNIT, P_ABSOLUTE ); SetSpriteVelX( hApple, 8 * C_UNIT, P_ABSOLUTE ); SetSpriteVelY( hApple, -4 * C_UNIT, P_ABSOLUTE ); SetSpriteAccX( hApple, 0, P_ABSOLUTE ); SetSpriteAccY( hApple, C_UNIT / 4, P_ABSOLUTE ); } else if( (GetSpriteBitmap(hFox) == 1) && (GetSpriteDirection(hFox) == LEFT) ) { SetSpriteActive( hApple, TRUE ); SetSpriteX( hApple, GetSpriteX(hFox) + 20 * C_UNIT, P_ABSOLUTE ); SetSpriteY( hApple, GetSpriteY(hFox) + 50 * C_UNIT, P_ABSOLUTE ); SetSpriteVelX( hApple, -8 * C_UNIT, P_ABSOLUTE ); SetSpriteVelY( hApple, -4 * C_UNIT, P_ABSOLUTE ); SetSpriteAccX( hApple, 0, P_ABSOLUTE ); SetSpriteAccY( hApple, C_UNIT / 4, P_ABSOLUTE ); } break; case JUMP: if( (foxSpeedY >= 0) && (!GetSpriteSwitchForward( hFox ) == FALSE) ) { SetSpriteSwitchForward( hFox, FALSE ); } else if( GetSpriteSwitchForward( hFox ) == FALSE ) { if( (!GetSurface( hForeground, hFox ) == FALSE) || (!GetSurface( hForeground, hFox ) == FALSE) ) { if( foxSpeedX >= C_FOX_RUNMOVE ) { SetSpriteAction( hFox, RUN, SAME ); SetSpriteSwitch( hFox, C_FOX_RUNSWITCH, P_ABSOLUTE ); } else if( foxSpeedX == 0 ) { SetSpriteAction( hFox, STILL, SAME ); SetSpriteSwitch( hFox, C_FOX_WALKSWITCH, P_ABSOLUTE ); } else { SetSpriteAction( hFox, WALK, SAME ); SetSpriteSwitch( hFox, C_FOX_WALKSWITCH, P_ABSOLUTE ); } SetSpriteAccY( hFox, 0, P_ABSOLUTE ); SetSpriteVelY( hFox, 0, P_ABSOLUTE ); SetSpriteSwitchType( hFox, HOR ); SetSpriteSwitchForward( hFox, TRUE ); // SetSurface( hForeground, hFox ); SetSpriteSwitchDone( hFox, FALSE ); } } break; } /* * Bear Actions */ bearActions: foxX = GetSpriteX( hFox ); foxY = GetSpriteY( hFox ); bearX = GetSpriteX( hBear ); bearY = GetSpriteY( hBear ); appleX = GetSpriteX( hApple ); appleY = GetSpriteY( hApple ); switch( GetSpriteAction( hBear ) ) { case STRIKE: if( GetSpriteBitmap( hBear ) == 2 ) { if( (bearX > foxX - C_UNIT * 30) && (bearX < foxX + C_UNIT * 40) && (bearY < foxY + C_UNIT * 60) ) { SetSpriteActive( hFox, FALSE ); if( !fBearPlaying ) { SoundPlayEffect( SOUND_BEARSTRIKE ); fBearPlaying = TRUE; } } else { SetSpriteAction( hBear, MISS, SAME ); SetSpriteSwitch( hBear, C_BEAR_MISSSWITCH, P_ABSOLUTE ); SetSpriteSwitchDone( hBear, FALSE ); } } else if( !GetSpriteSwitchDone( hBear ) == FALSE ) { SetSpriteAction( hBear, CHEW, SAME ); SetSpriteSwitchDone( hBear, FALSE ); chewCount = 0; fBearPlaying = FALSE; } break; case MISS: if( !fBearPlaying ) { SoundPlayEffect( SOUND_BEARMISS ); fBearPlaying = TRUE; } if( !GetSpriteSwitchDone( hBear ) == FALSE ) { SetSpriteAction( hBear, WALK, SAME ); SetSpriteVelX( hBear, -C_BEAR_WALKMOVE, P_ABSOLUTE ); SetSpriteSwitch( hBear, C_BEAR_WALKSWITCH, P_ABSOLUTE ); SetSpriteSwitchType( hBear, HOR ); fBearPlaying = FALSE; } break; case WALK: if( (!GetSpriteActive(hApple) == FALSE) && (appleX > bearX) && (appleX > bearX + 80 * C_UNIT) && (appleY > bearY + 30 * C_UNIT) ) { SetSpriteAction( hBear, STRIKE, SAME ); SetSpriteVelX( hBear, 0, P_ABSOLUTE ); SetSpriteSwitchType( hBear, TIME ); SetSpriteSwitch( hBear, C_BEAR_STRIKESWITCH, P_ABSOLUTE ); SetSpriteSwitchDone( hBear, FALSE ); } else if( (bearX > foxX - C_UNIT * 30) && (bearX < foxX + C_UNIT * 30) && (bearY < foxY + C_UNIT * 60) ) { SetSpriteAction( hBear, STRIKE, SAME ); SetSpriteVelX( hBear, 0, P_ABSOLUTE ); SetSpriteSwitchType( hBear, TIME ); SetSpriteSwitch( hBear, C_BEAR_STRIKESWITCH, P_ABSOLUTE ); SetSpriteSwitchDone( hBear, FALSE ); } break; case CHEW: ++chewCount; if( chewCount >= 200 ) { SetSpriteAction( hBear, STRIKE, SAME ); SetSpriteSwitch( hBear, C_BEAR_STRIKESWITCH, P_ABSOLUTE ); SetSpriteVelX( hBear, 0, P_ABSOLUTE ); SetSpriteSwitchDone( hBear, FALSE ); if( GetSpriteDirection(hFox) == RIGHT ) { SetPlaneSlideX( hForeground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneSlideX( hMidground, -C_BOUNDDIF, P_RELATIVE ); SetPlaneSlideX( hBackground, -C_BOUNDDIF, P_RELATIVE ); } chewDif = GetSpriteX(hFox); SetSpriteActive( hFox, TRUE ); SetSpriteAction( hFox, STUNNED, LEFT ); SetSpriteX( hFox, GetSpriteX(hBear), P_ABSOLUTE ); SetSpriteY( hFox, GetSpriteY(hBear), P_ABSOLUTE ); SetSpriteAccX( hFox, 0, P_ABSOLUTE ); SetSpriteAccY( hFox, C_UNIT / 2, P_ABSOLUTE ); SetSpriteVelX( hFox, -8 * C_UNIT, P_ABSOLUTE ); SetSpriteVelY( hFox, -10 * C_UNIT, P_ABSOLUTE ); SetSpriteSwitch( hFox, 0, P_ABSOLUTE ); SoundPlayEffect( SOUND_STUNNED ); chewDif -= GetSpriteX(hFox); SetPlaneSlideX( hForeground, -chewDif, P_RELATIVE ); SetPlaneSlideX( hMidground, -chewDif, P_RELATIVE ); SetPlaneSlideX( hBackground, -chewDif, P_RELATIVE ); SetPlaneIncremX( hForeground, C_BOUNDINCREM, P_ABSOLUTE ); SetPlaneIncremX( hMidground, C_BOUNDINCREM, P_ABSOLUTE ); SetPlaneIncremX( hBackground, C_BOUNDINCREM, P_ABSOLUTE ); } break; } /* * Apple actions... */ if( (GetSpriteVelY(hApple) != 0) && (GetSpriteY(hApple) >= 420 * C_UNIT) ) { SetSpriteX( hApple, 0, P_ABSOLUTE ); SetSpriteY( hApple, 0, P_ABSOLUTE ); SetSpriteAccX( hApple, 0, P_ABSOLUTE ); SetSpriteAccY( hApple, 0, P_ABSOLUTE ); SetSpriteVelX( hApple, 0, P_ABSOLUTE ); SetSpriteVelY( hApple, 0, P_ABSOLUTE ); SetSpriteActive( hApple, FALSE ); } return cont; } /* ProcessInput */ 在射击游戏中的子弹的发射,每一帧都要检测上一帧时子弹的位置 a 然后确定当 前帧子弹的位置 b 然后将该位置传给重画游戏单元的部分,在当前帧 b 的位置贴 上子弹的图象。 在即使战略游戏中两军对战时,程序在每一帧都要根据上一帧每个战斗单位的位置 和该战斗单位移动的目的、到该目的之间的障碍物的位置以及一定的路径算法确定 在当前帧该战斗单位的新位置;还有要取得在上一帧时该战斗单位的生命值和所受 的打击次数及强度,以确定该战斗单位的生命值。 通过阅读 ProcessInput()函数的代码,我想您一定已理解了刷新游戏单元的概念。 而从上面的两个例子中,您也一定发现用例程的方法很难实现这两类游戏的要求。 我们不可能对每一颗子弹,每一个战斗单位进行操作,而且我们并不知道游戏者会 一次发射多少颗子弹也不知道游戏者会造多少个战斗单位。我们应该怎么办呢? 考虑到每一个战斗单位(或每一颗子弹)都有相似(或相同)的属性,那么我们可 以采用结构数组来储存每一个战斗单位的位置和状态。这个办法好象可行!但是仔 细想想,我们又遇到了上面谈到的问题我们并不知道游戏者会一次发射多少颗子弹 也不知道游戏者会造多少个战斗单位。当然我们可以采用 Age of Empire 的方式---- 限制单位的数量(我并不是说 Age of Empire 采用的是这种办法)。但是这意味什 么呢!意味着,如果我们限定数量为 50 的话,在游戏者只有一个士兵时,计算机 却需要为这个士兵分配 50 倍的内存!而且游戏者还不一定造出 50 个士兵。显然 这并不是一个好办法! 我们应该怎么办呢?链表!链表能满足我们的要求。 class Node { //双向链表的指针。 Node* Next; Node* Pre; //节点数据。 NODE_DATA data; ... }; 链表是一种结构体的集合。在链表中的每一个结构体都包含了一个元素或指针,它 指向链表中的另一个结构体。这个指针用作两个结构体之间的联系。这个概念与数 组有些相似,但它允许链表的动态增长。现在的游戏中凡是遇到这种问题的一般都 是采用链表的。关于链表的更多的信息请阅读有关的资料。 第六节 画游戏单元 画游戏单位的作用是在每一桢往屏幕上画游戏单位的图象。 这就是本例程中画游戏单元的主函数: /* * NewGameFrame */ int NewGameFrame( void ) { //这里是设置游戏单元的位置: SetSpriteX( hFox, 0, P_AUTOMATIC ); SetSpriteY( hFox, 0, P_AUTOMATIC ); SetPlaneVelX( hBackground, GetSpriteVelX(hFox), P_ABSOLUTE ); SetPlaneVelX( hMidground, GetSpriteVelX(hFox), P_ABSOLUTE ); SetPlaneVelX( hForeground, GetSpriteVelX(hFox), P_ABSOLUTE ); SetPlaneX( hBackground, 0, P_AUTOMATIC ); SetPlaneX( hMidground, 0, P_AUTOMATIC ); SetPlaneX( hForeground, 0, P_AUTOMATIC ); SetSpriteX( hBear, 0, P_AUTOMATIC ); SetSpriteX( hApple, 0, P_AUTOMATIC ); SetSpriteY( hApple, 0, P_AUTOMATIC ); //将游戏单元的图形贴到 BackBuffer 上: if( bTransDest ) { gfxFillBack( dwColorKey ); DisplayFrameRate(); DisplaySprite( hBuffer, hApple, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hBear, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hFox, GetPlaneX(hForeground) ); DisplayPlane( hBuffer, hForeground ); DisplayPlane( hBuffer, hMidground ); DisplayPlane( hBuffer, hBackground ); } else { DisplayPlane( hBuffer, hBackground ); DisplayPlane( hBuffer, hMidground ); DisplayPlane( hBuffer, hForeground ); DisplaySprite( hBuffer, hFox, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hBear, GetPlaneX(hForeground) ); DisplaySprite( hBuffer, hApple, GetPlaneX(hForeground) ); DisplayFrameRate(); } //更新前景: gfxSwapBuffers(); return 0; } /* NewGameFrame */ 画游戏单元的顺序为: 1。清 BackBuffer; 这是清 BackBuffer 的函数: /* * gfxFillBack */ void gfxFillBack( DWORD dwColor ) { DDBLTFX ddbltfx; ddbltfx.dwSize = sizeof( ddbltfx ); ddbltfx.dwFillColor = dwColor; IDirectDrawSurface_Blt( lpBackBuffer, // dest surface NULL, // dest rect NULL, // src surface NULL, // src rect DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx); } /* gfxFillBack */ 2。检查游戏单元图形的 Surface 是否丢失; 这是检查游戏单元图形的 Surface 是否丢失的函数: /* * gfxRestoreAll * * restore the art when one or more surfaces are lost */ BOOL gfxRestoreAll() { GFX_BITMAP *curr; HWND hwndF = GetForegroundWindow(); Splash(); for( curr = lpVRAM; curr != NULL; curr = curr->link) { if (curr->lpSurface && (fForceRestore || IDirectDrawSurface_IsLost(curr->lpSurface) == DDERR_SURFACELOST)) { if( !gfxRestore(curr) ) { Msg( "gfxRestoreAll: ************ Restore FAILED!" ); return FALSE; } } } DDClear(); fForceRestore = FALSE; return TRUE; } /* gfxRestoreAll */ 3。将游戏单元的图形画到 BackBuffer 中; 这是画游戏单元图形的函数之一: /* * DisplayPlane */ BOOL DisplayPlane ( GFX_HBM hBuffer, HPLANE *hPlane ) { USHORT n; USHORT i; USHORT j; USHORT x1; USHORT y1; USHORT x2; USHORT y2; USHORT xmod; USHORT ymod; POINT src; RECT dst; x1 = (hPlane->x >> 16) / C_TILE_W; y1 = (hPlane->y >> 16) / C_TILE_H; x2 = x1 + C_SCREEN_W / C_TILE_W; y2 = y1 + C_SCREEN_H / C_TILE_H; xmod = (hPlane->x >> 16) % C_TILE_W; ymod = (hPlane->y >> 16) % C_TILE_H; for( j = y1; j < y2; ++j ) { for( i = x1; i <= x2; ++i ) { n = (i % hPlane->width) + j * hPlane->width; if( hPlane->hBM[n] != NULL ) { if( i == x1 ) { dst.left = 0; dst.right = dst.left + C_TILE_W - xmod; src.x = xmod; } else if( i == x2 ) { dst.left = (i - x1) * C_TILE_W - xmod; dst.right = dst.left + xmod; src.x = 0; } else { dst.left = (i - x1) * C_TILE_W - xmod; dst.right = dst.left + C_TILE_W; src.x = 0; } if( j == y1 ) { dst.top = 0; dst.bottom = dst.top + C_TILE_H - ymod; src.y = ymod; } else if( j == y2 ) { dst.top = (j - y1) * C_TILE_H - ymod; dst.bottom = dst.top + ymod; src.y = 0; } else { dst.top = (j - y1) * C_TILE_H - ymod; dst.bottom = dst.top + C_TILE_H; src.y = 0; } gfxBlt(&dst,hPlane->hBM[n],&src); } } } return TRUE; } /* DisplayPlane */ 4。将 BackBuffer 和 FrontBuffer 进行翻转; 这是全屏幕模式下的页面翻转函数: /* * gfxFlip */ BOOL gfxFlip( void ) { HRESULT ddrval; ddrval = IDirectDrawSurface_Flip( lpFrontBuffer, NULL, DDFLIP_WAIT ); if( ddrval != DD_OK ) { Msg( "Flip FAILED, rc=%08lx", ddrval ); return FALSE; } return TRUE; } /* gfxFlip */ 这是窗口模式下的页面翻转函数: /* * gfxUpdateWindow */ BOOL gfxUpdateWindow() { HRESULT ddrval; ddrval = IDirectDrawSurface_Blt( lpFrontBuffer, // dest surface &rcWindow, // dest rect lpBackBuffer, // src surface NULL, // src rect (all of it) DDBLT_WAIT, NULL); return ddrval == DD_OK; } /* gfxUpdateWindow */ 第七节 计算机人工智能 计算机人工智能 记得吗?在第五节刷新游戏单元中我们谈到在刷新游戏单元时,说到在取得游戏单 位的的位置后要经过一些算法的判断再确定游戏单位的新位置。包含这些算法的部 分就是游戏中实现人工智能的部分。 对于游戏中的人工智能,我比较赞同下面这个定义:一个非游戏者控制的对象在基 于各种复杂因素时的决策行为就象时由真正的人作出的,这是通过使用 一个决策 算法来完成的,这个决策算法根据设计者确定的规则和提供给程序的信息进行处 理。 现在在大部分的游戏中采用的的方式主要有以下几种: 检索 许多人工智能的算法中都涉及到对所有可能性的检索。这个算法的实现方式是这样 的,首先您应让您的程序列一个选项表,例如一个士兵到目的之间所有可能的路径。 然后再使用其他的人工智能技术如排除法等来找一个最优的选择。 排序 排序与检索都是基本的人工智能技术,您可以用排序来确定最佳的决策次序。比 如,在战略游戏中计算机对手不断地根据当前的环境修改行动的优先级。 专家系统 专家系统是指运用“if then”语句的逻辑表达式来表示所有的基本规则,然后计算机 根据这些规则作出智能决策。比如,在制作一个足球游戏时,就可以请一个足球专 家,记下他的的足球经验,他会说明在各种情况下,他采取的踢球方式。根据这些 信息,建立一套规则库,在游戏中计算机就可以按照这些规则作出决策。 其他的方式还有:机器学习和和神经网络系统,这两种方式的效果相当不错。但是 却很不容易掌握,这里我们就不再详述了。 第八节 游戏内存管理 游戏内存管理 这一部分对于优质高效的游戏软件是十分重要的,内存管理不当会导致游戏性能的 降低,甚至引起死机。现在的很多游戏都使用了大量的图象和复杂的规则,需要大 量的内存。这就需要我们对游戏者所用机器的内存进行精心的分配和组织了。首先, 我们应当调查一下现在的主流机型的内存是多少,再与达到游戏的设计目标所需的 内存量之间权衡一下,然后确定一个粗略的分配方案。 这个方案一般可以这样指定: 1.这个游戏从情节上可以分为几个部分,在开始时将每个部分所共有的资料调入, 然后根据情节的发展将该部分不用的资料所占用的内存释放再调入该部分所特有 的资料。比如说可以分为几关或者 RPG 游戏中的“世界”的地图可以分成几个场景。 然后在游戏者到达某一关或进入某一场景时再调入相应的图象或相应的资料。 2.在每个部分中有很多并不常用而且调用时没有严格的速度限制同时调用时并不 需要太多时间(通常 1 秒左右即可完成)的资料,也可以在使用时调用。比如角 色从大地图上走入一个城市,这个城市的图象和游戏规则等资料就可以在走入这个 城市时调入。 在完成这个方案后,我们就完成了内存和硬盘之间数据交换的规划了,接下来就应 考虑运行时内存内部的管理了。 在这一步中主要应注意两个问题: 1.迅速释放存放无效资料的内存; 例如: 描述 GAMEWORLD 的指针 temp 在初始化时分配了内存空间。 GAMEWORLD *temp=new GAMEWORLD(Init value); 。。。 在程序结束时要释放内存空间。 delete temp; 2.严禁使用空指针(这样会导致系统错误,甚至死机)。这里没有什么技巧,只有 靠您自己的认真和仔细了。 例如: 当在程序中已经释放了 temp; 下面的调用就可能导致死机: temp->Function(); 这两个问题的解决方法为: GAMEWORLD *temp=new GAMEWORLD(Init value); ... if(temp) delete temp; temp=NULL; ... if(temp) { temp->Function(); ... } else { 提示 temp 为空指针。 } 第九节 游戏交互设计 游戏交互设计 交互设计,实际上就是您想让游戏者怎么去操纵游戏的发展。说简单了交互设计就 是游戏者怎样去控制游戏角色的行动,在例程中对键盘的设置——用小建盘上的 “456237”控制狐狸的行为就是一种简单的交互设计。说复杂了呢!就是您提供了 一种什么样的方式让游戏者进入游戏之中成为游戏中的一员——他就是游戏中英 勇无敌、侠肝义胆的剑客,他就是游戏中足智多谋、威震天下的将军……这就是好 游戏的一个重要的要素——好的交互性。 交互性就是设计者创造的一个诱使人去玩的游戏所拥有的提供故事线索、情绪感 染、真实的声音和其他创造性的媒介所给予的特性。交互设计的目的就是让游戏者 进入“幻觉状态”,幻觉状态是游戏界的一个术语,它的意思是指游戏者的意识融入 到游戏世界中,这样,他或她就不是在玩游戏,而是在体验另一个世界。 怎样作到这一点呢?作为编程人员应考虑的是: 第一步考虑输入设备问题,设备即是游戏者控制游戏的手段,也就是输入设备的选 择和设置的问题。在这一步中应该考虑是选择键盘、鼠标、游戏杆还是几种结合的 方式,或是其他类型的输入设备。然后是设置各种操作所代表的含义(就象例程中 小键盘的“4”代表左行,“5”代表停止等等,或是鼠标单击、双击某个区域及拖动时 代表的含义)这些设置主要是考虑一个操作的方便性的问题。 typedef enum enum_ACTION { NONE, STILL, WALK, RUN, JUMP, THROW, CROUCH, STOP, STUNNED, JUMPTHROW, CROUCHWALK, BLURR, STRIKE, MISS, CHEW, } ACTION; WinMainProc 中: case WM_KEYDOWN: switch( wParam ) { case VK_NUMPAD5: lastInput=KEY_STOP; break; case VK_DOWN: case VK_NUMPAD2: lastInput=KEY_DOWN; break; case VK_LEFT: case VK_NUMPAD4: lastInput=KEY_LEFT; break; case VK_RIGHT: case VK_NUMPAD6: lastInput=KEY_RIGHT; break; case VK_UP: case VK_NUMPAD8: lastInput=KEY_UP; break; case VK_HOME: case VK_NUMPAD7: lastInput=KEY_JUMP; break; case VK_NUMPAD3: lastInput=KEY_THROW; break; case VK_F5: bShowFrameCount = !bShowFrameCount; if( bShowFrameCount ) { dwFrameCount = 0; dwFrameTime = timeGetTime(); } break; 第二步考虑信息返回的问题,这里主要是一个界面的设计的问题。这个问题我们在 第一章第六节游戏的界面设计中已经讨论过了,这里就不详述了。 第十节 游戏图形底层设计 在游戏中,计算机主要花时间在处理图象和画图象上,所以我们应尽力使这些操作 适合主流机型的硬件水平或尽量少占用系统资源,这就是游戏图形底层设计的目 的。在前面讲的 DirectDraw 和 DirectX5 SDK 中的 Direct3D 都是图形底层,还有 ID 在 QUAKE 发行后提供的 QUAKE C 也是一种不错的图形底层。建立一套游戏图 形底层需要大量的关于图形编程的知识和很多的时间精力,而且效果不一定好,同 时在市场上也有很多图形底层可供选择。所以对于一般的游戏开发者来说,只要作 的游戏使用的图象并没有使计算机不负重荷或并没有使用现有的底层所不支持的 特性,我建议还是使用现有的底层。 本例程的图形底层十分简单,采用 DirectDraw 提供的 IDirectDrawSurface_BltFast 和 IDirectDrawSurface_Blt 函数: if (pbm->lpSurface) { if( pbm->bTrans ) bltflags = bTransDest ? DDBLTFAST_DESTCOLORKEY : DDBLTFAST_SRCCOLORKEY; else bltflags = bTransDest ? DDBLTFAST_DESTCOLORKEY : DDBLTFAST_NOCOLORKEY; ddrval = IDirectDrawSurface_BltFast( lpBackBuffer, x, y, pbm->lpSurface, &rc, bltflags | DDBLTFAST_WAIT); if (ddrval != DD_OK) { Msg("BltFast failed err=%d", ddrval); } } else { DDBLTFX ddbltfx; rc.left = x; rc.top = y; rc.right = rc.left + dx; rc.bottom = rc.top + dy; ddbltfx.dwSize = sizeof( ddbltfx ); ddbltfx.dwFillColor = pbm->dwColor; ddrval = IDirectDrawSurface_Blt( lpBackBuffer, // dest surface &rc, // dest rect NULL, // src surface NULL, // src rect DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx); } 由于 DirectDraw 的通用性要求,所以虽然它提供的函数的速度很快,但是对特效 的支持比较少。深入的图形底层应包括大量的高效的特效处理功能,所以我们应该 能够直接对显存操作。 DirectDraw 的 DirectSurface 提供了这个入口,它的 DDSURFACEDESC 结构中的变 量 lpSurface 就是显存映射的入口指针。 typedef struct _DDSURFACEDESC { DWORD dwSize; DWORD dwFlags; DWORD dwHeight; DWORD dwWidth; union { LONG lPitch; DWORD dwLinearSize; }; DWORD dwBackBufferCount; union { DWORD dwMipMapCount; DWORD dwZBufferBitDepth; DWORD dwRefreshRate; }; DWORD dwAlphaBitDepth; DWORD dwReserved; LPVOID lpSurface; DDCOLORKEY ddckCKDestOverlay; DDCOLORKEY ddckCKDestBlt; DDCOLORKEY ddckCKSrcOverlay; DDCOLORKEY ddckCKSrcBlt; DDPIXELFORMAT ddpfPixelFormat; DDSCAPS ddsCaps; } DDSURFACEDESC; 但是使用它之前,必须调用 DirectDrawSurface3::Lock 函数将此图形内存锁住, 在处理后,调用 DirectDrawSurface3::Unlock 函数将此内存交还给系统。 游戏开发基础(8) 第八章 例程之初始化部分 第一节 DDInit(): DDInit()的作用是枚举驱动程序,它的具体运行过程如下: 首先调用 DirectDrawEnumerate(),这个函数的作用在第五章 DirectDraw 深入篇 第三节选择 DirectDraw 驱动程序中已经谈到了。在本例程中这个函数的参数是 &DDEnumCallback 和 NULL,&DDEnumCallback 是指回调函数 DDEnumCallback() 的地址,NULL 是指没有这个指向应用程序数据的指针。 回调函数 DDEnumCallback()的作用是将枚举过的的驱动程序的 GUDI、描述和名 字存入一个结构数组 aDDDevs[]中。待以后选择。 第二节定义命令行参数 1.根据命令行参数确定运行方式 首先,使用 while( lpCmdLine[0] == '-' || lpCmdLine[0] == '/')检测命令行参数的 标识符,然后再使用 switch (*lpCmdLine++)对部分参数的含义进行定义: -e Use emulator(使用软件模拟) -S No Sound(无声) -1 No backbuffer(不使用后备缓冲区) -2 One backbuffer(一个后备缓冲区) -4 Three backbuffers(三个后备缓冲区) -s Use stretch(使用拉伸算法,即是在窗口模式下改变窗口的形状时对图形使用 拉伸算法使图象比较匀称。) -x Demo or stress mode(使用重音模式) 2.根据命令行参数确定显示模式 对显示模式的横轴方向的像素数 GameMode.cx、纵轴方向的像素数 GameMode.cy 及颜色数 GameMode.BPP 调用 getint(char**p, int def)取得命令行参数对这些项目 的设定。 getint(char**p, int def)函数的运行过程为: 先检测命令行参数第一个字符是否是“”、“\r”、“\t”、“\n”或“x”。如果是就使指针 p 自加 1 并继续检测,否则检测该字符是否是小于 9 大于 0 的数。如果该字符不是 小于 9 大于 0 的数则返回默认值,反之则通过 while (IS_NUM(**p) i = i*10 + *(*p)++ - '0' 将输入的字符的 ASCII 值转变为数值。然后通过 while (IS_SPACE(**p)) (*p)++; 检测后面的字符是否是“”、“\r”、“\t”、“\n”或“x”,如果是就使指针 p 自加 1 并继 续检测,直到出现其他字符或字符串结束。 第三节初始化 Windows 程序在这一部分调用了 initApplication( HINSTANCE hInstance, int nCmdShow ) 函数来初始化 Windows。 initApplication( HINSTANCE hInstance, int nCmdShow )首先定义窗口类为: style:指明了类风格为向窗口发送一个鼠标双击的消息。 1pfnWndProc:指明了指向窗口函数的指针,该指针指向 MainWndProc。 cbClsExtra:指定在窗口类结构后面分配的字节数为 0。 cbWndExtra:指定在窗口实例后面分配的字节数为 0。 hInstance:注册窗口类的应用程序实例句柄是 hInstance。 hIconhIcon:划定利用窗口最小化时显示的图标通过调用 LoadIcon( hInstance, MAKEINTATOM(FOX_ICON))获得。 hCursorhCursor:定义应用程序使用的光标通过调用 LoadCursor( NULL, IDC_ARROW )获得。 hbrBackground:背景刷子的标识符通过调用 GetStockObject(BLACK_BRUSH)获 得。 1pszMenuName:菜单的资源名的指针为 NULL。 1pszClassName:窗口类的名字为 WinFoxClass。 然后用 if( !rc ) { return FALSE; } 注册这个窗口类,并在注册失败时结束程序。 接着用 hWndMain = CreateWindowEx(……)创建窗口并将窗口的句柄赋给 hWndMain 。该窗口被创建为: 窗口的扩展格式为 WS_EX_APPWINDOW 窗口类为 "WinFoxClass" 窗口名为 OUR_APP_NAME 窗口格式为 WS_VISIBLE |WS_SYSMENU |WS_POPUP(创建一个初始态可见的标 题条上有系统菜单的重叠窗口或弹出式窗口 窗口左上角的 X 坐标 0 窗口左上角的 Y 坐标 0 窗口宽度 GetSystemMetrics(SM_CXSCREEN)(屏幕宽度) 窗口高度 GetSystemMetrics(SM_CYSCREEN)(屏幕高度) 父窗口的句柄 NULL, 窗口菜单的句柄 NULL, 窗口类的应用程序的实例句柄是 hInstance 32 位附加信息为 NULL 然后,用 if( !hWndMain ) { return FALSE; } 检测窗口创建是否成功,并在创建失败时结束程序。 最后调用 UpdateWindow( hWndMain )和 SetFocus( hWndMain )显示窗口和将键 盘输入限定在 hWndMain 所指的窗口(即游戏窗口)中。 第四节帮助信息的显示 在这里调用了 MessageBoxA(HWND hWnd ,LPCSTR lpText,LPCSTR lpCaption,UINT uType)函数。它的调用方式如下: if( bHelp ) { MessageBox(hWndMain, "F12 - Quit\n" "NUMPAD 2 - crouch\n" "NUMPAD 3 - apple\n" "NUMPAD 4 - right\n" "NUMPAD 5 - stop\n" "NUMPAD 6 - left\n" "NUMPAD 7 - jump\n" "\n" "Command line parameters\n" "\n" "-e Use emulator\n" "-S No Sound\n" "-1 No backbuffer\n" "-2 One backbuffer\n" "-4 Three backbuffers\n" "-s Use stretch\n" "-x Demo or stress mode\n", OUR_APP_NAME, MB_OK ); } 它的含义是:如果 bHelp 为 TURE,则在 hWndMain 所指向的窗口创建一个消息框。 这个消息框的内容是“”所包含的部分,标题是 OUR_APP_NAME(在 foxbear.c 中 有#define OUR_APP_NAME "Win Fox Application"语句),并且这个消息框显示一 个 OK 按钮。 第五节初始化游戏 这个部分只是调用了 InitGame( void )函数,它的调用方式为: if( !InitGame() ) { return FALSE; } 即如果调用失败,则结束程序。 InitGame( void )是整个初始化部分最庞大的部分,它完成了剩下的初始化的工作。 在本章的后面几节中我们将分几部分讲解这个函数 第六节内存刷新 由于在窗口过程中会接受被定义为改变显示模式的消息这时需要对游戏进行重新 初始化,于是就会对 InitGame()函数进行调用(比如在消息 VK_F8 时就调用了该 函数)。这时的调用会是在游戏运行时发生的,这时在游戏运行中较重要的指针都 已被使用(指向了一定的内存区域),同时内存已被分配。如果这时不释放内存和 将指针指向 NULL(即不指向任何一个内存区域),以后的初始化将会出现错误 。 故在该函数的运行过程中调用了 ExitGame()函数已进行刷新指针的工作。 ExitGame()的运行过程为: 首先,检测 lpFrameRate、lpInfo、lpPalette 三个指针,若非 0,则释放其所指的 内存,并令该指针为 NULL 使其恢复为初始状态(通过利用 IUuknown 接口提供的 函数 Release()递减指针的内部引用值,直至引用值为 0 释放所分配的内存)。 然后,调用函数 DestroyGame()继续释放内存。DestroyGame()的运行过程为:首 先,检测 hBuffer 是否为非 0,如果非 0 则调用下列函数: DestroyTiles( hTileList )——释放句柄 hTileList 所指的局部内存块。 DestroyPlane( hForeground )——释放句柄 hForeground 和 hForeground->hBM 所 指向的内存块。 DestroyPlane( hMidground )——释放句柄 hMidground 和 hMidground->hBM 所指 向的内存块。 DestroyPlane( hBackground )——释放句柄 hBackground 和 hBackground->hBM 所指向的内存块。 DestroyBuffer( hBuffer )——释放所有位图所占用的内存及 lpClipper、 lpBackBuffer、 lpFrontBuffer 所指向过的内存区域。 DestroySound()——释放所有音效所占用的内存及 lpDS 所指向过的内存区域。 最后令 hTileList、 hForeground、hMidground、hBackground 和 hBuffer 指向空。 第七节初始化声音 这个部分主要调用函数 IinitSound()进行对 DirectSound 进行初始化和声音的播放 的工作。它的工作过程为: 调用 DSEnable(hwndOwner)进行 DirectSound 的初始化:先用 bUseDSound = GetProfileInt("FoxBear", "use_dsound", bWantSound)选择播放设备;再通过 if (!bUseDSound) { lpDS = NULL; return TRUE; } if (lpDS != NULL) { Msg( "DSEnable, already enabled" ); return TRUE; } 检测选择是否成功和声音是否已被初始化。然后是 dsrval = DirectSoundCreate(NULL, &lpDS, NULL); if (dsrval != DS_OK) { Msg("DirectSoundCreate FAILED"); return FALSE; } dsrval = IDirectSound_SetCooperativeLevel(lpDS, hwnd, DSSCL_NORMAL); if (dsrval != DS_OK) { DSDisable(); Msg("SetCooperativeLevel FAILED"); return FALSE; } 用于创建 DirectSound 的对象并检测创建是否成功和设置合作层并检测设置是否 成功。在这里为默认设备创建了一个对象,将合作层设置为普通(DSSCL_NORMAL) 即使用声卡的应用程序可以顺序地进行切换。 调用 DSEnable()之后用语句: for( idx = 0; idx < NUM_SOUND_EFFECTS; idx++ ) { if (SoundLoadEffect((EFFECT)idx)) { DSBCAPS caps; caps.dwSize = sizeof(caps); IDirectSoundBuffer_GetCaps(lpSoundEffects[idx], &caps); if (caps.dwFlags & DSBCAPS_LOCHARDWARE) Msg( "Sound effect %s in hardware", szSoundEffects[idx]); else Msg( "Sound effect %s in software", szSoundEffects[idx]); } else { Msg( "cant load sound effect %s", szSoundEffects[idx]); } } 将音效调入(利用函数 SoundLoadEffect((EFFECT)idx))和检测设备的特性并将之 存放在结构 caps 中的成员 dwsize 里(利用函数 IDirectSoundBuffer_GetCaps(lpSoundEffects[idx], &caps)。 最后是一部分是播放,这部分主要是设定 dsBD.dwFlags 为 DSBCAPS_PRIMARYBUFFER 确定可对主缓冲区进行操作,使用语句 if (SUCCEEDED(IDirectSound_CreateSoundBuffer(lpDS, &dsBD, &lpPrimary, NULL)))来创建副缓冲区并检测成功与否,使用语句 if (!SUCCEEDED(IDirectSoundBuffer_Play(lpPrimary, 0, 0, DSBPLAY_LOOPING))) 播 放并检测成功与否。 第八节 DirectDraw 的设置 这个部分完成了对 DirectDraw 的设置工作和游戏开始时初始化画面的显示。它的 运行过程如下: if( !PreInitializeGame() ) { return FALSE; } 在 PreInitializeGame()函数中只有语句 return InitBuffer( &hBuffer)。该语句调用 了 InitBuffer( &hBuffer)函数又只调用了 gfxBegin()函数,并在调用失败后结束程 序。函数 gfxBegin()完成了对显示模式的设置工作(通过对 DDEnable( void )函数 的调用)、创建表面的工作(通过调用函数 DDCreateFlippingSurface( void )、初 始化画面的显示(通过调用函数 Splash())。 函数 DDEnable( void )的运行过程如下: 1.获取系统信息(运用 GetProfileInt()函数)和决定是否使用软件模拟(应用 DirectDrawCreate() 为该驱动方式建立一个对象再用 DirectDraw_QueryInterface()查询该对象是否被支持)。 2.检测显示模式。首先判断是否使用全屏模式,若是则使用 IDirectDraw_SetCooperativeLevel(( lpdd, hWndMain,DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE |DDSCL_FULLSCREEN )创建一个对显示设备能够进行最大化 控制的合作层,然后用 IDirectDraw_EnumDisplayModes(lpdd, DDEDM_STANDARDVGAMODES, NULL, 0, EnumDisplayModesCallback)枚举 VGA 模式下的显示模式。如果枚举失败,则调用 IDirectDraw_EnumDisplayModes(lpdd, 0, NULL, 0, EnumDisplayModesCallback)(枚举所有的显示模式)。若不使用全屏 模式,则调用 ddrval = IDirectDraw_SetCooperativeLevel( lpdd, hWndMain,DDSCL_NORMAL )(创建一个 DDSCL_NORMAL 模式下的合作层即使 用一般 Windows 应用程序的方式)。随后将设定的显示模式放入结构数组 ModeList[]中。若合作层创建失败则返回错误信息并结束游戏。 接下来是选择显示模式部分。首先判断是否使用全屏模式,若是则判断使用哪种显 示大小和颜色数。若没选用全屏模式则获取系统当前的设备及所采用的颜色数,设 定窗口模式下的窗口的属性及判断是否使用拉伸算法并根据判断结果设定用户区 的大小(利用 SetRect(&rc, 0, 0, GameMode.cx*2, GameMode.cy*2)及 SetRect(&rc, 0, 0, GameMode.cx, GameMode.cy)),然后根据用户区的大小和窗 口风格的窗口属性设定窗口的大小,并由 SetWindowPos()确定窗口在多窗口的情 况下的位置。 利用 IDirectDraw_GetCaps( lpdd, &ddcaps, NULL )函数获取设备所支持的特性, 并确定那些特征由软件模拟,那些特征由硬件支持。由 nBufferCount 的值确定缓 冲区的数量。最后由设备所支持的特性确定最大显示模式。 函数 DDCreateFlippingSurface( void )的运行过程: 首先取得硬件支持的特性(应用 IDirectDraw_GetCaps( lpDD, &ddcaps, NULL ) )。 再将缓冲区中全部填上 0。在全屏模式下和缓冲区数大于 1 时,创建一个以 DD_CAPS 为标志的表面功能区及一个以 DDS D_BACKBUFFERCOUNT 为标志的后 备缓冲记数区,并给表面功能区分配 DDSCAPS_PRIMARYSURFACE、 DDSCAPS_FLIP 和 DDSCAPS_COMPLEX 三个标志(关于这三个标志,请详细阅读 DirectDraw 深入篇)。接下来用 ddscaps.dwCaps = DDSCAPS_BACKBUFFER; ddrval = IDirectDrawSurface_GetAttachedSurface( lpFrontBuffer, &ddscaps, &lpBackBuffer ); 取得后备缓冲区的指针。如果使用了拉伸算法或 ddcaps.dwCaps 的标志为 DDCAPS_BANKSWITCHED 时则创建再一个用于拉伸算法的缓冲区(在您详细阅读 函数 DDCreateSurface(DWORD width,DWORD height,BOOL sysmem,BOOL trans )后会发现该缓冲区的创建方法与上面谈到的缓冲区的创建是差不多的,只不 过少了后备缓冲记数区而多了两个关于高度和宽度的缓冲区,并使用了系统内存, 请注意这个技巧)。下面是在全屏模式和有一个缓冲区的情况下,如何进行创建表 面的工作。在这一步中可以看到程序只创建了一个表面功能区而无后备缓冲记数 区,同时取得后备缓冲区的指针的部分被 IDirectDrawSurface_AddRef(lpFrontBuffer); lpBackBuffer = lpFrontBuffer; 替代。然后是在窗口模式下的缓冲区的设置,首先创建一个表面功能区(在 VRAM 中) 然后用 lpBackBuffer = DDCreateSurface( GameSize.cx, GameSize.cy, FALSE, FALSE )创建一个后备缓冲区(在系统内存中)。接下来是对剪切板的设置,先创 建一个剪切板(ddrval = IDirectDraw_CreateClipper(lpDD, 0, &lpClipper, NULL)), 再取得剪切板的指针( ddrval = IDirectDrawClipper_SetHWnd(lpClipper, 0, hWndMain)),最后将剪切板连接到表面上( ddrval = IDirectDrawSurface_SetClipper(lpFrontBuffer, lpClipper))。下面是对颜色的操作, 首先使用 IDirectDrawSurface_GetPixelFormat(lpFrontBuffer, &ddpf)取得表面的 像素格式,然后设置 ColorKey,若是 8 位色则 ColorKey 为 256 反之则取 16 位色。 第九节用户区尺寸及调色板设置 1.用户区尺寸设置 在这部分中首先是对全屏模式或使用拉伸算法的情况下的用户区的大小设置,然后 是在窗口模式下的设置。在这些设置工作中可能需要解释的是 GameRect.left 为什 么等于 GameMode.cx - GameSize.cx,这是由于 GameRect.left(或 GameRect.right)是指用户区的左上角(右下角)的横坐标长度,而 GameMode.cx 是指整个用户区的宽度(全屏)而且 GameSize.cx= GameMode.cx / 2 。所以 GameRect.left=GameMode.cx - GameSize.cx。其他的比如 GameRect.top、 GameRect.right 和 GameRect.bottom 的计算式的含义都和 GameRect.left 的计算 方法差不多。 2 调色板设置 这一部分程序的功能主要是由函数 ReadPalFile()完成的,这个函数的功能是引入 一个调色板文件,如果找不到指定的文件则使用默认的调色板,而这个默认的调色 板的建立是使用了下面的语句: for( i=0; i<256; i++ ) { pal.ape[i].peRed = (BYTE)(((i >> 5) & 0x07) * 255 / 7); pal.ape[i].peGreen = (BYTE)(((i >> 2) & 0x07) * 255 / 7); pal.ape[i].peBlue = (BYTE)(((i >> 0) & 0x03) * 255 / 3); pal.ape[i].peFlags = (BYTE)0; }/*在结构数组 pal.ape[i]中存放 256 组由四个数值组成的颜色值*/ 和函数 ddrval = IDirectDraw_CreatePalette(lpDD,DDPCAPS_8BIT,pal.ape,&ppal,NULL )。从这些 语句中我们可以看到建立了一个 256 色的调色板(8 位色)。 在调用调色板之后用 IDirectDrawSurface_SetPalette()将调色板连接到表面上。 第十节调入图象 这个部分是通过调用 InitializeGame()函数实现的。InitializeGame()函数的运行过 程为:首先调用 Splash()函数,该函数的作用是将后备缓冲区全部写为蓝色并测 试全部缓冲区(用 DDClear()函数)和向屏幕输出 FoxBear is loading...... Device。 然后用 LoadBitmaps( void )函数从 foxbear.art 文件中将所有的位图读出并存入结 构数组 hBitmapList[]中。接着是: InitTiles( &hTileList, hBitmapList, C_TILETOTAL ); InitPlane( &hForeground, &hForePosList, "FORELIST", C_FORE_W, C_FORE_H, C_FORE_DENOM ); TilePlane( hForeground, hTileList, hForePosList ); InitPlane( &hMidground, &hMidPosList, "MIDLIST", C_MID_W, C_MID_H, C_MID_DENOM ); TilePlane( hMidground, hTileList, hMidPosList ); InitPlane( &hBackground, &hBackPosList, "BACKLIST", C_BACK_W, C_BACK_H, C_BACK_DENOM ); TilePlane( hBackground, hTileList, hBackPosList ); InitSurface( &hSurfaceList, "SURFLIST", C_FORE_W, C_FORE_H ); SurfacePlane( hForeground, hSurfaceList ); InitFox( &hFox, hBitmapList ); InitBear( &hBear, hBitmapList ); InitApple( &hApple, hBitmapList ); 这些语句将 hBitmapList[]中的位图分类存入相应的列表中并建立各自的内存区。 如:InitPlane( &hForeground, &hForePosList, "FORELIST", C_FORE_W, C_FORE _H, C_FORE_DENOM )在主缓冲区建立了一个存放前景的区域和一个存放前景的 位图的区域(第一帧)。 TilePlane( hForeground, hTileList, hForePosList )在主缓冲区建立一个存放前景中 其他位图的区域。 最后用 DDClear()将所有后备缓冲区清空。 第十一节输出设备信息 在游戏的过程中会显示出当前的 FPS 值及显示模式和“ALT+ENTER=WINDOWS” 的信息。这部分的功能就是输出这些信息。 这一功能的是通过函数 makeFontStuff()的调用而实现的。makeFontStuff()的实现 过程为:删除已有的字体句柄,创建一个新的字体,调用 initNumSurface( )为 FPS 及 FPS 的值和显示模式及字符串“ALT+ENTER=WINDOWS”设定输出字体的大小、 颜色、在用户区的位置等等属性。然后各为 FPS 和显示模式等的信息创建一个内 存区域以存放它们。然后用 if( bTransDest ) BackColor = RGB(255,255,255); else BackColor = RGB(128,64,255); ddck.dwColorSpaceLowValue = DDColorMatch(lpInfo, BackColor); ddck.dwColorSpaceHighValue = ddck.dwColorSpaceLowValue; IDirectDrawSurface_SetColorKey( lpInfo, DDCKEY_SRCBLT, &ddck); IDirectDrawSurface_SetColorKey( lpFrameRate, DDCKEY_SRCBLT, &ddck); 来设定 FPS 信息和显示模式信息显示的色彩键码。 最后,再次调用 initNumSurface( )显示这些信息。 游戏开发基础(9) 第一节窗口的移动和改变大小时 case WM_SIZE: case WM_MOVE: if (IsIconic(hWnd)) { Msg("FoxBear is minimized, pausing"); PauseGame(); } if (bFullscreen) { SetRect(&rcWindow, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)); } else { GetClientRect(hWnd, &rcWindow); ClientToScreen(hWnd, (LPPOINT)&rcWindow); ClientToScreen(hWnd, (LPPOINT)&rcWindow+1); } Msg("WINDOW RECT: [%d,%d,%d,%d]", rcWindow.left, rcWindow.top, rcWindow.right, rcWindow.bottom); break; case WM_SIZE:是在窗口的大小被改变时发送的。 case WM_MOVE:是在窗口被移动时发送的。 当收到这两个消息后,首先检测窗口是否最小化了,如果是则暂停游戏。否则再检 测游戏是否转为全屏模式,若是则改变用户区的大小为全屏。如果只是改变了窗口 的大小但并没有使显示模式变成全屏或窗口最小化,则先取得窗口的用户区坐标 (左上角的 x、y 及右下角的 x、y 的值)将之存入结构 rcWindows,再将 rcWindows 中的坐标转换为屏幕坐标。请注意 rcWindows 是 RECT 型的结构,但 ClientToScreen()要求的是 POINT 型的结构,在结构 RECT 中存放的是一个矩形的 左上角的 x、y 及右下角的 x、y 的值而结构 POINT 中存放的就只是 x 和 y 的值。 故此在 ClientToScreen(hWnd, (LPPOINT)&rcWindow); ClientToScreen(hWnd, (LPPOINT)&rcWindow+1); 中先对 rcWindow 进行强制类型转换,而且两次调用该函数,同时在第二次掉用时 使 rcWindow 的地址自加 1。 第二节窗口被激活时 case WM_ACTIVATEAPP: bIsActive = (BOOL)wParam && GetForegroundWindow() == hWnd; if (bIsActive) Msg("FoxBear is active"); else Msg("FoxBear is not active"); if (bPaused && bIsActive) { if (RestoreGame()) { UnPauseGame(); } else { if (GetForegroundWindow() == hWnd) { if (InitGame()) { UnPauseGame(); } } } } break; case WM_ACTIVATEAPP:当不同于当前窗口的应用程序的窗口被激活时发送本消 息。 当接收到这个消息时,首先令 bIsActive 等于 (BOOL)wParam && GetForegroundWindow() == hWnd,然后检测 bIsActive 是否为 TURE。这里的意 思就是检测游戏的窗口是否被激活且处于当前系统中最高优先级。接下来先检测游 戏窗口是否是从暂停状态到激活的状态,若是则运行 RestoreGame()从新开始,若 RestoreGame()成功则运行 UnPauseGame(),如果不是从暂停状态到激活的状态, 则检测本程序的窗口是否拥有最高的优先级,若有则从新初始化游戏(运行 InitGame()),初始化成功后则运行 UnPauseGame()。 第三节 实现逻辑调色板时 case WM_QUERYNEWPALETTE: if (!bFullscreen && lpPalette && lpFrontBuffer) { HRESULT ddrval; ddrval = IDirectDrawSurface_SetPalette(lpFrontBuffer,lpPalette); if( ddrval == DDERR_SURFACELOST ) { IDirectDrawSurface_Restore( lpFrontBuffer ); ddrval= IDirectDrawSurface_SetPalette(lpFrontBuffer,lpPalette); if( ddrval == DDERR_SURFACELOST ) { Msg(" Failed to restore palette after second try"); } } if( ddrval == DD_OK ) { SetWindowText( hWnd, OUR_APP_NAME ); } } break; WM_QUERYNEWPALETTE:在窗口收到输入焦点前发出,当窗口收到输入焦点后将 返回显示它是否能实现逻辑调色板。 在这个消息获得后,程序先检测是否运行于窗口模式且调色板和前缓冲区已设定, 然后设定一次调色板,若失败则恢复前缓冲区然后再试一次,如果仍然失败则输出 错误信息。两次设定若成功一次则将标题改为“Win Fox Application”。 第四节 改变系统调色板时 case WM_PALETTECHANGED: if ((HWND)wParam != hWnd) { if( !bFullscreen ) { if( !bStress ) { Msg("***** PALETTE CHANGED, PAUSING GAME"); PauseGame(); } else { Msg("Lost palette but continuing"); SetWindowText( hWnd, OUR_APP_NAME " - palette changed COLORS PROBABLY WRONG" ) } } } break; WM_PALETTECHANGED:本消息在拥有输入焦点的当前窗口实现其逻辑调色板时 送往所有的窗口。这时,系统调色板被改变,本消息允许不带输入焦点的窗口使用 调色板去实现自己的逻辑调色板和更新其用户区域。 在得到这个消息后,程序首先是否是当前窗口改变了系统调色板,如果是则直接跳 出窗口过程,若不是则再检测是否是全屏模式,若是则直接跳出窗口过程,若非则 先检测 bStress 是否为 FLASE,若为 FLASE 则暂停游戏,若为 TURE 则将标题条改 为“ - palette changed COLORS PROBABLY WRONG”。 第五节当操作键按下时 WM_KEYDOWN 消息是在一个非系统键按下时产生的,非系统键是指没有按下 ALT 键时按下的键,或是当某窗口已有输入焦点时按下的键。 在该消息的 wParam 参数中包含了识别所按下的键的虚键码,由不同的虚键码就 可以完成键盘对游戏的操作。我们知道键盘对游戏的操作中所按的键可以分为:操 作键和功能键两类。下面让我们先看看例程中是如何定义操作键的吧。 在上一章我们就介绍过在本例程中操作键是小建盘上的“2345678”,但一直没有谈 到如何实现的,现在就让我们来看一看。 对操作键的功能的定义是在 case VK_NUMPAD5: lastInput=KEY_STOP; break; case VK_DOWN: case VK_NUMPAD2: lastInput=KEY_DOWN; break; case VK_LEFT: case VK_NUMPAD4: lastInput=KEY_LEFT; break; case VK_RIGHT: case VK_NUMPAD6: lastInput=KEY_RIGHT; break; case VK_UP: case VK_NUMPAD8: lastInput=KEY_UP; break; case VK_HOME: case VK_NUMPAD7: lastInput=KEY_JUMP; break; case VK_NUMPAD3: lastInput=KEY_THROW; break; 您可以看到在得到每个虚键码之后都对 lastInput 进行赋值,这时就完成了对操作 键的定义了,至于操作键是如何其作用的,在本章的第八节中您可以看到。 第六节 当功能键按下时 在本游戏中的功能键是 F3、F4、F5、F6、F7、F8、F9。这些键的作用时什么,是 如何实现的呢?下面就让我们一个一个的看看吧! 1.F3 的作用是暂停游戏和解除暂停。在程序中这个作用是这样实现的: case VK_F3: bPaused = !bPaused; break; 2.F4 的作用是实现 ALT+ENTER 的作用。在程序中是使用 case VK_F4: PostMessage(hWnd, WM_SYSKEYUP, VK_RETURN, 0); break; 4.F6 的作用是逐个使用在显示模式列表中的显示模式。 case VK_F6: { static i; if(bFullscreen) { for (i=0; i= NumModes) { i = 0; } Msg("ModeList %d %d",i,NumModes); GameMode.cx = ModeList[i].w; GameMode.cy = ModeList[i].h; GameBPP = ModeList[i].bpp; bStretch = FALSE; InitGame(); } break; 在收到 VK_F6 的消息后,程序设立一个初值为 0 的变量,并以该变量为存放显示 模式列表的数组的下标,然后令其自加直至找到一个与当前模式相同的。然后令其 自加 1,取以该值为下标的元素为显示模式,最后重新初始化游戏。 5.F7 的作用是改变颜色数。 case VK_F7: GameBPP = GameBPP == 8 ? 16 : 8; InitGame(); break; 6.F8 的作用是决定是否使用拉伸算法。 case VK_F8: if (bFullscreen) { bStretch = !bStretch; InitGame(); } else { RECT rc; GetClientRect(hWnd, &rc); bStretch = (rc.right != GameSize.cx) || (rc.bottom != GameSize.cy); if (bStretch = !bStretch) SetRect(&rc, 0, 0, GameMode.cx*2, GameMode.cy*2); else SetRect(&rc, 0, 0, GameMode.cx, GameMode.cy); AdjustWindowRectEx(&rc,GetWindowStyle(hWnd),GetMenu(hWnd) != NULL,GetWindowExStyle(hWnd)); SetWindowPos(hWnd, NULL, 0, 0, rc.right-rc.left, rc.bottom-rc.top,SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); } break; 这部分的运行过程为:若为全屏模式则对 bStretch 取非,然后初始化游戏。若为 窗口模式,则取得当时窗口的特征,以确定 bStretch 的值,然后从新显示窗口。 7.F9 的作用是取消使用软件模拟并逐个使用已有的驱动程序 case VK_F9: DevIndex ++; bUseEmulation = FALSE; if (DevIndex >= MaxDevIndex) DevIndex = 0; ExitGame(); DDDisable(TRUE); // destroy DirectDraw object InitGame(); break; 第七节 其他消息 case WM_DISPLAYCHANGE: break; case WM_CREATE: break; 这两个消息收到后,将不做任何反应。 case WM_SETCURSOR: if (bFullscreen && bIsActive) { SetCursor(NULL); return TRUE; } break; 该信息在光标随鼠标的移动而输入未被捕获时发出。 由于本游戏不需鼠标,故在随后的检测是否是全屏模式或是被激活的语句,获得肯 定的答案后,将光标从屏幕上删除。 case WM_PAINT: hdc = BeginPaint( hWnd, &ps ); if (bPaused) { char *sz = "Game is paused, this is not a bug."; TextOut(ps.hdc, 0, 0, sz, lstrlen(sz)); } EndPaint( hWnd, &ps ); return 1; 该消息在请求重新绘制应用程序窗口是发出 。 程序在接收到这个消息后就调用 BeginPaint()为 hWnd 所指的窗口作好绘画准备。 然后判断游戏是否暂停,若是则向屏幕输出 Game is paused, this is not a bug.最 后调用 EndPaint() case WM_DESTROY: hWndMain = NULL; lastInput=0; DestroyGame(); // end of game DDDisable(TRUE); // destroy DirectDraw object PostQuitMessage( 0 ); break; 该消息在要撤消某窗口时向该窗口发送。 在收到这个消息后程序令指向窗口的句柄为 NULL,对游戏的刷新单元的下一次输 入设为 0,然后清除游戏及游戏工所占的内存。 第八节 刷新游戏单元 在讨论完窗口过程后,我们应开始介绍这个游戏的消息循环部分了。在这个游戏的 消息循环部分中大部分在第二章 windows 编程基础中已经谈到过了,所以在这里 我们将只介绍刷新游戏单元和重画游戏单元部分。在消息循环中程序是调用函数 ProcessFox(SHORT sInput)来进行这两部分的工作的。在 ProcessFox(S HORT sInput)函数中,先对游戏是否正在运行或恢复游戏运行是否成功作了一个检测, 然后就先调用 ProcessInput(sInput)函数进行刷新游戏单元部分,再调用 NewGameFrame()进行重画游戏单元的工作。下面就让我们先看看刷新游戏单元 部分吧。 这一部分的运行过程为: 1.狐狸的行为刷新 首先取得当前狐狸的的速度、行为和方向。然后检测是否获得狐狸的位置或输入是 否是 4209,若检测的表达式为 TURE 则输入为 0,即没有输入,若为 FLASE 则开 始对各种输入进行响应。在响应的过程中程序先对狐狸的当前状态进行判断,然后 根据狐狸的当前状态决定下一帧狐狸的基本状态。比如“↓”键或小键盘的“2”被按 下时的响应过程为 case KEY_DOWN: if( foxAction == STOP ) { break; } else if( foxAction == STILL ) { SetSpriteAction( hFox, CROUCH, SAME ); } else if( foxAction == WALK ) { SetSpriteAction( hFox, CROUCHWALK, SAME ); } break; 在“↓”键被按下时,如果狐狸的动作是在急停的,则跳出;如果狐狸是静止的,则 狐狸蹲下不动;若狐狸是在移动,则狐狸的行动改为爬行。 在对输入的初步处理后,程序便结合狐狸行为的其他属性,确定下一帧狐狸的状态。 在这里同样是用 switch——case 语句和大量的 if——else 语句完成的。 2.熊和苹果的行为刷新 这两部分的运行过程实际上是一致的,都是首先进行碰撞检测,然后再根据检测结 果决定角色新的行为属性。 第九节 重画游戏单元 这部分的工作是由 NewGameFrame()完成的。这个函数首先调用 SetSpriteX() 、 SetSpriteY()两函数设定角色的新位置,再调用 SetPlaneVelX()和 SetPlaneX()函数 设定三层背景的移动速度和下一帧的位置。然后通过检测 bTransDest 的值决定是 先将角色的位图粘贴到 Backbuffer 还是先将背景的位图粘贴到 BackiBuffer, 最后调用函数 gfxSwapBuffers()进行页面翻转将后备缓冲区中的图显示到屏幕上。 该函数是这样实现的: BOOL gfxSwapBuffers( void ) { if( bFullscreen ) { if( bStretch ) { gfxStretchBackbuffer(); } if (nBufferCount > 1) return gfxFlip(); else return TRUE; } else { return gfxUpdateWindow(); } 它的运行过程是这样的:在全屏模式下且使用拉伸算法时用 gfxStretchBackbuffer()对后备缓冲区和拉伸缓冲区之间进行 blt 操作,然后在有一 个以上(不含一)的后备缓冲区时用 gfxFlip()调用 IDirectDrawSurface_Flip( lpFrontBuffer, NULL, DDFLIP_WAIT )进行一次页面翻转 操作将后备缓冲区中的图显示到屏幕上。如果是窗口模式下,则通过 gfxUpdateWindow()调用 IDirectDrawSurface_Blt( lpFrontBuffer, &rcWindow,lpBackBuffer,NULL,DDBLT_WAIT,NULL)直接将后备缓冲区中的内容 blt 到主缓冲区中。这样就完成了重画游戏单元的任务了。 游戏开发基础(10) 第一节 3D 类游戏 设计 3D 光线投射游戏包括从高解析度图形到快速动画的许多技术。尽管 3D 场景 在这些 不同的游戏中可能变化很大,玩游戏的方法和设计游戏的基本技术却是类似的。场 景中较明显的组成部分包括生成的墙壁、地板、天棚、可移动的物体和静止的物体。 当然,这些游戏也充分利用了大量的其它技术如声效和音乐、3D 动画、故事、难 度级别、秘密门等等。 1.速度、力量和简单性原则 在设计像 Doom 这样的游戏过程中,关键的因素看来都很简单:使用一个强大快 速的光线投射引擎以提供优秀的 3D 图形和一个优秀但简单的操作环境。增加动态 的多游戏者能力和难度级别设计以使您的游戏不断前进。当然,说的容易做起来难。 正像 Doom“看起来” 像是设计得比较简单而实际上并不是。 2.图像力量 在 Doom 初次登台之后,大量的仿制品出现了。尽管有些游戏也很有自己的特点, 但玩的时候似乎没有人获得 Doom 的感觉。这是一个很好的例子,证明在一个由 图形力量占主要地位的游戏引擎中,设计与难度级别编辑是具有决定性的。 据说,ID 尽管使用了大量的优秀编辑人员和工具设计了很多新的 Doom 难度级别, 可是只有少量(可能少到百分之一)的难度级别投入实际的游戏使用中。 当然,在游戏开发中只重视图像质量还是远远不够的,只有提供整体水平很高的游 戏才能让游戏老不断玩您的游戏。 3.尽在射线中 光线投射技术是一种确定游戏者移动时一个物体应该出现在场景中什么位置的技 术。它的原理是:当人在场景中移动时,射线改变它们的位置并和场景中不同的点 相交。相交的点就是墙壁和其它物体被显示的地方。通常,射线是沿着一个弧线投 射出去的,这样在正前方的墙壁和物体与在边上的可以同样被发现。 4.在迷官中迷失 许多游戏开发者认为 3D光线投射游戏和迷宫游戏差不多。因为这类游戏经常发生 在类 似迷宫的环境中。创建像 Doom 这样的 3D 游戏包括建立一个迷宫和随后将各种不 同的对像放在迷宫中。 在一个 3D世界中迷宫的组成部分是房间、墙壁、地板、天花板等等。在迷宫中房 间是由墙 壁分隔形成的。墙壁实质上是静态的物体,只能置于迷宫网格的边界上。 5.多难度级别 大多数 3D游戏提供多难度级别,使游戏者玩上几天甚至几个星期。每级游戏都可 以建立在不同的迷宫中。当一个游戏者以自己的方式成功地玩到了某一级的终点 时,游戏继续进行到更高的一级,在那里游戏者要接受新的挑战。多级别非常重要, 它可以让新的布景和迷宫出现并且使游戏看起来不让人很厌烦。在这个方面,许多 3D游戏是与 2D Arcade 游戏相同的。它们让您在迷宫中搜寻到足够的物品,达到 一定的分数才可以进入下一关。您可以设计允许游戏者使用秘技,如穿过一个秘密 门自动进入下一级或得到一个隐藏的物体以获得特殊能力。如果 您真的很音欢暴力,您也可设计一个让游戏者不得不杀完所有怪物才能进入下一级 的游戏。 未来展望 未来的 3D 光线投射游戏不但会加进多人游戏功能,更快速的 3D 引擎也会开发出 来。准备好吧,这一类游戏的未来蒸蒸日上。 第二节探险游戏 探险游戏设计包括下面几个概念,这些概念不是具体某个游戏本身具有的概念,而 是关于这类游戏的基本要素。 1.情节和引擎 很多游戏都是使用了公司内部建造的游戏引擎和游戏创建系统开发出来的产品。使 用这些游戏引擎处理大量的图像和其它琐碎的工作可以让游戏公司真正集中力量 用于推进故事情节和角色特征的开发。 首先说着建立一个核心引擎,并确定您的探险游戏可能需要的一般内容,包括地图、 动作、对话接口等等。然后,在这些基础上再编写您自己的游戏。但是记住,用相 同的引擎开发出来的好游戏必须有一个新鲜复杂的故事情节,而且要有足够的特征 使它从同系列中的其它游戏中脱颖而出。这意味着您的引擎要能适应开发宽范围的 创意和有可能进行的升级。 简单他说,需要建立一个系统以支持您创建大量的故事情节。有了这个武器,探险 游戏开发才会向成功的方向前进。 2.成熟的内容 揉险游戏设计的关键是内容设计。玩这些游戏的大都是受过良好教育的成年人,他 们喜欢井有能力理解复杂的故事情节。 传统上,大多数探险游戏与其它大部分游戏一样,倾向于避免某些不好的主题,但 因为玩游戏变得更加普及了,所以创建一个成人级的探险游戏的可能性更大了。 未来展望 可以说探险游戏是最接近交互式故事的东西。所以,这一类游戏未来的形式就是变 得更富于交互性。 探险游戏毫无疑问将继续成为主要的游戏类型之一,而且随着优秀开发人员编写出 更有趣的创意,我们将在这个领域看到更令人激动的发展。 第三节寓教于乐游戏设计问题 寓教于乐游戏设计包括一些独特的技术。当您开发这一类游戏时,您必须努力对付 许多问题,包括游戏类型、年龄特征、性别特征。让我们仔细看看这些问题。 1.寓教于乐游戏分类 寓教于乐产品有两大类:传统的和非传统的。我们先看看按教育内容的多少来分类 传统类型: 1)操作与训练游戏 在这一类游戏中,孩子们要做完一定数量的训练题目,在全部做完之后通常有一段 动画作为奖励。 这些游戏对于孩子们是相当合适的。还没有在学校被作业压死的小孩子可以把这些 练习当做一个游戏来做。 2)教育和游戏各占一半的游戏 有些游戏在游戏中混合了教育内容。例如在一个叫做“圣地亚哥在哪里?”的游戏 中,游戏内容分为教育和游戏两部分,在教育部分中,游戏者作为负责审问嫌疑犯 的角色必须收集到足够多的线索以进一步探索凶犯的下落。游戏部分则表现出它的 挑战性:游戏者只有有限的时间用来搜寻和查找下一个地点的线索。如果游戏者在 错误的地点浪费了时间,他就无法完成游戏。在游戏中提供了地理资料,所以游戏 者玩游戏时不知不觉地学到了东西。 3)内容游戏 这些游戏在训练和探险游戏中处于中间位置,它们包括三个关键的成分:探索、冒 险和搜集。(后面详细讨论这些成分。)教育内容仍然很清晰但在游戏进行时已处 于第二位了。孩子(特别是大点的孩子)把这些练习看作游戏而不是训练。 4)发现式游戏 现在美国公立教育的热点问题是探索和问题解决。这个思想是在给予孩子们结构化 的练习之后,他们靠自己的力量发现信息并最终获得学术技能和知识。在我看来, 这近似于给孩子一台电脑然后让他们写一篇小文章,然而不先教他们如何打字。在 这类游戏中孩子用鼠标点各种热区,然后等待结果。无论是播放一段动画,阅读一 个故事,还是开始一段游戏,这些都很适合学龄前儿童。 非传统的寓教于乐游戏是那些因为本身固有教育价值的原因,即使它们并不是专门 设计 用于教育目的,但都有教育效果。 2.年龄的考虑 大概在设计寓教于乐游戏产品中最关键的因素是确定您的年龄分组。孩子们变化太 快,所以您必须对您要面对的年龄组有一个清晰的认识。 注意分组有些是重叠在一起的,同一组中不是所有的孩子都达到了相同的程度,也 有孩子会适合相邻年龄组的某些游戏。详细的年龄组讨论可以参阅第一章。 3.许可特征 使用迪斯尼角色(如骗蝠侠)的游戏含有“注册许可特性”。出版商出一笔大约游戏 出售总收入的百分之十用于注册,以保证可以使用游戏中角色的名字等权利。反过 来,开发者必须承诺坚持他们关于内容、外观和角色特征方面的标准。事实上,大 部分商标持有者都要求开发者将剧本、艺术处理等有关内容上交审核。为什么您要 使用别人的角色而让商标持有者赚大钱呢?因为注册许可的角色会给您的游戏现 成的知名度。任何其它的地方(电视、电影、T 恤衫等等)建立的知名度会给您的 计算机游戏带来潜在的销售额。您的出版商会很高兴地发现因为这个知名度游戏非 常容易进行零售。 4.谁买走游戏? 孩子们的寓教于乐游戏难于走位的一个原因是买游戏的人(孩子的父母)不是玩游 戏的人。因此,您必须在两类很不同的顾客之间进行游戏对像确定的工作。这对于 小于七岁的年龄组尤其重要。 另外,您还必须投合教师的心意。如果您的游戏被学校采纳,虽然这不会使您发财, 但学校的知名度和权威性会对顾客的购买产生影响。 未来展望 未来的寓教于乐游戏会进一步发展引人入胜的学习体验,将游戏与教育融合起来。 在向这个目标迈进的过程中,更多的多媒体内容、联机成分和更多精练的游戏成分 将会加入到教育软件中。寓教于乐游戏最大的发展可能是将所有主要的软件种类结 合到完整的学习环境中。不久的将来,学生会发现某一学科的多媒体参考书,或是 可以与其它学生在网络上互相交流娱乐,这些教育游戏将有助于他们深刻理解所学 的知识。 第五节打斗游戏设计问题 打斗游戏设计包括几个概念,如:角色创建、无故技法、暴力等。 1.角色创建 在打斗游戏中最动人的是使用了真实的、令人信服的角色特征。例如在真人快打中, 开发者在创建每一个具有不同体力、智力和个性的敌手时表现出令人难以置信聪明 才智。尽管在游戏中角色的特征被壮丽的图像和动画掩盖了,作为设计者仍然要仔 细地开发设计出好的角色。 2.无故技法 打斗游戏的一个关键设计是“无敌技法”,有时也被称为“致命技法”或者“最终技法”。 每个角色都具有打、踢、摔和跳等特别动作,他们也具有各自不同的几个特殊攻击 手段。例如,当游戏者依次快速按下跳起、左移和右移键时,角色就会跳到空中然 后像一阵旋风旋转着向对手进攻,只要与对手的距离足够远,每个游戏都有很多这 样的按键组合! 致命技法与打、踢、跳等待别动作之间的不同之处在于:致命技法是不公开。在游 戏手册上是没有的。致命技法是不会轻易被发现的,一般需要玩几个星期甚至是几 个月才能发现。致命技法也是游戏吸引人的一个部分。 当设计您自己的游戏时,记住这些技巧。您要让您的游戏者对您的游戏感兴趣,并 且能吸引他们。如果游戏者掌握角色的特别动作和致命技法或秘技时,他们就会玩 上瘾。但是,如果游戏太简单,游戏者很快就学会使用致命技法赢得胜利时,游戏 对他们就失去吸引力。 3.暴力 不用说,打斗游戏中的主要成分是暴力。暴力是必需的吗?对于很多国家来说,这 是个问题。不过如果您不在乎将您的游戏列为“少儿不宜”就没什么可怕的了。 4,持续的创造力 打斗游戏的生存依赖于开发者持续的创造力。大部分游戏在故事发展中都很少有自 己的创造,全都是一对一的打斗场景。也许您会创造出更好的打斗游戏,并使它提 高到一个新的阶段。 未来展望 开始我就说过,运动捕捉和 3D技术会给打斗游戏带来新的活力;没错,现在正是 这样。未来的打斗游戏将在以下几个方面发展: 1.联机对手 计算机时代的虚拟打斗已经开始了。多人联机对打游戏将是打斗游戏的新发展。联 机版本给游戏老一试身手的机会。打斗游戏会是众多联机游戏中最流行的一种。 2。身临其境的虚拟现实 打斗游戏的一个主要趋势是倾向于允许游戏者进入神奇的虚拟现实模拟世界。确 实,未来的所有游戏都将在虚拟现实环境中获得自己的新生,而打斗游戏尤其令人 振奋。 3.集成更大的游戏系统 在 1995 年举办的计算机游戏开发者会议中,我参加了一个关于 RPG 前途的座谈 会。会议的一个议题是围绕在传统打斗游戏的战斗系统中制作一个 RPG 游戏。虽 然大多数 RPG 迷不太喜欢动作游戏,但这个概念表明打斗游戏将被集成为复杂的 线索故事并成为更大的游戏的寻一个组成部分的可能性。 第六节主管类游戏 主管类游戏设计包括几个概念如建模和模拟。这些概念并非是某个具体主管类游戏 特有的,而是这类游戏的基本要素。主管类游戏是模拟游戏的最基本形式。“模拟 游戏?”您会问“是不是类似飞行模拟的游戏?”是的,但那些游戏模拟的只是军事 或非军事的硬件设备,而不是像主管类游戏模拟一个就像人类社会或蚂蚁社会的实 体系统。 主管类游戏很难建立一个与现实完全相同的模型。设计昔花费大量的时间和情力用 于研究社会,例如,人们进入或离开城市的原因,等等。这些努力没有白费,您得 到的是一个好玩而且真实的游戏。但不要为设计者而悲伤,因为在一个主管游戏中, 设计者才是最终的上帝! 未来展望 未来是非常明朗的。更复杂的人工智能技术和建模技术能让设计者创建更具智能的 系统,这将导致更真实的游戏,毫无疑问,这类游戏还将进入网络。 第七节联机游戏 联机游戏中不需要人工智能了。这对吗?不对。事实上在许多多人游戏中人工游戏 者变得更加重要了。原因很简单,因为您的游戏具有多人游戏功能,但并不意味着 多个游戏者一直登录在线。例如在一个多人赛车游戏中,如果只有一个游戏者参加 游戏,您就需要控制其它的赛车以提供一个真实的游戏环境。 另外,如果一个游戏者中途退出,他的赛车也不能立刻就消失。我的结论是在一个 大规模的在线游戏中,您仍然需要人工角色在您创造的世界中参与游戏。 未来展望 可能比游戏发展更快的是支持游戏的联机服务设备和 Internet。 多人游戏还只是刚刚开始。新的创意不断出现,但新的问题也很多。如果您想开发 此类游戏,您就要有冒大风险的准备。 第八节 RFG设计问题 RPG 设计包括以下几个概念:保持连续和谐、与众不同的世界和 NPC。 1.保持连续和谐 RPG 是非常值得开发的产品,因为它们能产生续篇,当然首先您的第一个游戏应 获得成功!如果您用一个好故事创建了一个动态的世界,那么您可以不断地用新的 创意来更新这个世界,而游戏者对您的游戏也会很喜欢。看看主要的 RPG 游戏就 会发现它们都是成系列的。 当您创建您最初的游戏时,考虑加点将来的新版本中可以扩展的创意。例如,您可 以介绍一个在稍后的游戏中作为对手出现的龟色。 您也应该仔细考虑您创建的世界。您要让游戏者投入到这个世界中,并且要让他们 喜欢这里发生的每一件事情。下一个主题就是设计这样的世界。· 2.与众不同的世界 因为计算机探险游戏已经越来越复杂化了,创建世界就变得更加重要了。在创建世 界过程叭设计者追寻创建一个完整的运转着的社会,其中包括全部角色和他们那些 由情节决定的“命运”。基本思想是将故事放到一个环境中,而不是让环境适应故事。 另外,建造的世界要能产生小小的难题。尽管世界是假想的,但它必需真实。游戏 老会这样评价您的世界: 这个假想的新世界是否与众不同而又很有趣? 这个世界中的系统是否运转得很真实?(例如,当我将货物卖到一个城镇时,货物 的价格 是否会降下来?) 3.NPC 交互和交流 设计 RPG 的最大问题是非游戏者角色(NPC-Non-player Character)的交互作 用。简单说就是,如果 RPG 角色不能产生动态交互作用,那么游戏的深度就会降 低。 解决的办法是很显然的:在内部仿真引擎中增加灵活性并在游戏中加入人工智能功 能。目的是通过他们前面的动作将 NPC 与游戏者联系起来,使他们之间的交互更 多,让游戏者通过自然的交互投入到虚拟的世界中。对于像天气和佩带武器这方面, 开发者也应该考虑一下。开发者同样要力争创造出更好的 RPG 对话,要使角色具 有记忆能力,不同的对话要与不同的游戏内容相适应。 当设计您的 RPG 时,应仔细考虑如何实现逐步增加难度级别和对话交互。 4.战斗和战争 在几乎每一类的 RPG 中最关键的要素都是游戏中的战斗类型。战斗是划分 RPG 和 探险 游戏的主要差别。尽管大多数探险游戏都有一定次序的战斗,但战斗并不是重复出 现的成分。 RPG 游戏中的战斗部分就像一个小型战争游戏。游戏者控制他们的角色,用武器 或魔法武装他们。然后,在各种选择和统计预测的基础上,战斗开始了。活下来的 角色通常战斗力得到了提高。 更具体的还有,设计者必需决定让游戏者确定战斗的策略还是把战略部署留给程序 去做。 某些开发者设计出允许游戏者在任何时候重新控制战斗的游戏。另一个实现是给游 戏者许多战术选项以供挑选,如移动、攻击类型等等。还有一些游戏提供了“自动” 方式,它允许游戏者观看计算机控制他(或她)的军队的战略部署。自动方式的目 的有两个:开始游戏者通过观看可以学习如何战斗;而具有绝对优势的游戏者可以 让计算机操作战斗,以减少自己控制的工作,并尽快结束战斗。” 未来展望 RPG 现在看来处于一种不稳定的状态,但是数量可能比其它任何一种类型的游戏 都多。 第九节模拟游戏 模拟游戏在飞行模拟器的带动下曾经是最强有力的游戏之一。模拟常常与某种类型 的军 事硬件有联系,例如 F16 战斗机或坦克、潜艇、直升飞机模拟器。另外,除了这 些战争工具模拟器,还有驾驶模拟器与核动力飞机模拟器。 我们只讨论那些基于“交通工具”轨迹的模拟器,包括飞行和其它军事模拟器、驾驶 游戏和宇宙飞船战斗游戏。 1.模拟游戏设计问题 1)模拟对象 现有模拟游戏很容易分为两类,如下: A。军事的与非军事的交通工具模拟器 主要的模拟器产品集中在军事交通工具模拟器上。像 Microprose 和 spectrum Holy-byte 等公司几乎整个公司的力量都投在飞机、坦克和战舰等军事模拟器上。 B 字航模拟器 大量的模拟器产品,特别是那些为虚构世界设计的产品,与宇宙空间有些联系。太 空 是最后的未知领域,可以也产生了令人难以置信的模拟器游戏。 2.似乎真实的细节 游戏设计者要充分考虑细节。有些游戏需要的细节很少,但有些需要的就多。许多 模拟器 设计者在设计的全过程中都能很好地把握这个问题。但是也有些设计者感到模拟的 世界意味着产品必须尽可能与现实接近。然而,有时为真实世界奋斗的结果会使一 个好游戏变成烦人的或很难玩的东西。 例如,您想设计一个包括起飞前检查等各种细节的飞行模拟器,这真的对整个游戏 有好处 吗?这对局部精确度也许有好处,但是它损害了整体的操作特性。 最后,您和测试者必须决定多少细节是必要的,但是无论细节多么真实,无用的细 节必须 去除。别忘记,人们玩游戏就是为了从现实世界的琐碎中解脱出来。 3.令人难受的传道区 许多流行的模拟游戏在游戏中设计一些“传道区”(以讲解或说明情况),除了赞许, 此外并没听到别的什么。当您设计您的模拟器时,考虑在您的产品上设计各种各样 的传道区,并把最主要的故事线索结合到您的产品中。 4.引擎 模拟游戏也经常用复杂的 3D 图像引擎来开发。许多游戏具有续集或是使用相同的 引擎 来开发其它的模拟类型游戏是不奇怪的。生产一系列优秀模拟优秀的关键是要有一 个优秀的 3D 图像引擎和发现一个潜在的模拟游戏领域来应用那些技术。 5.寻找新的机会 在游戏世界中模拟游戏是一个对图像很敏感的类型。因此,开发者通过提高图像质 量就可 以很“容易”并有效地提高这些游戏的质量。您只要看看市场上的大量飞行模拟器就 知道了。但是,更流行的摸拟器是把精力集中在新的创意上的。 第十节 战争类游戏 战争类游戏可以被分为即时战略游戏和回合制游戏两类。 在即时战略游戏中主要要考虑的问题是人工智能和力量均衡的问题。对于人工智 能,主要是电脑对手的智能和双方战斗单位的智能的设计问题。大部分游戏者是不 会接受一个太蠢的只会作弊的对手——人工智能设计太差,也不会有太多耐性去挑 战一个功防之间简直毫无破绽的“专业级”的对手——太专业的人工智能设计,也不 会容忍自己的手下只会走直线——简陋的路径搜索算法。所以对人工智能部分应仔 细的考虑和测试。在力量均衡问题上主要是考虑各种高级武器和基本武器间性能的 差异、各种高级武器的造价、建造时间、出现时间及条件的设置和个民族所特有的 武器的特点的构思。 相对于即时战略游戏,回合制游戏在设计时仅仅可以不考虑路径算法的问题,其他 的问题是基本相似的。 第十一节 总结 上面谈到的游戏类型仅仅是从技术上而言的,在现在的大部分游戏中实际上是包含 了这些类型之中的一种以上的特性,或与其相似。比如说文明和七个王国就包含了 主管类游戏和即时战略游戏的特性,现在大部分的即时战略游戏都有连线作战的能 力等等。所以您在设及您的游戏是应考虑所有可加入的特性。 好我们的介绍到此就算是结束了,但对于您来说,还有很长的路要走。游戏中可以 说包含了整个计算机技术的方方面面,作为一个游戏开发的程序员来说您还需要了 解很多的知识掌握很多的技巧。 俗话说“师傅领进们,修行靠个人”,我们就只能言尽于此了,剩下的就靠您自己了。 祝您开发出世界级的游戏,成为振兴中国游戏业的功臣。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 5 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档