游戏里%tlps%if1507是什么意思思

查看: 13331|回复: 16
大家说的PS游戏是什么意思?
猥币101158
在线时间67 小时
阅读权限30
帖子猥币巴士票
猥乎其微 Lv.3, 积分 313, 距离下一级还需 187 积分
什么?模拟器吗
尼玛,万恶的验证码
猥币116920
在线时间921 小时
阅读权限80
帖子猥币巴士票
猥风八面 Lv.8, 积分 25018, 距离下一级还需 4982 积分
PS1& && && && && &PS2之前的家用机
猥币624012
在线时间868 小时
阅读权限100
帖子猥币巴士票
上旋高手2 PC版 联机群:
PC版可以免正版联机哦!
猥币624012
在线时间868 小时
阅读权限100
帖子猥币巴士票
上旋高手2 PC版 联机群:
PC版可以免正版联机哦!
猥币205997
在线时间652 小时
阅读权限90
帖子猥币巴士票
猥震海内 Lv.9, 积分 42258, 距离下一级还需 17742 积分
猥币212232
在线时间99 小时
阅读权限60
帖子猥币巴士票
猥人师表 Lv.6, 积分 2545, 距离下一级还需 2455 积分
猥币101158
在线时间67 小时
阅读权限30
帖子猥币巴士票
猥乎其微 Lv.3, 积分 313, 距离下一级还需 187 积分
楼上几位在说什么?
尼玛,万恶的验证码
猥币2164500
在线时间1872 小时
头像被屏蔽
帖子猥币巴士票
提示: 作者被禁止或删除 内容自动屏蔽
猥币2243780
在线时间2829 小时
阅读权限100
帖子猥币巴士票
12.。。LS碉堡了= =&&LZ只是掌机党而已
猥币101158
在线时间67 小时
阅读权限30
帖子猥币巴士票
猥乎其微 Lv.3, 积分 313, 距离下一级还需 187 积分
vivi 发表于
12.。。LS碉堡了= =&&LZ只是掌机党而已
在说什么?听不懂。。。什么90 00 12的?
尼玛,万恶的验证码
水元素【普通成就】
水、H2O、みず、water,额...总之怎么叫都行了
(累计发帖1000即可解锁)
常客【普通成就】
《我的业余生活》从前有一个论坛叫TGBUS…(在线时间超过1000小时)
铜质奖杯【普通成就】
好歹有个名次,哈哈哈哈哈(曾在论坛任意板块内组织的活动中获得第三名)
银质奖杯【稀有成就】
差点就是第一名了! --不甘心的菲尼如是说(曾在论坛任意板块内组织的活动中获得第二名)
成就猎人Rank.D【普通成就】
你已经踏上了成就收集的不归路,5个成就怎么可能满足你的需求,继续努力吧! (获得累计超过5个成就--不包括负面成就)
我的朋友很多【普通成就】
朋友不只是踏脚石,偶尔也可以当坐垫的哦(好友数超过100名)
水精灵【稀有成就】
我很喜欢水,因为这就是我存在的意义...(累计发帖10000即可解锁)
浮云爱好者【普通成就】
一朵,两朵,三朵,呵呵呵… (累计获得1000朵浮云)
Powered by当前位置: >>
C语言[游戏]
游戏编程指南彭博 著 《游戏编程指南》目录第一章 游戏编程必需的 C++基础知识1.1 1.2 1.3 1.4 1.5 1.6 1.7 概述 入门知识 控制语句 函数 指针、数组与字符串 预编译指令 多文件程序的结构第二章 游戏编程必需的 C++高级知识2.1 2.2 2.3 2.4 2.5 2.6 类的定义和使用 构造函数 类的静态成员 运算符重载 类的继承 虚函数与抽象类第三章 游戏编程必需的 Windows 编程知识3.1 游戏编程中基本的 Windows 程序 3.2 Windows 程序中的 WinMain 函数 3.3 Windows 程序中的消息处理函数第四章 DirectDraw 编程基础4.1 DirectDraw 使用前的准备工作 4.2 DirectDraw 的初始化 4.3 DirectDraw 对象的释放第五章 DirectDraw 编程实战5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 调入图象 透明色 图象传送 图象缩放 后台缓存和换页 页面的丢失与恢复 页面的填色 输出文字第六章 DirectDraw 动画制作6.1 定时器 6.2 播放声音 6.3 动画例子 第七章 DirectDraw 编程技巧7.1 7.2 7.3 7.4 页面的锁定 程序的提速 排除错误的技巧 C++程序优化技巧第八章 内嵌汇编8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8 8.9 内嵌汇编简介 基本指令 算术指令 逻辑与移位指令 比较、测试、转移与循环指令 MMX 指令集之基本指令 MMX 指令集之算术与比较指令 MMX 指令集之逻辑与移位指令 MMX 指令集之格式调整指令第九章 RPG 制作方法9.1 9.2 9.3 9.4 RPG 的结构 RPG 制作方法 常用 C++函数 常用算法第十章 RPG 实例10.1 全部程序的头文件10.2 主程序 10.3 HUMAN 类 10.4 MAP 类 10.5 BLOCK 类 10.6 BULLET 类 10.7 DirectDraw 相关函数 10.8 初始化程序 10.9 定时器程序 10.10 屏幕刷新程序 10.11 其他函数 10.12 函数定义 10.13 全局变量定义 10.14 常量定义附录 一、 Windows 常见消息列表 二、 Windows 常用虚拟键列表 三、 DirectDraw 函数返回值列表 四、 游戏编程常用网址 内容提要本书在吸收国内外游戏编程新技术的基础上, 结合本人多年游戏编程的经验和体会, 不 仅由浅入深地介绍了游戏制作所必须的 C++语言、Windows 编程、DirectDraw 编程、内嵌 汇编等编程知识、而且还阐述了 DirectDraw 编程技巧与 RPG 的制作方法。为方便读者更 好地理解本书的内容和节省编程时间, 本书的最后一章还提供了供读者参考的游戏实例及附 录。本书语言简明扼要,通俗易懂,内容新颖、充实,例子生动、应用性强,读者阅后能编 出自己所喜爱的游戏。 本书适合于有一定编程基础的大中学生、 社会上的游戏爱好者、 游戏编程人员及其他有 兴趣的电脑工作者阅读。前言西方电脑游戏编程的历史可以追溯到 1972 年,那一年一个叫 Will Crowther 的人编写 了一段简单的 FORTRAN 程序,并在这个程序里设计了一张分布着陷阱的地图,游戏者必 须寻找路径避开陷阱。这个程序在后来被认为是最早的电脑游戏。自那以后,编制电脑游戏 就开始在程序员之间流行起来, 不过大多数还是娱己的贵族游戏, 因为当时接触计算机的还 只有科技精英们。将近三十年过去了,从“挖金子” 、 “波斯王子”到今天的“母巢之战” 、 “黑与白” ,电脑游戏在西方早已成为一种重要的娱乐,北美的游戏产业的收入已经与整个 电影产业的收入相等。 相比之下,中国大陆的游戏产业起步较晚,直到 1997 年尚洋的“血狮”问世,才算真 正走上市场化的道路,可惜的是这第一次尝试的结果不尽如人意。四年已经过去了,国内的 游戏产业还尚未形成气候。 造成这种状况的原因固然是多方面的, 但游戏编程人员缺乏且水 平与国外有一定差距也是重要的原因之一。 而这又与国内市场上缺乏游戏编程一类的书有着 密切的关联。鉴于此,本人撰写了此书。 本书共分十章:第一章 游戏编程必需的 C++基础知识;第二章 游戏编程必需的 C++ 高级知识;第三章 游戏编程必需的 Windows 编程知识;第四章 DirectDraw 编程基础;第 五章 DirectDraw 编程实战;第六章 DirectDraw 动画制作;第七章 DirectDraw 编程技巧; 第八章 内嵌汇编;第九章 RPG 制作方法;第十章 RPG 实例。 全书紧密围绕着游戏编程这一主题,由浅入深地介绍了游戏编程的知识、方法与技巧。 在编写过程中,作者力图遵循简明实用的原则,旨在让读者尽快掌握游戏编程,编出自己所 喜爱的游戏。本书语言简明扼要,内容充实,例子贴近游戏编程且实用性强,适合有一点编 程基础的广大有志于游戏编程的爱好者阅读。 由于作者水平有限,难免有不足之处,恳请广大读者指教。 第一章 游戏编程必需的 C++基础知识C 语言是公认的游戏编程首选语言。 用它编制的程序不仅执行速度快, 还可以充分地使 用硬件的各种资源。而 C++语言是对 C 语言的完善和升华,它的最大特点是提供了“类” , 成为了面向对象的语言。关于此,我们会在第二章详细介绍,本章将先介绍一些游戏编程所 必需的 C++基础知识。最后需要提醒大家的是,在学习本章时最好边学边实践,自己试着 写写 C++程序。1.1 概述在切入 C++语言之前,我们有必要简略地介绍一下 Visual C++的基本使用方法。首先 当然是安装 VC++,装的时候可以不选与 MFC 有关的选项。然后启动 VC++,在“文件”菜单 中选择“新建”,并在出现的窗口选择“工程”一栏中的“Win32 Console Application”,表示我们 需要编制 DOS 窗口下的程序。 (如果你想编 WINDOWS 下的程序,如第三章的程序,请选 择“Win32 Application”) ,定好工程的名字(比如说“test”)和位置(比如说“C:\test”) ,OK, 在弹出的对话框内选“An Empty project”,一路确定即可。现在我们已经建好了一个新工程。 在屏幕的左边,我们可以看到出现了“ClassView”和“FileView”两栏,里面列出了工程中 的类和文件。 接下去我们需要建立一个源文件:还是在“文件”菜单中按“新建”,出现窗口后这次要选 择“文件”栏中的“C++ Source File”,起一个名字(比如“main”) ,按确定即可。这时屏幕右边 出现了一片空白区域,左边的“Source File”展开后下面出现了“main.cpp”。我们可以开始编 程了! 在“编辑”菜单中,有“放置可运行设置”一栏,使用它可以更改程序为 Debug 阶段或 Release 阶段。 在一般情况下, 建议大家选择 Release, 可以减少文件大小并增加运行速度; 而在调试程序时,必须选择 Debug。在默认的情况下,编译好的程序会相应的放在工程目 录下的“Debug”或“Release”子目录内。 OK,让我们看一个简单的 C++程序: /*------------------------------------------------First C++ Program --------------------------------------------------*/ #include &iostream.h& //现在你只需知道要使用输入输出语句就必须写这行。 //这一行结尾不用加分号因为它不是真正的 C++语句。 //在 1.6 节将会对此做出解释。 int a=5; //定义变量 a, 同时顺便赋 5 给它。C++中变量都需先定义后使用。 //int 说明了 a 的数据类型,代表整数。 int square(int x); //声明函数 square, 参数为 int 类型, 即整数。 返回值也为 int 类型。 //C++中的函数都需要先声明后给出定义。 int square(int x) //函数真正定义。 { return x* //返回 x*x,可以在一个语句中的间隔位置插入回车将其分成几行。 } void main( ) //主函数,每个 DOS 下的 C++程序都需要它 { cout&&“Input b:”; cin&&b; cout&&“b=”&&b&& //定义变量 b。C++中变量定义的位置是比较随意的。 //输出“Input b:”,箭头的方向很直观。 //输入 b, 注意箭头方向的更改。 //依次输出“b=”、b 的值并换行,endl 代表换行。cout&&“a+b=”&&a+b; cout&&“b*b=”&&square(b); } 从这个程序中,我们可以看到 C++程序的一些特点: (1) 区分大小写,关键字全部是小写的。 (2) 每一行语句都需要用“;”结尾。 (3) 用“{”和“}”括起的句被称为块语句,形式上被认为是一个语句(就像 PASCAL 中的 begin 和 end)。 (4) “//”至行尾为注释,“/*”至“*/”中全为注释,它们不会被编译。 (5) 主体是由一个个函数所构成的。在 1.4 节将会详细地介绍函数。1.2 入门知识1.2.1 数据 对于十进制数的表示,C++与其它语言一致,同样可以使用科学记数法,如 3.145e-4。 在 C++中还可以直接表示十六进制数,只要在前面加上“0x”即可。如 0x23。如果要表示的 是负十六进制数,可以直接在“0x”前加上负号。 下面列出了 C++中的基本数据类型: bool(1 位) / char(字符或 8 位整数) / short(16 位整数) / int(16 位或 32 位整数) / long(32 位整数) / float(32 位浮点数) / double(64 位浮点数) / long double (80 位浮点数) bool 类型用 true 和 false 代表真与假。 某一 char 类型的变量如果等于‘a’(注意 C++中字符用单引号,字符串用双引号),则它 又等于 a 的 ASCII 码,即 97。 int 类型在 DOS 下一般为 16 位,在 WINDOWS 下一般为 32 位,如果想保险一点自己 试试就知道了。 在整数数据类型前可加上“unsigned”表示为无符号数,数的范围可增大一倍。比如说 char 是-128 到 127,unsigned char 则为 0 到 255。 使用 sizeof( )可以得到任何对象占用的字节数, 例如如果有一个 char 类型的变量 a,则 sizeof(a)会返回 1。 有的类型之间是可以自动转换的, 如可以把一个 float 类型的变量的值赋给一个 int 类型 的变量,小数点后的部分将会被自动截掉。如果不放心可使用强制类型转换,形式为(目标 类型)变量名。 比如说如果有一个 char 类型的变量 c 值为‘b’, 直接输出 c 会得到‘b’这个字符 但输出(int)c 会得到’b’的 ASCII 码。强制类型转换不会改变变量的值,它只是返回转换后的 值。字符串和整数之间不能用强制类型转换实现转换。 1.2.2 变量 C++中的变量几乎可在任何地方处定义,而且可以同时定义多个变量,如 int a,b;。但 每一个变量只在最紧挨它的一对{和}符号内起作用,只有在所有函数之外定义的变量才为全 局变量, 即在整个程序中有效。 这里建议大家尽量少用全局变量, 其原因将在 1.3.3 节讲述。 所有变量定义的最前面可以加上修饰符“const”表示它是常量,不能在程序中改变它的 值;还可加上修饰符“static”表示它是静态变量,这个要举一个例子以方便说明:比如说在 某一个函数内有这样一条定义:static int count=0; ,那么程序执行前就会为 count 这个变 量开辟一块固定的空间并把 count 的初值设为 0。以后每次执行这个函数时,程序不会象普 通变量那样重新为它分配空间, 也就是不会改变它的位置和数值。 这样只要在函数中加一句 count=count+1 即可统计这个函数执行了多少次。 1.2.3 操作符与表达式 接着要说的就是 C++中的操作符和表达式,与其它语言相同的就不在此赘述,讲讲一 些与其它语言不同的内容: %为取余数,比如说 20%3=2。 在逻辑表达式中,用==表示相等,!=表示不等,比如说(4==5)为 FALSE;大于等于用 &=表示,小于等于则是&=。&&表示逻辑与,||表示逻辑或,!表示逻辑非。例如如果 a=8, 则( (a!=9) && ( (a==3) || (a==6) ) )为 false。 &&(左移)和&&(右移)非常好用,作用是把这个数的二进制形式向左或右移位(cin 和 cout 中的&&和&&被使用了运算符重载,具体可参阅第二章),举两个例子也许会好说明些: 18(二进制形式为 0010010)&&2 得到 72(二进制形式为 (二进制形式为 1001101)&&3 得到 9(二进制形式为 0001001) 我们可以看到,左移和右移可以代替乘或除 2 的 n 次方的作用,而且这样做可以节省 大量 CPU 运算时间。在程序优化中这一种方法是十分重要的, 例如 a*9 可用(a&&3)+a 代替 (注意,“+”运算比“&&”运算优先)。 C++ 还提供了算术与 & 、算术或 | 、算术非 ~ ,算术异或 ^ 等重要的二进制运算。比如 25(101)等于 8(01000)。 这些运算都是逐位对二进制数进行的。 0&0=0, 0&1=0, 1&0=0, 1&1=1; 0|0=0, 0|1=1, 1|0=1, 1|1=1; ~0=1, ~1=0; 0^0=0, 0^1=1, 1^0=1, 1^1=0。算 术与可以部分代替求余的工作。 C++的特色之一是++/--操作符,即自增/自减。a=7; a++; 则 a 变为 8。(C++语言岂不 是成了 D 语言??) 注意 a++和++a 不同,++a 是先自增后给值,a++是先给值后自增。若 a=12,(a++)+5 为 17,(++a)+5 却为 18,不过 a 后来都变成了 8。 最后要说的是一个很爽的操作符,就是“?:”,它可以在一定程度上代替 if 语句的作用, 因为“A?B:C”等价于“if A then 返回 B else 返回 C”。举一个例子,(a&b)?a:b 可返回 a 和 b 中的较大者。 值得注意的是由于 C++的操作符众多,所以运算先后次序较复杂,如果出现了出人意 料的结果请试着加几个括号。 在表达式方面,C++基本与其它语言相同。只是 C++为了简化程序,提供了便利表达 式, “ 左值 操作符 = 表达式 ” 等价于 “ 左值 = 左值 操作符 表达式 ” 。例如 a*=6+b 等价于 a=a*(6+b),c+=8 等价于 c=c+8。 1.3 控制语句C++中的控制语句格式简洁且功能强大,充分证明了它是程序员的语言。 1.3.1 判断和跳转语句 C++中的判断语句格式如下: if (条件) 真时执行语句; else 假时执行语句; (省去了 then) 例如: if (a&=9) a++; else a--; 臭名昭著的跳转语句(不过有时候你还是不得不用)则是这样的: 标号:语句;(一般来说标号推荐用“_”开头) goto 标号; 举个例子方便大家理解: #include &iostream.h& void main( ) { int target=245; cout&&“Guess Game”&& cout&&“Please guess:”; _input: cin&&a; if (a&target) { cout&&“a is too big!”&& cout&&“Input again:”; goto _ } else if (a!=target) { cout&&“a is too small!”&& cout&&“Input again:”; goto _ } else cout&&“You are right!”;}1.3.2 选择语句 C++中的选择语句很灵活,我们先看看与其它高级语言相似的形式: switch (变量) { case 常量/常数 1: 语句;//注意,可有多个语句且不需用{ }括起,不过其中不能定义变量。 //为什么要加这一句呢?下面会介绍。 case 常量/常数 2: 语句; …… case 常量/常数 n: 语句; default: //如所有条件都不满足则执行这里的语句。 语句;//这后面就没有必要加了。 } break 的作用其实是防止继续执行后面的语句,试试下面的程序: #include &iostream.h& const aaa=5; void main( ) { cin&&a; switch(a) { case 0: cout&&endl&&“It’s 0!”; case 3: cout&&endl&&“It’s 3!”; case aaa: cout&&endl&&“It’s aaa!”; default: cout&&endl&&“I don't know!”; }}按照一般人的想法,当你输入 0、2、3、5 时会分别得到“It’s 0!”、 “I don't know!”、 “It’s 3!”、 “It’s aaa!”,不过你可以试试结果是否真的是这样。试完后,你可以加上 break 再看看 结果又将是怎样。 1.3.3 循环语句 先介绍 while 循环语句,共有两种形式:第一种是 while (条件) 语句,意义为先判断条 件是否满足, 如果满足则执行语句(否则退出循环), 然后重复这个过程。 第二种形式是 do 语 句 while (条件),意义为先执行语句再判断条件,如果条件成立则继续执行语句(不成立就 退出循环),这个过程也会不断重复下去。 然后就是 C++最强大的 for 循环,它的形式如下: for (语句 1;条件;语句 2) 语句 3 (其中任何一部分都可省略) 看上去好象很古怪,其实它就等价于这样: 语句 1; while (条件) { 语句 2; 语句 3; } 比如 for (i=1;i&=100;i++) cout&&i&& 结果将会是输出 1 到 100 的数。 又比如 for (cin&&i; i&=0; ;) cin&&i;将会不断输入 i 至 i 大于 0 为止(省略了语句 2)。 for (;1;);将会陷入死循环。 在循环语句中可顺便定义变量,如 for (int i=1;i&=100;i++) cout&&i&& 有时我们需在循环中途跳至循环外, 此时 break 又可以派上用场了。 有时又需要在循环 中途跳至下一次循环,continue 可以帮你这个忙。1.4 函数C++中的函数是这样定义的: 返回值数据类型 函数名(参数表) { 语句; } 例如: int Get_X_2(int x) { return x*x; } 当返回值数据类型为 void 时表示无返回值。返回某值是通过在函数内使用 return XYZ; 实现的,该语句会返回 XYZ 并立刻退出该函数。 C++的函数都需在 main 函数开始前声明(除 main 函数),声明格式为: 返回值数据类型 函数名(参数表); (注意有一个分号) 参数表中在不引起歧义的情况下可有缺省值,例如 void xyz(int a, int b=0);,则 xyz(12) 等价于 xyz(12,0)。 在函数最前加上“inline”说明其为内联函数可提高一点速度,但增大了文件的大小。 就象其它语言一样,C++中的函数可以递归调用(自己调用自己)。它还有一个很好的特 点就是可以重载,例如如果在上面那个函数的基础上又定义了这样一个函数: float Get_X_2(float x) { return x*x*x; } 那么如果 a 为一个 float 类型变量,值为 4,Get_X_2(a)会返回 64。但若 a 为 int 类型, 值也为 4,Get_X_2(a)会返回 16。 有人想编一个交换 a 和 b 的函数,于是他这样写: void swap(int a, int b) { int t=a; a=b; b=t; cout&&“a=”&&a&&“ b=”&&b; } 在函数中的输出是正常的,但运行完后 a 和 b 并没有被交换,你知道这是为什么吗? 其实原因在于: 在默认状况下函数的参数都为形式参数, 函数所得到的只是参数的数值而不 是参数,所以这段程序并不能真正地改变这两个变量的值。正确的解决方案是使用实参,即 在参数名前加上“&” (当你看完 1.5.1 节后你将会对此有更深的理解) ,就象下面的函数那 样: void swap(int &a, int &b) { int t=a; a=b; b=t; } 下面举一个使用了函数的程序例子, 这个程序的作用是计算一些几何体的体积 (比较无 聊,不过挺经典) : #include &iostream.h& float pi=3.14159; float s_circle(float r); float v_cylinder(float r, float h); float v_cone(float r, float h); float v_all(float stop, float smiddle, float sbottom,float h); float v_all(float stop, float smiddle, float sbottom,float h) { return (stop+4*smiddle+sbottom)*h/6; } float v_cone(float r, float h) { return s_circle(r)*h/3; } float v_cylinder(float r, float h) { return s_circle(r)*h; } float s_circle(float r) { return pi*r*r; } void main( ) { float r,h; float st,sm, cout&&&Calculate some geometric form's volume&&& cout&&endl&&&0. Cone&&& cout&&&1. Cylinder&&& cout&&&2. Customize...&&& cin&& cout&& switch(choice) { case 0: cout&&&r=?&; cin&&r; cout&&&h=?&; cin&&h; cout&&endl&&&V=&&&v_cone(r,h); case 1: cout&&&r=?&; cin&&r; cout&&&h=?&; cin&&h; cout&&endl&&&V=&&&v_cylinder(r,h); case 2: cout&&&S of top surface=?&; cin&& cout&&&S of middle surface=?&; cin&& cout&&&S of bottom surface=?&; cin&& cout&&&h=?&; cin&&h; cout&&endl&&&V=&&&v_all(st,sm,sb,h); } }1.5 指针、数组与字符串 1.5.1 指针 指针是 C++强大功能的体现,也是一个令人又爱又恨的东西。所谓指针,顾名思义, 就是一个保存着某变量在内存中的位置的变量。指针的定义是这样的:数据类型 *指针名; 其中数据类型这一项为指针所指向的变量的类型。 这里又要提到两个操作符,即&和*。&的作用是取变量的地址。而*则是&的逆运算,即 取位于该地址的变量的值。所以我们用 p=&a 即可把 p 指向 a,执行后*p 就完全等价于 a, 比如用*p=(*p)+2 可将 a 的值加上 2。 需注意的是,如果没有把一个有效的地址给指针,却直接修改指针所指向的内容,可能 会出现不可预料的结果。有时程序死机就是由此引起的。 指针有什么用呢?第一个用处是可以动态分配大量内存。我们知道 DOS 下很多语言对 数组的大小有很严格的限制,但 C++却可以开辟非常大的数组,而且可以用完就释放内存, 这就是指针的功劳。具体会在介绍数组时介绍。 C++的下一代 C#去掉了指针,理由是它很危险,这是事实,但我觉得去掉指针还是得 不偿失的。 1.5.2 数组 C++中的数组和指针有着千丝万缕的联系。象其它语言一样,C++可以直接定义数组, 如 int a[100];即可定义一个由 100 个 char 类型变量组成的数组; 也可以在定义时顺便赋值, 例如 char b[5]={‘a’, ‘b’, ‘c’, ‘f’, ‘z’};;还可以定义高维数组,如 char c[200][50];(相当于 BASIC 中的 c(200, 50))。使用数组时要注意几点: (1)数组的下标是从 0 开始的,上面所定义的 a 数组的下标范围为 0 到 99,刚好是 100 个元素。 (2)数组越界不会有任何提示。 (3)数组需要你自己清零。 如果你使用直接定义的方法产生数组,还需注意下面两点: (1)数组的大小必须是常数或常量,象 int b[a];这样是错误的。 (2)你得到的实际上是一个特殊的与“数组名”同名的指针。 第二点也许有些费解,你可以试试这段程序就会明白: #include &iostream.h& void main( ) { int abc[1000]; abc[0]=987; cout&&*abc&& *abc=787; cout&&abc[0];}我们还可以直接使用指针创建数组。比如说我们要临时分配一块空间,存储 100000 个 int 类型数据,那么就可以这样做:int *p; p=new int[100000];(你可以将其合并为 int *p=new int[100000], 在这里又出现了一个新操作符“new”) , 则系统会在内存找到一块足够 大的空闲空间,再将 p 指向这块空间的起始位置,以后就可以把 p 当成一个数组来使用了。 这种办法的第一个好处是用完这块内存后可以释放内存, 就象这样即可: (“delete” 也是操作符) ;第二个好处是可以动态定义数组,例如: cin&&a; int *p=new int[a]; 所以,建议大家多使用 new 来创建数组。 不过这两种 “指针” 都不好用(试试 p=&p[100]能实现把 p 指向 p[100]吗?可能会死机!), 最灵活的办法是把一个另外的指针指向数组的元素, 因为指针可以进行加减运算。 比如说如 果 p=a[0], 执行 p+=46; 即可使 p 指向 a[46], 再执行 p--;则 p 指向 a[45]。 看看下面的例子: #include &iostream.h& void main( ) { int *p,*q; p=new int[100000]; q=&p[0]; for (int i=0;i&100000;i++) *(q++)=0; //这样就可以清零,想一想! //而且比 p[i]=0;速度更快。 q=&p[1]; *q=128; //把 p[1]变成 128 cout&&p[1]; } 还要讲讲使用指针创建高维数组的方法, 因为此时要用到指针的指针(指针也要占内存, 也有自己的地址)!下面的一段程序演示了如何创建一个高维数组 p[40][60](很难懂,做好 心理准备! ) : int **p; //指向指针的指针! *p=new int *[40]; //执行完后 p 就是一个元素为指针的数组! //可以将这句与*p=new int[40];对照一下。 for (int i=0;i&40;i++) p[i]=new int[60]; //为 p 数组中的每一指针分配内存,将其也变为一个个数组 如果你弄懂了上面的程序,你就可以再玩点新花样:定义不对称数组。就象这样: int **p; *p=new int *[10]; for (int i=0;i&10;i++) p[i]=new int[i+1]; 1.5.3 字符串 C++中的字符串其实也是指针的一种,因为并没有一种基本数据类型是字符串,所谓字 符串实际是一个以“/0”(这叫做转义符,代表一个 ASCII 码为 0 的符号)作为结束标志的一个 字符指针(char *),或者说是一个字符数组。用过 BASIC 的人要注意 C++中的字符串并不能 相加或相减,这些操作一般是靠使用系统提供的字符串操作函数实现的,请参阅 9.3 节。1.6 预编译指令 现在该解释在第一个例子中#include &iostream.h&的意义了,其实这句是预编译指令。 预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。 常见的预编译指令有: (1)#include &xxx.xxx&指令 该指令指示编译器将 xxx.xxx 文件的全部内容插入此处。若用&&括起文件则在系统的 INCLUDE 目录中寻找文件,若用“ ”括起文件则在当前目录中寻找文件。一般来说,该文件 后缀名都为“h”或“hpp”,被称为头文件,其中主要内容为对常量的定义和对函数的声明。所 以我们#include &iostream.h&之后编译器会看到其中对输入输出函数的声明, 于是知道你要 使用这些函数, 就会将包含有输入输出函数定义的库文件与编译好的你的程序连接, 形成可 执行程序。 (2)#define 指令 该指令有三种用法,第一种是定义标识,标识有效范围为整个程序,形如#define XXX, 常与#if 配合使用; 第二种是定义常数, 如#define max_sprite 100, 则 max_sprite 代表 100; 第三种是定义“函数”, 如#define get_max(int a,int b) (a&b:a?b) 则以后使用 get_max(x,y) 可得到 x 和 y 中大者(这种“函数”的本质为直接替换) 。 (3)#if、#else 和#endif 指令 这些指令一般这样配合使用: #if defined(标识) //如果定义了标识 要执行的指令 #else 要执行的指令 #endif 在头文件中为了避免重复调用(比如说两个头文件互相包含对方),常采用这样的结构: #if !(defined XXX) //XXX 为一个在你的程序中唯一的标识符, //每个头文件的标识符都不应相同。 //起标识符的常见方法是若头文件名为“abc.h” //则标识为“abc_h” #define XXX 真正的内容,如函数声明之类 #endif1.7 多文件程序的结构记得以前我第一次使用 Visual C++编游戏的时候,由于当时对 C++还不是很熟,调试 了很久都没有成功。后来把程序 E-mail 给了一位高手叫他看看问题在哪里,过了几天他把 程 序 送 回 来时 已 经 可 以运 行 了 , 原来 他 在 我 的头 文 件 中 声明 变 量 的 语句 前 都 加 了一 个”extern”。这是什么意思呢?当时我还不清楚,因为很多书上并没有讲多文件的程序应该 怎么写。不过现在当你看完这一节时我想你就应该明白了。 首先我们来看看多文件程序成为可执行程序的全过程: 1.cpp + 2.cpp + 3.cpp + 1.h 2.h 3.h 1.obj 2.obj 3.obj program. exe *.lib | 预编译 | 编译 | 连接 |我们可以发现,库文件(扩展名为 LIB,其实是一种特殊的已经编译好的程序。系统函 数的定义都是存在 LIB 内,以使你看不到它们的源代码)是在最后的连接一步加入程序的, 各个文件也是在这一步才建立联系的。 extern 的作用就是告诉编译器此变量会在其它程序文件中声明。把这种外部变量声明 放在头文件里,再在每个文件中都包含这个头文件,然后只要在任何一个文件中声明变量, 所有文件就都可以使用这个变量了。 如果不加 extern, 各个文件使用的变量虽然同名但内容 不会统一。 函数需要在各个文件中都声明之后才能使用。 下面我们就来看一个简单的例子: /*---------------main.h----------------*/ #if !(defined MAIN_H) #include &iostream.h& void print(); #define MAIN_H #endif/*---------------main.cpp----------------*/ #include &main.h& void main() { a=3; print(); }/*---------------function.cpp----------------*/ #include &main.h& void print() { cout&&a; }第二章 游戏编程必需的 C++高级知识 C++和 C 最大的区别在于 C++是一种面向对象(OOP)的语言。类(class)正是实现面 向对象的关键。类是一种数据类型,是对事物的一种表达和抽象。类拥有各种成员,其中有 的是数据,标识类的各种属性;有的是函数(在类中称为方法) ,表示对类可进行的各种操 作。举一个例子,我们可以建立一个“草”类,它可以有“高度”等各种属性和“割”、 “浇水”等 各种操作。2.1 类的定义和使用让我们先看一个使用了类的程序: //-----------------------------grass.h--------------------------------class grass //定义 grass 类 { private: //声明下面的成员为私有。类外的函数如果试图访问,编译器会告诉你发生错 //误并拒绝继续编译。 public: //下面的成员为公有,谁都可以访问。 void cut( ); void water( ); int get_height( ); void set_height(int newh); }; //这个分号不要漏了!//-----------------------------grass.cpp------------------------------#include &iostream.h& #include “grass.h” //下面对类的方法进行定义 void grass::cut( ) // “::”表示 cut( )是 grass 的成员。 { if (height&=10) height-=10; //可自由访问 grass 中的任何成员。 } void grass::water( ) { height+=10; } int grass::get_height( ) //因为在类的外部不能直接访问 height 所以要写这个函数。 { } void grass::set_height(int newh) //同样我们写了这个函数。 { if (newh&=0) height= } void main( ) { grass grass1,grass2; //其实这一句和“int a,b;”没什么区别,想一想!这一句语 //句被称为实例化。 grass1.set_height(20); //如果你用过 VB 一定会觉得很亲切,类以外的函数即使 //是访问类的公有部分也要用“.”。 cout&&grass1.get_height( )&& grass1.set_height(-100); //因为 set_height 作了保护措施,所以这一句不会给 //height 一个荒唐的值, 这里我们可以看到类被封装的 //一个好处。 cout&&grass1.get_height( )&& grass1.cut( ); cout&&grass1.get_height( )&& grass2=grass1; //同一种对象可直接互相赋值。 cout&&grass2.get_height( )&& grass *grass3; //也可定义指向类的指针。 grass3= //同样要 new。 grass3-&set_height(40); //由于 grass3 是指针,这里要用“-&”。其实也可以 //使用(*grass3).set_height(40); (“.”操作符比“*” //操作符执行时优先) ,不过这样写比较麻烦。 grass3-&water( ); cout&&grass3-&get_height( ); } 看了注释你应该可以读懂这个程序,现在我们可以看到类的第一个优点了:封装性。封 装指的就是象上面这样似乎故弄玄虚地把 height 隐藏起来,并写几个好象很无聊的读取和 改写 height 的函数。然而在程序中我们已经可以看到这样可以保护数据。而且在大型软件 和多人协作中,私有成员可以隐藏类的核心部分,只是通过公有的接口与其它函数沟通,当 我们修改类的数据结构时, 只要再改一改接口函数, 别的函数还是可以象以前一样调用类中 的数据。这样就可以使一个类作为一个模块而出现,有利于减少错误和大家的协作。 有 的人也许会认为写接口函数会减慢速度,那么你可以使用内联函数 类以外的函数其实也有办法直接访问类的私有部分, 只要在类中声明类的方法时加入形 如”friend int XXX(int xxx, int xxx)”这样的语句,类以外的”int XXX(int xxx, int xxx)”函 数就可访问类的私有部分了。此时这个函数称为类的友元。 除了 public 和 private 两种权限外还有 protected 权限,平时是和 private 一样的,后面 在讲类的继承时会进一步解释它的用途。 在类的定义中要注意定义成员数据时不能同时初始化(好象 int a=0 这样) ,且不能用 extern 说明成员数据。 一种类的对象可以作为另一种类的成员。例如: class x { }; class y { }; 同一种类可以互相赋值。类可作为数组的元素。可以定义指向类的指针。总之类拥有普 通的数据类型的性质。 只要定义一次类, 就可以大批量地通过实例化建立一批对象, 且建立的对象都有直观的 属性和方法。这也是类的好处之一。 结构其实是一种特殊的类, 只不过类的缺省访问权限是私有, 而结构的缺省访问权限是 公有。定义结构时只需把“class”换为“struct”。 一般我们在仅描述数据时使用结构,在既要 描述数据,又要描述对数据进行的操作时使用类。2.2 构造函数当将一个类实例化时,我们希望能同时将它的一些成员初始化。为此,我们可以使用构 造函数。构造函数是一个无返回值(void 都不行)且与类同名的函数。就象下面这样: #include &iostream.h& class grass { public: grass(int height); //构造函数 }; grass::grass(int height) { this-&height= //对于任何一个对象的方法来说,this 永远是一个指向这个 //对象的指针。 所以这样写能使编译器知道是类中的 height } void main( ) { grass grass1(10); //普通对象实例化时就要给出初始化参数 grass *grass2; grass2=new grass(30); //指针此时要给出初始化参数 cout&&grass1. cout&&grass2-& } 有时候我们需要用另一个类的属性来初始化一个类, 此时我们可以使用拷贝初始化构造 函数,好象这样即可: grass::grass(grass &g) { height=g. } 然后我们可以这样调用它: grass grass2(grass1); 来实现用 grass1 的属性初始化 grass2 的属性。 此时会有一个有趣的问题,grass1 初始化时要给出参数,其中有一个为一个 grass 类 的对象, 但此时何来这种东西呢?为此, 我们还需要将构造函数重载, 保留以前的构造函数, 以后就可以各取所需了。 构造函数还有一个用处就是可以进行类型转换。 例如, 我们定义了一个这样的构造函数: grass::grass(int x) { height=x; } 现在, 如果我们定义了一个 grass 类的 gg 对象, 以后就可以执行 gg=5; 这样的语句了, 也可将 int 类型的变量赋值给 gg。 还有一种叫析构函数的东西,形如 grass::~grass( ),在我们 delete 一个指向对象的指 针时会调用,你可以在里面释放类的指针型成员。2.3 类的静态成员类的静态数据成员和普通的静态变量含义不同, 它的意思是: 在每一个类实例化时并不 分配存储空间, 而是该类的每个对象共享一个存储空间, 并且该类的所有对象都可以直接访 问该存储空间。 在类中定义静态数据成员, 只须在定义时在前面加上“static”。 类的静态数据成员只能在 类外进行初始化,若没有对其进行初始化,则自动被初始化为 0。在类外引用静态数据成 员必须始终用类名::变量名的形式。 举一个例子: #include &iostream.h& class AA { private: public: //定义类的静态成员 AA(int aa=0) { a= b++; } //对于类的方法,如果是较简单的可以这样写以使程序紧凑 int get_a( ) { } }; int AA::b=0; //在类外初始化 void main() { cout&&& AA::b=&&&AA::b&& AA x(10),y(20); cout &&&x.a=&&&x.get_a( )&&& x.b=&&&x.b&& cout &&&y.a=&&&y.get_a( )&&& y.b=&&&y.b; }2.4 运算符重载运算符重载可以使类变得非常直观和易用。比如说,我们定义了一个复数类(什么,你 没学过复数?你读几年级?) ,然后再将加、减、乘、除等运算符重载,就可以自由地对复 数对象好象整数一样进行这些运算了!这是一件多么好的事情啊! 使用运算符重载很简单,我们就举一个复数类的例子来说明怎样使用: #include &iostream.h& class complex { private: public: complex ( ); //缺省构造函数 complex (double r, double i); //顺便初始化值的构造函数 complex operator +(complex & x); //计算 A+B complex operator ++( ); //计算++A complex operator --(int); //计算 A-void print(); //输出复数 }; complex::complex( ) { real=0.0; image=0.0; } complex::complex(double r, double i) { real=r; image=i; } complex complex::operator +(complex & x) { c.real=real+x. c.image=image+x. } complex complex::operator ++( ) { ++ c.real= c.image= } complex complex::operator --(int) { c.real= c.image= real--; } void complex::print( ) { cout&&real&&&+&&&image&&&i&&& } void main( ) { complex a(1,2); complex b(4,5); complex c=a+b; complex d=++a; complex e=b--; c.print( ); d.print( ); e.print( ); } 除了“.”、 “.*”、 “::”、 “?:”四个运算符外,其它运算符(包括 new、delete)都可被重载, cin 和 cout 就是两个典型的例子。 对于双目运算符(即 A?B),如加、减、乘、除等,可这样重载: ”complex operator ?(complex &B);”,运算时就好像调用 A 的这个方法一样。 对于前置的单目运算符(即?A),如“-”、 “―”、“++”等,可这样重载: “complex complex::operator ?( );”。 对于后置的单目运算符,如“--”、 “++”,可这样重载: “complex complex::operator ?(int);”,其中参数表中的 int 不能省去。 下面出一道题让大家考虑考虑吧:创建一个字符串类并将+、-运算符重载,使我们可 以直观地操作字符串。2.5 类的继承可以继承是类的第二个优点,它使大型程序的结构变得严谨并减少了程序员的重复劳 动。继承到底是什么呢?举个例子,比如说树和猫这两样东西,看起来好象毫不相干,但它 们都有质量、体积等共有的属性和买卖、称量等共有的方法。所以我们可不可以先定义一个 基类,它只包含两样事物共有的属性和方法,然后再从它派生出树和猫这两样事物,使它们 继承基类的所有性质, 以避免重复的定义呢?答案是可以的。 由于一个类可以同时继承多个 类,一个类也可同时被多个类继承,我们可以建立起一个复杂的继承关系,就象这样: 电脑硬件 其它设备 储存设备 输入设备 输出设备软件 操作系统 应用软件触摸屏 我们可以看到,继承是很灵活多样的。说明继承很简单,只要象这样定义派生类即可: class 派生类名 :派生性质 基类名 1,派生性质 基类名 2,...,派生性质 基类名 n { 这里面同定义普通类一样,不必再说明基类中的成员 }; 关于这里的派生性质,有这样一张表可供参考: 派生性质 在基类中的访问权限 在派生类中的访问权限 public public protected protected public private 不可访问 public protected protected protected protected private 不可访问 public private protected private private private 不可访问 这张表中的后两栏意思是: 当基类中设置了这种访问权限的成员被派生类继承了时, 它 将等价于设置了什么访问权限的派生类的成员。 下面我们看一个例子: #include &iostream.h& class THING //定义基类 { protected: int mass, public: int get_mass( ); void set_mass(int new_mass); }; int THING::get_mass( ) { } void THING::set_mass(int new_mass) { if (new_mass&=0) mass=new_ } class ANIMAL: public THING //定义派生类 { private: public: void set_life(int new_life) { if (new_life&=0) life=new_ }; int get_life( ) { }; void kill( ) { life=0; }; }; ANIMAL void main( ) { cat.set_mass(50); //cat 继承了 THING 类的方法 cat.set_life(100); //也有自己的方法 cat.kill( ); cout&&cat.get_mass( )&& cout&&cat.get_life( )&& }2.6 虚函数和抽象类虚函数体现了类的第三个优点:多态性(看上去好象很深奥) 。 在一个含有基类和派生类的程序中, 在派生类中可以定义一个和继承来的基类的方法具 有相同的函数名、返回类型和参数表,但函数的具体内容不同的方法,即重载这个函数。当 一个基类的指针指向一个派生类的对象时(这是合法的) ,基类指针还是只能访问基类的这 种方法,而不是在派生类中重新定义的方法。但在实际应用中,有时需要这个指针在这时访 问派生类中的新方法(这就是类的多态性) 。解决问题的方法是在基类和派生类中都把这个 方法定义为虚函数。 虚函数的使用方法是在基类声明成员函数在最前加关键字“virtual”。 我们也举一个例子来说明虚函数的使用方法: #include &iostream.h& class Base { public: virtual int get_a( ) { }; }; class Child: public Base { public: int get_a( ) {return a*a; }; Child(int aa) {a= }; }; Child child(10); void main( ) { Base *p; p=& cout&&p-&get_a( ); } 从运行结果可以看到,调用的是 Child 类的 get_a( )。试一试删去 virtual 看看输出有什 么变化。 有时候我们不需要用基类来定义对象, 则可把基类的函数定义为纯虚函数, 也不需再在 基类中给出函数的实现。这时基类就被称为抽象类。定义纯虚函数的方法是在加了“virtual” 后再去掉函数体并在声明后加上“=0”。就象这样:“virtual int get_a( )=0;”。 到这里 C++语言已经基本上讲完了,如果你想知道常用的 C++函数的用法请参阅 9.3 节。你也许会注意到我没有讲文件的操作方法,如果你想了解也可以看看 9.3 节。第三章 游戏编程必需的 Windows 编程知识因为我们编好的游戏将在 Windows 下运行,所以学习一点 Windows 编程是必需的。 Microsoft 为了方便 Windows 编程制作了一个庞大的类库 MFC, 把 Windows 的方方面面都 封装了起来。但此类库只是比较适合编写字板之类的标准 Windows 程序,对于游戏来说它 实在是过于烦琐和累赘,所以我们一般都不使用它,自己从头写 Windows 程序。3.1 游戏编程中基本的 Windows 程序这个程序看起来有点长,但你不必担心 Windows 编程过于复杂。在所有的 Windows 程序中,都需要一个初始化的过程,而这个过程对于任何 Windows 程序而言,都是大同小 异的。你也许会想到使用 VB 做一个最简单的程序不用敲一行代码,其实这是因为 VB 已经 暗地里帮你敲好了。 //******************************************************************* // 工程:Test // 文件:main.cpp // 内容:一个基本的 Windows 程序 //******************************************************************* #include &windows.h& //函数声明 BOOL InitWindow( HINSTANCE hInstance, int nCmdShow ); LRESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ); //变量说明 HWND //窗口句柄 //******************************************************************* //函数:WinMain( ) //功能:Windows 程序入口函数。创建主窗口,处理消息循环 //******************************************************************* int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MSG if ( !InitWindow( hInstance, nCmdShow ) ) return FALSE; //创建主窗口 //进入消息循环: while(1) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if ( msg.message==WM_QUIT) TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wP } //****************************************************************** //函数:InitWindow( ) //功能:创建窗口。 //****************************************************************** static BOOL InitWindow( HINSTANCE hInstance, int nCmdShow ) { //定义窗口风格: WNDCLASS wc.style = NULL; wc.lpfnWndProc = (WNDPROC)WinP wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hI wc.hIcon = NULL; wc.hCursor = NULL; wc.hbrBackground = NULL; wc.lpszMenuName = NULL; wc.lpszClassName = &My_Test&; RegisterClass(&wc);//注册窗口 //按所给参数创造窗口 hwnd = CreateWindow(&My_Test&, &My first program&, WS_POPUP|WS_MAXIMIZE,0,0, GetSystemMetrics( SM_CXSCREEN ), //此函数返回屏幕宽度 GetSystemMetrics( SM_CYSCREEN ), //此函数返回屏幕高度 NULL,NULL,hInstance,NULL); if( !hwnd ) return FALSE; ShowWindow(hwnd,nCmdShow);//显示窗口 UpdateWindow(hwnd);//刷新窗口 return TRUE; } //****************************************************************** //函数:WinProc( ) //功能:处理主窗口消息 //****************************************************************** LRESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) { switch( message ) { case WM_KEYDOWN://击键消息 switch( wParam ) { case VK_ESCAPE: MessageBox(hWnd,&ESC 键按下了! 确定后退出!&,&Keyboard&,MB_OK); PostQuitMessage( 0 );//退出 } case WM_RBUTTONDOWN: MessageBox(hWnd,&鼠标右键按下了!&,&Mouse&,MB_OK); case WM_DESTROY: PostQuitMessage( 0 ); } return DefWindowProc(hWnd, message, wParam, lParam); //调用缺省消息处理过程 } 输入完程序后,按 Ctrl+F5 执行一下,就会出现一个透明的“窗口”(在任务栏上可见 窗口名,不是死机!),然后你可以试试按按鼠标右键或 Esc 键看看效果。怎么样?VB 要 做到同样的效果恐怕很难, 这也算是从头写代码的一点好处吧。 下面让我们分析一下这个程 序的组成。3.2 Windows 程序中的 WinMain 函数3.2.1 简介 WinMain( )函数与 DOS 程序的 main ( )函数基本起同样的作用,但有一点不同的是 WinMain( )函数必须带有四个系统传递给它的参数。WinMain( )函数的原型如下: int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) 第一个参数 hInstance 是标识该应用程序当前的实例的句柄。 它唯一的代表了该应用程 序,十分重要。在后面初始化程序主窗口的过程中就需要使用它作为参数。 实例代表的是应用程序执行的整个过程和方法, 一个应用程序如果没有被执行, 只是存 在于磁盘上,那么就是没有被实例化的;只要一执行这个程序,就说明该程序的一个实例在 运行。句柄是一个指向该对象所占据的内存区的指针,Windows 使用它管理内存中的各种 对象。 第二个参数是 hPrevInstance,它是用来标识该应用程序的前一个实例的句柄。给它 NULL 吧,这个参数只是为了提供与 16 位 Windows 的应用程序形式上的兼容性。 第三个参数是 lpCmdLine,是指向应用程序命令行参数字符串的指针。比如说我们运 行“test hello”,则此参数指向的字符串为“hello”。 最后一个参数是 nCmdShow,是一个用来指定窗口显示方式的整数。关于窗口显示方 式的种类,将在下面说明。 3.2.2 注册窗口类 一个程序可以有许多窗口, 但只有一个是主窗口, 它是与该应用程序的实例句柄唯一对 应的。 创建窗口前通常要填充一个窗口类结构 WNDCLASS,并调用 RegisterClass( )对该窗 口类进行注册。 每个窗口都有一些基本的属性, 如窗口标题栏文字、 窗口大小和位置、 鼠标、 背景色,处理窗口消息函数的名称等等。注册的过程也就是将这些属性告诉系统,然后再调 用 CreateWindow( )函数创建出窗口。 下面列出了 WNDCLASS 结构的成员: UINT //窗口的风格 WNDPROC lpfnWndP //窗口的消息处理函数的远指针 int cbClsE //分配给窗口类结构之后的额外字节数 int cbWndE //分配给窗口实例之后的额外字节数 HANDLE hI //窗口过程所对应的实例句柄 HICON hI //窗口的图标 HCURSOR hC //窗口的鼠标 HBRUSH hbrB //窗口的背景 LPCTSTR lpszMenuN //窗口的菜单资源名称 LPCTSTR lpszClassN //窗口类的名称 WNDCLASS 结构的第一个成员 style 表示窗口类的风格,它往往是由一些基本的风格 通过位的“或”操作(操作符”|”)组合而成。下表列出了一些常用的基本窗口风格: 风格 含义 CS_HREDRAW 如果窗口宽度发生改变,重绘整个窗口 CS_VREDRAW 如果窗口高度发生改变,重绘整个窗口 CS_DBLCLKS 能感受用户在窗口中的双击消息 CS_NOCLOSE 禁用系统菜单中的“关闭”命令 CS_SAVEBITS 把被窗口遮掩的屏幕图象部分作为位图保存 起来。当该窗口被移动时,Windows 使用被 保存的位图来重建屏幕图象 第二个成员是 lpfnWndProc, 给它消息处理函数的函数名称即可, 必要时应该进行强制 类型转换,将其转换成 WNDPROC 型。 接下来的 cbClsExtra 和 wc.cbWndExtra 一般都可以设为 0。 然后的 hInstance 成员,给它的值是应用程序的实例句柄,表明该窗口与该实例是相关 联的。 下面的 hIcon,是让我们给这个窗口指定一个图标,这个程序没有设置。 鼠标也没有设置,因为编游戏时的鼠标都是在刷新屏幕时自己画上去的。 hbrBackground 成员用来定义窗口的背景色。这里设为 NULL,即透明。 lpszMenuName 成员的值我们给它 NULL,表示该窗口没有菜单。 WNDCLASS 结构的最后一个成员 lpszClassName 是让我们给这个窗口类起一个唯一 的名称,因为 Windows 操作系统中有许许多多的窗口类,必须用一个独一无二的名称来代 表它们。通常,我们可以用程序名来命名这个窗口类的名称。在调用 CreateWindow( )函数 时将要用到这个名称。 填充完 WNDCLASS 结构后,我们需要调用 RegisterClass( )函数进行注册;该函数如 调用成功,则返回一个非 0 值,表明系统中已经注册了一个的窗口类。 如果失败, 则返回 0。 3.2.3 创建窗口 当窗口类注册完毕之后,我们就可以创建一个窗口,这是通过调用 CreateWindow( ) 函数完成的。窗口类中已经预先定义了窗口的一般属性,而在 CreateWindow( )中的参数中 可以进一步指定窗口更具体的属性。下面举一个例子来说明 CreatWindow( )的用法: hwnd = CreateWindow( &Simple_Program&, //创建窗口所用的窗口类的名称 &A Simple Windows Program&, //窗口标题 WS_OVERLAPPEDWINDOW, //窗口风格,定义为普通型 100, //窗口位置的 x 坐标 100, //窗口位置的 y 坐标 400, //窗口的宽度 300, //窗口的高度 NULL, //父窗口句柄 NULL, //菜单句柄 hInstance, //应用程序实例句柄 NULL ); //一般都为 NULL 第一个参数是创建该窗口所使用的窗口类的名称, 注意这个名称应与前面所注册的窗口 类的名称一致。 第三个参数为创建的窗口的风格,下表列出了常用的窗口风格: 风格 含义 WS_OVERLAPPEDWINDOW 创建一个层叠式窗口,有边框、标题栏、系 统菜单、最大最小化按钮,是以下几种风格 的集合:WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX WS_POPUPWINDOW 创建一个弹出式窗口,是以下几种风格的集 合: WS_BORDER, WS_POPUP, WS_SYSMENU。必须再加上 WS_CAPTION 与才能使窗口菜单可见。 WS_OVERLAPPED & WS_TILED 创建一个层叠式窗口,它有标题栏和边框。 WS_POPUP 该窗口为弹出式窗口, 不能与 WS_CHILD 同 时使用。 WS_BORDER 窗口有单线边框。 WS_CAPTION 窗口有标题栏。 WS_CHILD 该窗口为子窗口,不能与 WS_POPUP 同时 使用。 WS_DISABLED 该窗口为无效,即对用户操作不产生任何反 应。 WS_HSCROLL / WS_VSCROLL 窗口有水平滚动条 / 垂直滚动条。 WS_MAXIMIZE / WS_MINIMIZE 窗口初始化为最大化 / 最小化。 窗口有最大化按钮 / 最小化按钮 边框可进行大小控制的窗口 创建一个有系统菜单的窗口,必须与 WS_CAPTION 风格同时使用 WS_TILED 创建一个层叠式窗口,有标题栏 WS_VISIBLE 窗口为可见 在 DirectX 编程中, 我们一般使用的是 WS_POPUP|WS_MAXIMIZE, 用这个标志创建 的窗口没有标题栏和系统菜单且窗口为最大化,可以充分满足 DirectX 编程的需要。 如果窗口创建成功,CreateWindow( )返回新窗口的句柄,否则返回 NULL。 WS_MAXIMIZEBOX / WS_MINIMIZEBOX WS_SIZEBOX & WS_THICKFRAME WS_SYSMENU 3.2.4 显示和更新窗口 窗口创建后,并不会在屏幕上显示出来,要真正把窗口显示在屏幕上,还得使用 ShowWindow( )函数,其原型如下: BOOL ShowWindow( HWND hWnd, int nCmdShow ); 参数 hWnd 就是要显示的窗口的句柄。 nCmdShow 是窗口的显示方式,一般给它 WinMain( )函数得到的 nCmdShow 的值就 可以了。常用的窗口显示方式有: 方式 含义 SW_HIDE 隐藏窗口 SW_MINIMIZE 最小化窗口 SW_RESTORE 恢复并激活窗口 SW_SHOW 显示并激活窗口 SW_SHOWMAXIMIZED 最大化并激活窗口 SW_SHOWMINIMIZED 最小化并激活窗口 ShowWindow( )函数的执行优先级不高, 当系统正忙着执行其它的任务时窗口不会立即 显示出来。所以我们使用 ShowWindow( )函数后还要再调用 UpdateWindow(HWND hWnd);函数以保证立即显示窗口。 3.2.5 消息循环 在 WinMain( )函数中,调用 InitWindow( )函数成功的创建了应用程序主窗口之后,就 要启动消息循环,其代码如下: while(1) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if ( msg.message==WM_QUIT) TranslateMessage(&msg); DispatchMessage(&msg); } } Windows 应用程序可以接收各种形式的信息,这包括键盘和鼠标的动作、记时器消息, 其它应用程序发来的消息等等。Windows 系统会自动将这些消息放入应用程序的消息队列 中。 PeekMessage( )函数就是用来从应用程序的消息队列中按照先进先出的原则将这些消 息一个个的取出来,放进一个 MSG 结构中去。PeekMessage( )函数原型如下: BOOL PeekMessage ( LPMSG lpMsg, //指向一个 MSG 结构的指针,用来保存消息 HWND hWnd, //指定哪个窗口的消息将被获取 UINT wMsgFilterMin, //指定获取的主消息值的最小值 UINT wMsgFilterMax //指定获取的主消息值的最大值 UINT wRemoveMsg//是否移除消息 ); 如果队列中没有任何消息,PeekMessage( )函数将立即返回。如果队列中已有消息, 它将取出一个后返回。MSG 结构包含了一条 Windows 消息的完整信息,它由下面的几部 分组成: HWND //接收消息的窗口句柄 UINT //主消息值 WPARAM wP //副消息值 1,其具体含义依赖于主消息值 LPARAM lP //副消息值 2,其具体含义依赖于主消息值 DWORD //消息被投递的时间 POINT //鼠标的位置 该结构中的主消息表明了消息的类型, 例如是键盘消息还是鼠标消息等, 副消息的含义 则依赖于主消息值,比如说如果主消息是键盘消息,那么 wParam 中存储了是键盘的哪个 具体键;如果主消息是鼠标消息,那么 LOWORD(lParam)和 HIWORD(lParam)分别为鼠标 位置的 x 和 y 坐标。 PeekMessage( )的第二个参数是用来指定从哪个窗口的消息队列中获取消息,其它窗 口的消息将被过滤掉。如果该参数为 NULL,则 PeekMessage( )从该应用程序所有窗口的 消息队列中获取消息。 第三个和第四个参数是用来过滤 MSG 结构中主消息值的,主消息值在 wMsgFilterMin 和 wMsgFilterMax 之外的消息将被过滤掉。如果这两个参数均为 0,表示接收所有消息。 第五个参数用来设置分发完消息后是否将消息从队列中移除,一般设为 PM_REMOVE 即移除。 TranslateMessage( )函数的作用是把虚拟键消息转换到字符消息,以满足键盘输入的 需要。DispatchMessage( )函数所完成的工作是把当前的消息发送到对应的窗口过程中去。 开启消息循环其实是很简单的一个步骤,几乎所有的程序都是按照 Test 的这个方法。 我们完全不必去深究这些函数的作用,只是简单的照抄就可以了。 另外, 我这里介绍的消息循环开启方法比很多书上所介绍的用 GetMessage( )的方法要 好一些,因为 GetMessage( )如果得不到消息会一直等待,结果就耗费了许多宝贵的时间, 使游戏不能及时刷新。3.3 Windows 程序中的消息处理函数消息处理函数又叫窗口过程,在这个函数中,不同的消息将被 switch 语句分配到不同 的处理程序中去。Windows 的消息处理函数的原型是这样定义的: LRESULT CALLBACK WindowProc( HWND hwnd, //接收消息窗口的句柄 UINT uMsg, //主消息值 WPARAM wParam, //副消息值 1 LPARAM lParam //副消息值 2 ); 消息处理函数必须按照上面的这个样式来定义,但函数名称可以随便取。 Test 中的 WinProc( )函数就是一个典型的消息处理函数。 在这个函数中明确的处理了 3 个消息,分别是 WM_KEYDOWN(击键消息) 、WM_RBUTTONDOWN(鼠标右键按下消 息) 、WM_DESTROY(销毁窗口消息) 。值得注意的是,应用程序发送到窗口的消息远远 不止以上这几条,象 WM_SIZE、WM_MINIMIZE、WM_CREATE、WM_MOVE 等频繁使 用的消息就有几十条。在附录中可以查到 Windows 常见消息列表。 为了减轻编程的负担,Windows 提供了 DefWindowProc( )函数来处理这些最常用的消 息,调用了这个函数后,这些消息将按照系统默认的方式得到处理。因此,在 switch_case 语句中,只须明确的处理那些有必要进行特别响应的消息,把其余的消息交给 DefWindowProc( )函数来处理。 当调用了 PostQuitMessage( )函数, 该函数会给窗口的消息队列中发送一条 WM_QUIT 的消息,从而结束消息循环。随后,程序也结束。 顺便提一提 MessageBox 函数,它是用来显示对话框的函数,原形是:int MessageBox(HWND hwndParent, LPCSTR lpszText, LPCSTR lpszTitle, UINT fuStyle);其中的四个参数依次为:窗口句柄,文字内容,标题,风格。常用风格有:MB_OK、 MB_OKCANCEL 、 MB_RETRYCANCEL 、 MB_YESNO 、 MB_YESNOCANCEL ,代表对 话框有哪些按钮。常用返回值有 IDCANCEL、IDNO、IDOK、IDRETRY、IDYES,代表哪 个按钮被按下。第四章 DirectDraw 编程基础看完上一章后,我想你已经大致地掌握了 Windows 编程的方法。这意味着你为进行 DirectDraw 编程打下了坚实的基础。那么就让我们进入激动人心的 DirectDraw 部分吧。 4.1 DirectDraw 使用前的准备工作为了告诉编译器我们要使用 DirectDraw,我们需要做几件事:首先在配套光盘中找到 DirectX 的 SDK,将其中的 ddraw.lib 复制到安装目录\VC98\LIB 下,覆盖原有文件,再将 ddraw.h 复制到安装目录\VC98\INCLUDE 下,也覆盖原有文件。然后在文件头#include &ddraw.h&,再在菜单中找到工程―设置―Link―对象/库模块,在“kernel32.lib”之前加入 “ddraw.lib”。现在我们就可以使用 DirectDraw 了。4.2 DirectDraw 的初始化4.2.1 简介 让我们先看一看一个常见的 DirectDraw 初始化函数:LPDIRECTDRAW lpDD; // DirectDraw 对象的指针 LPDIRECTDRAWSURFACE lpDDSP // DirectDraw 主页面的指针 LPDIRECTDRAWSURFACE lpDDSB // DirectDraw 后台缓存的指针 LPDIRECTDRAWSURFACE lpDDSB // 存放背景图的页面的指针BOOL InitDDraw( ) { DDSURFACEDESC // DirectDraw 的页面描述 if ( DirectDrawCreate( NULL, &lpDD, NULL ) != DD_OK ) return FALSE; //创建 DirectDraw 对象 if ( lpDD-&SetCooperativeLevel (hwnd,DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN) != DD_OK ) return FALSE; //设置 DirectDraw 控制级 if ( lpDD-&SetDisplayMode( 640, 480, 32 ) != DD_OK ) return FALSE; //设置显示模式 //开始创建主页面,先填充页面描述 ddsd.dwSize = sizeof( ddsd ); ddsd.dwFlags = DDSD_CAPS|DDSD_BACKBUFFERCOUNT; //有后台缓存 ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE|DDSCAPS_FLIP|DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 1; //一个后台缓存 if ( lpDD-&CreateSurface( &ddsd, &lpDDSPrimary, NULL ) != DD_OK ) return FALSE; //创建主页面 ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER; //这是后台缓存 if ( DD_OK != lpDDSPrimary-&GetAttachedSurface( &ddsd.ddsCaps, &lpDDSBuffer ) ) return FALSE; //创建后台缓存 ddsd.dwSize = sizeof( ddsd ); ddsd.dwFlags = DDSD_CAPS|DDSD_WIDTH|DDSD_HEIGHT; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; //这是离屏页面 ddsd.dwHeight=480; //高 ddsd.dwWidth=640; //宽 if ( DD_OK != lpDD-&CreateSurface( &ddsd, &lpDDSBack, NULL ) ) return FALSE; //创建放背景图的页面 //如还有别的页面可在此处继续创建 return TRUE; }我们可以看到,在开头首先定义了指向 DirectDraw 对象和 DirectDraw 页面(又称 DirectDraw 表面)对象的指针。LPDIRECTDRAW 和 LPDIRECTDRAWSURFACE 类型是 在 ddraw.h 头文件里预定义的, 指向 IDirectDraw 和 IDirectDrawSurface 类型的长型指针 (前 面加的 LP 代表 Long Point) ,从后面我们用的是”-&”而不是”.”也可以看出这一点。DD 是 DirectDraw 的缩写, DDS 是 DirectDrawSurface 的缩写, 因此习惯上我们把变量名起为 lpDD 和 lpDDSXXX。 4.2.2 DirectDraw 对象 如果要使用 DirectDraw,必须创建一个 DirectDraw 对象,它是 DirectDraw 接口的核 心。用 DirectDrawCreate( )函数可以创建 DirectDraw 对象,DirectDrawCreate( )函数是在 ddraw.h 中定义的,它的原型如下: HRESULT DirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter ); 第一个参数是 lpGUID :指向 DirectDraw 接口的全局唯一标志符( Global Unique IDentify)的指针。在这里,我们给它 NULL,表示我们将使用当前的 DirectDraw 接口。 第二个参数是 lplpDD: 这个参数是用来接受初始化的 DirectDraw 对象的地址。 在这里, 我们给它&lpdd。(指针的指针) 第三个参数是 pUnkOuter:如果你不想惹麻烦就给它 NULL 吧。Microsoft 是这么说的。 所有的 DirectDraw 函数的返回值都是 HRESULT 类型,它是一个 32 位的值。函数调 用成功用 “DD_OK”表示,所有的错误值标志开头都为“DDERR”,如: DDERR_DIRECTDRAWALREADYCREATED DDERR_OUTOFMEMORY 在附录中可查到这些错误值的列表。 我们一般用“if(DirectDrawCreate(NULL,&lpDD,NULL) != DD_OK) return FALSE;” 来创建 DirectDraw 对象,这样当创建不成功时就会退出函数并返回 FALSE。 4.2.3 设置控制程度和显示模式 DirectDrawCreate 函数调用成功后,lpDD 已经指向了一个 DirectDraw 对象,它是整 个 DirectDraw 接口的最高层领导,以后的步骤都是在它的控制之下。 我们用 IDirectDraw::SetCooperativeLevel( )来设置 DirectDraw 程序对系统的控制级。 它的原型如下: HRESULT SetCooperativeLevel (HWND hWnd, DWORD dwFlags ) 第一个参数是窗口句柄,我们给它 hWnd,使 DirectDraw 对象与主窗口联系上。 第二个参数是控制级标志。这里使用 DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN, 表示我们期望 DirectDraw 以独占和全屏方式工作。 控制级描述了 DirectDraw 是怎样与显示设备及系统作用的。DirectDraw 控制级一般被 用来决定应用程序是运行于全屏模式(必须与独占模式同时使用) ,还是运行于窗口模式。 但 DirectDraw 的控制级还可设置如下几项: (1)允许 DirectDraw 使用 Mode X 分辨率 (这是一种著名的低分辨率, 320x240x8 位色, DOS 下常用,如仙剑,现在已经废弃了) 。 (2)允许按 Ctrl + Alt + Del 重新启动 (仅用于独占模式, 为 DDSCL_ALLOWREBOOT) 。 (3)不允许对 DirectDraw 应用程序最小化或还原(DDSCL_NOWINDOWCHANGES)。 普通的控制级(DDSCL_NORMAL)表明我们的 DirectDraw 应用程序将以窗口的形式运 行。在这种控制级下,我们将不能改变显示器分辨率,主页面的调色板,或进行换页操作(这 是一个重要的操作,在第五章会介绍)。除此之外,我们也不能够调用那些会使显存产生激 烈反应的函数,如第七章要讲的 Lock( )。 当应用程序为全屏并且独占的控制级时, 我们就可以充分的利用硬件资源了。 此时其它 应用程序仍可分配页面内存、使用 DirectDraw 或 GDI 的函数,只是无法改变显示模式或调 色板。 下一步我们使用 IDirectDraw::SetDisplayMode( )来设置显示模式,其原形为: HRESULT SetDisplayMode( DWORD dwWidth, DWORD dwHeight, DWORD dwBPP ); dwWidth and dwHeight 用来设置显示模式的宽度和高度。 dwBPP 用来设置显示模式的颜色位数。 要注意的是,只有当 DirectDraw 对象为独占访问的控制程度时才能改变显示器的显示 模式,如果 DirectDraw 对象运行为窗口模式,调用该函数会返回一个错误。 4.2.4 创建主页面 接下来应该创建一个 DirectDrawSurface 对象。 DirectDrawSurface 对象代表了一个页面。页面可以有很多种表现形式,它既可以是可 见的(屏幕的一部分或全部) ,称之为主页面(Primary Surface) ;也可以是作换页用的不 可见页面,称之后台缓存(Back Buffer) ,在换页后,它成为可见;还有一种始终不可见的, 称之为离屏页面(Off-screen Surface) ,用它来存储图象。其中,最重要的页面是主页面, 每个 DirectDraw 应用程序都必须创建至少一个主页面,用它来代表屏幕上可见的区域,即 我们的屏幕。 创建一个页面之前,首先需要填充一个 DDSURFACEDESC 的结构,它是 DirectDraw Surface Description 的缩写,意思是 DirectDraw 的页面描述。它的结构非常庞大,这里只 能作一个最简单的介绍。下面是主页面的页面描述: ddsd.dwSize = sizeof( ddsd ); //给 dwSize 页面描述的大小 ddsd.dwFlags = DDSD_CAPS|DDSD_BACKBUFFERCOUNT; //有后台缓存 ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE|DDSCAPS_FLIP|DDSC APS_COMPLEX; //为主页面,有后台缓存,有换页链 ddsd.dwBackBufferCount = 1; //一个后台缓存 再看看一个普通表面的页面描述: ddsd.dwSize = sizeof( ddsd ); ddsd.dwFlags = DDSD_CAPS|DDSD_WIDTH|DDSD_HEIGHT; //编程者指定高、宽 ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; //这是离屏页面 ddsd.dwHeight=480; //高 ddsd.dwWidth=640; //宽 页 面 描 述 填 充 完 毕 后 , 把 它 传 递 给 CreateSurface( ) 函 数 即 可 创 造 页 面 。 CreateSurface( )的原形是: HRESULT CreateSurface( LPDDSURFACEDESC lpDDSurfaceDesc, LPDIRECTDRAWSURFACE FAR *lplpDDSurface, IUnknown FAR *pUnkOuter ); CreateSurface( )函数的第一个参数是被填充了页面信息的 DDSURFACEDESC 结构 的地址,此处为&ddsd;第二个参数是接收主页面指针的地址,此处为&lpDDSPrimary;第 三个参数现在必须为 NULL,为该函数所保留。 如果函数调用成功,lpDDSPrimary 将成为一个合法的主页面对象。由于在前面已经设 置了该程序的工作模式为独占和全屏, 所以, 此时主页面所代表的实际上是我们的整个显示 屏幕。在主页面上所绘制的图形将立即反映到我们的显示屏幕上。 换页和后台缓存将会在第五章介绍。 DirectDraw 初始化函数最后创造了一个离屏页面,如果我们想创造更多的页面只需象 下面这样先设置高度和宽度再创建页面即可: ddsd.dwHeight=XXX; ddsd.dwWidth=XXX; if ( DD_OK != lpDD-&CreateSurface( &ddsd, &lpDDSABC, NULL) ) return FALSE;4.3 DirectDraw 对象的释放一个完整的 DirectDraw 程序还需要在最后释放所有 DirectDraw 对象。 就好像下面的一 段程序: void FreeDDraw( ) { if( lpDD != NULL ) //判断 DirectDraw 对象是否为空 { if( lpDDSPrimary != NULL ) //判断主页面对象是否为空 { lpDDSPrimary-&Release( ); //释放 lpDDSPrimary = NULL; } //如果还有别的页面也像 lpDDSPrimary 一样释放 lpDD-&Release( ); //释放 lpDD = NULL; } }第五章 DirectDraw 编程实战上一章可以说是纸上谈兵,并没有牵涉到 DirectDraw 的精彩部分,但它也是使用 DirectDraw 必要的基础。这一章我们将具体讲讲对图象和页面的操作方法,你就可以体验 一下 DirectDraw 的强大了。 5.1 调入图象要 将 位 图 ( *.bmp ) 调 入 页 面 是 非 常 简 单 的 , 对 于 普 通 的 高 彩 图 象 我 们 只 需 用 DDReLoadBitmap(页面,”图象名.bmp”)即可调入图象。这个函数在 ddutil.cpp 中,你需 要把它和 ddutil.h 拷贝到你的程序的目录下,将其加入你的 Project 并在主程序的开头 #include “ddutil.h”。注意,DDReLoadBitmap( )函数会自动缩放图象以适应页面的大小。5.2 透明色透明色(又称关键色)对实现不规则物体的移动至关重要。所谓透明色,指的就是位块传 送中不会被传送的区域的颜色。 比如说我们先在一个绿色的背景上画了一个人物, 把这幅画 调入一个页面,再将纯绿色设为透明色。以后当我们进行位块传送时,只需指定范围为一个 规则的包括了人物的矩形。DirectDraw 只会把不规则的人物传到新的页面上而不会把纯绿 色的背景一起传送。当然,人物本身不能包含纯绿色,否则就不能完整地传送了。 一个页面在同一时刻只能有一种透明色,设置透明色的方法是 DDSetColorKey(页面 名,RGB(红,绿,蓝)); ,这里的“红” “绿” “蓝”即为平常我们在许多图象处理软件中看到的 R/G/B 值。例如 DDSetColorKey(lpDDSMap, RGB(255,0,255)); 。 因为这个函数也是 ddutil 提供的,所以你要确认 ddutil.cpp 在工程内且 #include “ddutil.h”。5.3 图象传送用 DirectDraw 做游戏,最常用的函数就是图象传送(又称位块传送)函数。它的作用 是 在 各 个 页面 之 间 传 送指 定 矩 形 范围 内 的 图 象, 并 可 同 时对 其 进 行 各种 处 理 。 使用 IDirectDrawSurface::Blt( )和 IDirectDrawSurface::Bltfast( ) 函数可以进行图象传送。 Blt ( ) 函数功能很强大,可对图象进行缩放、旋转、镜象等操作。不过平常我们用简单但够用的 Bltfast( )就可以了。它的原形是: HRESULT BltFast( DWORD dwX, DWORD dwY, LPDIRECTDRAWSURFACE lpDDSrcSurface, LPRECT lpSrcRect, DWORD dwTrans ); 下面将逐一介绍这几个参数: (1)dwX 和 dwY 目标页面上将被位块传送的矩形的左上角的 x 和 y 坐标。 (2)lpDDSrcSurface 位块传送操作的源页面。 (3)lpSrcRect 一个 RECT (Rectangle, 即矩形)结构的地址, 作为源页面上将被位块传送的区域。 如果该参数是 NULL, 整个源页面将被使用。 RECT 结构在 DirectDraw 中非常常用, 最好在程序中定义一个 RECT 类型的全局变量,如 rect,再象这样写一个函数: void MakeRect(int left,int top,int right,int bottom) { rect.bottom= rect.left= rect.right= rect.top= } 用时对它的 left、top、right、bottom 参数分别赋予矩形的左上角的 x 和 y 坐标、 右下角的 x 和 y 坐标。 (4)dwTrans 指定传送类型。有如下几种: DDBLTFAST_NOCOLORKEY 指定进行一次普通的复制,不带透明成分。 DDBLTFAST_SRCCOLORKEY 指定进行一次带透明色的位块传送,使用源页面的透明色。 DDBLTFAST_WAIT 如果位块传送器正忙,不断重试直到位块传送器准备好并传送好时才返 回。一般都使用这个参数。 这几种类型很长又经常用,最好定义两个全局变量: DWORD SrcKey= DDBLTFAST_SRCCOLORKEY| DDBLTFAST_WAIT DWORD NoKey= DDBLTFAST_NOCOLORKEY| DDBLTFAST_WAIT 举一些例子。如果我们想把 lpDDSBack 上的所有内容传到 lpDDSBuffer 上作为背 景,则使用: lpDDSBuffer-&BltFast(0,0,NULL,lpDDSBack,NoKey); 如果我们想将 lpDDSSpirit 上(20,30)到(50,60)的一个人物放到 lpDDSBuffer 上, 且左上角在 lpDDSBuffer 的(400,300)处,要使用透明色,则使用: MakeRect(20,30,50,60); lpDDSBuffer-&BltFast(400,300,&rect,lpDDSSpirit,SrcKey); 请注意,DirectDraw 的 BLT 函数只要源矩形或被传送到目标页面后的图象有一点 在页面外,例如 MakeRect(100,200,500,400)后将其 BLT 到一个 640x480 的页面的 (400,200)处,就什么都不会 BLT!解决问题最好的办法是自己写一个新的 BLT,剪 裁一下(有的书介绍用 Clipper 来剪裁,速度比这种方法慢不少)。 下面就将这个新的 BLT 的内容写出来(我用了一个数组存储页面的指针和高度、 宽度等信息,这样做确实很方便) : void MyBlt(int x,int y,int src_id,int dest_id,DWORD method) { int rl,rt,tx1,tx2,ty1,ty2,tl, RECT rect2= //保存原 rect 的内容 rl=rect. rt=rect. if (rect.left&SW[src_id]) //SW 存储页面宽度 //不进行位块传送 if (rect.top&SH[src_id]) //SH 存储页面高度 if (rect.right&0) if (rect.bottom&0) if (rect.left&0) rect.left=0; if (rect.top&0) rect.top=0; if (rect.right&SW[src_id]) rect.right=SW[src_id]; if (rect.bottom&SH[src_id]) rect.bottom=SH[src_id]; tx1=x+rect.left- ty1=y+rect.top- tx2=x+rect.right- ty2=y+rect.bottom- if (tx2&0) if (ty2&0) if (tx1&SW[dest_id]) if (ty1&SH[dest_id]) tl=tx1; tt=ty1; if (tx1&0) tx1=0; if (ty1&0) ty1=0; if (tx2&SW[dest_id]) tx2=SW[dest_id]; if (ty2&SH[dest_id]) ty2=SH[dest_id]; rl=rect. rt=rect. rect.left=tx1-tl+ rect.top=ty1-tt+ rect.right=tx2-tl+ rect.bottom=ty2-tt+ DDS[dest_id]-&BltFast(tx1,ty1,DDS[src_id],&rect,method); //DDS 为存储页面指针的数组 noblt: rect=rect2; //恢复原来的 rect }5.4 图象缩放图象缩放是一种常见的特效,功能强大的函数 IDirectDrawSurface::Blt( )理所当然地提 供了这种功能。 执行的速度还过得去 (不如用汇编写的快) 可惜效果不太好, 就象用 Windows 中的画笔缩放图象一样。Blt 函数的原形是这样的: HRESULT Blt( LPRECT lpDestRect, LPDIRECTDRAWSURFACE4 lpDDSrcSurface, LPRECT lpSrcRect, DWORD dwFlags, LPDDBLTFX lpDDBltFx ); lpDDSrcSurface 是源页面的指针,lpDestRect 和 lpSrcRect 分别是目标和源页面的矩 形的指针,如果两个矩形的大小不一致就会自动进行缩放。至于 dwFlags 嘛,按照透明色 的 情 况 给 它 DDBLT_KEYDEST|DDBLT_WAIT 、 DDBLT_KEYSRC|DDBLT_WAIT 或 是 DDBLT_WAIT 即可。最后一个参数 lpDDBltFx 指明了要使用的特效,可惜没什么有价值的 特效(除了下面将讲述的填色) ,给它 NULL 吧。5.5 后台缓存和换页后台缓存和换页对形成无闪烁的动画至关重要。 举一个例子, 要显示一个物体在一张图 片上运动,我们需要先画物体(a) ,再擦去物体(b),再把同时擦去的一开始被物体遮住的 背景画好(c),最后把物体画在新位置上(a’)。但这些操作需要一定时间,如果我们直接改主 页面则 b 和 c 时刻用户就会看到画面上没有物体,但 a 和 a’时刻画面上又有物体,那么用 户就会觉得画面有些闪烁。如何解决这个问题呢? DirectDraw 中的换页就可以帮我们这个忙。首先,我们得设置好一个换页链结构,它 由一组 DirectDraw 页面组成,每一个页面都可以被轮流换页至显示屏幕。当前正好位于显 示屏幕的页面,我们称之为主页面。等待换页至屏幕的页面,我们称之为后台缓存。应用程 序在后台缓存上进行绘图操作, 然后将此页面换页成为主页面, 原来的后台缓存就显示在屏 幕上了,而原来的主页面就成为了后台缓存。因为换页时主要只是改了改指针,所以这个过 程进行得挺快,我们的眼睛感觉不到闪动。当操作系统在屏幕上显示图像时,我们再对后台 缓存进行新的操作。所以我们只须不断改变缓存上的图象并不断换页即可。 我们既可以创建一个简单的双换页链结构(一个主页面和一个后台缓存),也可以创建一 个有时候更加快的多缓存换页链结构。一般我们只需像 4.2.1 节的例子那样使用双缓存换页 链结构即可。 换页所使用的函数是 IDirectDrawSurface::Flip( )。它的原形是: HRESULT Flip(LPDIRECTDRAWSURFACE lpDDSurface, DWORD dwFlags); 下面介绍它的参数: (1)lpDDSurface 换页链中另一个页面的 IDirectDrawSurface 接口的地址,代表换页操作的目 标页面。这个页面必须是换页链中的一员。该参数缺省值是 NULL, 在这种情况 下, DirectDraw 从换页链中按照前后隶属关系依次换页。 (2)dwFlags 换页的标志选项,常用 DDFLIP_WAIT,同 BltFast 中的 DDBLTFAST_WAIT 差不多。 一般我们这样即可换页: lpDDSPrimary-&Flip(NULL,DDFLIP_WAIT);5.6 页面的丢失与恢复当一个 DirectDraw 程序被最小化时,它就丧失了对页面的控制权,如果我们的程序不 知道,继续改变页面时就会发生“ DDERR_SURFACELOST ”错误。而当我们重新回到 DirectDraw 程序时 Windows 不会帮我们把页面恢复, 如果我们不自己恢复页面用户就会看 到黑屏。为了避免出现这种情况,我们可以写一个恢复页面的函数: void RestoreSurface( ) { lpDDSPrimary-&Restore( ); lpDDSBuffer-&Restore( ); lpDDSBack-&Restore( );//恢复所有页面 ReloadBitmap( );//自己的调图函数 } 值得注意的是 Windows 也不会帮我们恢复页面的实际内容, 我们要象上面的程序那样, 再调用自己的调图函数才行。应该什么时候调用 RestoreSurface( )呢?是不是每一条改变 页面的语句都要测试一下有没有发生 DDERR_SURFACELOST 错误呢?其实并不需要。 一 般游戏的引擎都是频繁刷新式,每秒钟要刷新几十次,每一次刷新必然要调用上面提到的 Flip( )。所以我们可以写一个 FlipSurface( )然后以后调用它来换页: void FlipSurface( ) { HRESULT ddrval=lpDDSPrimary-&Flip(NULL,DDFLIP_WAIT); if (ddrval==DDERR_SURFACELOST) RestoreSurface( ); } 5.7 页面的填色要对页面指定范围内填充某种颜色(一般是填充页面的透明色以达到清除的目的) ,可 以利用 IDirectDrawSurface::Blt( )函数。用法如下: MakeRect(x,x,x,x); //指定范围 DDBLTFX ddBltFx; ddBltFx.dwSize=sizeof(DDBLTFX); ddBltFx.dwFillColor=RGB(x,x,x); //要填充的颜色 lpDDS-&Blt(&rect,NULL,&rect,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx); 这里顺便讲一个重要的问题,即所谓的“555”和“565”。我们经常用 16 位色,但 16 除 3 不是整数,那么要各用多少位来表示红、绿、蓝呢?有的显卡是三种颜色各用 5 位,即 0rrrrrgggggbbbbb,被称为“555”。但其它显卡用 rrrrrggggggbbbbb 的形式来表示这三种颜 色,因为人眼对绿色更敏感,这就是“565”。所以如果你初始化时把屏幕置为 16 位色,填色 时就不能用 RGB( )了(但设置透明色时仍可) ,要写一段程序来判断显卡是“555”还是“565” 再自己根据情况转换颜色。不过如果我们想填充纯红、绿、蓝、黑则不用这么麻烦,这样就 可以: MakeRect(x,x,x,x); //指定范围 DDPIXELFORMAT ddpf.dwSize = sizeof(ddpf); lpDDSBuffer-&GetPixelFormat(&ddpf); DDBLTFX ddBltFx; ddBltFx.dwSize=sizeof(DDBLTFX); ddBltFx.dwFillColor=(WORD)ddpf.dwGBitM //如是纯红则为 dwRBitMask, //纯蓝则为 dwBBitMask lpDDS-&Blt(&rect,NULL,&rect,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx); 如果我们想填充纯黑色,可以把 dwFillColor 设为 0,并去掉所有与 ddpf 有关的语句。5.8 输出文字为了向页面输出文字, 我们首先要获得页面的 HDC(设备描述句柄), 然后调用 Windows GDI 函数向页面输出文字。由于获得句柄后将不能使用 DirectDraw 函数改动页面,所以输 出完文字后要立刻释放句柄。 HDC if (lpDDSXXX-&GetDC(&hdc) == DD_OK) //拿句柄 { SetBkColor(hdc, RGB(0, 0, 255)); //设置文字背景色, 如为透明则把这一句改为: //SetBkMode(hdc,TRANSPARENT); SetTextColor(hdc, RGB(255, 255, 0)); //设置文字颜色 TextOut(hdc,100,400, text, strlen(text)); //句柄, 左上角 X, 左上角 Y, //文字(char *), 文字长度 lpDDSXXX-&ReleaseDC(hdc); //释放句柄 } 第六章 DirectDraw 动画制作看完了上一章,我想你应该会迫不及待地想编个程序来试试 DirectDraw 了。这一章将 会举一个使用 DirectDraw 制作动画的例子,可以供你参考。 6.1 定时器定时器在动画和游戏中非常重要, 它可以使程序每隔一段时间执行一个函数。 用法如下: SetTimer(HWND hwnd, UINT ID, UINT Elapse, TIMERPROC TimerFunc); 四个参数分别为窗口句柄、定时器标识(同一程序内各个定时器的标识应不相同,一般 从 1、2、3...一直排下去) 、每隔多少毫秒(千分之一秒)执行一次程序,要执行的过程。 这个要执行的过程应这样定义: void CALLBACK MyTimer(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime); 这几个规定的参数都没什么用, 我们在过程里作自己的事就行了, 不用理这几个给我们 的参数。注意,有的 Windows API 函数在 TimerFunc 中用不了,而且在 TimerFunc 里不要 做一些费时间的东西。6.2 播放声音没有声音的世界是无法想象的, 好的音乐和音效可以为一个动画或游戏增色不少。 要在 程序中播放声音可以使用 MCI 或 DirectSound。DirectSound 可以同时播放超过 8 个文件, 不过使用起来不如 MCI 简单易懂,所以下面就讲讲如何用 MCI 播放 MIDI 和 WAV 吧。 使用 MCI 需要预先声明,我们可以在文件头#include &mmsystem.h&,并在菜单中找 到工程―设置―Link―对象/库模块,在“kernel32.lib”之前加入“winmm.lib” 要播放 MIDI 我们先要打开设备: MCI_OPEN_PARMS OpenP OpenParms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_SEQUENCER; //是 MIDI 类型文件 OpenParms.lpstrElementName = (LPCSTR) //文件名 OpenParms.wDeviceID = 0; //打开的设备的标识,后面需要使用 mciSendCommand (NULL, MCI_OPEN, MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT, (DWORD)(LPVOID) &OpenParms); //打开设备 接着就可以播放 MIDI 了: MCI_PLAY_PARMS PlayP PlayParms.dwFrom = 0; //从什么时间位置播放,单位为毫秒 mciSendCommand (DeviceID, MCI_PLAY, //DeviceID 需等于上面的设备标识 MCI_FROM, (DWORD)(LPVOID)&PlayParms); //播放 MIDI 停止播放: mciSendCommand (DeviceID, MCI_STOP, NULL, NULL); 最后要关闭设备: mciSendCommand (DeviceID, MCI_CLOSE, NULL, NULL); 打 开 WAV 文 件 与 打 开 MIDI 文 件 的 方 法 几 乎 完 全 相 同 , 只 是 需 要 将}

我要回帖

更多关于 外盘内盘是什么意思 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信