游戏编程

2020年05月09日 阅读数:64
这篇文章主要向大家介绍游戏编程,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。
游戏编程指南
A Guide to Game Programming

v1.10alpha
最后更新于2003.1.14

本文基于VC7.0 / DirectX 9.0 / Winsock 2.2
推荐使用Word 2000及以上版本阅读


你们看完以后若是有什么意见和建议请务必在留言簿提出,谢谢!!!
若是你认为任何地方写错了,请告诉我…
若是你认为任何地方难以理解,请告诉我…
若是你以为这篇东西还不算太垃圾,欢迎推荐给你的朋友?…


本文99%为原创内容,转载时请只给出链接,谢谢!
也但愿你们不要随便修改,谢谢!


使用"查看"----"文档结构图"可大大方便阅读本文档



彭博 著
By Peng Bo
Email: Kane_Peng@netease.com
QQ: 4982526
http://www.kanepeng.com
目 录
游戏编程指南 1
目 录 1
导 读 1
第一章 表述游戏的语言 1
1.1 VC.net概述 1
1.2 入门知识 4
1.2.1 数与数据类型 4
1.2.2 变量与常量 4
1.2.3 Namespace 5
1.2.4 操做符与表达式 6
1.3 预编译指令 7
1.4 结构,联合和枚举 8
1.4.1 结构 8
1.4.2 联合 9
1.4.3 枚举 10
1.5 控制语句 10
1.5.1 判断和跳转语句 10
1.5.2 选择语句 11
1.5.3 循环语句 13
1.6 函数 13
1.7 指针、数组与字符串 17
1.7.1 指针 17
1.7.2 数组 19
1.7.3 字符串 22
1.7.4 小结 23
1.8 多文件程序的结构 23
1.9 经常使用函数 25
第二章 如何说得更地道 29
2.1 定义和使用类 29
2.2 类的构造函数 32
2.3 类的静态成员 34
2.4 运算符重载 35
2.5 类的继承 38
2.6 虚函数和抽象类 41
2.7 模板 42
2.8 优化程序 45
2.9 调试程序 47
第三章 容纳游戏的空间 49
3.1 基本Windows程序 49
3.2 WinMain函数 53
3.2.1 简介 53
3.2.2 注册窗口类 53
3.2.3 建立窗口 55
3.2.4 显示和更新窗口 56
3.2.5 消息循环 57
3.3 消息处理函数 58
3.4 经常使用Windows函数 59
3.4.1 显示对话框 59
3.4.2 定时器 59
3.4.3 获得时间 60
3.4.4 播放声音 60
第四章 描绘游戏的画笔 61
4.1 初始化DirectDraw 61
4.1.1 简介 61
4.1.2 DirectDraw对象 62
4.1.3 设置控制级和显示模式 63
4.1.4 建立页面 64
4.2 后台缓存和换页 66
4.3 调入图像 67
4.4 页面的丢失与恢复 67
4.5 透明色 68
4.6 图像传送 68
4.7 程序实例 72
4.8 图像缩放 72
4.9 释放DirectDraw对象 72
第五章 丰富画面的技巧 74
5.1 填涂颜色 74
5.2 输出文字 75
5.3 GDI做图 75
5.4 程序实例 76
5.5 锁定页面 76
5.6 程序提速 78
5.7 特殊效果 82
5.7.1 减暗和加亮 82
5.7.2 淡入淡出 83
5.7.3 半透明 83
5.7.4 光照 84
5.7.5 动态光照 85
5.7.6 光照系统 88
5.7.7 天气效果 88
第六章 加速游戏的魔法 89
6.1 内嵌汇编简介 89
6.2 基本指令 90
6.3 算术指令 91
6.4 逻辑与移位指令 93
6.5 比较、测试、转移与循环指令 93
6.6 MMX指令集之基本指令 96
6.7 MMX指令集之算术与比较指令 98
6.8 MMX指令集之逻辑与移位指令 99
6.9 MMX指令集之格式调整指令 100
第七章 我没有想好名字 102
7.1 读取键盘数据 102
7.2 读取鼠标数据 103
7.3 恢复和关闭DirectInput 104
7.3.1 恢复DirectInput设备 104
7.3.2 关闭DirectInput 104
7.4 初始化和关闭DirectX Audio 104
7.4.1 初始化DirectX Audio 104
7.4.2 关闭DirectX Audio 105
7.5 播放MIDI和WAV音乐 105
7.5.1 调入MIDI和WAV文件 105
7.5.2 播放MIDI和WAV文件 106
7.5.3 中止播放 107
7.6 在3D空间中播放音乐 107
7.7 播放MP3音乐 109
7.7.1 调入MP3文件 109
7.7.2 播放MP3文件 109
7.7.3 中止播放和释放对象 110
第八章 支撑游戏的基石 111
8.1 链表 111
8.2 哈希表 111
8.3 快速排序 112
8.4 深度优先搜索 113
8.5 广度优先搜索 117
8.6 启发式搜索 120
8.7 动态规划 126
8.8 神经网络 128
8.9 遗传规划 129
第九章 向三维世界迈进 131
9.1 概述 131
9.2 基本知识 133
9.2.1 初始化DXGraphics 133
9.2.2 关闭DXGraphics 135
9.2.3 恢复DXGraphics设备 135
9.3 设置场景 135
9.3.1 设置渲染状态 135
9.3.2 设置矩阵 136
9.4 建立场景 137
9.4.1 调入3D场景 138
9.4.2 调入2D图像 139
9.5 刷新场景 140
9.6 渲染场景 141
9.6.1 渲染3D场景 141
9.6.2 渲染2D图像 141
9.7 改变场景 141
9.8 显示文字 142
9.9 程序实例 143
第十章 我没有想好名字 144
10.1 灯光 144
10.2 半透明 145
10.3 纹理混合 146
10.4 雾 148
10.5 凹凸贴图与环境贴图 149
10.6 粒子系统 149
10.7 骨骼动画 149
10.8 镜子 151
10.9 影子 151
第十一章 我没有想好名字 152
11.1 基本概念 152
11.2 程序流程 152
11.2.1 服务器端 152
11.2.2 客户端 153
11.3 程序实例 153
11.4 错误处理 158
11.5 显示IP地址 158
11.6 更有效地传送数据 159
第十二章 创造咱们的世界 161
12.1 程序流程 161
12.2 程序结构 162
12.3 基本方法 163
12.4 SLG编程要点 163
12.4.1 电脑AI 163
12.5 RPG & ARPG编程要点 163
12.5.1 迷宫的生成 163
12.5.2 脚本技术 163
12.6 RTS编程要点 163
12.6.1 寻路 163
12.6.2 电脑AI 163
12.7 FPS编程要点 164
12.7.1 移动 164
12.7.2 碰撞检测 164
12.8 游戏中的物理学 165
附 录 166
附录一 Windows常见消息列表 166
附录二 虚拟键列表 171
Windows消息中的虚拟键 171
DirectInput中的虚拟键 172
附录三 DirectX函数返回值列表 174
DirectDraw部分 174
Direct3D部分 181
附录四 Winsock函数返回值列表 183
附录五 游戏编程经常使用网址 187
附录六 中英文名词对照 188
附录七 常见问题及解决办法 189
1. 程序编译时出现"Warning" 189
2. "Cannot Execute Program" 189
3. "Unresolved External Symbol" 189
4. 运行时出错 189
5. 你们还有什么问题,能够告诉我 189
导 读

在开始阅读全文以前,但愿你能抽出一些时间阅读这里的内容…

1、你想编一个怎样的游戏?
(1)星际争霸,帝国时代,英雄无敌,大富翁4,轩辕剑3,传奇,石器时代…
这些都是正宗的2D游戏,其标志是:视角彻底固定或只有四个观察方向。这些游戏中特效很少,即便有也不须要使用汇编进行加速。
推荐阅读:第一、二、三、四、5章及第12章的相关部分。
可选阅读:第七、8章。若是须要网络功能,需阅读第11章。

(2)暗黑2,秦殇…
这是一类比较特殊的2D游戏,其特色在于各类特效(半透明,光影效果等)的大规模使用。有的此类游戏还能够使用3D加速卡来加速2D特效。
推荐阅读:第一、二、三、四、五、6章及第12章的相关部分。
可选阅读:第七、八、九、10章。若是须要网络功能,需阅读第11章。
因为如今的显卡几乎都能很好地支持3D加速功能,因此若是你打算放弃对没有3D加速卡的计算机的支持,可不阅读第四、五、6章,而推荐阅读第9章和第10章的第一、2节。

(3)反恐精英,雷神,魔兽争霸3,地牢围攻,FIFA,极品飞车,MU…
这些都是纯3D游戏,也表明了目前游戏的发展趋势。
推荐阅读:第一、二、三、七、九、10章及第12章的相关部分。
可选阅读:第8章。若是须要网络功能,需阅读第11章。
第一章 表述游戏的语言

想必你们都据说过“计算机语言”吧,咱们就要靠它来向计算机表述咱们的游戏究竟是怎样的----这个过程就是所谓“编程”。因为游戏对速度的要求较高,过去咱们通常使用C语言,由于用它编制的程序不只执行速度快,还能够充分地使用硬件的各类资源。而如今(不过也是十多年前的事了?)有了C++语言,它是对C语言的重大改进。C++语言的最大特色是提供了“类” ,成为了“面向对象”的语言。关于此,咱们会在第二章详细介绍。本章将先介绍一些游戏编程所必需的C++语言基础知识。最后须要提醒你们的是,在学习本章时最好边学边实践,本身试着写写C++程序。

1.1 VC.net概述
在切入C++语言以前,咱们有必要简略地介绍一下VC.net的基本使用方法。首先固然是安装VC.net,值得注意的是,VC.net中附带的DirectX SDK并非8.1最终版,推荐访问http://www.microsoft.com/msdownload/platformsdk/sdkupdate/更新之。而后启动VC.net,会看见一个"Start Page",在"Profile"一栏选择"Visual C++ Developer"。第二步是转到"Get Started"一栏,选择"New Project",并在出现的窗口选择"Visual C++ Projects"一栏中的"Win32 Project",填好"Name"和"Location",按"OK",这时会出现一个"Win32 Application Wizard"。此时须要在"Application Settings"一栏把"Empty project"前的方框勾上,以防VC.net在工程中加入一些无心义的垃圾;若是想编DOS窗口下的程序,例如这一章和下一章的程序,还要把"Console Application"选上。最后按"Finish"就完成了工程的建立。

图1.1 Start Page
在屏幕的左边,咱们能够看到出现了"Solution Explorer"和"Dynamic Help"两栏,其中"Solution Explorer"又可变为"Class View"或"Resource View",其内容分别为:工程中包含有什么文件,工程中的类、变量和函数的结构,工程中包含有什么资源。至于"Dynamic Help"就是动态帮助,很是方便。
你们会注意到如今工程尚未文件,因此接下去咱们须要学习如何新建一个文件,若是你想新建的文件是C++程序文件(.cpp),那么应该在"Source Files"上按右键,选择"Add" --- "Add New Item",在出现的窗口中选择"C++ File",定好名字,再按"Open"便可(假如你加入的文件叫"Haha.cpp",Solution Explorer将如右图所示);若是你想新建的文件是头文件(如今先不要管头文件是什么),在"Header Files"按右键,也选择"Add" --- "Add New Item",在出现的窗口中选择"Header File",也定好名字并按"Open"就好了。
在工具栏的中部能够更改程序的模式:Debug或是Release。在通常状况下,建议你们选择Release,能够减小文件大小并增长运行速度;而在调试程序时,必须选择Debug。在默认的状况下,编译好的程序会相应的放在工程目录下的"Debug"或"Release"子目录内。
最后咱们来看看一个重要的操做,即如何把LIB文件(如今也不要管LIB文件是什么…)加入工程中:首先在"Solution Explorer"窗口中找到工程名,而后在上面按右
图1.2 键并选择"Properties",在出现的窗口中选择"Linker" --- "Input" --- "Additional Dependencies",最后填上要加入的LIB文件名便可。

OK,下面让咱们看一个简单的C++程序:
/*-------------------------------------------------
First C++ Program
--------------------------------------------------*/

#include //如今你只需知道要使用输入输出语句就必须写这行。
//这一行结尾不用加分号由于它不是真正的C++语句。
//在1.3节将会对此作出解释。
using namespace std; //这是什么意思呢…在1.2.3节会有解释。

int a=5; //声明变量a,同时顺便赋5给它。C++中变量都需先声明后使用。
//int说明了a的数据类型为整数。

int square(int x); //声明函数square,它有一个参数,为int类型,即整数。返回值也
//为int类型。C++中的函数都须要先声明后给出定义。

int square(int x) //函数真正定义。
{
return x*
x; //返回x*x,能够在一个语句中的间隔位置插入回车将其分红几行。
}

int main( ) //主函数,每一个DOS窗口下的C++程序都须要它
{
int A; //声明变量A。C++中变量声明的位置是比较随意的。
cout<<"请输入A:"; //输出"请输入A:",箭头的方向很直观。
cin>>A; //输入A, 注意箭头方向的更改。
cout<<"A="< < (3) 用"{"和"}"括起的句被称为块语句,形式上被认为是一个语句(就像PASCAL中的begin和end)。
(4) "//"至行尾为注释,"/*"至"*/"中全为注释,它们不会被编译。
(5) 主体是由一个个函数所构成的。在1.6节将会详细地介绍函数。

1.2 入门知识
1.2.1 数与数据类型
对于十进制数的表示,C++与其它语言一致,一样能够使用科学记数法,如3.145e-4。在C++中还能够直接表示十六进制数,只要在前面加上"0x"便可。如0x23。若是要表示的是负十六进制数,能够直接在"0x"前加上负号。为了清楚说明一个数是float类型,咱们能够在数的结尾加上"f",例如1.00f。不然,该数默认为double类型。
下面咱们来看看C++中的基本数据类型:
bool(逻辑型) char(字符或8位整数) short(16位整数)
int(16位或32位整数) long(32位整数) float(32位浮点数)
double(64位浮点数) long double (80位浮点数)
bool类型用true和false表明真与假,其实际占用空间是8位。
某一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.9节。
咱们还能够借助typedef定义本身的数据类型,例如typedef myint unsigned int;后myint就等价于unsigned int。VC.net系统已经预先用typedef定义好了很多类型,例如BYTE等价于unsigned char,WORD等价于unsigned short,DWORD 等价于unsigned long等等。

1.2.2 变量与常量
C++中的变量几乎可在任何地方处定义,并且能够同时定义多个变量,如int a,b;。但每个变量只在最紧挨它的一对{和}符号内起做用,只有在全部函数以外定义的变量才为全局变量,即在整个cpp文件中有效。若是局部变量和全局变量重名,调用时会使用局部变量,若是必定要使用那个全局变量,调用时在变量名前加上"::"便可。这里建议你们尽可能少用全局变量,由于它可能使程序变得混乱和难于调试。
全部变量定义的前面都可加上修饰符"const"表示它是常量,不能在程序中改变它的值。其实若是咱们不打算在程序中改变某变量的值,咱们就能够把它声明为常量以防止意外改动。咱们还可加上修饰符"static"表示此变量是静态变量,这个要举一个例子以方便说明:好比说在某一个函数内有这样一条定义:static int count=0; ,那么程序执行前就会为count这个变量开辟一块固定的空间并把count的初值设为0。之后每次执行这个函数时,程序不会象普通变量那样从新为它分配空间,也就是不会改变它的位置和数值,换句话说,它的生命周期与整个程序同样。这样只要在函数中再加一句count=count+1便可统计这个函数执行了多少次。

1.2.3 Namespace
Namespace是一个挺有趣的东西,它的引入是为了方便咱们使用相同名字的变量、常量、类(在第二章咱们会接触类)或是函数。一个Namespace是这样定义的:
namespace xxx //xxx是namespace的名字
{
在这里能够像日常同样定义各类东西
}

之后要使用某个namespace中的东西,好比说xxx中的aaa,像这样:xxx::aaa便可。不过这样好像挺麻烦的----无缘无故就多出了一个"xxx::"。因而有了"using namespace xxx;"这种语句,能够帮你省下这几个字符。记住,"using namespace"也只是在最紧挨它的一对{和}符号内起做用,在全部函数以外执行的这条语句才在整个文件中有效。注意:
namespace s1
{
int a=0;
}

namespace s2
{
float a=0;
}

void main( )
{
using namespace s1;
using namespace s2;
//a=a+1; //这句是错误的!由于编译器此时没法肯定a在哪一个namespace
s1::a = s2::a + 1; //这样就是正确的
}

那么咱们在第一个程序中为什么要using namespace std;呢?其实也是为了把"std::cout"变成简洁一点的"cout"。请看1.3节。

1.2.4 操做符与表达式
最后要说的就是C++中的操做符和表达式,与其它语言相同的就不在此赘述,讲讲一些与其它语言不一样的内容:
%为取余数,好比说20%3=2。
在逻辑表达式中,用==表示相等,!=表示不等,好比说(4==5)为FALSE;大于等于用>=表示,小于等于则是<=。&&表示逻辑与,||表示逻辑或,!表示逻辑非。例如若是a=8,则( (a!=9) && ( (a==3) || (a==6) ) )为false。
<<(左移)和>>(右移)很是好用,做用是把这个数的二进制形式向左或右移位(cin和cout中的<<和>>被使用了运算符重载,因此意义不一样,具体可参阅2.4节),举两个例子也许会好说明些:
18(二进制形式为0010010)<<2获得72(二进制形式为1001000)
77(二进制形式为1001101)>>3获得9(二进制形式为0001001)
咱们能够看到,左移和右移能够代替乘或除2的n次方的做用,并且这样作能够节省很多CPU运算时间。在程序优化中这一种方法是十分重要的,例如a*9可用(a<<3)+a代替(注意,"+"运算比"<<"运算优先)。
C++还提供了算术与&、算术或|、算术非~,算术异或^等重要的二进制运算。好比25(11001)^17(10001)等于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。
++/--操做符,即自增1/自减1,是C++的特点之一。a=7; a++; 则a变为8。(C++语言岂不是成了D语言??) 注意a++和++a不一样:++a是先自增后给值,a++是先给值后自增:若a=12,(a++)+5为17,(++a)+5却为18,不过a后来都变成了13。
最后要说的是一个颇有趣的操做符,就是"?:",它能够在必定程度上代替if语句的做用,由于"A?B:C"等价于"if A then 返回B else 返回 C"。举一个例子,(a>b)?a:b可返回a和b中的较大者。
值得注意的是因为C++的操做符众多,因此运算前后次序较复杂,若是没有注意到这一点而少加了几个括号将出现出人意料的结果。下面按优先级高到低列出了C++中的操做符:
1. ()(小括号) [](数组下标) .(类的成员) ->(指向的类的成员)
2. !(逻辑非) .(位取反) -(负号) ++(加1) --(减1) &(变量地址)
3. *(指针所指内容) sizeof(长度计算)
4. *(乘) /(除) %(取模)
5. +(加) -(减)
6. <<(位左移) >> (位右移)
7. < (小于) <= (小于等于) > (大于) >= (大于等于)
8. == (等于) != (不等于)
9. & (位与)
10. ^ (位异或)
11. | (位或)
12. && (逻辑与)
13. || (逻辑或)
14. ? : (?表达式)
15. = += -=(联合操做)
在表达式方面,C++基本与其它语言相同。只是C++为了简化程序,提供了联合操做:"左值 操做符=表达式"等价于"左值= 左值 操做符 表达式"。例如a*=6+b等价于a=a*(6+b),c+=8等价于c=c+8。
在C++中,全部表达式都有返回值。通常来讲,(左值 操做符 右值)表达式的返回值与右值相同;条件表达式如(a>b)的返回值在条件成立时为1,不成立时为0。

1.3 预编译指令
如今该解释在第一个例子中#include 的意义了,其实这句是预编译指令。预编译指令指示了在程序正式编译前就由编译器进行的操做,能够放在程序中的任何位置。常见的预编译指令有:
(1)#include 指令
该指令指示编译器将xxx.xxx文件的所有内容插入此处。若用<>括起文件则在系统的INCLUDE目录中寻找文件,若用" "括起文件则在当前目录中寻找文件。通常来讲,该文件后缀名都为"h"或"hpp",被称为头文件,其中主要内容为各类东西的声明。
那么为何在第一个程序中咱们能够省略"iostream.h"的".h"呢(你们能够本身找找,会发现并无一个叫iostream的文件)?这有一个小故事。当初ANSI在规范化C++的时候对iostream.h进行了一些修改,好比说吧其中的全部东西放进了一个叫std的namespace里(还有许多头件都被这样修改了)。可是程序员就不答应了,由于这意味着他们的程序都要被修改才能适应新编译器。因而ANSI只好保留了对原来调用iostream.h的方法(#include )的支持,并把调用新的iostream.h的方法修改为如今的样子(#include )。
言归正传,咱们#include 以后编译器会看到iostream.h中对输入输出函数的声明,因而知道你要使用这些函数,就会将包含有输入输出函数定义的库文件与编译好的你的程序链接,造成可执行程序。
注意<>不会在当前目录下搜索头文件,若是咱们不用<>而用""把头文件名扩起,其意义为在先在当前目录下搜索头文件,再在系统默认目录下搜索。

(2)#define指令
该指令有三种用法,第一种是定义标识,标识有效范围为整个程序,形如#define XXX,常与#if配合使用;第二种是定义常数,如#define max_sprite 100,则max_sprite表明100(建议你们尽可能使用const定义常数);第三种是定义"函数",如#define get_max(a, b) ((a)>(b)?(a):(b)) 则之后使用get_max(x,y)可获得x和y中大者(这种方法存在一些弊病,例如get_max(a++, b)时,a++会被执行多少次取决于a和b的大小!因此建议你们仍是用内联函数而不是这种方法提升速度。关于函数,请参阅1.6节。不过这种方法的确很是灵活,由于a和b能够是各类数据类型,这个特色咱们能够换用2.7节介绍的模板实现)。

(3)#if、#else和#endif指令
这些指令通常这样配合使用:
#if defined(标识) //若是定义了标识
要执行的指令
#else
要执行的指令
#endif

在头文件中为了不重复调用(好比说两个头文件互相包含对方),常采用这样的结构:
#if !(defined XXX) //XXX为一个在你的程序中惟一的标识符,
//每一个头文件的标识符都不该相同。
//起标识符的常见方法是若头文件名为"abc.h"
//则标识为"abc_h"
#define XXX
真正的内容,如函数声明之类
#endif

1.4 结构,联合和枚举
1.4.1 结构
结构能够把不一样变量变为一个变量的成员。例如:
struct S //定义结构S
{
short hi; //结构S的第一个成员
short lo; //结构S的第二个成员
};
S s; //定义S类型的变量s

而后咱们就能够像s.hi和s.lo同样使用s的成员。

1.4.2 联合
联合可让不一样变量共享相同的一块空间,举个例子:
#include
using namespace std;

struct S
{
short hi;
short lo;
};

union MIX
{
long l;
S s;
char c[4];
};

void main ( )
{
MIX mix;
mix.l=100000;
cout< <
cout<
}

此时mix所在的内存位置的状况是这样的:

图1.2

1.4.3 枚举
枚举的用处是迅速定义大量常量。例如:
enum YEAR //能够给枚举起一个名字
{
january=1, //若是不加上"=1"月份将依次为0-11,不符合咱们平时的习惯,因此
//能够加上它。
february,
march,
april,
may,
june,
july,
august,
september,
october,
november,
december
};

1.5 控制语句
C++中的控制语句格式简洁且功能强大,充分证实了它是程序员的语言。

1.5.1 判断和跳转语句
C++中的判断语句格式以下:
if (条件) 真时执行语句; else假时执行语句;
例如:
if (a>=9) a++; else a--;
值得注意的是C++中的“真”与“假”的意义就是这条表达式不为0 仍是为0。好比if (a-b) do_stuff; 的做用与 if (a!=b) do_stuff; 相同。
臭名昭著的跳转语句(不过有时候你仍是不得不用)则是这样的:
标号:语句;(通常来讲标号用"_"开头)
goto标号;
举个例子方便你们理解:

#include
using namespace std;
void main( )
{
int target=245; int a;
cout<<"欢迎您玩这个无聊的猜数游戏"<
cout<<"您的目标是猜中我想好的数"<
cout<<"请输入第一次猜的数:";
_input: cin>>a;
if (a>target)
{
cout<<"您刚才输入的数太大了!"<
cout<<"";
goto _input;
}
else if (a
{
cout<<"您刚才输入的数过小了!"<
cout<<"再猜一次:";
goto _input;
}
else
cout<<"恭喜你,猜对了!"<
}

1.5.2 选择语句
C++中的选择语句很灵活,咱们先看看与其它高级语言类似的形式:
switch (变量)
{
case常量/常数1:
语句;//注意,这里可有多个语句且不需用{ }括起,不过其中不能定义变量。
break; //为何要加这一句呢?下面会解释。
case常量/常数2:
语句;
break;
……
case 常量/常数n:
语句;
break;
default: //如全部条件都不知足则执行这里的语句。
语句;//这后面就没有必要加break;了。
}

break的做用实际上是防止继续执行后面的语句,试试下面的程序:
#include
using namespace std;

const aaa=5;

void main( )
{
int a;
cin>>a;
switch(a)
{
case 0:
cout< <<"您输入的是0";
case 3:
cout< <<"您输入的是3";
case aaa:
cout< <<"您输入的数与AAA相等";
default:
cout< <<"???";
}
}

按照通常人的想法,当你输入0、二、三、5时会分别获得"您输入的是0"、 "???"、 "您输入的是3"、 "您输入的数与aaa相等",不过你能够试试结果是否真的是这样。试完后,你能够加上一些break再看看结果又将是怎样。

1.5.3 循环语句
先介绍while循环语句,共有两种形式:第一种是while (条件) 语句,意义为先判断条件是否知足,若是知足则执行语句(不然退出循环),而后重复这个过程。第二种形式是do 语句 while (条件),意义为先执行语句再判断条件,若是条件成立则继续执行语句(不成立就退出循环),这个过程也会不断重复下去。例如while((x+=1)=y);语句能够使x不断加1直到变成与y的值相同。
而后就是C++最强大的for循环,它的形式以下:
for (语句1;条件;语句2) 语句3 (其中任何一部分均可省略)
看上去好像很古怪,其实它就等价于这样:
语句1;
while (条件)
{
语句3;
语句2;
}
好比for (i=1;i<=100;i++) cout< <
又好比for (cin<
for (;;);将会陷入死循环,注意它比while(1);执行速度快。
在循环语句中可顺便定义变量,如for (int i=1;i<=100;i++) cout< <
有时咱们需在循环中途跳至循环外,此时break又能够派上用场了。有时又须要在循环中途跳至下一次循环,continue能够帮你这个忙。

1.6 函数
C++中的函数是这样定义的:
返回值数据类型 函数名(参数表)
{
语句;
}
例如:
int fun(int x, int y)
{
x=x+1; //注意这一句只能在函数内改变x的值,请参阅下文
return x*y; //返回x*y,并会马上退出该函数
}
当返回值数据类型为void时表示无返回值,就像其它语言中的“过程”。
参数表中在不引发歧义的状况下可有缺省值,例如void xyz(int a, int b=0); (只需在声明函数时说明缺省值),则xyz(12)等价于xyz(12,0)。
在main函数开始前最好声明一下程序中的函数(main函数没必要声明),声明格式为:
返回值的数据类型 函数名(参数表); (注意有一个分号)
在声明的参数表里能够省略变量名,例如void myfunc(int,float);
在函数的定义(而不是声明)的最前面加上"inline"说明其为内联函数可提升一点速度,但增大了文件的大小。
就象其它语言同样,C++中的函数能够递归调用(本身调用本身)。它还有一个区别于其它语言的重要特性----能够"重载",例如若是有这样两个函数:
float fun(float x)
{
return x*x*x;
}

int fun(int x)
{
return x*x;
}
假设a为4,那么若是a为一个float类型变量,fun(a)会返回64;但若a为int类型,fun(a)会返回16。能够想像,这个特性在实际编程中将十分有用。
下面咱们再看看一个问题:有人想编一个交换a和b的函数,因而他这样写:
void swap(int a, int b)
{
int t=a;
a=b;
b=t;
cout<<"a="<
<<"
void swap(int &a, int &b)
{
int t=a;
a=b;
b=t;
}
在默认状况下,函数的返回值也只是一个复制品,若是你必定要让它返回真正的东西,能够像这样写函数:int &foo(){do_something;}。不过注意在1.7.1节中说明的限制----咱们不能这样返回在函数中建立的变量。
下面举一个使用了函数的程序例子(比较无聊?):

#include
using namespace std;

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,sb;
cout<<"这个十分无趣的程序会帮您计算一些几何体的体积"<
cout< <<"0表明要计算圆锥体"<
cout<<"1表明要计算圆柱体"<
cout<<"2表明要计算拟柱体"<
cout<<"请选择:";
int choice;
cin>>choice;
cout<
switch(choice)
{
case 0:
cout<<"底面半径=?";
cin>>r;
cout<<"高=?";
cin>>h;
cout< <<"体积="<
cin>>h;
cout< <<"体积="<
cin>>sm;
cout<<"下表面的面积=?";
cin>>sb;
cout<<"高=?";
cin>>h;
cout< <<"体积="<
这里顺便提醒一下,咱们毫不应该将在函数中建立的变量的地址或引用返回。由于在退出一个函数时,在函数体中所建立的全部变量都将被销毁,因此虽然地址是能够传回去,但它所指向的内容已经毫无心义。那么指针呢?在函数里new的指针能传回去吗?答案是能够,可是,你必须为极有可能发生的内存泄漏负责,由于要把这些指针找出来一个个delete掉实在很麻烦。“咱们能够试试返回静态变量的地址或引用”,有的人会这样想。这在大多数状况下是个好办法,可是仍然存在可能的漏洞----由于这个静态变量的地址由始到终都是不变的。以下:
int &foo(int a)
{
static int t;
t=a;
return t;
}

int main()
{

if ( foo(1) == foo(2) ) //这个条件将会成立!

}

指针有什么用呢?第一个用处是能够动态分配大量内存。咱们知道DOS下不少语言对数组的大小有很严格的限制,但C++ 却能够开辟很是大的数组,并且能够用完就释放内存,这就是指针的功劳。具体会在介绍数组时介绍。
咱们还能够建立函数指针,这也是C++的特点之一。所谓函数指针,顾名思义就是指向一个函数的指针。举个例子,若是有一些函数:
float aaa ( int p );
float bbb ( int q );
float ccc ( int r );
那么咱们能够这样定义一个函数指针:float (*p) (int);
这时就能够将p指向上面的各个函数,如p = bbb;执行后p(100);就等价于bbb(100);
若是在某一段程序中须要根据状况(例如某变量的值)调用aaa、bbb,ccc函数,那么咱们能够没必要使用烦琐的switch,只需使用函数指针数组便可,很是方便。
顺便说一下这时应该如何用typedef定义p的数据类型:typedef float(*pFunction)(int);后直接用pFunction p;便可定义上面的那个指针p。

1.7.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 a; int b[a];这样是错误的。
(2)你获得的其实是一个特殊的与"数组名"同名的指针。

第二点也许有些费解,你能够试试这段程序就会明白:
#include
using namespace std;

void main( )
{
int abc[1000]={0}; //这样就能够使数组被预先清0
//注意int abc[1000]={1};会使abc[0]=1而其它元素=0
abc[0]=987;
cout<<*abc<
*abc=787;
cout<
}

咱们还能够直接使用指针建立数组。好比说咱们要临时分配一块空间,存储100000个int类型数据,那么就能够这样作:int *p; p=new int[100000];(你能够将其合并为int *p=new int[100000],在这里又出现了一个新操做符"new"),则系统会在内存找到一块足够大的空闲空间,再将p指向这块空间的起始位置,之后就能够把p当成一个数组来使用了。这种办法的第一个好处是用完这块内存后能够释放内存(其实你应该永远这样作,不然会形成所谓Memory Leak,即内存资源泄漏),就象这样便可:delete[ ] p;("delete[ ]"也是C++中的操做符);第二个好处是能够动态定义数组,例如:
int a; cin<
因此,建议你们使用new来建立数组。
不过直接使用刚才用来建立数组的指针并不方便(试试p=&p[100]能实现把p指向p[100]吗?可能会死机!),最灵活的办法是把一个另外的指针指向数组的元素,由于指针能够进行加减运算。好比说若是p=a[0],执行p+=46; 便可使p指向a[46],再执行p--;则p指向a[45]。看看下面的例子:
#include
using namespace std;

void main( )
{
int *p,*q;
p=new int[100000];
q=&p[0];
for (int i=0;i<100000;i++)
*(q++)=0; //这样也能够清零
q=&p[1];
*q=128; //把p[1]变成128
cout<
delete[ ] p; //删除数组用delete[ ]
delete q; //删除指针用delete
//要养成用完指针就释放的良好习惯
}

有时候你可能会忘记已经释放了一个指针,仍去使用它,结果会出现不可预料的结果。为了防止出现这种状况,你能够在释放完指针后再把它设为NULL以保证它不被继续使用。咱们能够用这样两个宏:
#define SAFE_DELETE(p) { if(p) { delete (p); (p)=NULL; } }
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] (p); (p)=NULL; } }

下面还要讲讲使用指针建立高维数组的方法,由于此时要用到指针的指针(指针也是变量,也要占内存,因此也有本身的地址)甚至指针的指针的指针的……(啊!有一位听众晕倒了!谁抬他出去?)下面的一段程序演示了如何建立一个高维数组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数组中的每一指针分配内存,将其也变为一个个数组
下面是一个二维数组p[n][m]的结构:
图1.4
若是你弄懂了上面的程序,你就能够再玩点新花样:定义不对称数组。好比这样:
int **p; *p=new int *[10];
for (int i=0;i<10;i++)
p[i]=new int[i+1];

1.7.3 字符串
C++中的字符串其实也是指针的一种,由于并无一种基本数据类型是字符串,所谓字符串实际是一个以"/0"(这叫作转义符,表明一个ASCII码为0的符号)做为结束标志的一个字符指针(char *),它其实是一个字符数组,就像图1.2中那样。因此若是有一个字符串s为"abc",实际上它为"abc/0",sizeof(s)会返回4,而不是3。定义字符数组时也要记住多留一位。
通常是用字符指针的方法定义字符串的:char* str = "muhahaha";,但咱们知道使用指针前必定要先找好初地址,因此事实上执行的是将str指向一个const char[]。因为这里有个const,咱们没有必要用delete[]释放这个字符指针。用过BASIC的人要注意C++中的字符串并不能比较(用==比较两个指针时它只会比较两个指针的所指向的地址是否相同)、相互赋值、相加和相减,这些操做通常是靠使用系统提供的字符串操做函数实现的,请参阅1.9节。请特别注意字符串不能相互赋值,请看下面一段代码:
char *str="aaaa";
char *str1="oh";
str1=str; //!!!
cout<

输出很正常,彷佛咱们实现了拷贝字符串的目的。然而仔细想想,把一个指针赋给另外一个指针时到底会发生什么?假设str指针原本指向地址0x0048d0c0,而str1指针指向0x0048d0bc,那么执行str1=str;后两指针将同时指向地址0x0048d0c0!C++并不会为了字符串搞特殊化,指针的赋值操做只会简单地拷贝地址,而不是拷贝内容。要拷贝内容,还得靠1.9节介绍的strcpy( )。

1.7.4 小结
学了这么多C++知识,你们是否是有点疲倦了呢?若是你想兴奋一下,那么就看看下面这段程序吧,它能够输出π的前781位。
#include
using namespace std;
long a=10000,b=0,c=2800,d,e=0,f[2801],g;
void main()
{
for(;b-c;)f[b++]=a/5;
for(;d=0,g=c*2;c-=14,cout<
for(b=c;d+=f[b]*a,f[b]=d%--g,d/=g--,--b;d*=b);
}
程序使用了C++提供的全部能简化代码的手段,包括前面没提到的逗号(能够将几个语句硬拼在一块儿,返回值以最靠右的语句为准)。它表现出来的数学功底是很惊人的,值得你们研究研究。固然,不提倡写这样费解的代码!

1.8 多文件程序的结构
记得之前我第一次使用Visual C++编游戏的时候,因为当时对C++还不是很熟,调试了好久都没有成功。后来把程序email给了一位高手叫他看看问题在哪里,过了几天他把程序送回来时已经能够运行了,原来他在个人头文件中声明变量的语句前都加了一个"extern"。这是什么意思呢?当时我还不清楚,由于不少书上并无讲多文件的程序应该怎么写。不过如今当你看完这一节时我想你就应该明白了。
首先咱们来看看多文件程序成为可执行程序的全过程:
图1.5
咱们能够发现,库文件(扩展名为LIB,实际上是一种特殊的已经编译好的程序。系统函数的定义都是存在LIB内,以使你看不到它们的源代码)是在最后的链接一步加入程序的,各个文件也是在这一步才创建联系的。
extern的做用就是告诉编译器此变量会在其它程序文件中声明。把这种外部变量声明放在头文件里,再在每一个文件中都包含这个头文件,而后只要在任何一个文件中声明变量,全部文件就均可以使用这个变量了。若是不加extern,各个文件使用的变量虽然同名但内容不会统一。
在各个文件中也须要先声明函数,以后才能使用它。不过这时用不着使用extern了。
最后咱们来看看一个简单的多文件程序的例子:

/*---------------main.h----------------*/

#if !(defined MAIN_H)

#include
using namespace std;

extern int a;

void print();

#define MAIN_H
#endif


/*---------------main.cpp----------------*/
#include "main.h"

int a;

void main()
{
a=3;
print();
}


/*---------------function.cpp----------------*/
#include "main.h"

void print()
{
cout<
}

1.9 经常使用函数
C++与其它语言的一大区别是提供了庞大的函数库,能用好它就能够提升你的效率。
先看看 里面的:
int rand( ):返回一个随机的整数。
void srand(int):根据参数从新初始化随机数产生器。
int/float abs(int/float):返回数的绝对值。
min/max(a,b):返回a和b中的较小/大者,用#define定义的,你们不用担忧效率。
int atoi(char *s);,返回由s字符串转换成的整数。
double atof(char *s);,返回由s字符串转换成的浮点数。
char* gcvt(double num, int sig, char *str);,num为待转换浮点数,sig为转换后数的有效数字数,str为目标字符串起点。函数返回指向str的指针。举个例子,若是sig=5那么9.876会转换成"9.876",-123.4578会变成"-123.46",6.7898e5就成了"6.7898e+05"。

而后是 里面的数学函数:
sin、cos、tan:这个你应该懂吧?。
asin、acos、atan:反三角函数。
sinh、cosh、tanh:双曲三角函数。
log、log10:天然和经常使用对数。
exp、pow10:上面两个函数的反函数。
pow(x,y):返回x的y次幂。
sqrt:开平方根。
ceil:返回最小的不小于x的整数。
floor:返回最大的不大于x的整数。
hypot(x,y):返回x的平方加上y的平方再开方的值。

文件读写函数在 里面,使用方法是:
首先定义指向文件的指针并打开文件,例如FILE *file = fopen ("aa.bbb", "rb");,其中aa.bbb为你要打开的文件名(注意,若是在VC.net的开发环境中按F5或Ctrl+F5执行程序,程序的默认文件读取目录是工程的目录,而不是工程目录下的Debug或是Release目录),若是有路径则要用"//"或"/"代替"/"。"rb"是打开的模式,基本模式有这些:
表1.1
可读数据? 可写数据? 打开文件时读写指针位置 如不存在则创立新文件?
r 是 否 文件头 否
w 否 是 文件头 否
a 否 是 文件尾 是
r+ 是 是 文件头 否
w+ 是 是 文件头 是
a+ 是 是 文件尾 是
在基本模式后加上b或t可设定要打开的是二进制文件仍是文本文件。对于前者,咱们打开文件以后用fread能够读入数据,读写指针也会随以后移。fread的使用方法为:fread(p, size, n, file);,p为指向读出的数据将存放的位置的指针,size为每个数据块的字节数,n为要读多少个数据块,file则为刚才定义的指向文件的指针。例如fread(&a[0][0], sizeof(a[0][0]), sizeof(a)/sizeof(a[0][0]), file);可将数据读入二维数组a。须要注意的是这时你获得的数据为ASCII码!例如若是文件的内容为"10",你将读出49和48这两个数(1和0的ASCII码)。用fwrite则能够写数据,形式与fread如出一辙,使用方法也相同。
对于文本文件,咱们应该使用fscanf( )和fprintf( )函数来读取和写入数据。这两个函数比较灵活,让咱们看下面一段程序:
#include
using namespace std;

char s[5]="abcd";
int i=967;
float f=3.1415;
char c='x';

void main()
{
FILE *file=fopen("aa.txt","wt+");
fprintf(file,"str1=%s",s); //%s表示在这个位置上是一个字符串
fprintf(file,"/nint2 = %d",i); //%d表示整数,/n表示换行
fprintf(file,"/nfloat3=/n%f/nCH AR 4 = %c",f,c);
//%f表示浮点数,%c表示字符,能够把几个fprintf合起来写
fclose(file);
}
运行完后aa.txt的内容是:
str1=abcd
int2 = 967
float3=
3.141500
CH AR 4 = x
fscanf( )和fprintf( )的使用方法几乎是彻底同样的,惟一的区别是,若是你要把数据读入普通变量,要在变量的前面加一个"&",使fprintf能够修改变量的值。固然,若是要读入的是字符串之类的指针就没必要这样了。
fseek能够移动读写指针,形式为fseek(file, offset, whence);,file为文件指针,whence为寻址开始地点,0表明开头,1表明当前位置,2表明文件尾。offset则为需移动的字节数。
使用ftell(file);能够得知当前的读写指针位置(离文件头有多少个字节的距离)。
其实还有一种简单不少的文件读写方法(也更符合C++标准):
#include
using namespace std;
int a;
……
iofstream file;
file.open("abc.dat"); //使用file.open("abc.dat", ios::binary);可指定为二进制模式
file>>a; //就像cin
file<<"abcdefg"; //就像cout
……

接着要说的是经常使用的字符串函数,在 内有它们的定义。
char *strcpy(char *dest, char *src);,该函数使dest=src并返回新的dest。使用它还能够实现字符串和字符数组之间的转换。
char* strcat(char *dest, char *src);,将src链接到dest的后面,并返回新的dest。
char* strstr(char *s1, char *s2);,返回指向s2在s1中第一次出现的位置的指针。
char* strchr(char *s1, char c);,返回指向c在s1中第一次出现的位置的指针。
char* strlwr(char *s);,将s中的全部大写字母转为小写。
char* strset(char *s, char c);,将s内全部字符替换为字符c。
int strlen(char *s);,返回字符串的长度。

最后是 中的内存函数:
memcpy(char *dest, char *src, int n);,将从src开始的n个字节的内存内容拷贝到从dest开始的内存中。注意dest和src在内存中的位置不能重叠。
memmove(char *dest, char *src, int n);,也能够实现拷贝,dest和src在内存中的位置能够重叠。固然,它比memcpy慢。
memset(s, c, n);,将从s开始的n个字节都设为c。能够用来将数组和结构清零。

第二章 如何说得更地道

C++和C最大的区别在于C++是一种面向对象(object-oriented)的语言,即程序是以对象而不是函数为基础,因此严格说来,咱们在第一章所讨论的还不是地道的C++程序。类(class)正是实现面向对象的关键,它是一种数据类型,是对事物的一种表达和抽象。类拥有各类成员,其中有的是数据,标识类的各类属性;有的是函数(类中的函数又叫方法),表示对类可进行的各类操做。举一个例子,咱们能够创建一个“草”类,它能够有“高度”等各类属性和“割”、“浇水”等各类方法。

2.1 定义和使用类
让咱们先看一个使用了类的程序:
//-----------------------------grass.h---------------------------------
class grass //定义grass类
{
private: //声明下面的成员为私有。类外的函数若是试图访问,编译器会告诉你发生错
//误并拒绝继续编译。缺省状况下类中的一切均为私有,因此这一行能够省略。
int height; //通常来讲,类中的全部数据成员都应为私有
//不过本章后面的程序为了便于说明也拥有公有数据成员
public: //下面的成员为公有,谁均可以访问。
void cut( );
void water( );
int get_height( );
void set_height(int newh);
}; //这个分号不要漏了!


//-----------------------------grass.cpp-------------------------------
#include
using namespace std;
#include "grass.h"

//下面对类的方法进行定义
void grass::cut( ) // "::"表示cut( )是grass的成员。
{
if (height>=10)
height-=10; //可自由访问grass中的任何成员。
}

void grass::water( )
{
height+=10;
}

int grass::get_height( ) //在类的外部不能直接访问height,因此要写这个函数
{
return height;
}

void grass::set_height(int newh) //一样咱们写了这个函数
{
if (newh>=0)
height=newh;
}

void main( )
{
grass grass1,grass2; //其实这一句和"int a,b;"没什么区别,想想!这一句语
//句被称为实例化。
grass1.set_height(20); //若是你用过VB必定会以为很亲切。类之外的函数即便
//是访问类的公有部分也要用"."。
cout<
grass1.set_height(-100); //由于set_height做了保护措施,因此这一句不会给
//height一个荒唐的值
cout<
grass1.cut( );
cout<
grass2=grass1; //同一种对象可直接互相赋值
cout<
grass *grass3; //也可定义指向类的指针
grass3=new grass; //一样要new
grass3->set_height(40); //因为grass3是指针,这里要用"->"。其实也能够
//使用(*grass3).set_height(40); ("."操做符比"*"
//操做符执行时优先) ,不过这样写比较麻烦。
grass3->water( );
cout< get_height( );
delete grass3; //释放指针
}

看了注释你应该能够读懂这个程序,如今咱们能够看到类的第一个优势了:封装性。封装指的就是像上面这样彷佛故弄玄虚地把height隐藏起来,并写几个好像很无聊的读取和改写height的函数。然而在程序中咱们已经能够看到这样能够保护数据。并且在大型软件和多人协做中,因为私有成员能够隐藏类的核心部分,只是经过公有的接口与其它函数沟通,因此当咱们修改类的数据结构时,只要再改一改接口函数,别的函数仍是能够象之前同样调用类中的数据,这样就能够使一个类做为一个模块而出现,有利于你们的协做和减小错误。
有的人也许会认为写接口函数会减慢速度,那么你能够在定义前面加上"inline"使其成为内联函数。
类之外的函数其实也有办法直接访问类的私有部分,只要在类中声明类的方法时加入形如"friend int XXX (int xxx, int xxx) "这样的语句,类之外的"int XXX (int xxx, int xxx) "函数就可访问类的私有部分了。此时这个函数称为类的友元。
注意类中的函数最好不要返回类中的私有成员的引用或指针,不然咱们将显然能够经过它z强行访问类中的私有成员。
除了public和private两种权限外还有protected权限,平时是和private同样的,后面在讲类的继承时会进一步解释它的用途。
在类的定义中要注意定义成员数据时不能同时初始化(好像int a=0这样),且不能用extern说明成员数据。
一种类的对象能够做为另外一种类的成员。例如:
class x
{
int a;
};

class y
{
x b;
};
若是咱们把上面两个类的声明互调,那么因为执行x b;时x类还根本未被定义,编译器会报错。那么应如何解决呢?很简单,在最前面加一句class x;预先声明一下便可。
同一种类能够互相赋值。类可做为数组的元素。能够定义指向类的指针。总之类拥有普通的数据类型的性质。
只要定义一次类,就能够大批量地经过实例化创建一批对象,且创建的对象都有直观的属性和方法。这也是类的好处之一。
结构其实也是一种类,只不过结构的缺省访问权限是公有。定义结构时只需把"class"换为"struct"。 通常咱们在仅描述数据时使用结构,在既要描述数据,又要描述对数据进行的操做时使用类。
最后介绍一下用什么办法能够获得一个变量是哪一个类的对象:typeid(aaa).name( )能返回aaa变量所属类的名称,注意这是在程序运行期实现的,很酷吧,不过不要滥用它。

2.2 类的构造函数
当咱们将一个类实例化时,常常会但愿能同时将它的一些成员初始化。为此,咱们能够使用构造函数。构造函数是一个无返回值(void都不用写)且与类同名的函数,它将在类被实例化时自动执行。构造函数的使用就象下面这样:
#include
using namespace std;
class grass
{
public:
int height;
grass(int height); //构造函数。固然,它需为public权限
//虽然在这个程序中它有参数,但并没必要需
};

grass::grass(int height)
{
this->height=height; //对于任何一个对象的方法来讲,this永远是一个指向这个
//对象的指针。因此这样写能使编译器知道是类中的height
}


void main( )
{
grass grass1(10); //普通对象实例化时就要给出初始化参数
//若是构造函数无参数就不须要写"(10)"
grass *grass2;
grass2=new grass(30); //指针此时要给出初始化参数
cout<
cout< height;
}

值得注意的是,当你使用grass grass1=grass2;或grass grass1(grass2);这样的方式来初始化对象时,构造函数将不会被执行,执行的将是所谓的拷贝构造函数。若是你偷懒没写它,系统会自动生成一个,它的行为将是逐字节拷贝grass2到grass1。这个行为看上去很正常,然而若是类中有指针型成员时它却存在着灾难性的后果。看看下面的一端代码片断:
grass grass1;
grass1.x=”hehe”; //假设x是grass类的一个char*类型成员
{
grass grass2=grass1; //此时grass2的x将和grass1的x指向同一个值!
} //grass2和它的x成员一块儿被销毁
//如今grass1.x也已无辜地失去意义

因此,当咱们的类中有指针型成员时,咱们必须像这样写一个本身的拷贝构造函数:
grass::grass(grass& grass1) //名称应与类相同,参数应为对同类数据的引用
//若是咱们写成grass::grass(grass grass1),显然会陷入死循环,由于此时编译器须要
//调用拷贝构造函数来生成参数的复制品,因此咱们必须使用实参
{
//在这里正确地拷贝各个数据
}

可是这实在挺麻烦的,有没有办法干脆禁止掉这种意义通常来讲并不大的拷贝初始化呢?很简单,本身写一个只有声明没有定义的拷贝构造函数,并声明其为private权限,便可防止编译器自作聪明----编译时若是它发现grass grass1=grass2;这样的语句时会报错,不过你也别想用foo(grass a);这样的函数了,必须用foo(grass &a);……

构造函数还有一个用处就是能够进行类型转换。例如,咱们定义了一个这样的构造函数:
grass::grass(int x)
{
height=x;
}
如今,若是咱们定义了一个grass类的gg对象,之后就能够执行gg=5; 这样的语句了,也可将int类型的变量赋值给gg,由于这时实际上执行了gg=grass(5);这样的语句(若是咱们使用了2.4节介绍的方法重载了=运算符,那么只会执行重载的=运算符)。
还有一种叫析构函数的东西,形如grass::~grass( ),在咱们delete一个指向对象的指针时会自动调用,你应该在里面释放类的指针型成员。

2.3 类的静态成员
类的静态数据成员和普通的静态变量含义不一样,它的意思是:在每个类实例化时并不分配存储空间,而是该类的每一个对象共享一个存储空间,而且该类的全部对象均可以直接访问该存储空间。其实它就是一个专门供这个类的对象使用的变量----若是你把它声明为private权限的话。
在类中定义静态数据成员,只须在定义时在前面加上"static"。类的静态数据成员只能在类外进行初始化,若没有对其进行初始化,则自动被初始化为 0。在类外引用静态数据成员必须始终用类名::变量名的形式。静态数据成员能够用来统计建立了多少个这种对象。
举一个例子:
#include
using namespace std;
class AA
{
private:
int a;
public:
static int count; //定义类的静态成员
AA(int aa=0) { a=aa; count++; }
//对于类的方法,若是是较简单的能够这样写以使程序紧凑
int get_a( ) { return a; }
};

int AA::count=0; //在类外初始化

void main()
{
cout<<"Count="< <
AA x(10),y(20);
cout <<"x.a="<

2.4 运算符重载
运算符重载能够使类变得很是直观和易用。好比说,咱们定义了一个复数类(什么,你没学过复数?你读几年级?),而后再将加、减、乘、除等运算符重载,就能够自由地对复数对象好像整数同样进行这些运算了!能够想象,这将大大方便咱们。
使用运算符重载很简单,咱们就举一个复数类的例子来讲明怎样使用:

#include
using namespace std;

class complex
{
private:
double real;
double image;
public:
complex ( ); //缺省构造函数
complex (double r, double i); //顺便初始化值的构造函数
complex operator +(complex x); //计算A+B
complex operator ++( ); //计算++A
complex operator --(int); //计算A—
complex operator =(double x); //把一个double赋给一个complex时该怎么办
//系统还自动生成了一个complex operator=(complex);,它的实现是简单拷贝
//因此若是类中有指针成员,它会像默认的拷贝构造函数那样出问题?
//咱们若是要重写它,还要注意检查本身赋给本身的状况
void print(); //输出复数
};

complex::complex( )
{
real=0.0f;
image=0.0f;
}

complex::complex(double r, double i)
{
real=r;
image=i;
}

complex complex::operator +(complex x)
{
complex c;
c.real=real+x.real;
c.image=image+x.image;
return c;
}

complex complex::operator ++( )
{
complex c;
++real;
c.real=real;
c.image=image;
return c;
}

complex complex::operator --(int)
{
complex c;
c.real=real;
c.image=image;
real--;
return c;
}

complex complex::operator =(double x)
{
real=x;
return *this; //按照C++的惯例,返回*this,以便实现链式表达式
}

void complex::print( )
{
cout< <<"+"< <<"I"<
}

void main( )
{
complex a(1,2);
complex b(4,5);
complex c=a+b;
complex d=++a;
complex e=b--;
//complex f=0.234; //这样写如今还不行,由于上面没写相应的拷贝构造函数
//你能够试着写一个
complex f;
f=a=0.234; //链式表达式
a.print( );
c.print( );
d.print( );
e.print( );
f.print( );
}

除了"."、 ".*"、 "::"、 "?:"四个运算符外,其它运算符(包括new、delete)均可被重载,cin和cout就是两个典型的例子。
对于双目运算符(即A?B),如加、减、乘、除等,可这样重载:
"complex operator ?(complex B);",运算时就好像调用A的这个方法同样。
对于前置的单目运算符(即?A),如"-A"、 "--A"、"++A"等,可这样重载:
"complex complex::operator ?( );"。
对于后置的单目运算符,如"A--"、 "A++",可这样重载:
"complex complex::operator ?(int);",其中参数表中的int不能省去。
下面出一道题让你们考虑考虑吧:建立一个字符串类并将+、-、=、==等运算符重载,使咱们能够直观地操做字符串。

2.5 类的继承
能够继承是类的第二个优势,它使大型程序的结构变得严谨并减小了程序员的重复劳动。继承究竟是什么呢?举个例子,好比说树和猫这两样东西,看起来好像绝不相干,但它们都有质量、体积等共有的属性和买卖、称量等共有的方法。因此咱们可不能够先定义一个基类,它只包含两样事物共有的属性和方法,而后再从它派生出树和猫这两样事物,使它们继承基类的全部性质,以免重复的定义呢?答案是确定的。因为一个类能够同时继承多个类,一个类也可同时被多个类继承,咱们能够创建起一个复杂的继承关系,就象这样:
图2.1
咱们能够看到,继承是很灵活样的。要说明继承是很简单的,只要像这样定义派生类便可:
class 派生类名 :派生性质 基类名1,派生性质 基类名2,...,派生性质 基类名n
{
这里面同定义普通类同样,没必要再说明基类中的成员
};
关于这里的派生性质,有这样一张表可供参考:
表2.1
派生性质 在基类中的访问权限 在派生类中的访问权限
public public public
protected protected
private 不可访问
protected public protected
protected protected
private 不可访问
private public private
protected private
private 不可访问
这张表中的后两栏意思是:当基类中设置了这种访问权限的成员被派生类继承时,它将等价于设置了什么访问权限的派生类的成员。
下面咱们看一个例子:
#include
using namespace std;

class THING //定义基类
{
protected:
int mass,volume;
public:
THING(int m, int v);
int get_mass( );
void set_mass(int new_mass);
};

THING::THING(int m, int v)
{
mass=m;
volume=v;
}

int THING::get_mass( )
{
return mass;
}

void THING::set_mass(int new_mass)
{
if (new_mass>=0)
mass=new_mass;
}

class ANIMAL: public THING //定义派生类
{
private:
int life;
public:
ANIMAL(int x) : THING(10+x,7) { life=x; }; //定义派生类的构造函数时须要给
//出初始化基类的办法
//若有多个基类,用逗号隔开分别//提供参数
void set_life(int new_life) { if (new_life>=0) life=new_life; };
int get_life( ) { return life; };
void kill( ) { life=0; };
};

ANIMAL cat(50);

void main( )
{
cout<
cout<
cat.set_life(100); //也有本身的方法
cat.kill( );
cout<
}
当某类同时继承了多个类而这些类又拥有相同名称的函数时,咱们能够使用像这样的语句说明要使用的是哪个类的方法:child->father::get_life();。

2.6 虚函数和抽象类
虚函数体现了类的第三个优势:多态性(看上去好像很深奥)。
有时候,在一个含有基类和派生类的程序中,咱们须要在派生类中定义一个和基类的方法具备相同的函数名、返回类型和参数表,但函数的具体内容不一样的方法。好比说,咱们首先定义了一个"植物"类,而后又定义了一些它的派生类"松树"、"柳树"、"杨树"等等,而后在派生类中重载了"种植"方法,由于咱们知道它的实现随着树种的不一样而不一样。但此时当一个"植物"类的指针指向一个"柳树"类的对象时(这是合法的),基类指针仍是只能访问基类的"种植"方法,而不是在派生类中从新定义的方法!解决问题的办法是在基类中把这个方法定义为虚函数。
虚函数的定义方法是在基类声明成员函数时在最前加关键字"virtual"。
咱们也举一个例子来讲明虚函数的使用方法:
#include
using namespace std;

class Base
{
public:
int a;
virtual int get_a( ) { return a; };
};

class Child: public Base
{
public:
int get_a( ) {return a*a; };
Child(int aa) {a=aa; };
};

Child child(10);

void main( )
{
Base *p;
p=&child;
cout< get_a( );
}

从运行结果能够看到,调用的是Child类的get_a( )。你能够试一试删去virtual看看输出有什么变化。
值得注意的是基类的析构函数必定要是虚函数,不然在你经过基类的指针delete派生类的对象时显然将不会调用派生类的析构函数,这可不是咱们但愿看到的。另外,在派生类重载基类的函数是没有做用的,编译器只会根据指针的类型选择调用哪一个函数。
有时候咱们不须要用基类来定义对象,则可把基类的函数定义为纯虚函数,也不需再在基类中给出函数的实现。这时基类就被称为抽象类。仍是上面的哪一个植物的例子,因为咱们这时显然不会定义一个"植物"类的对象,只会根据具体的树的不一样而选择一个相应的类,因此咱们彻底能够把"植物"类中的虚函数所有定义为纯虚函数。定义纯虚函数的方法是在加了"virtual"后再去掉函数体并在声明后加上"=0"。就象这样:"virtual int get_a( )=0;"。

2.7 模板
模板(template)是C++语言提供的一个有趣而有用的东西,它能够使咱们快速地定义一系列类似的类或函数。下面咱们先看看如何用使用模板来定义类:
#include
using namespace std;

template //这是一个所谓的prefix,即前缀。< >内为模板参数,在这
//里是一个类T。有了这个前缀,下面的半条语句就能够把T
//看成一个类的名称使用
class List
{
private:
T *a; //a是一个指向T类型的数据的指针
public:
int size;
List(int n);
T operator[ ](int i); //List[ ]的返回值为T类型的数据
~List();
};

template List ::List( int n ) //哇!这是天书吗......
//其实很好懂,首先把前缀去掉,
//剩下的List 中的 是必须
//重复一次的参数表
{
a = new T[n]; //使a成为一个成员为T类型的数据的数组
for (int i=0; i
a[i]=(T)(i+47); //给a数组分配内容
size = n;
}

template List ::~List()
{
delete[] a;
}

template T List ::operator[ ](int i) //注意List 前的T是
//这个函数的返回值的类型
{
return a[i]+1; //和普通的[]有一点小小的区别?
}

void main()
{
List c(10); // 给模板提供参数,说明T即char
//咱们彻底能够把char当作是一个类
for (int i=0;i
cout<
}

咱们能够用相似的方法定义有多个参数的模板,好比:
#include
using namespace std;

template class List
{
private:
T *a;
public:
int size;
List();
T operator[ ](int i);
~List();
};

template List ::List()
{
a = new T[U];
for (int i=0; i
size = U;
}

template T List ::operator[ ](int i)
{
return a[i]+1;
}

template List ::~List()
{
delete[] a;
}

void main()
{
List c; //注意,你只能把常数赋值给模板的"实际"参数
//由于模板的本质是直接替换!就像#define同样
//因此你不用担忧它的效率?
for (int i=0;i
cout<
}

下面咱们再来看看如何用模板定义函数:
#include
using namespace std;

template T print(T n); //和定义类时差很少,返回值为T类型,参数
//n也为T类型

template T print(T n)
{
cout< <
return n;
}

void main()
{
float x=3.14;
print(x);
char y='m';
cout<
}

你们是否是以为有点像函数重载呢?不过没必要浪费时间去写几乎彻底同样的函数了。还记得1.3节所介绍的用#include定义的函数吗?它的好处是适用于全部数据类型,但如今,咱们用模板也能够实现彻底相同的功能了。注意编译器实现模板的办法实际上也是根据数据类型的多少建立一堆差很少的类或函数。
其实模板的引入就像当初类的引入同样有着重大的的意义,一种新的编程思想应运而生:Generic Programming (GP)。这种编程思想的核心是使算法抽象化,从而能够适用于一切数据类型。著名的STL(Standard Template Library)就是这种思想的应用成果。感兴趣的读者能够本身找一些这方面的书看看,对本身的编程水平的提升会有好处。

2.8 优化程序
首先提醒你们一句,再好的语句上的优化也比不上算法上的优化所带来的巨大效益,因此我以为对这方面不太熟悉的人都应该买本讲数据结构与算法的书来看看。在第八章讲述了几种经常使用的算法,若是你感兴趣能够看看。
下面就转入正题,讲一讲通常的优化技巧吧:
(1)使用内联函数。

(2)展开循环。
for (i = 0; i < 100; i++)
{
do_stuff(i);
}
能够展开成:
for (i = 0; i < 100; )
{
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
do_stuff(i); i++;
}

(3)运算强度减弱。
例如若是有这样一段程序:
int x = w % 8;
int y = x * 33;
float z = x/5;
for (i = 0; i < 100; i++)
{
h = 14 * i;
cout<
}

上面的程序这样改动能够大大加快速度:
int x = w & 7;
int y = (x << 5) + x; //<<比+的运算优先级低!
float z = x*0.2;
for (i = h = 0; i < 100; i++)
{
cout<
h += 14;
}

(4)查表。这种方法挺有用。好比说咱们定义了一个函数f(x)能够返回x*x*x,其中x的范围是0~1,精度为0.001,那么咱们能够创建一个数组a[1000],a[t*1000]存储的是预先计算好的t*t*t的值,之后调用函数就能够用查找数组代替了。

2.9 调试程序
每一个人都会犯错误,在编程中也如此。写完程序后第一次运行就直接经过的状况实在是很少的,偶尔出现一两次都是值得高兴的事。有错固然要改,但不少时候最难的并非改正错误,而是找到错误,有时候写程序的时间还不如找错误的时间长。为了帮助你们节省一点时间,下面就讲一讲一点找错误的经验。
首先固然要说说常见的错误有哪些,最常常出现的是:漏分号、多分号、漏各类括号、多各类括号、"=="写成了"="(上面的错误看上去很弱智,不过也容易犯)、数组(指针)越界(最多见的错误之一!)、变量越界、指针使用前未赋初值、释放了指针以后继续使用它……等等。若是你的程序有时出错有时又不出错,极可能就是指针的问题。
有一点要注意的是VC.net显示出的出错的那一行有可能不是真正出错的位置!
经常使用的找错办法就是先确认你刚刚改动了哪些语句,而后用/*和*/把可能出错的语句屏障掉,若是运行后还不经过就再扩大范围。即便有一段程序你以为不可能有什么问题或之前工做正常也要试试将它屏障,有时就是在彷佛最不可能出错的地方出了问题。
还有一种你们都常常用的找错办法就是把一些变量的值显示在屏幕上,或是把程序运行的详细过程存入文件中,出什么问题一目了然。若是再像QuakeIII同样用一个"控制台"显示出来就很酷了。
象其它编译器同样,VC.net提供了变量观察(Watch)、单步执行(Step)等常规调试手段,固然你首先须要把工程设为Debug模式。而后设置好断点(在要设置断点的那一行左边的灰色区域按一下便可,会出现一个红圆,程序运行到此处会暂停),按F5就能够开始调试。此时会出现一个调试工具栏:

图2.2 调试工具栏
图标的意义分别为:执行此语句,中止此语句的执行,中止调试,从新调试,显示即将执行的语句,调试入函数,跳过函数,调试出此{},用十六进制显示数据,显示断点状况。
你们还会注意到左下角出现了一个变量观察窗口,在这里能够很是方便地观察变量的值和改变状况。
咱们还能够打开反汇编窗口、内存观察窗口和寄存器观察窗口,它们但是威力无比的,用起来很是爽。观察编译器生成的代码也是深刻了解C++语言华丽的外表背后的真相的好办法。
VC++还提供了两条调试语句能够帮助你调试,第一条语句是assert。它的使用方法是assert(条件),你能够把它放到须要的地方,当条件不知足时就会显示一个对话框,说明在哪一个程序哪一行出现了条件不知足,而后你能够选择中止,继续或是忽略。这条语句很是有用,由于直接执行程序时(而不是在VC++中调试)它也能工做。第二条语句是OutputDebugString (要输出的字符串),能够在屏幕下方编译窗口的调试那一栏显示这个字符串。
利用VC.net的开发环境调试是一项十分方便的事,只要你多调试(这不用刻意追求,由于它不可避免?),必定能够愈来愈熟练。
本书的C++语言部分到此能够告一段落了,但这里所讲述的只是C++语言的冰山一角,由于C++语言可被称为博大精深,并且它还在不断发展。但愿你们在之后的日子里不要中止对C++语言的学习和研究,你必定会不断有新的感觉和发现。最后推荐两本必读的好书:Scott Douglas Meyers的Effective C++和More Effective C++(其中很多内容我已经穿插到了前面的文字中?)。
第三章 容纳游戏的空间

由于咱们编好的游戏将在Windows下运行,因此学习一点Windows编程知识是必需的。Microsoft为了方便Windows编程制做了一个庞大的类库MFC,把Windows的方方面面都封装了起来。但此类库只是比较适合编写字板之类的标准Windows程序,对于游戏来讲它实在是过于烦琐和累赘,因此咱们通常都不使用它,本身从头用Windows API(Application Programming Interface 应用编程接口,其实就是一堆Windows为开发者提供的函数)写Windows程序。

3.1 基本Windows程序
最基本的Windows程序看起来都有点长,它的流程图是这样的:
图3.1
但你没必要担忧Windows编程过于复杂。在全部的Windows程序中,都须要一个初始化的过程,而这个过程对于任何Windows程序而言,都是大同小异的。你也许会想到使用VB作一个最简单的程序不用敲一行代码,其实这是由于VB已经暗地里帮你敲好了。

#include

//函数声明
BOOL InitWindow( HINSTANCE hInstance, int nCmdShow );
LRESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam );

//变量说明
HWND hWnd; //窗口句柄
//************************************************************
//函数:WinMain( )
//功能:Windows程序入口函数。建立主窗口,处理消息循环
//************************************************************
int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if ( !InitWindow( hInstance, nCmdShow ) ) return FALSE; //建立主窗口
//若是建立不成功则返回FALSE并同时退出程序
MSG msg;
//进入消息循环:
for(;;)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if ( msg.message==WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}

//************************************************************
//函数:InitWindow( )
//功能:建立窗口
//************************************************************

static BOOL InitWindow( HINSTANCE hInstance, int nCmdShow )
{
//定义窗口风格:
WNDCLASS wc;
wc.style = NULL;
wc.lpfnWndProc = (WNDPROC)WinProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = CreateSolidBrush (RGB(100, 0, 0)); //暗红色的背景
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);
PostMessage(hWnd, WM_CLOSE, 0, 0);//给窗口发送WM_CLOSE消息
break;
}
return 0; //处理完一个消息后返回0

case WM_CLOSE: //准备退出
DestroyWindow( hWnd ); //释放窗口
return 0;

case WM_RBUTTONDOWN:
MessageBox(hWnd,"鼠标右键按下了!","Mouse",MB_OK);
return 0;

case WM_DESTROY: //若是窗口被人释放…
PostQuitMessage( 0 ); //给窗口发送WM_QUIT消息
return 0;
}
//调用缺省消息处理过程
return DefWindowProc(hWnd, message, wParam, lParam);
}

按1.1节的方法创建一个工程后,输入程序,按Ctrl+F5执行一下,就会出现一个暗红色的"窗口"。而后你能够试试按按鼠标右键或Esc键看看效果,就像图3. 2。怎么样?VB要作到一样的效果恐怕有点麻烦,这也算是从头写代码的一点好处吧。

图3.2

3.2 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 style; //窗口的风格
WNDPROC lpfnWndProc; //窗口消息处理函数的指针
int cbClsExtra; //分配给窗口类结构以后的额外字节数
int cbWndExtra; //分配给窗口实例以后的额外字节数
HANDLE hInstance; //窗口所对应的应用程序的句柄
HICON hIcon; //窗口的图标
HCURSOR hCursor; //窗口的鼠标
HBRUSH hbrBackground; //窗口的背景
LPCTSTR lpszMenuName; //窗口的菜单资源名称
LPCTSTR lpszClassName; //窗口类的名称

WNDCLASS的第一个成员style表示窗口类的风格,它每每是由一些基本的风格经过位的"或"操做(操做符"|")组合而成。下表列出了一些经常使用的基本窗口风格:
表3.1
风格 含义
CS_HREDRAW 若是窗口宽度发生改变,重绘整个窗口
CS_VREDRAW 若是窗口高度发生改变,重绘整个窗口
CS_DBLCLKS 能感觉用户在窗口中的双击消息
CS_NOCLOSE 禁用系统菜单中的"关闭"命令
CS_SAVEBITS 把被窗口遮掩的屏幕图像部分做为位图保存起来。当该窗口被移动时,Windows使用被保存的位图来重建屏幕图像
第二个成员是lpfnWndProc,给它消息处理函数的函数名称便可,必要时应该进行强制类型转换,将其转换成WNDPROC型。
接下来的cbClsExtra和wc.cbWndExtra通常均可以设为0。
而后的hInstance成员,给它的值是窗口所对应的应用程序的句柄,代表该窗口与此应用程序是相关联的。
下面的hIcon是让咱们给这个窗口指定一个图标,这个程序没有设置。
鼠标也没有设置,由于编游戏时的鼠标都是在刷新屏幕时本身画上去的。
hbrBackground成员用来定义窗口的背景色。这里设为CreateSolidBrush (RGB(100, 0, 0)),即暗红色。关于CreateSolidBrush函数,请参阅4.10节。
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

第一个参数是建立该窗口所使用的窗口类的名称,注意这个名称应与前面所注册的窗口类的名称一致。
第三个参数为建立的窗口的风格,下表列出了经常使用的窗口风格:
表3.2
风格 含义
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_MAXIMIZEBOX / WS_MINIMIZEBOX 窗口有最大化按钮 / 最小化按钮
WS_SIZEBOX & WS_THICKFRAME 边框可进行大小控制的窗口
WS_SYSMENU 建立一个有系统菜单的窗口,必须与WS_CAPTION风格同时使用
WS_TILED 建立一个层叠式窗口,有标题栏
WS_VISIBLE 窗口为可见
在DirectX编程中,咱们通常使用的是WS_POPUP | WS_MAXIMIZE,用这个标志建立的窗口没有标题栏和系统菜单且窗口为最大化,能够充分知足DirectX编程的须要。
若是窗口建立成功,CreateWindow( )返回新窗口的句柄,不然返回NULL。

3.2.4 显示和更新窗口
窗口建立后,并不会在屏幕上显示出来,要真正把窗口显示在屏幕上,还得使用ShowWindow( )函数,其原型以下:
BOOL ShowWindow( HWND hWnd, int nCmdShow );
参数hWnd就是要显示的窗口的句柄。
nCmdShow是窗口的显示方式,通常给它WinMain( )函数获得的nCmdShow的值就能够了。经常使用的窗口显示方式有:
表3.3
方式 含义
SW_HIDE 隐藏窗口
SW_MINIMIZE 最小化窗口
SW_RESTORE 恢复并激活窗口
SW_SHOW 显示并激活窗口
SW_SHOWMAXIMIZED 最大化并激活窗口
SW_SHOWMINIMIZED 最小化并激活窗口
ShowWindow( )函数的执行优先级不高,当系统正忙着执行其它的任务时窗口不会当即显示出来。因此咱们使用ShowWindow( )函数后还要再调用UpdateWindow(HWND hWnd); 函数以保证当即显示窗口。

3.2.5 消息循环
在WinMain( )函数中,调用InitWindow( )函数成功地建立了应用程序主窗口以后,就要启动消息循环,其代码以下:
for(;;)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if ( msg.message==WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
Windows应用程序能够接收各类形式的信息,这包括键盘和鼠标的动做、记时器消息,其它应用程序发来的消息等等。Windows系统会自动将这些消息放入应用程序的消息队列中。
PeekMessage( )函数就是用来从应用程序的消息队列中按照先进先出的原则将这些消息一个个的取出来,放进一个MSG结构中去。若是队列中没有任何消息,PeekMessage( )函数将当即返回。若是队列中有消息,它将取出一个后返回。
MSG结构包含了一条Windows消息的完整信息,它由下面的几部分组成:

HWND hwnd; //接收消息的窗口句柄
UINT message; //主消息值
WPARAM wParam; //副消息值1,其具体含义依赖于主消息值
LPARAM lParam; //副消息值2,其具体含义依赖于主消息值
DWORD time; //消息被投递的时间
POINT pt; //鼠标的位置

该结构中的主消息代表了消息的类型,例如是键盘消息仍是鼠标消息等。副消息的含义则依赖于主消息值,好比说若是主消息是键盘消息,那么wParam中存储了是键盘的哪一个具体键;若是主消息是鼠标消息,那么LOWORD(lParam)和HIWORD(lParam)分别为鼠标位置的x和y坐标;若是主消息是WM_ACTIVATE,wParam就表示了程序是否处于激活状态。这里顺便说一下,定义一个POINT类型的变量curpos后,在程序的任意位置使用GetCursorPos(&curpos)均可以将鼠标坐标存储在curpos.x和curpos.y中。
PeekMessage( )函数的原型以下:

BOOL PeekMessage (
LPMSG lpMsg, //指向一个MSG结构的指针,用来保存消息
HWND hWnd, //指定哪一个窗口的消息将被获取
UINT wMsgFilterMin, //指定获取的主消息值的最小值
UINT wMsgFilterMax, //指定获取的主消息值的最大值
UINT wRemoveMsg //获得消息后是否移除消息
);

PeekMessage( )的第一个参数的意义上面已解释。
第二个参数是用来指定从哪一个窗口的消息队列中获取消息,其它窗口的消息将被过滤掉。若是该参数为NULL,则PeekMessage( )从该应用程序全部窗口的消息队列中获取消息。
第三个和第四个参数是用来过滤MSG结构中主消息值的,主消息值在wMsgFilterMin和wMsgFilterMax以外的消息将被过滤掉。若是这两个参数均为0,表示接收全部消息。
第五个参数用来设置分发完消息后是否将消息从队列中移除,通常设为PM_REMOVE即移除。
TranslateMessage( )函数的做用是把虚拟键消息转换到字符消息,以知足键盘输入的须要。DispatchMessage( )函数所完成的工做是把当前的消息发送到对应的窗口过程当中去。
开启消息循环实际上是很简单的一个步骤,几乎全部的程序都是按照Test的这个方法。咱们彻底没必要去深究这些函数的做用,只是简单的照抄就能够了。
另外,这里介绍的消息循环开启方法比某些书上所介绍的用GetMessage( )的方法要好一些,由于GetMessage( )若是得不到消息会一直等待,结果就耗费了许多宝贵的时间,使游戏不能及时刷新。

3.3 消息处理函数
消息处理函数又叫窗口过程,在这个函数中,不一样的消息将被switch语句分配到不一样的处理程序中去。Windows的消息处理函数的原型是这样定义的:

LRESULT CALLBACK WindowProc(
HWND hwnd, //接收消息窗口的句柄
UINT uMsg, //主消息值
WPARAM wParam, //副消息值1
LPARAM lParam //副消息值2
);

消息处理函数必须按照上面的这个样式来定义,固然函数名称能够随便取。
Test中的WinProc( )函数就是一个典型的消息处理函数。在这个函数中明确的处理了3个消息,分别是WM_KEYDOWN(击键)、WM_RBUTTONDOWN(鼠标右键按下)、WM_CLOSE(关闭窗口)、WM_DESTROY(销毁窗口)。值得注意的是,应用程序发送到窗口的消息远远不止以上这几条,象WM_SIZE、WM_MINIMIZE、WM_CREATE、WM_MOVE等频繁使用的消息就有几十条。在附录中能够查到Windows常见消息列表。
为了减轻编程的负担,Windows提供了DefWindowProc( )函数来处理这些最经常使用的消息,调用了这个函数后,这些消息将按照系统默认的方式获得处理。所以,在消息处理函数中,只须处理那些有必要进行特别响应的消息,其他的消息均可交给DefWindowProc( )函数来处理。

3.4 经常使用Windows函数
3.4.1 显示对话框
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,表明哪一个按钮被按下。

3.4.2 定时器
定时器能够使程序每隔一段时间执行一个函数。用法以下:
SetTimer(HWND hwnd, UINT ID, UINT Elapse, TIMERPROC TimerFunc);
四个参数依次为窗口句柄、定时器标识(同一程序内各个定时器的标识应不相同,通常从一、二、3...一直排下去)、每隔多少毫秒(千分之一秒)执行一次程序,要执行的过程。
这个要执行的过程应这样定义:
void CALLBACK MyTimer(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime);
这几个规定的参数都没什么用,咱们在过程里做本身的事就好了,不用理这几个给咱们的参数。
注意:定时器的优先级不高,当处理器很忙时咱们须要定时执行的程序经常不能按时地执行;不管你把定时器的Elapse设得多小,它实际上最小只能是55ms;有的Windows函数在TimerFunc中用不了,并且在TimerFunc里不要作一些费时间的东西。

3.4.3 获得时间
咱们常常须要在程序中获得当前的准确时间来完成测试速度等工做。这时咱们能够使用GetTickCount( ),由于该函数能够返回Windows已经运行了多少毫秒。然而有时咱们须要获得更准确的时间,这时可以使用这种方法:
__int64 time2, freq; //时间,计时器频率
double time; //以秒为单位的时间
QueryPerformanceCounter((LARGE_INTEGER*)&time2); //获得计时开始的时间
QueryPerformanceFrequency((LARGE_INTEGER*)&freq); //获得计时器频率
time = (double)(time2) / (double)freq; //将时间转为以秒为单位

3.4.4 播放声音
咱们能够使用MCI来简易地实如今程序中播放MIDI和WAV等声音。使用它须要预先声明,咱们须要在文件头#include ,并在工程中加入"winmm.lib"
下面先让咱们看看播放MIDI的过程。首先咱们要打开设备:
MCI_OPEN_PARMS OpenParms;
OpenParms.lpstrDeviceType =
(LPCSTR) MCI_DEVTYPE_SEQUENCER; //是MIDI类型文件
OpenParms.lpstrElementName = (LPCSTR) filename; //文件名
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 PlayParms;
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文件的方法几乎彻底相同,只是须要将MCI_DEVTYPE_SEQUENCER 改成MCI_DEVTYPE_WAVEFORM_AUDIO。
第四章 描绘游戏的画笔

看完上一章后,咱们已经大体地掌握了Windows编程的方法。这意味着咱们为进行DirectDraw编程打下了坚实的基础。DirectDraw是DirectX的重要组成部分,它就像一支画笔,主要负责各类把各类图像显示在屏幕上,对Windows环境中的游戏很是重要。如今就让咱们进入激动人心的DirectDraw部分吧。

4.1 初始化DirectDraw
为了告诉编译器咱们须要使用DirectDraw,咱们要在程序文件中#include ,并把"ddraw.lib"和"dxguid.lib"加入工程(若是你不会加,请看1.1节)。记住,作完了这些工做后DirectDraw程序才能被正常编译。

4.1.1 简介
让咱们先看一看一个常见的DirectDraw初始化函数:

LPDIRECTDRAW7 lpDD; // DirectDraw对象的指针
LPDIRECTDRAWSURFACE7 lpDDSPrimary; // DirectDraw主页面的指针
LPDIRECTDRAWSURFACE7 lpDDSBuffer; // DirectDraw后台缓存的指针
LPDIRECTDRAWSURFACE7 lpDDSBack; // 存放背景图的页面的指针

BOOL InitDDraw( )
{
DDSURFACEDESC2 ddsd; // DirectDraw的页面描述
if ( DirectDrawCreateEx (NULL, (void **)&lpDD, IID_IDirectDraw7, NULL) != DD_OK )
return FALSE; //建立DirectDraw对象
//这里使用了 if ( xxx != DD_OK) 的方法进行错误检测,这是最经常使用的方法
if (lpDD->SetCooperativeLevel(hwnd,DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN) != DD_OK )
return FALSE; //设置DirectDraw控制级
if ( lpDD->SetDisplayMode( 640, 480, 32, 0, DDSDM_STANDARDVGAMODE ) != DD_OK )
return FALSE; //设置显示模式
//开始建立主页面,先清空页面描述
memset(&ddsd, 0, sizeof(DDSURFACEDESC2));
//填充页面描述
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表面)对象的指针。LPDIRECTDRAW7和LPDIRECTDRAWSURFACE7类型(7是版本号)是在ddraw.h头文件里预约义的,指向IDirectDraw7和IDirectDrawSurface7类型的长型指针(前面加的LP表明Long Point),从后面咱们用的是"->"而不是"."也能够看出这一点。DD是DirectDraw的缩写,DDS是DirectDrawSurface的缩写,所以习惯上咱们把变量名起为lpDD和lpDDSXXX。
你们须要注意的是:虽然VC.net自带的DirectX SDK是8.1版的,可是因为Microsoft从DirectX 8.0起中止了对DirectDraw的更新,因此DirectDraw目前的最新版本仍是7.0。

4.1.2 DirectDraw对象
若是要使用DirectDraw,必须建立一个DirectDraw对象,它是DirectDraw接口的核心。用DirectDrawCreateEx( )函数能够建立DirectDraw对象,DirectDrawCreateEx( )函数是在ddraw.h中定义的,它的原型以下:

HRESULT WINAPI DirectDrawCreateEx(
GUID FAR *lpGUID,
LPVOID *lplpDD,
REFIID iid,
IUnknown FAR *pUnkOuter
);

第一个参数是lpGUID:指向DirectDraw接口的全局惟一标志符(Global Unique IDentify)的指针。在这里,咱们给它NULL,表示咱们将使用当前的DirectDraw接口。
第二个参数是lplpDD:这个参数是用来接受初始化的DirectDraw对象的地址。在这里,咱们给它用强制类型转换为void**类型的&lpdd(传递指针的指针,这样这个函数才能改变指针的指向)。
第三个参数是iid:给它IID_IDirectDraw7吧,表示咱们要建立IDirectDraw7对象。
第四个参数是pUnkOuter:目前必须是NULL。
全部的DirectDraw函数的返回值都是HRESULT类型,它是一个32位的值。函数调用成功用 "DD_OK"表示,全部的错误值标志开头都为"DDERR",如:
DDERR_DIRECTDRAWALREADYCREATED
DDERR_OUTOFMEMORY
在附录中可查到这些错误值的列表。
咱们通常用"if ( DirectDrawCreateEx (NULL, (void **)&lpDD, IID_IDirectDraw7, NULL) != DD_OK ) return FALSE;"来建立DirectDraw对象,这样当建立不成功时就会退出函数并返回FALSE。

4.1.3 设置控制级和显示模式
DirectDrawCreate函数调用成功后,lpDD已经指向了一个DirectDraw对象,它是整个DirectDraw接口的最高层领导,之后的步骤都是在它的控制之下。
咱们用IDirectDraw7::SetCooperativeLevel( )来设置DirectDraw程序对系统的控制级。它的原型以下:
HRESULT SetCooperativeLevel (HWND hWnd, DWORD dwFlags )
第一个参数是窗口句柄,咱们给它hWnd,使DirectDraw对象与主窗口联系上。
第二个参数是控制级标志。这里使用DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN,表示咱们指望DirectDraw以独占和全屏方式工做。
控制级描述了DirectDraw是怎样与显示设备及系统做用的。DirectDraw控制级通常被用来决定应用程序是运行于全屏模式(必须与独占模式同时使用),仍是运行于窗口模式。但DirectDraw的控制级还可设置以下两项:
(1)容许按Ctrl + Alt + Del从新启动(仅用于独占模式,为DDSCL_ALLOWREBOOT)。
(2)不容许对DirectDraw应用程序最小化或还原 (DDSCL_NOWINDOWCHANGES)。
普通的控制级(DDSCL_NORMAL)代表咱们的DirectDraw应用程序将以窗口的形式运行。在这种控制级下,咱们将不能改变显示器分辨率,或进行换页操做(这是一个重要的操做,在4.2节会介绍)。除此以外,咱们也不可以调用那些会对显存产生激烈反应的函数,如第五章要讲的Lock( )。
当应用程序为全屏而且独占的控制级时,咱们就能够充分的利用硬件资源了。此时其它应用程序仍可建立页面、使用DirectDraw或GDI的函数,只是没法改变显示模式。
下一步咱们使用IDirectDraw7::SetDisplayMode( )来设置显示模式,其原形为:

HRESULT SetDisplayMode(
DWORD dwWidth,
DWORD dwHeight,
DWORD dwBPP,
DWORD dwRefreshRate,
DWORD dwFlags
);

dwWidth and dwHeight用来设置显示模式的宽度和高度。
dwBPP用来设置显示模式的颜色位数。
dwRefreshRate设置屏幕的刷新率,0为使用默认值。
dwFlags如今惟一有效的值是DDSDM_STANDARDVGAMODE。

4.1.4 建立页面
下一步是建立一个DirectDrawSurface对象。
DirectDrawSurface对象表明了一个页面。你能够把页面想象为一张张可供DirectDraw描绘的画布。页面能够有不少种表现形式,它既能够是可见的,称为主页面(Primary Surface);也能够是做换页用的不可见页面,称为后台缓存(Back Buffer),在换页后,它成为可见(换页在4.2节会讲);还有一种始终不可见的,称为离屏页面(Off-screen Surface),它的做用是存储图像。其中,最重要的页面是主页面,每一个DirectDraw应用程序都必须建立至少一个主页面,通常来讲它就表明着咱们的屏幕。
建立一个页面以前,首先须要填充一个DDSURFACEDESC2结构,它是DirectDraw Surface Description的缩写,意思是DirectDraw的页面描述。它的结构很是庞大,这里只能做一个最简单的介绍。要注意的是在填充此结构前必定要将其清空!下面是一个典型的主页面的页面描述:
ddsd.dwSize = sizeof( ddsd ); //给dwSize页面描述的大小
ddsd.dwFlags = DDSD_CAPS|DDSD_BACKBUFFERCOUNT; //有后台缓存
ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE|DDSCAPS_FLIP|DDSCAPS_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; //宽

页面描述填充完毕后,把它传递给IDirectDraw7::CreateSurface( )方法便可创造页面。CreateSurface( )的原形是:

HRESULT CreateSurface(
LPDDSURFACEDESC2 lpDDSurfaceDesc,
LPDIRECTDRAWSURFACE FAR *lplpDDSurface,
IUnknown FAR *pUnkOuter
);

CreateSurface( )函数的第一个参数是被填充了页面信息的DDSURFACEDESC2结构的地址,此处为&ddsd;第二个参数是接收主页面指针的地址,此处为&lpDDSPrimary;第三个参数如今必须为NULL,为该函数所保留。
若是函数调用成功,lpDDSPrimary将成为一个合法的主页面对象。因为在前面已经设置了该程序的工做模式为独占和全屏,因此,此时主页面所表明的其实是咱们的整个显示屏幕。在主页面上所绘制的图形将当即反映到咱们的显示屏幕上。
DirectDraw初始化函数最后创造了一个离屏页面,若是咱们想创造更多的页面,因为页面描述已被填充好,只需接着它像下面这样先设置高度和宽度再建立页面便可:
ddsd.dwHeight=XXX;
ddsd.dwWidth=XXX;
if ( DD_OK != lpDD->CreateSurface( &ddsd, &lpDDSABC, NULL) )
return FALSE;

4.2 后台缓存和换页

图4.1
后台缓存和换页对造成无闪烁的动画相当重要。举一个例子,要显示一个物体在一张图片上运动,咱们须要在a时刻先画物体,在b时刻把一开始被物体遮住的背景画好,最后在c时刻把物体画在新位置上。但这些操做须要必定时间,若是咱们直接改主页面,那么b时刻用户就会看到画面上没有物体,但a和c时刻画面上又有物体,用户就会以为画面有些闪烁。如何解决这个问题呢?
DirectDraw 中的换页就能够帮咱们这个忙。首先,咱们得设置好一个换页链结构,它由一组 DirectDraw 页面组成,每个页面均可以被轮流换页至显示屏幕。当前正好位于显示屏幕的页面叫主页面。等待换页至屏幕的页面叫后台缓存。应用程序在后台缓存上进行绘图操做,而后将此页面换页成为主页面,原来的后台缓存就显示在屏幕上了,而原来的主页面就成为了后台缓存,在通常状况下咱们只需改变后台缓存的内容。因此,咱们在完成了b、c两个步骤后再换页便可避免闪烁现象。
咱们既能够建立一个简单的双换页链结构(一个主页面和一个后台缓存),也能够建立一个有时候更快的多缓存换页链结构。通常咱们只需像4.2.1节的例子那样使用双缓存换页链结构便可。
换页所使用的函数是IDirectDrawSurface7::Flip( )。它的原形是:
HRESULT Flip(LPDIRECTDRAWSURFACE lpDDSurface, DWORD dwFlags);
下面介绍它的参数:
(1)lpDDSurface
换页链中另外一个页面的 IDirectDrawSurface7接口的地址,表明换页操做的目标页面。这个页面必须是换页链中的一员。该参数缺省值是 NULL, 在这种状况下, DirectDraw 从换页链中按照先后隶属关系依次换页。
(2)dwFlags
换页的标志选项,经常使用DDFLIP_WAIT,同BltFast中的DDBLTFAST_WAIT差很少。

通常咱们这样便可换页:
lpDDSPrimary->Flip(NULL,DDFLIP_WAIT);

4.3 调入图像
初始化DirectDraw后,要将位图(*.bmp)调入页面是很是简单的,对于普通的真彩(24位)图像咱们只需用DDReLoadBitmap(页面,"图像名.bmp")便可调入图像。这个函数在ddutil.cpp中,你须要把它和ddutil.h拷贝到你的程序的目录下,将其加入你的Project并在主程序的开头#include "ddutil.h"(在Chapter IV.zip中你能够找到我修改过以适应DirectDraw7的ddutil.h和ddutil.cpp)。注意,DDReLoadBitmap( )函数会自动缩放图像以适应页面的大小。

4.4 页面的丢失与恢复
当一个DirectDraw程序被最小化时,它就丧失了对页面的控制权,若是咱们的程序不知道,继续改变页面时就会发生"DDERR_SURFACELOST"错误。而当咱们从新回到DirectDraw程序时Windows不会帮咱们把页面恢复,若是咱们不本身恢复页面用户就会看到黑屏。为了不出现这种状况,咱们能够写一个恢复页面的函数:
void RestoreSurface( )
{
lpDD->RestoreAllSurfaces( ); //恢复全部页面
ReloadBitmap( );//本身的调图函数,从新画上页面内容
}

值得注意的是Windows也不会帮咱们恢复页面的实际内容,咱们要象上面的程序那样,再调用本身的调图函数才行。应该何时调用RestoreSurface( )呢?是否是每一条改变页面的语句都要测试一下有没有发生DDERR_SURFACELOST错误呢?其实并不须要。通常游戏的引擎都是频繁刷新式,每秒钟要刷新几十次,每一次刷新必然要调用上面提到的Flip( )。因此咱们能够写一个FlipSurface( )而后之后调用它来换页:
void FlipSurface( )
{
HRESULT ddrval;
ddrval=lpDDSPrimary->Flip(NULL,DDFLIP_WAIT);
if (ddrval==DDERR_SURFACELOST)
RestoreSurface( );
}

4.5 透明色
透明色(又称关键色)对实现不规则物体的移动相当重要。所谓透明色,指的就是图像传送中不会被传送的区域的颜色。好比说咱们先在一个纯绿色的背景上画了一我的物,把这幅画调入一个页面,再将纯绿色设为这个页面的透明色。之后当咱们进行图像传送时,只需指定传送范围为一个包括了人物的矩形,DirectDraw将只会把不规则的人物传到新的页面上而不会把纯绿色的背景一块儿传送。固然,人物自己不能包含纯绿色,不然就不能完整地传送了。
一个页面在同一时刻只能有一种透明色,设置透明色的方法是DDSetColorKey(页面名,RGB(红,绿,蓝)); ,这里的"红""绿""蓝"即为日常咱们在许多图像处理软件中看到的R/G/B值。例如DDSetColorKey(lpDDSMap, RGB(255,0,255));。
由于这个函数也是ddutil提供的,因此你要确认ddutil.cpp在工程内且#include "ddutil.h"。注意这个函数的调用不是很快,由于它还要用一个技巧转换一下你提供的颜色,以保证在任何色彩位数下透明色均能工做。事实上,若是你使用的是32位色,咱们彻底能够更快速地直接设置Color Key:
DDCOLORKEY ddck;
ddck.dwColorSpaceLowValue = RGB(x,x,x);
ddck.dwColorSpaceHighValue = ddck.dwColorSpaceLowValue;
lpDDSXXX->SetColorKey(DDCKEY_SRCBLT, &ddck);

4.6 图像传送
用DirectDraw作游戏,最经常使用的函数就是图像传送(又称位块传送)函数。它的做用是在各个页面之间传送指定矩形范围内的图像,并可同时对其进行各类处理。使用IDirectDrawSurface7::Blt( )和IDirectDrawSurface7::Bltfast( ) 函数能够进行图像传送。Blt( )函数功能很强大,可对图像进行缩放、旋转、镜象等操做。不过日常咱们用简单但够用的Bltfast( )就能够了。它的原形是:

HRESULT BltFast(
DWORD dwX,
DWORD dwY,
LPDIRECTDRAWSURFACE lpDDSrcSurface,
LPRECT lpSrcRect,
DWORD dwTrans
);

下面将逐一介绍这几个参数:
(1)dwX和dwY
图像将被传送到目标页面何处。
(2)lpDDSrcSurface
图像传送操做的源页面。目标页面就是调用此方法的页面。
(3)lpSrcRect
一个 RECT (Rectangle,即矩形)结构的地址,指明源页面上将被传送的区域。若是该参数是 NULL, 整个源页面将被使用。 RECT结构在DirectDraw中很是经常使用,最好在程序中定义一个RECT类型的全局变量,如rect,再象这样写一个函数:
void MakeRect (int left, int top, int right, int bottom)
{
rect.bottom = bottom;
rect.left = left;
rect.right = right;
rect.top = 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, lpDDSBack, NULL,NoKey);
若是咱们想将lpDDSSpirit上(20,30)到(50,60)的一我的物放到lpDDSBuffer上,且左上角在lpDDSBuffer的(400,300)处,要使用透明色,则使用:
MakeRect (20,30,50,60);
LpDDSBuffer -> BltFast(400,300,lpDDSSpirit,&rect,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,tt;
RECT rect2=rect; //保存原rect的内容

rl=rect.left;
rt=rect.top;

if (rect.left>SW[src_id]) //SW中存储页面宽度
goto noblt; //不进行图像传送
if (rect.top>SH[src_id]) //SH中存储页面高度
goto noblt;
if (rect.right<0)
goto noblt;
if (rect.bottom<0)
goto noblt;

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-rl;
ty1=y+rect.top-rt;
tx2=x+rect.right-rl;
ty2=y+rect.bottom-rt;

if (tx2<0)
goto noblt;
if (ty2<0)
goto noblt;
if (tx1>SW[dest_id])
goto noblt;
if (ty1>SH[dest_id])
goto noblt;

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.left;
rt=rect.top;

rect.left=tx1-tl+rl;
rect.top=ty1-tt+rt;
rect.right=tx2-tl+rl;
rect.bottom=ty2-tt+rt;

DDS[dest_id]->BltFast(tx1,ty1,DDS[src_id],&rect,method);
//DDS为存储页面指针的数组
noblt:
rect=rect2; //恢复原来的rect
}

4.7 程序实例
见Chapter IV.zip

4.8 图像缩放
图像缩放是一种常见的特效,功能强大的函数IDirectDrawSurface7::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指明了要使用的特效,惋惜没什么有价值的特效(除了5.1节将讲述的填色),给它NULL吧。

4.9 释放DirectDraw对象
一个完整的DirectDraw程序还须要在最后释放全部DirectDraw对象。为了方便这个过程,咱们能够定义几个宏:
#define SAFE_DELETE(p) { if(p) { delete (p); (p)=NULL; } }
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] (p); (p)=NULL; } }
#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }
而后能够像下面的一段程序那样释放DirectDraw对象:
void FreeDDraw( )
{
SAFE_RELEASE(lpDDSPrimary); //释放主页面对象
//若是还有别的页面也像lpDDSPrimary同样释放
SAFE_RELEASE(lpDD); //释放DirectDraw对象
}
第五章 丰富画面的技巧

在上一章中,咱们学习了很多DirectDraw的基础知识。但为了作出一个真正的有可玩性的游戏,咱们还要继续学点东西。这一章的第四节是一个很简单的游戏实例,你们能够参考参考。

5.1 填涂颜色
要对页面指定范围内填充某种颜色(通常是填充页面的透明色以达到清除的目的),能够利用IDirectDrawSurface7::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;
ddpf.dwSize = sizeof(ddpf);
lpDDSBuffer->GetPixelFormat(&ddpf);
DDBLTFX ddBltFx;
ddBltFx.dwSize=sizeof(DDBLTFX);
ddBltFx.dwFillColor=(WORD)ddpf.dwGBitMask; //如是纯红则为dwRBitMask,
//纯蓝则为dwBBitMask
lpDDS->Blt(&rect,NULL,&rect,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
若是咱们想填充纯黑色,能够把dwFillColor设为0,并去掉全部与ddpf有关的语句。

5.2 输出文字
为了向页面输出文字,咱们首先要得到页面的HDC(设备描述句柄),而后调用Windows GDI 函数向页面输出文字。因为得到句柄后将不能使用DirectDraw函数改动页面,因此输出完文字后要马上释放句柄。
HDC 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); //释放句柄
}

5.3 GDI做图
因为DirectDraw并无提供画点、线,圆等的语句,因此咱们要借助Windows GDI函数来完成这些工做。就像输出文字时同样,咱们先要得到页面的HDC:
HDC hdc;
lpDDSXXX->GetDC(&hdc);
画点是最简单的,SetPixel (hdc, x, y, RGB(r, g, b)); 便可在屏幕的(x,y)坐标处画上一个指定颜色的点。
若是须要画线等,咱们须要建立"画笔":
HPEN hpen = CreatePen (PS_SOLID, 5, RGB(r, g, b));
CreatePen的第一个参数意义为画笔样式,经常使用的有PS_SOLID(普通画笔)和PS_DOT(由间断点组成的画笔,须要设置画笔宽度为1)。第二个参数是画笔的宽度,第三个参数是画笔的颜色。
接着将画笔给HDC:
SelectObject (hdc, hpen);
移动画笔到(x1,y1):
MoveToEx (hdc, x1, y1, NULL);
从画图起始位置向(x2,y2)坐标处画线:
LineTo (hdc, x2, y2);
下面列出一些经常使用的画图语句,使用方法和画线差很少,设定完画笔便可使用:
Rectangle(hdc, x1, y1, x2, y2); //画矩形
Ellipse(hdc, x1, y1, x2, y2); //画椭圆
值得注意的是咱们画的图形将由一个"刷子"来填充,使用最简单的单色刷子的方法是:
HBRUSH hbrush = CreateSolidBrush (RGB(r, g, b)); //建立刷子
SelectObject (hdc, hbrush); //使用刷子
画完后,咱们要记住释放HDC:
lpDDSXXX->ReleaseDC(hdc);

5.4 程序实例

5.5 锁定页面
学完了上面的知识,还有不少特效咱们是作不出来的,好比淡入淡出、半透明等,由于DirectDraw中现成的函数根本没有这些功能。固然,要作这些东西仍是有办法的,使用IDirectDrawSurface7::Lock( )就能让咱们为所欲为,由于此函数能够容许咱们直接修改页面。
Lock( )函数的用法以下:

HRESULT Lock(
LPRECT lpDestRect,
LPDDSURFACEDESC2 lpDDSurfaceDesc,
DWORD dwFlags,
HANDLE hEvent
);

第一个参数为一个指向某个RECT的指针,它指定将被锁定的页面区域。若是该参数为 NULL,整个页面将被锁定。
第二个参数为一个 DDSURFACEDESC2结构的地址,将被填充页面的相关信息。
第三个参数,即dwFlags,仍是象之前同样给它DDLOCK_WAIT。
第四个参数规定要为NULL。
如今举一个例子来讲明怎样使用Lock( ),咱们的目标是使lpDDSBack半透明地浮如今lpDDSBuffer上。先看看完整的锁屏部分(注意,这一节只讨论24和32位色下如何操做):

DDSURFACEDESC2 ddsd, ddsd2; //DirectDraw页面描述
ZeroMemory(&ddsd, sizeof(ddsd)); //ddsd用前要清空
ddsd.dwSize = sizeof(ddsd); //DirectDraw中的对象都要这样
ZeroMemory(&ddsd2, sizeof(ddsd2));
ddsd2.dwSize = sizeof(ddsd2);
lpDDSBuffer->Lock(NULL, &ddsd, DDLOCK_WAIT, NULL); //Lock!
lpDDSBack->Lock(NULL, &ddsd2, DDLOCK_WAIT, NULL);

BYTE *Bitmap = (BYTE*)ddsd.lpSurface; //Lock后页面的信息被存在这里,请注意
//这个指针可能每次Lock( )后都不一样!
BYTE *Bitmap2 = (BYTE*)ddsd2.lpSurface;

锁住页面后,Bitmap数组的存储格式是这样的:如为32位色则页面上坐标为(x,y)的点的R/G/B值分别存在Bitmap的y*ddsd.lPitch+x*4,y*ddsd.lPitch+x*4+1,y*ddsd.lPitch+x*4+2处;如为24位色则页面上坐标为(x,y)的点的R/G/B值分别存在Bitmap的y*ddsd.lPitch+x*3,y*ddsd.lPitch+x*3+1,y*ddsd.lPitch+x*3+2处。事实上,lPitch就是行的宽度。因此,如今咱们就能够发挥想象,作出想要的一切效果了,好比说光照效果(将一目标页面按光照表改变亮度便可)!下面是接下来的代码(32位色时):

int pos;
for (int y=0;y<480; y++)
{
for (int x=0; x<640; x++)
{
Bitmap[pos] =(Bitmap[pos]+Bitmap2[pos])>>1; //改R
pos++;
Bitmap[pos] =(Bitmap[pos]+Bitmap2[pos])>>1; //改G
pos++;
Bitmap[pos] =(Bitmap[pos]+Bitmap2[pos])>>1; //改B
pos+=2;//到下一个R处
}
pos+=ddsd.lPitch;
}
lpDDSBack->Unlock(NULL); //Unlock!
lpDDSBuffer->Unlock(NULL);
因为使用Lock后DirectDraw要锁定页面,在没有使用Unlock( )前咱们是没法用其余办法如Blt来修改页面的。因此用完Lock( )要赶快象上面的程序那样Unlock( )。Unlock的方法很简单,lpDDSXXX->Unlock(LPRECT lpDestRect)便可。

5.6 程序提速
上面的程序看起来好像很简单,但运行速度极可能会很慢,即便你直接用汇编重写也不会快多少。缘由是读显存很是慢,写显存的速度也比写内存慢。解决这个问题的方法是:
(1) 把除了主页面外的全部页面放在内存中(初始化页面时将ddsd.ddsCaps.dwCaps中的 DDSCAPS_OFFSCREENPLAIN后再或( | )一项DDSCAPS_SYSTEMMEMORY),固然也要将主页面的页面描述改一下:
ddsd.dwSize=sizeof(ddsd);
ddsd.dwFlags=DDSD_CAPS;
ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE;

后台缓冲改成一个普通的离屏页面。这样作的另外一个好处是你Lock( )一次后就永远获得了页面指针,并且而后一Unlock( )就又能够使用Blt了。因此你就拥有了两种改变页面的手段。

(2) 将Flip( )和不带透明色的BltFast( )改为直接用memcpy( )拷贝。注意要一行一行地拷贝,好比说640x480x24位色下的全屏幕拷贝是这样的:
BYTE *pSrc=(BYTE *)ddsd_src.lpSurface; //源页面
BYTE *pDest=(BYTE *)ddsd_dest.lpSurface; //目标页面

for (int y=0;y<480;y++)
{
memcpy(pDest, pSrc, 1920); //若为32位色则为2560=640*32/8
pSrc+=ddsd_src.lPitch; //移至下一行
pDest+=ddsd_dest.lPitch; //移至下一行
}

事实上memcpy( )还有进一步“压榨”的余地,下面是用SSE指令实现的超高速memcpy( )。把它拷贝到你的程序中享受完美的速度吧(快80%以上),呵呵(nQWORDs为要拷贝多少个8字节,注意它应能整除8!)。
void Qmemcpy(void *dst, void *src, int nQWORDs)
{
#define CACHEBLOCK 1024 //一个块中有多少QWORDs
//修改此值有可能实现更高的速度
int n=((int)(nQWORDs/CACHEBLOCK))*CACHEBLOCK;
int m=nQWORDs-n;
if (n)
{
_asm //下面先拷贝整数个块
{
mov esi, src
mov edi, dst
mov ecx, n //要拷贝多少个块
lea esi, [esi+ecx*8]
lea edi, [edi+ecx*8]
neg ecx
mainloop:
mov eax, CACHEBLOCK / 16
prefetchloop:
mov ebx, [esi+ecx*8] //预读此循环
mov ebx, [esi+ecx*8+64] //预读下循环
add ecx, 16
dec eax
jnz prefetchloop
sub ecx, CACHEBLOCK
mov eax, CACHEBLOCK / 8
writeloop:
movq mm0, qword ptr [esi+ecx*8 ]
movq mm1, qword ptr [esi+ecx*8+8 ]
movq mm2, qword ptr [esi+ecx*8+16]
movq mm3, qword ptr [esi+ecx*8+24]
movq mm4, qword ptr [esi+ecx*8+32]
movq mm5, qword ptr [esi+ecx*8+40]
movq mm6, qword ptr [esi+ecx*8+48]
movq mm7, qword ptr [esi+ecx*8+56]

movntq qword ptr [edi+ecx*8 ], mm0
movntq qword ptr [edi+ecx*8+8 ], mm1
movntq qword ptr [edi+ecx*8+16], mm2
movntq qword ptr [edi+ecx*8+24], mm3
movntq qword ptr [edi+ecx*8+32], mm4
movntq qword ptr [edi+ecx*8+40], mm5
movntq qword ptr [edi+ecx*8+48], mm6
movntq qword ptr [edi+ecx*8+56], mm7
add ecx, 8
dec eax
jnz writeloop
or ecx, ecx
jnz mainloop
}
}
if (m)
{
_asm
{
mov esi, src
mov edi, dst
mov ecx, m
mov ebx, nQWORDs
lea esi, [esi+ebx*8]
lea edi, [edi+ebx*8]
neg ecx
copyloop:
prefetchnta [esi+ecx*8+512] //预读
movq mm0, qword ptr [esi+ecx*8 ]
movq mm1, qword ptr [esi+ecx*8+8 ]
movq mm2, qword ptr [esi+ecx*8+16]
movq mm3, qword ptr [esi+ecx*8+24]
movq mm4, qword ptr [esi+ecx*8+32]
movq mm5, qword ptr [esi+ecx*8+40]
movq mm6, qword ptr [esi+ecx*8+48]
movq mm7, qword ptr [esi+ecx*8+56]

movntq qword ptr [edi+ecx*8 ], mm0
movntq qword ptr [edi+ecx*8+8 ], mm1
movntq qword ptr [edi+ecx*8+16], mm2
movntq qword ptr [edi+ecx*8+24], mm3
movntq qword ptr [edi+ecx*8+32], mm4
movntq qword ptr [edi+ecx*8+40], mm5
movntq qword ptr [edi+ecx*8+48], mm6
movntq qword ptr [edi+ecx*8+56], mm7
add ecx, 8
jnz copyloop
sfence
emms
}
}
else
{
_asm
{
sfence
emms
}
}
}

一样的,memset也可用SSE指令优化(快300%以上),代码在此(nQWORDs应能被8整除):
void Qmemset(void *dst, int c, unsigned long nQWORDs)
{
__asm
{
movq mm0, c
punpcklbw mm0, mm0
punpcklwd mm0, mm0
punpckldq mm0, mm0
mov edi, dst

mov ecx, nQWORDs
lea edi, [edi + ecx * 8]
neg ecx

movq mm1, mm0
movq mm2, mm0
movq mm3, mm0
movq mm4, mm0
movq mm5, mm0
movq mm6, mm0
movq mm7, mm0

loopwrite:
movntq [edi + ecx * 8 ], mm0
movntq [edi + ecx * 8 + 8 ], mm1
movntq [edi + ecx * 8 + 16], mm2
movntq [edi + ecx * 8 + 24], mm3
movntq [edi + ecx * 8 + 32], mm4
movntq [edi + ecx * 8 + 40], mm5
movntq [edi + ecx * 8 + 48], mm6
movntq [edi + ecx * 8 + 56], mm7

add ecx, 8
jnz loopwrite

emms
}
}

(3) 因为DirectDraw提供的带透明色的BltFast( )此时的工做效率不容乐观,因此咱们能够使用RLE压缩掉透明色,同时本身写一个能传送带透明色的图像的函数。你可能已经听过RLE压缩,没听过也不要紧,下面就举个例子。若是你定义透明色为(255,0,255),那么这样的一串点:(255,0,255), (255,0,255) , (255,0,255), (255,0,255), (100,23,43), (213,29,85), (255,0,255), (34,56,112), (255,0,255), (255,0,255)可用这样的方式存储:
首先是4个透明点,那么咱们在开头放一个0,表示由透明色开头,不然需放一个1。而后是0,12,(表示需跳过4x3=0012个字节),接着是0,2,(即后面为002个图像点),100,23,43,213,29,85,0,3(需跳过003个字节),0,1(后面为001个图像点),34,56,112,0,6(需跳过006个字节)。这样用20个字节就表示出了原来须要30个字节的图像,并且BLT时速度更快,由于无须对每个点进行判断是否透明色。
通常来讲,RLE压缩只在图像的每一行内进行(而不是将整块图像做为一个总体)以方便编程。
若是你还须要更快的速度而不在意文件大小的话,还有一种比较“邪”的解决方案,其思路是用另一个程序自动生成成千上万条赋值指令实现BLT。这种办法能够省去大量判断和跳转指令所耗的时间,速度显然达到顶峰。

看起来好像要重写不少东西,其实改动的部分并很少。这样改了以后整个程序的速度就会快不少,但还不能很好地知足全屏幕特效的要求,由于全屏幕特效实在很耗时间,只有用汇编和MMX等指令重写速度才能比较快。因此在下面一章中,咱们将介绍内嵌汇编和MMX指令。固然,你直接用5.7节的现成代码也行。

5.7 特殊效果
为了方便你们编游戏,这里列出了一些经常使用特效的制做方法。若是你想知道这些代码是如何工做的,请阅读第六章。下面的程序基于640x480x32位色,若是你使用的分辨率不一样也很容易修改,但若是使用的色彩数不一样就很麻烦了。

5.7.1 减暗和加亮
减暗和加亮是两个用处很大的特效,在游戏中有许多应用。
__int64 mask=0x4040404040404040; //要减暗或加亮的程度,应为
//0xABCDEFGHABCDEFGH格式
//改一改就能够作出彩色灯光效果
int tt=y*lP +x*4; //lP应为后台缓存的lPitch,x和y应为要处理的区域的左上
//角的坐标
__asm
{
movq mm1,mask;
mov eax,pS; //pS应为后台缓存的指针(lpSurfase)
add eax,tt;
mov ecx,h; //h为要处理的区域的高度
outloop:
push eax;
mov ebx,w; //w为要处理的区域的宽度/2
innloop:
movq mm0,[eax];
psubusb mm0,mm1; //若是要加亮,请把psubusb改为paddusb
movq [eax],mm0;
add eax,8;
dec ebx;
jnz innloop;
pop eax;
add eax,lP;
dec ecx;
jnz outloop;
emms;
}

5.7.2 淡入淡出
淡入淡出常被用在切换游戏场景的时候,实现它是很是简单的,只要不断执行5.7.1节的程序就好了。

5.7.3 半透明
半透明是一个很炫的特效,其原理是把两张图像的每个点的颜色值作一个平均。下面就说说最容易作,速度最快,同时也是最多见的50%半透明的作法:
(1)将特效图像(例如火焰什么的)预先用图像处理软件把RGB值都削减一半。
(2)在刷新屏幕函数中执行此段代码:
__int64 mask=0x7F7F7F7F7F7F7F7F;
int tt=y*lP +x*4; //lP应为后台缓存的lPitch,x和y应为要处理的区域的左上
//角的坐标
__asm
{
movq edx, pB; //pB应为储存特效图像的页面的指针
mov eax, pS; //pS应为后台缓存的指针(lpSurfase)
add eax, tt;
mov ecx, h; //h为要处理的区域的高度
outloop:
push eax;
push edx;
mov ebx, w; //w为要处理的区域的宽度/2
innloop:
movq mm0, [eax];
psrlw mm0, 1;
movq mm1, [edx];
pand mm0, mask; //实现"psrlb"
paddusb mm0, mm1;
movq [eax], mm0;
add eax, 8;
add edx, 8;
dec ebx;
jnz innloop;
pop edx;
pop eax;
add eax, lP;
add edx, lP1; //lP1应为储存特效图像的页面的lPitch
dec ecx;
jnz outloop;
emms;
}

5.7.4 光照
在程序开始时的初始化光照表:
void InitLight( )
{
//BaseLight为基本光照表
BaseLight=new unsigned char [307200]; //640*480=307200
for (int i=0;i<640;i++)
for (int j=0;j<480;j++)
BaseLight[i+j*640]=sqrt((i-320)*(i-320)+(j-240)*
(j-240))*224/400; //最暗处亮度减224
}

按某光照表(LightTable[ ])减低页面上图像的亮度:
BYTE *pl=(BYTE *)&LightTable[0];
__asm
{
mov ecx,480;
mov edi, pb; //pb应为后台缓存的指针(lpSurfase)
mov esi, pl;
outloop:
push edi;
mov ebx, 320; // 640/2=320
innloop:
movq mm0,[edi];
movq mm1,[esi]; //读入八个点的亮度,设其为hgfedcba
punpcklbw mm1,mm1; //ddccbbaa
punpcklbw mm1,mm1; //bbbbaaaa,同时处理两个点
psubusb mm0,mm1; //减去亮度
movq [edi],mm0; //放回去(在内存中是aaaabbbb)
add edi,8;
add esi,2; //下两个点
dec ebx;
jnz innloop;
pop edi;
add edi, lP; //lP应为后台缓存的lPitch
dec ecx;
jnz outloop;
emms;
}

5.7.5 动态光照
首先要弄清楚一个问题:若是一束光的亮度是200,一束光的亮度是150,混合后的结果会是怎样呢?不多是200+150=350,由于这样就超出了255的上界。答案应该是(1-(255-200)/255*(255-150)*255)*255,即255-(255-200)*(255-150)/255=232。若是咱们用"暗度"代替亮度的话,算法就更简单了,只要把两个暗度相乘再除以255便可。
其实最麻烦的不是算出最后的"暗度",而是处理当光照范围超出屏幕时的状况。假设BaseLight[ ]为环境光照表,Light[ ]为要加上的灯光的光照表,那么当灯的位置靠近屏幕边缘时显然就要作一个剪裁。因为MMX指令一次能够算好4个点的光照状况,因此问题变得更加复杂。那么怎么作才最好呢?把BaseLight[ ]扩大几个像素是一种方便的办法,不过此时放光照时就要多加一条指令。下面请看代码:
void InitLight( ) //初始化光照表
{
LightTable=new unsigned char [310080]; //实际光照表,646*480=310080
BaseLight=new unsigned char [310080]; //基本光照表
for (int i=0;i<646;i++) //多开几个字节防止越界
{
for (int j=0;j<480;j++)
{
BaseLight[i+j*646]=sqrt((i-323)*(i-323)+(j-240)*(j-240))*224/400;
}
}
SmallLight=new unsigned char [10004]; //灯光光照表,多开4个字节以防止越界
for (i=0;i<100;i++)
{
for (int j=0;j<100;j++)
{
int dis=sqrt((i-50)*(i-50)+(j-50)*(j-50))*255/45;
if (dis<255)
SmallLight[i+j*100]=dis;
else
SmallLight[i+j*100]=255;
}
}
for (i=10000;i<10004;i++)
SmallLight[i]=255;
}

每次刷新屏幕时,咱们都要先把BaseLight拷贝到LightTable:
memcpy(LightTable,BaseLight,310080);

而后把SmallLight加入LightTable:
p1=&(LightTable[y*646+x]); //y和x为SmallLight[]在屏幕上的左上角坐标
p2=&(SmallLight[rect.top*100+rect.left]); //rect为通过剪裁计算出的SmallLight[]
//的有效区域
//剪裁的思路可参考4.6节的程序
int height=rect.bottom-rect.top;
int width=(rect.right-rect.left+3)/4; //加上3以保证所有处理
if ((height>0)&&(width>0))
{
__asm
{
pxor mm2,mm2; //将mm2清0
mov edi,p1;
mov esi,p2;
mov ebx,height;
_oloop:
mov ecx,width;
push edi;
push esi;
_iloop:
movq mm0,[edi];
movq mm1,[esi];
punpcklbw mm0,mm2;
punpcklbw mm1,mm2;
pmullw mm0,mm1; //乘起来
psrlw mm0,8; //除以256,由于显然比255快
packuswb mm0,mm2;
movd [edi],mm0; //放回去
add esi,4;
add edi,4;
dec ecx;
jnz _iloop;
pop esi;
pop edi;
add esi,100;
add edi,646;
dec ebx;
jnz _oloop;
emms;
}
}

放光照表LightTable的程序改成:
BYTE *pl=(BYTE *)&LightTable[0];
__asm
{
mov ecx,480;
mov edi, pb; //pb应为后台缓存的指针(lpSurfase)
mov esi, pl;
outloop:
push edi;
mov ebx, 320; // 640/2=320
innloop:
movq mm0,[edi];
movq mm1,[esi]; //读入八个点的亮度,设其为hgfedcba
punpcklbw mm1,mm1; //ddccbbaa
punpcklbw mm1,mm1; //bbbbaaaa,同时处理两个点
psubusb mm0,mm1; //减去亮度
movq [edi],mm0; //放回去(在内存中是aaaabbbb)
add edi,8;
add esi,2; //下两个点
dec ebx;
jnz innloop;
pop edi;
add edi, lP; //lP应为后台缓存的lPitch
add esi, 6;
dec ecx;
jnz outloop;
emms;
}

5.7.6 光照系统
为了实现相似Diablo II的光照系统,咱们还有许多工做要作。核心任务是解决墙壁和精灵的精确光照问题和墙壁对光线的遮挡问题。若是你直接使用5.7.5节的代码的话而不对墙壁和精灵作额外处理的话,你首先会发现它们的光照不太正常----一点立体感都没有,由于它们在Z方向有伸展,离原点的距离可不能拿它们在屏幕上的位置来算;第二个问题是墙壁竟然挡不住光。好,接下去咱们来看看该怎么作。
第一个问题是比较好解决的,若是墙壁和精灵很高,只要预先作好一张“若是底部光强为n那么高h处光照为多少”的表,而后刷新屏幕时先用5.7.5节的代码处理地表,再按这张表和光照表画精灵和墙壁便可;若是它们不高,那能够近似处理一下,把它们所有用底部的光照画上去就行啦,Diablo II就是这样作的。
第二个问题就比较麻烦了。因为直接对屏幕上全部点判断是否被墙遮挡显然是不可能的,咱们最好把屏幕分红一个个小格子,再依次判断它们是否被墙遮挡,若是是就在光照表中置这个格子中的全部点一个很暗的值,最后再进行插值运算使光照过渡平滑。挺难作啊,看看具体代码吧:


5.7.7 天气效果
先顺便说一下火焰特效和闪电特效,它们通常是经过放已经作好的小动画实现的。
下面言归正传,先来看看下雨的效果该如何实现。这个效果主要由雨滴和水波特效组成。雨滴效果经过画一些方向(基本均随风向)、亮度、长度基本相同的白线,并在timer里设置它们朝本身的方向移动便可实现;水波特效比较麻烦一点,后面将会详细讲;另外加入一点忽然的淡入淡出模拟闪电效果能够更真实。

下雪也是一种常见的天气特效。

第六章 加速游戏的魔法

为了加速游戏,一提起汇编语言,你们也许会感到很神秘。其实若是你学起来就会发现,它并不是想象中那样难。特别是内嵌汇编,因为它和C++紧密结合,使你没必要考虑不少烦琐的细节(例如输入输出函数的写法),学习起来比较容易。使用内嵌汇编,特别是使用MMX指令,能够大大提升各类游戏中常见特效的速度,对于编出一个漂亮的游戏很是重要。学好汇编语言还有一个特别有趣的用处:能够观察和看懂VC++生成的汇编代码,从而更好地了解C++语言自己和优化代码。

6.1 内嵌汇编简介
在高级语言中,咱们能够无所顾忌地使用各类语句,再由编译器将语句通过很是复杂的编译过程将其转换为机器指令后运行。事实上,处理器自己所能处理的指令很少;更糟糕的是,大部分指令不能直接施用在内存中的变量上,要借助寄存器这个中间存储单元(你能够把寄存器看作是一个变量)。Pentium级处理器的寄存器很少,只有8个32位通用寄存器,分别被称为EAX, EBX, ECX, EDX, EBP, ESP, EDI , ESI。每个通用寄存器的低16位又分别被称为AX, BX, CX, DX, BP, SP, DI , SI。其中AX, BX, CX, DX的高8位被称为AH, BH, CH, DH;低8位被称为AL, BL, CL, DL。注意在内嵌汇编中不该使用EBP和ESP,它们存储着重要的堆栈信息。
还有一个很是重要的寄存器,叫作标志寄存器(EFLAGS),标明了运算结果的各个属性,你不能直接读取或修改它。这些属性有:不溢出/溢出(OF)、正/负(SF)、非零/零(ZF)、偶/奇(PF)、不进位/进位(CF)等。
汇编语言中若要表示有符号整数,需先写出该整数的绝对值的二进制形式,若此数为正数或零则已获得结果,不然将其取反(0->1,1->0)后再加上一即为结果。因此一个8位寄存器可表示的有符号整数范围为从-128到127。
与C++相似,汇编语言提供了获得指针所指内存的方法,这被称为"寻址"。用法很简单,象这样:[寄存器+寄存器*1/2/4/8+32位当即数]就能够获得这个位置的数了。举一个例子,若是有一个数组unsigned short A[100],且EAX中存储着A[0]的地址,那么[EAX+58]即为A[29]的值;若是此时EBX=9,那么[EAX+EBX*2+4]将是A[11]的值。
那么又怎么把一个变量的地址装载进寄存器呢?后面将会介绍。
内嵌汇编的使用方法是:
_asm
{
语句 //后面可加可不加分号
}
你能够把它插入程序中的任何位置,很是灵活。

6.2 基本指令
基本指令均不影响标志寄存器。
第一条指令是传送指令:MOV DEST, SRC。其做用为将DEST赋以值SRC。其中DEST和SRC可为整数(称为当即数)、变量或[地址](存储器),寄存器。需注意的是有的操做是不容许的:在汇编语言中你永远不能将存储器或寄存器内容赋给当即数(你见过5=a这样的语句吗?);也不能将存储器内容直接赋给另外一存储器,必须借助寄存器做为中间变量来实现。关于MOV还有一点要注意的是DEST和SRC必须都为32位/16位/8位,即同一大小。值得特别注意的是,数据在内存中的存储方式是以字节为单位颠倒的,即:若是内存地址0000存储的字节是5F,地址0001存储的字节是34,地址0002存储的字节是6A,地址0003存储的字节是C4,那么地址0000处存储的字(WORD,16位)为345F,双字(DWORD,32位)为C46A345F。
第二条指令是地址装载指令:LEA A, B。其做用为将B变量的地址装载进A寄存器(A需为32位)。要注意的是不能像LEA EAX, Temp[5]这样直接调数组中某个元素的地址。这个指令还能够用来进行简单的运算,考虑下面的语句:LEA EAX, [EBX+ECX*4+8],此语句可将EBX+ECX*4+8的值赋给EAX。
OK,让咱们看一个能够将两个正整数相加的程序:
#include
using namespace std;
//此程序也展现了内嵌汇编应如何使用C++中的指针
void main( )
{
unsigned int a,b;
cin>>a;
cin>>b;
int *c = &a;
__asm //下面是内嵌汇编...
{
mov eax, c; //c中存储的a的地址->eax
mov eax, [eax]; //a的值->eax
//注意直接mov eax, [c]是错误的
mov ebx, b; //能够像这样直接对ebx赋值
lea eax, [eax+ebx];
mov a, eax; //能够直接将eax的值->a
} //内嵌汇编部分结束...
cout<
}
第三条指令是交换指令,形式为XCHG A, B。A和B中至少有一个须为寄存器。若是你想交换两处内存中的数据则要使用寄存器做为中间人。
接着是扩展传送指令,共有两条,为MOVSX DEST, SRC和MOVZX DEST, SRC,它们的用处分别是将SRC中的有符号数或无符号数赋给DEST。这时你就能够将字长较短的寄存器的内容赋给字长较长的寄存器,反之则不行。
你们会发现,8个通用寄存器实在没法知足编程的要求。为了解决这一矛盾,引入了堆栈这一聪明的设想。你能够把堆栈想象为一块放箱子的区域,用入栈(PUSH)可将一个箱子放在现有箱子的最顶端,而出栈(POP)可将现有箱子最顶端的那个箱子取出。看看下面的指令吧:
push eax //eax进栈, 堆栈为eax
push ebx //eax进栈, 堆栈为eax ebx
push ecx //eax进栈, 堆栈为eax ebx ecx
pop ebx //ebx=ecx, 堆栈为eax ebx
pop eax //eax=ebx, 堆栈为eax
pop ecx //ecx=eax, 堆栈空
能够看到,堆栈不只能够方便地暂时存储数据并且还能够调整他们的次序。

6.3 算术指令
算术指令大都影响标志寄存器。这些指令比较容易明白,如今将其列出:
表6.1
clc CF=0
stc CF=1
cmc CF=1-CF
add a,b a=a+b (结果过大可能会有古怪的结果,且置CF 1)
adc a,b a=a+b+CF (加上进位)
sub a,b a=a-b (如结果小于0会加上2的16或32次方,且置CF 1)
sbb a,b a=a-b-CF (减去退位)
inc a a++
dec a a- -
neg a a=-a
mul a eax=eax*a后的低32位, edx=高32位
例: mov eax,234723
mov edx, 12912189
mul edx;
则eax=2835794967
edx=705
div a eax=(edx eax)/a的商, edx=余数
例: mov eax,12121
mov edx,2
此时(edx eax)=8589946713
mov ebx,121
div ebx;
则eax=70991295
edx=18
imul / idiv dest, src 有符号数乘 / 除法,dest=dest乘 / 除src
imul / idiv dest, s1, s2 有符号数乘 / 除法,dest=s1乘 / 除s2

为了让你们弄懂标志,请看两段程序(出现的数都为十六进制数):
表6.2
指令 CF ZF SF OF PF AX或BX
mov ax, 7896 ? ? ? ? ? 7896
add al, ah 1 0 0 0 0 780e
add ah, al 0 0 1 1 0 860e
add al, f2 1 1 0 0 1 8600
add al, 1234 0 0 1 0 0 9834

mov bx, 9048 ? ? ? ? ? 9048
sub bh, bl 0 0 0 1 1 4848
sub bl, bh 0 1 0 0 1 4800
sub bl, 5 1 0 1 0 0 48fb
sub bx, 8f34 1 0 1 1 0 b9c7

6.4 逻辑与移位指令
逻辑指令会将标志寄存器中的OF和CF清零。
表6.3
not a a=~a(注意not与neg不一样!)
and a, b a=a&b
or a, b a=a|b
xor a, b a=a^b

下面是移位指令,其中x可为8位当即数或CL寄存器。
表6.4
sal(也可写成shl) a, x 将a左移x位,CF=移出的那一位数
空位用0补足
sar a, x 将有符号a右移x位,CF=移出的那一位数
空位按a的符号用0/1补足
shr a, x 将无符号a右移x位,CF=移出的那一位数
空位用0补足
rol a, x 将a循环左移(左边出去的数又从最右边回来)
ror a, x 将a循环右移(右边出去的数又从最左边回来)
rcl / rcr a, x 把CF放在目标最左边而后循环左/右移
shld a, b, x 将a左移x位, 空出位用b高端m位填充
例:shld edx, eax, 16可将eax的高16位 放入dx中。
shrd a, b, x 将a右移x位, 空出位用b低端m位填充

6.5 比较、测试、转移与循环指令
比较与测试指令基本上老是与转移指令相配合使用,其形式分别为CMP a, b和TEST a, b。CMP其实是根据a-b的值改变标志寄存器但不改变a和b,能够检测两个数的大小关系。TEST则是根据a&b的值改变标志寄存器,一样不改变a和b。这条指令能够用来测试a中哪些位为1。执行完这些指令后,马上用转移指令就可实现条件转移,由于条件转移语句会根据标志寄存器决定是否转移。转移指令的使用方法就像这样:
__asm{
_addax: add ax,1; //_addax是标号
jmp _addax;
}
转移指令有:
JMP 无条件转移
JE / JZ ZF=1时转移
JNE / JNZ ZF=0时转移

JS SF=1时转移
JNS SF=0时转移
JO OF=1时转移
JNO OF=0时转移
JP / JPE PF=1时转移
JNP / JPO PF=0时转移

根据两无符号数关系转移:
JA / JNBE 大于时转移 (CF或ZF=0)
JBE / JNA 不大于时转移 (CF或ZF=1)
JB / JNAE / JC 小于时转移 (CF=1)
JNB / JAE / JNC 不小于时转移 (CF=0)

根据两有符号数关系转移:
JNLE / JG 大于时转移 ((SF异或OF)或ZF)=0 )
JLE / JNG 不大于时转移 ((SF异或OF)或ZF)=1 )
JL / JNGE 小于时转移 (SF异或OF=1)
JNL / JGE 不小于时转移 (SF异或OF=0)

特殊转移语句:
JECXZ CX=0时转移

为了记住这么多条指令,你只需知道一点,就是无符号数之间的关系分别被称为Above,Equal,Below,分别表明大于,等于,小于;有符号数之间相应的关系则分别被称为Great,Equal,Less。
事实上,有些转移是能够避免的。举个例子,要算一个数的绝对值是否要用转移呢?请看一段程序:
MOV EDX,EAX
SAR EDX,31 //EDX如今全为EAX的符号位
XOR EAX,EDX
SUB EAX,EDX

找出两个数中较大的一个应该要用转移吧?不过也能够象下面的解决方案那样利用标志,真是绝了:
SUB EBX,EAX
SBB ECX,ECX //若是EBX≥EAX,如今ECX=0,不然ECX=FFFFFFFF
AND ECX,EBX
ADD EAX,ECX

下面的一段程序实现了if (a != 0) a = b; else a = c;
CMP EAX,1
SBB EAX,EAX
XOR ECX,EBX
AND EAX,ECX
XOR EAX,EBX

循环语句经常使用的是LOOP,它等价于DEC CX加上JNZ。

下面看一个汇编的综合运用:冒泡排序。
#include
using namespace std;

#define array_size 10

int a[array_size]={42, 73, 65, 97, 23, 59, 18, 84, 36, 6};

void main()
{
int *p;
p=&a[0];
p--;

__asm
{
mov esi,p;
mov ecx,array_size;
_outloop:
mov edx,ecx;
_inloop:
mov eax, [ esi+ecx*4 ]; //一个int占4字节
mov ebx, [ esi+edx*4 ];
cmp eax, ebx;
jnb _noxchg; //不交换
mov [ esi+ecx*4 ], ebx;
mov [ esi+edx*4 ], eax;
_noxchg:
dec edx;
jnz _inloop;
loop _outloop;
}

for (int i=0;i<10;i++)
cout< <<"
mov eax, R;
add eax, 100;
mov R, eax;
mov eax, G;
add eax, 100; // !!!
mov G, eax;
mov eax, B;
add eax, 100;
mov B, eax;
CPU在执行做了标记的一行时会出现咱们不想要的结果,由于G值不会停留在255上。而使用色饱和功能可以使其停留在255上,一样减暗时也会停留在0上。
能够使用下面这个函数检测CPU是否支持MMX指令:
int CheckMMX( )
{
int isMMX=0;
__asm
{
mov eax,1;
cpuid;
test edx,00800000h;
jz NotSupport;
mov isMMX,1;
NotSupport:
}
return isMMX;
}

顺便看看如何判断CPU是否支持SSE吧:
int CheckSSE()
{
int isSSE = 0;
_asm
{
mov eax, 1
cpuid
shr edx,0x1A
jnc NotSupport
mov isSSE, 1
NotSupport:
}
return isSSE;
}

下面是几条最基本的MMX指令:
EMMS
因为执行MMX指令时占用了浮点运算单元,使用完MMX指令后要记住执行这条指令以释放浮点运算单元。
MOVD dest, src
dest为MMX寄存器 / 通用寄存器 / 存储器,src为MMX寄存器 / 通用寄存器 / 存储器。其做用是将src中的32位传送到dest的低32位并置dest的高32位零。
MOVQ dest, src
dest为MMX寄存器 / 存储器,src为MMX寄存器 / 存储器。其做用是将src中的64位传送到dest。

6.7 MMX指令集之算术与比较指令
MMX提供了一些很是好用的算术指令,能够大大加快2D特效的速度。
首先是加减法指令,共有8条,形式为PADD/PSUBxy dest, src,其中x为 空/ "S" / "US",分别表明无色饱和、有符号色饱和,无符号色饱和。y为"B" / "W" 分别表明以一字节或一字为运算单位,即同时执行8条字节加法或4条字加法。若x为空则y还可取D,表明同时执行2条双字加法。dest和src均需为MMX寄存器。举三个例子:
mm0= 008 000 005 000 255 000 141 045
mm1= 000 057 005 000 005 000 131 002
PADDB mm0, mm1后 mm0= 008 057 010 000 004 000 016 047

mm0= 008 000 005 000 255 000 141 045
mm1= 000 057 005 000 005 000 131 002
PADDSB mm0, mm1后 mm0= 008 057 010 000 255 000 255 047

mm0= 001234 000010 000005 008516
mm1= 000001 000020 000001 009343
PSUBSW mm0, mm1后 mm0= 001233 000000 000004 000000

而后是乘法语句,共有三条:
PMADDWD dest, src
假设src原为 A, B, C, D;dest为E, F, G, H。则执行完后dest为A*E+B*F, C*G+D*H。
PMULHW dest, src
假设src原为 A, B, C, D;dest为E, F, G, H。则执行完后dest为A*E高16位, B*F高16位, C*G高16位, D*H高16位。
PMULLW dest, src
该指令与PMULHW几乎相同,就是改为了低16位。
这三条指令在图像变形与旋转、半透明等特效中均有应用。

下面是比较指令:
PCMPEQx dest, src x=B / W / D dest和src均为MMX寄存器
当dest与src相等时,置相应的dest全1,不然置相应的dest全0。例:
mm0=012321, 000912, 023849, 005634
mm1=032123, 000912, 022234, 005634
PCMPEQD mm0, mm1后 mm1=000000, 065535, 000000, 065535

PCMPGTx dest, src x=B / W / D dest和src均为MMX寄存器
当dest大于src时,置相应的dest全1,不然置相应的dest全0。
举一个使用比较指令的小技巧:PCMPEQD mm?, mm? 便可把mm?清零。固然用下面的PXOR mm?, mm?也能够。

6.8 MMX指令集之逻辑与移位指令
PAND dest, src dest为MMX寄存器,src为MMX寄存器 / 内存单元
dest= dest & src。
PANDN dest, src dest为MMX寄存器,src为MMX寄存器 / 内存单元
这条指令的做用是dest= (~dest)&src。
POR dest, src dest为MMX寄存器,src为MMX寄存器 / 内存单元
dest= dest | src。
PXOR dest, src dest为MMX寄存器,src为MMX寄存器 / 内存单元
dest= dest ^ src。
注意MMX指令集中无PNOT这样的指令,你想到了解决方法吗?

PSLLx dest, src x=W / D / Q,表示位移的单位
dest为MMX寄存器, src可为各类寄存器 / 存储器 / 当即数
进行左移,举两个例子:
mm0=86E1 04C7 19F8 42EE
PSLLW mm0, 4后 mm0=6E10 4C70 9F80 2EE0
mm0=028F 76AA 85C9 BEE1
PSLLQ mm0, 8后 mm0=8F76 AA85 C9BE E100
PSRAx dest, src x=W / D
dest为MMX寄存器, src可为各类寄存器 / 存储器 / 当即数
进行有符号数右移。
PSRLx dest, src x=W / D / Q
dest为MMX寄存器, src可为各类寄存器 / 存储器 / 当即数
进行无符号数右移。

咱们会注意到,MMX指令集中没有提供常常要使用的以字节为单位左/右移的指令,解决问题的方法其实也很简单。分析一下以字为单位左/右移后获得的结果和以字节为单位左/右移应获得的结果你会发现它们有类似之处,又由于通常来讲咱们左/右移的位数是固定的,因此咱们只要先以字为单位左/右移后再根据左/右移的位数PAND一个常数便可。若是你左/右移的位数不定,那就只能借助下面的格式调整指令了。

6.9 MMX指令集之格式调整指令
格式调整指令是MMX指令中很是重要的组成部分,包括打包和扩展两大类。
打包指令有:
PACKSSx dest, src x=WB / DW 最后获得有符号数,带色饱和
例: mm0= 8000, -200, 55, 34
mm1= -1281, 27, -99, 127
PACKSSWB mm0, mm1后 mm0= -128, 27, -99, 127, 127, -128, 55, 34
PACKUSx dest, src x=WB
除了最后获得无符号数(0-255)以外与PACKSSx相同。

扩展指令有:
PUNPCKHx dest, src x=BW / WD / DQ
例: mm0= AF, 45, 0E, 8A, 12, 67, FF, 00
mm1= 11, 91, AB, 5C, 93, B8, 0F, 09
PUNPCKHBW mm0, mm1后 mm0= 11AF, 9145, AB0E, 5C8A

PUNPCKLx dest, src x=BW / WD / DQ
例: mm0= AF, 45, 0E, 8A, 12, 67, FF, 00
mm1= 11, 91, AB, 5C, 93, B8, 0F, 09
PUNPCKLBW mm0, mm1后 mm0= 9312, B867, 0FFF, 0900

MMX指令集已经基本上介绍完了,如今你就应该想一想怎么把它运用到程序中去了。在5.7节给出了一些经常使用的2D特效用MMX指令的实现,可供参考。
第七章 我没有想好名字

若是你只靠上面几章所讲述的知识编了个游戏,喜欢的人恐怕会很少?,为何?由于没有人会玩一个控制不流畅并且声音效果不佳的游戏。为了在游戏中更好地管理各类输入设备,咱们须要使用DirectInput。而经过使用DirectX Audio能够在游戏中实现各类更逼真的音乐效果。它们都是DirectX的重要组成部分。使用DirectInput前咱们须要#include 并在工程中加入dinput8.lib和dxguid.lib,而使用DirectX Audio前咱们须要#include 并在工程中加入dsound.lib和dxguid.lib。

7.1 读取键盘数据
首先,咱们必须建立一个DirectInput8对象(DirectX 9.0并无对DInput和DAudio作多大改动),就像这样:
LPDIRECTINPUT8 pInput;
DirectInput8Create(GetModuleHandle(NULL),
DIRECTINPUT_VERSION,
IID_IDirectInput8,
(void**)&pInput,
NULL);
而后,咱们须要建立一个DirectInput设备:
LPDIRECTINPUTDEVICE8 pDev;
pInput->CreateDevice(GUID_SysKeyboard, &pDev, NULL);
设置好它的数据格式:
pDev->SetDataFormat(&c_dfDIKeyboard);
设置它的协做级,这里设为独占设备+前台:
pDev->SetCooperativeLevel(hwnd,DISCL_EXCLUSIVE|DISCL_FOREGROUND);
获取设备:
pDev->Acquire();
像上面那样初始化后,咱们就已经把Windows对键盘的控制权剥夺了,之后的键盘消息将不会被送入消息循环,咱们能够把消息循环中处理键盘消息的语句拿掉了。固然,这时咱们须要在程序的适当地方,好比说在刷新游戏时,加入对键盘数据进行读取和处理的语句,就像下面的一段程序:
#define KEYDOWN(key) (buffer[key] & 0x80) //定义一个宏,方便处理键盘数据
char buffer[256]; //键盘数据
pDev->GetDeviceState(sizeof(buffer),(LPVOID)&buffer); //获得键盘数据
if (KEYDOWN(DIK_XXX)) //若是XXX键被按下…(请参阅附录二)
{
…… //处理之
}
…… //处理其它键

哈哈,真是挺方便的。有时候真的有点怀疑DirectX是否是一种回到遥远的可爱的DOS时代的“倒退”。由于不管是DirectInput仍是DirectDraw都是太像DOS下的作法了。

7.2 读取鼠标数据
读取鼠标数据和读取键盘数据的步骤差很少,首先也是要建立设备:
pInput->CreateDevice(GUID_SysMouse, &pDev, NULL);
设置数据格式:
pDev->SetDataFormat(&c_dfDIMouse);
设置协做级:
pDev->SetCooperativeLevel(hwnd,DISCL_EXCLUSIVE | DISCL_FOREGROUND);
获取设备:
pDev->Acquire();
那么怎样读取鼠标数据呢?若是要取得鼠标的当前状态,这样便可:
DIMOUSESTATE mouse_stat; //鼠标状态
//获得鼠标状态
pDev->GetDeviceState(sizeof(DIMOUSESTATE),(LPVOID)&mouse_stat);
获得的mouse_stat是一个DIMOUSESTATE类型的结构,它有四个成员:lX,lY,lZ和rgbButtons[4]。其中lX、lY和lZ分别是自上次调用此函数以来鼠标在X轴、Y轴和Z轴(滚轮)方向上移动的距离,而不是鼠标此时的坐标;其距离单位不是像素,但你彻底能够把它看作以像素为单位。因此,咱们须要定义两个变量mousex=0和mousey=0,而后把lX和lY累加上去便可。这样作的好处是鼠标坐标再也不受屏幕的制约,并且屏幕中心的mousex和mousey值能够永远是0,不随屏幕分辨率而改变。rgbButtons是一个存储哪些鼠标键被按下的数组,咱们能够这样作来读取它:
//定义一个宏,方便处理鼠标数据
#define MOUSEBUTTONDOWN(b) (mouse_stat.rgbButtons[b]&0x80)
if (MOUSEBUTTONDOWN(0)) //若是左键被按下…
{
…… //处理之
}
…… //处理右键(1)和中键(2)

7.3 恢复和关闭DirectInput
7.3.1 恢复DirectInput设备
就像在DirectDraw中那样,使用DirectInput的程序被最小化时DirectInput设备会出现"丢失"现象。恢复的办法很干脆:先关闭DirectInput再从新初始化便可。

7.3.2 关闭DirectInput
关闭DirectInput也是很是简单的(SAFE_RELEASE的定义在4.8节):
pDev->Unacquire( );
SAFE_RELEASE(pDev);
SAFE_RELEASE(pInput);

7.4 初始化和关闭DirectX Audio
7.4.1 初始化DirectX Audio
使用DirectX Audio前,按规矩仍是要先初始化。在下面的这段初始化程序中要用到三个DXAudio提供的对象:IDirectMusicLoader八、IDirectMusicPerformance8和IDirectMusicSegment8。IDirectMusicLoader8顾名思义是用来调入音乐的,IDirectMusicPerformance8能够认为是音频设备,而IDirectMusicSegment8就是表明音乐。

#include
IDirectMusicLoader8* pLoader= NULL;
IDirectMusicPerformance8* pPerf = NULL;
IDirectMusicSegment8* pSeg = NULL;

CoInitialize(NULL); //初始化COM
CoCreateInstance(CLSID_DirectMusicLoader, NULL,
CLSCTX_INPROC, IID_IDirectMusicLoader8,
(void**)&pLoader); //建立pLoader对象
CoCreateInstance(CLSID_DirectMusicPerformance, NULL,
CLSCTX_INPROC, IID_IDirectMusicPerformance8,
(void**)&pPerf ); //建立pPerf对象
pPerf->InitAudio(
NULL, //这里能够是一个指向IDirectMusic*对象的指针
NULL, //这里能够是一个指向IDirectSound*对象的指针
hwnd, //窗口句柄
DMUS_APATH_SHARED_STEREOPLUSREVERB, //AudioPath类型
//这里打开了立体声及混响,效果很不错
64, //音乐通道数
DMUS_AUDIOF_ALL, //使用声卡的全部特性
NULL //能够指向一个DMUS_AUDIOPARAMS对象,更详细地说明各类参数
);

7.4.2 关闭DirectX Audio
关闭DXAudio仍是老思路,先按7.5.3节的办法中止音乐,而后Release便可:

pPerf->CloseDown(); //关闭

SAFE_RELEASE(pLoader); //释放对象
SAFE_RELEASE(pPerf);
SAFE_RELEASE(pSeg);

CoUninitialize(); //中止使用COM

7.5 播放MIDI和WAV音乐
MIDI音乐和WAV音乐在游戏编程中常常用到。其中前者通常是用做背景音乐,然后者可能是用在各类音效方面,如发射导弹等等。虽然咱们能够用3.4.4节的方法,但使用DXAudio能够更充分地利用硬件资源,从而实现更少的CPU占用率。方法以下:

7.5.1 调入MIDI和WAV文件
在播放音乐以前,第一步固然是调入音乐文件:
CHAR strSoundPath[MAX_PATH]; //存储音乐所在路径
GetCurrentDirectory(MAX_PATH, strSoundPath); //获得程序所在路径
strcat(strSoundPath, "//Sounds"); //这里设置音乐在程序路径下的Sounds子目录

WCHAR wstrSoundPath[MAX_PATH]; //存储UNICODE形式的路径
//将路径转为UNICODE形式
MultiByteToWideChar(CP_ACP, 0,strSoundPath, -1, wstrSoundPath, MAX_PATH);

pLoader->SetSearchDirectory(
GUID_DirectMusicAllTypes, //搜索全部支持的格式
wstrSoundPath,
FALSE
);

WCHAR wstrFileName[MAX_PATH]; //存储UNICODE形式的文件名
//将文件名转为UNICODE形式
MultiByteToWideChar(CP_ACP, 0, "a.mid", -1, wstrFileName, MAX_PATH);
pLoader->LoadObjectFromFile(
CLSID_DirectMusicSegment, //文件类型
IID_IDirectMusicSegment8, //目标对象类型
wstrFileName, //文件名,一样应为UNICODE形式
(LPVOID*) &pSeg //目标对象
);

7.5.2 播放MIDI和WAV文件
调入完音乐以后,在适当的时候能够开始播放音乐:
pSeg->SetRepeats(音乐要重复的次数); //DMUS_SEG_REPEAT_INFINITE为无限次
pSeg->Download( pPerf ); //将音乐数据传给pPerf

pPerf->PlaySegmentEx(
pSeg, //要播放的音乐
NULL, //如今只能是NULL
NULL,
0,
0,
NULL,
NULL,
NULL //Audiopath,如今先不要管它是什么
);

7.5.3 中止播放
中止播放音乐也是很是简单的:
pPerf->Stop(
NULL, //中止哪一个通道,NULL表明全部通道
NULL,
0, //通过多少时间才中止播放
0
);

7.6 在3D空间中播放音乐
咱们的下一个问题是如何使音乐更逼真,最好能使音乐3D化,这将大大增强真实性。要实现这个其实也不难,首先咱们要设定所谓的AudioPath,它能够指定音乐的播放方式。
IDirectMusicAudioPath8* pPath; //AudioPath

pPerf->CreateStandardAudioPath( //建立AudioPath
DMUS_APATH_DYNAMIC_3D, //3D的
64, //通道数
TRUE, //是否当即激活AudioPath
&pPath //要建立的AudioPath
);

而后,咱们要从AudioPath中获得一个"3D缓存",它将存储音乐的播放位置等信息。

IDirectSound3DBuffer8* pBuffer; //3D缓存

pPath->GetObjectInPath(
DMUS_PCHANNEL_ALL, //搜索所有通道
DMUS_PATH_BUFFER, //为DirectSound 缓存
0,
GUID_NULL,
0,
IID_IDirectSound3DBuffer8, //缓存类型
(LPVOID*) &pBuffer //要建立的缓存
);

下一步是设定音乐在3D空间的何处播放。例如:
pBuffer->SetPosition( -0.1f, 0.0f, 0.0f, DS3D_IMMEDIATE );
这条指令把音乐的播放位置设为(-0.1f, 0.0f, 0.0f),因为默认的听众位置在坐标(0, 0, 0)处,脸朝着Z轴的正方向,头朝着Y轴正方向,因此其效果将是听众的右耳听获得声音但左耳听不到声音,就像音乐是从本身的正右方发出。
若是把最后一个参数设为DS3D_DEFERRED,此操做将被挂起直到调用IDirectSound3DListener8 :: CommitDeferredSettings( )为止,这样能够一次处理多个设定防止反复计算。
咱们还能够设置音乐源的速度及其播放角度:
pBuffer->SetVelocity(vx,vy,vz,DS3D_IMMEDIATE);//设置在x,y,z轴方向的速度
//设置播放角度大小(度),inncone为内角度,outcone为外角度
//音乐在内角度范围内不衰减,在内外角度之间慢慢衰减,超出外角度时消失
pBuffer->SetConeAngles(inncone, outcone, DS3D_IMMEDIATE);
pBuffer->SetConeOrientation(x, y, z, DS3D_IMMEDIATE); //设置朝哪一个方向播放

那么咱们如何设定听众的位置呢?能够这样:
IDirectSound3DListener8* pListener;

pPath->GetObjectInPath( //建立听众
0,
DMUS_PATH_PRIMARY_BUFFER,
0,
GUID_NULL,
0,
IID_IDirectSound3DListener8,
(LPVOID*) &pListener
);
//设置听众面向(x1,y1,z1),头朝着(x2,y2,z2)
pListener->SetOrientation(x1,y1,z1,x2,y2,z2,DS3D_IMMEDIATE);
pListener->SetPosition(x,y,z,DS3D_IMMEDIATE); //听众位置
pListener->SetVelocity(vx,vy,vz,DS3D_IMMEDIATE); //听众速度

7.7 播放MP3音乐
MIDI音乐的问题是对声卡的依赖性过大,好声卡和差声卡的播放效果实在相差太远。WAV音乐虽然绝对足够精确,但占用的空间之大不可小视。MP3恐怕是一个较好的解决方案。值得注意的是,播放MP3并不须要DirectX Audio,须要的是DirectShow。因此,咱们要#include ,并在工程中加入strmiids.lib。

7.7.1 调入MP3文件
下面把初始化DirectShow和调入MP3合起来讲说吧。首先,咱们要定义三个对象,其中IGraphBuilder*类型的能够认为是媒体播放设备,IMediaControl*类型的变量负责媒体的播放控制,而IMediaPosition*类型的变量负责媒体的播放位置设定。

IGraphBuilder* pGBuilder;
IMediaControl* pMControl;
IMediaPosition* pMPos;
CoInitialize(NULL); //初始化COM
//建立各个对象
CoCreateInstance(CLSID_FilterGraph, NULL,
CLSCTX_INPROC, IID_IGraphBuilder, (void**)&pGBuilder);
pGBuilder->QueryInterface(IID_IMediaControl, (void**)&pMControl);
pGBuilder->QueryInterface(IID_IMediaPosition, (void**)&pMPos);

CHAR strSoundPath[MAX_PATH]; //存储音乐所在路径
WCHAR wstrSoundPath[MAX_PATH]; //存储UNICODE形式的路径
GetCurrentDirectory(MAX_PATH, strSoundPath);
strcat(strSoundPath, "//Sounds//");
strcat(strSoundPath, "a.mp3"); //假设要播放的是Sounds子目录下的a.mp3
MultiByteToWideChar(CP_ACP, 0, strSoundPath, -1,wstrSoundPath, MAX_PATH);
pGBuilder->RenderFile(wstrSoundPath, NULL); //调入文件

7.7.2 播放MP3文件
播放MP3的方法十分简单:
pMPos->put_CurrentPosition(0); //移动到文件头
pMControl->Run(); //播放

7.7.3 中止播放和释放对象
最后,咱们要中止播放音乐并释放各个对象:
pMControl->Stop(); //中止播放
//释放对象
SAFE_RELEASE(pMControl);
SAFE_RELEASE(pMPos);
SAFE_RELEASE(pGBuilder);
CoUninitialize(); //释放COM
第八章 支撑游戏的基石

在这一章中,咱们将看看数据结构,算法和人工智能在游戏中的应用。我想每一个人都知道“人工智能”这个字眼吧,但数据结构和算法是干什么的呢?说简单点,数据结构就是在程序中各类数据的组织形式,而算法就是处理这些数据的方法。Niklaus Wirth曾说过“数据结构+算法=程序”,可见其重要性。

8.1 链表
链表是一种灵活的数据结构,它能够说是把指针用到了极致。最简单的链表是由一个个像这样的节点组成的:
struct node //节点
{
int data; //节点数据
node* next; //指向下一个节点的指针
};
一个个链表的节点就像一节节火车车箱同样经过next指针一个接一个地链接着,当咱们在链表中查找数据时,咱们也要一个接一个地往下找。能够想象,在链表的任何位置添加新节点都是十分简单的,而删除链表中的某个节点时也只要把它的父节点指向它的子节点便可。正由于链表有这些特色,它被普遍地应用于各类元素的个数或是元素的排列顺序常常须要改变的场合。
咱们还能够使用双向链表,即再使用一个指向上一个节点的指针。这将使链表变得更加方便——能够从后往前查找节点,但同时也增大了链表的大小。
链表在游戏编程中有很多应用,例如组织游戏中像精灵(Sprite,指游戏中会移动的东西)这样的常常须要修改的元素。

8.2 哈希表
使用哈希表(Hash Table)能够大大减小查找工做的时间。举一个简单的例子,若是你要在一本字典中找某单词,那你应该怎样作呢?若是不使用哈希表,那么你彷佛只能一个个找下去。固然,咱们知道字典是排好序的,因此大可以使用二分查找等更快的方法。但若是是职工编号等彻底无序的数据呢?这时,咱们须要一张哈希表。
怎么创建哈希表呢?所谓哈希表,实际上是一个很简单的东西,它能够说是为数据创建了一个索引。仍是上面那个例子,咱们首先应该经过某一个函数的变换,把字典里的全部单词变成一些尽可能不相同的数。若是能作到彻底不相同的话,这个函数就是一个完美的哈希函数。固然,这显然比较难。一个比较糟糕的哈希函数——咱们给它起个名字叫f(x) ——就是按单词的头一个字母,把单词转换成0到25之间的数,就像咱们日常查字典时那样。好一点的解决方案是把单词的全部字母都这样转换一下,而后再加起来,对某一个大数取模。下一步,咱们创建一个大数组HashTable,它的大小要能容纳全部哈希函数的可能取值,对于f(x),咱们要开一个HashTable[26]。而后咱们把这个数组的每个元素都变成一个链表,把对应的单词一个接一个地放进去(其实把单词转换成数后就应该马上把它放进数组)。此时HashTable[0]的内容就像这样:"a"?"Aachen"?"Aalborg"?"aardvark"?…
如今你们看出来了吧,是的,咱们只要把咱们要找的单词经过f(x)转换成一个数,而后再在HashTable[f(x)]中查找便可。哈希函数取得越好,相同哈希函数的单词就越少,咱们的查找就越快。然而不容忽视的是,数组极可能也变大了。因此哈希表是用空间换时间。
关于哈希函数的选取,和若是有两个元素哈希函数值相同时的处理,如今都研究得比较多。好比说有一种办法是:在HashTable[f(x)]已被其它元素占据时,看看HashTable[g(f(x))]是不是空的,若是还不行就看HashTable[g(g(f(x)))],直到行为止。g(x)能够是x+1之类。显然,若是使用这种办法,HashTable[]的大小要比元素的个数多。这种办法避免了链表的使用,但增长了计算g(x)的花费。总之,一切要根据实际状况选择。
最后给一个比较好用的Hash函数给你们吧,它能将一个单词转为一个整数:
int Hashf(const char *s)
{
int hash, i, l;
hash = 0;
l = strlen(s);
for(i=0; i
hash=(hv*26+s[i]-'a')%size; //size为hash表大小
return hv;
}

8.3 快速排序
最经常使用的算法之一是排序算法。因为对排序算法的速度要求较高,咱们一般使用快速排序。其算法以下:
void QuickSort(int begin, int end) //对数组a排序,start为开始排序的位置,end为排
{ //序结束位置,例如a[10]则start=0,end=9。
int p=begin;
int q=end;
int mid=a[p]; //标准数
int temp;
while(p
{
while (a[p]
while (a[q]>mid) q--; //数组右边的数大于等于标准数
if (p<=q)
{
temp=a[p];
a[p]=a[q];
a[q]=temp; //交换a[p]、a[q]。
p++; q--;
}
}
if (q>begin) QuickSort(begin,q); //继续对前半部分排序
if (p
}

其实快速排序的思路是不难理解的,首先在头尾设置两个指针而后向中间移动,发现两指针所指的数交换会改善排序情况则交换,如两指针到达或越过了对方那么就代表已经把数分红了两组,再递归调用本身对这两组分别实施同一过程便可。

8.4 深度优先搜索
下面就讲讲图的搜索算法,它们在找路和AI中都颇有用。最经常使用的图搜索算法是深度优先搜索(DFS)和广度优先搜索(BFS)。
深度优先搜索,即能走就走,如有多条路可走则按照必定的次序选择(如上下左右),但不走回头路。若是无路可走就退回。显然这种方法不必定能找到最短的路径,但它对内存的要求很小。因为与真实的找路过程有类似之处,因此可让精灵直接按搜索的过程移动,不需任何等待。不过因为上下左右的次序太机械,精灵一开始并非朝着最短的路线走去,因此移动路线还不够真实,特别在比较空阔的时候会容易找不到路。
例如,下面的地图(黑色的格子表明墙,其它格子都是能够行走的)若是要从左上角走到右下角,深度优先搜索的次序以下(A->Z->a->o):
?????????????? ??? ???????????????????????
??? ????A???a???d?e?f??????
??? ????B???Z?b?c???g??????
??C?X?Y???????h??????
??? ????D???????????i?j????
??? ????E???L?????T???k?l??
??? ????F?J?K?N?O?S?U???m??
??? ????G???M???P???V???n??
??? ????H?I???R?Q???W???o??
??? ???????????????????????
图8.1
下面就是标准的深度优先搜索程序:
#include
#include
using namespace std;

#define SX 10 //宽
#define SY 10 //长

int dx[4]={0,0,-1,1}; //四种移动方向对x和y坐标的影响
int dy[4]={-1,1,0,0};

char Block[SY][SX]= //障碍表
{{ 0,1,0,0,0,0,0,0,0,0 },
{ 0,1,1,0,1,1,1,0,0,0 },
{ 0,0,0,0,0,0,0,0,0,0 },
{ 1,1,1,0,1,0,0,0,1,0 },
{ 0,1,0,0,1,0,1,1,1,0 },
{ 0,1,0,0,1,1,1,1,1,0 },
{ 0,0,0,1,1,0,0,0,1,0 },
{ 0,1,0,0,0,0,1,0,1,0 },
{ 0,1,1,1,0,1,1,0,1,1 },
{ 0,0,0,0,0,0,1,0,0,0 }};

int MaxAct=4; //移动方向总数
char Table[SY][SX]={0}; //是否已到过
int Level=-1; //第几步
int LevelComplete=0; //这一步的搜索是否完成
int AllComplete=0; //所有搜索是否完成
char Act[1000]={0}; //每一步的移动方向,搜索1000步
int x=0,y=0; //如今的x和y坐标
int TargetX=9,TargetY=9; //目标x和y坐标

void Test( );
void Back( );
int ActOK( );

void main( )
{
Table[y][x]=1; //作已到过标记
while (!AllComplete) //是否所有搜索完
{
Level++;LevelComplete=0; //搜索下一步
while (!LevelComplete)
{
Act[Level]++; //改变移动方向
if (ActOK( )) //移动方向是否合理
{
Test( ); //测试是否已到目标
LevelComplete=1; //该步搜索完成
}
else
{
if (Act[Level]>MaxAct) //已搜索完全部方向
Back( ); //回上一步
if (Level<0) //所有搜索完仍无结果
LevelComplete=AllComplete=1; //退出
}
}
}
}

void Test( )
{
if ((x==TargetX)&&(y==TargetY)) //已到目标
{
for (int i=0;i<=Level;i++)
cout<<(int)Act[i]; //输出结果
LevelComplete=AllComplete=1; //完成搜索
}
}

int ActOK( )
{
int tx=x+dx[Act[Level]-1]; //将到点的x坐标
int ty=y+dy[Act[Level]-1]; //将到点的y坐标
if (Act[Level]>MaxAct) //方向错误?
return 0;
if ((tx>=SX)||(tx<0)) //x坐标出界?
return 0;
if ((ty>=SY)||(ty<0)) //y坐标出界?
return 0;
if (Table[ty][tx]==1) //已到过?
return 0;
if (Block[ty][tx]==1) //有障碍?
return 0;
x=tx;
y=ty; //移动
Table[y][x]=1; //作已到过标记
return 1;
}

void Back( )
{
x-=dx[Act[Level-1]-1];
y-=dy[Act[Level-1]-1]; //退回原来的点
Table[y][x]=0; //清除已到过标记
Act[Level]=0; //清除方向
Level--; //回上一层
}

输出结果是224442221322244414422244,其中1表明上,2表明下,3表明左,4表明右。

8.5 广度优先搜索
与深度优先搜索相对应的是广度优先搜索。这种方法的思路很简单,就是先搜索一步可到的点,再搜索两步可到的点......如此直到找到目标点为止。这种搜索方法显然能保证走的是最短路径,搜索速度也较快,不过对空间的占用较大。请看标准广度优先搜索程序:
#include
#include
using namespace std;

#define SX 10 //宽
#define SY 10 //长

int dx[4]={0,0,-1,1}; //四种移动方向对x和y坐标的影响
int dy[4]={-1,1,0,0};

char Block[SY][SX]= //障碍表
{{ 0,1,0,0,0,0,0,0,0,0 },
{ 0,1,1,0,1,1,1,0,0,0 },
{ 0,0,0,0,0,0,0,0,0,0 },
{ 1,1,1,0,1,0,0,0,1,0 },
{ 0,1,0,0,1,0,1,1,1,0 },
{ 0,1,0,0,1,1,1,1,1,0 },
{ 0,0,0,1,1,0,0,0,1,0 },
{ 0,1,0,0,0,0,1,0,1,0 },
{ 0,1,1,1,0,1,1,0,1,1 },
{ 0,0,0,0,0,0,1,0,0,0 }};
int AllComplete=0; //所有完成标志

int LevelNow=1, //搜索到第几层
Act, //如今的移动方向
ActBefore, //如今的节点的父节点
MaxAct=4, //移动方向总数
ActNow, //如今的节点
tx,ty; //当前坐标
int LevelFoot[200] = {0}, //每一层最后的节点
ActHead[3000] = {0}; //每个节点的父节点
char AllAct[3000] = {0}; //每个节点的移动方向
char ActX[3000] = {0}, ActY[3000] = {0}; //按节点移动后的坐标
char Table[SY][SX] = {0}; //已到过标记
char TargetX=9,TargetY=9; //目标点

int ActOK( );
int Test( );

int ActOK( )
{
tx=ActX[ActBefore]+dx[Act-1]; //将到点的x坐标
ty=ActY[ActBefore]+dy[Act-1]; //将到点的y坐标
if ((tx>=SX)||(tx<0)) //x坐标出界?
return 0;
if ((ty>=SY)||(ty<0)) //y坐标出界?
return 0;
if (Table[ty][tx]==1) //已到过?
return 0;
if (Block[ty][tx]==1) //有障碍?
return 0;
return 1;
}

int Test( )
{
if ((tx==TargetX)&&(ty==TargetY)) //已到目标
{
int act=ActNow;
while (act!=0)
{
cout<<(int)AllAct[act]; //一步步向前推出全部移动方向
act=ActHead[act]; //因此输出倒了过来
}
return 1;
}
return 0;
}

void main()
{
LevelNow=1;
LevelFoot[1]=0;
LevelFoot[0]=-1;
ActX[0]=0;
ActY[0]=0;
while (!AllComplete)
{
LevelNow++; //开始搜索下一层
LevelFoot[LevelNow]=LevelFoot[LevelNow-1];
//新一层的尾节点先设为与上一层相同
for (ActBefore=LevelFoot[LevelNow-2]+1;
ActBefore<=LevelFoot[LevelNow-1];
ActBefore++) //对上一层全部节点扩展
{
for (Act=1;Act<=MaxAct;Act++) //尝试全部方向
{
if ((ActOK( )) && (!AllComplete)) //操做可行?
{
LevelFoot[LevelNow]++; //移动尾指针准备加入新节点
ActNow=LevelFoot[LevelNow]; //找到加入新节点位置
ActHead[ActNow]=ActBefore; //置头指针
AllAct[ActNow]=Act; //加入新节点
ActX[ActNow]=tx;
ActY[ActNow]=ty; //存储移动后位置
Table[ty][tx]=1; //作已到过标记
if (Test( )) AllComplete=1; //完成?
}
}
}
}
}

输出结果是4422244144422322244422,倒过来就是2244422232244414422244,确实比DFS找到的路径短。
使用DFS和BFS可解决各式各样的问题。下面就给你们出一道著名的中学生计算机竞赛题,看看你能不能较快地解决它:
有三个没有刻度的桶,容量分别为3升、5升、8升。如今8升的桶是满的,你能够将水在桶中倒来倒去。例如,首先8->3,那么8升桶内将会有5升水,3升桶会被装满;而后3->5,那么3升桶将被倒空,5升桶内将有3升水。你的目标是平分这8升水,即便5升桶和8升桶内均有4升水。

8.6 启发式搜索
你们听过一个叫A*的东东吗?A*就是一种启发式搜索方法。固然,你没听过也没关系,下面就讲讲启发式搜索。启发式搜索的核心是一个估价函数F(x),扩展节点时先扩展F(x)值小的节点。F(x)又等于G(x)+H(x),其中G(x)为从起始状态到当前状态的代价,通常就是已经搜索了多少步;而H(x)则是当前状态到目标状态的估计代价,即估计还有多少步就可到目标。为了保证搜索到的是最优解,H(x)必须大于或等于实际代价,F(子节点)也必须大于或等于F(父节点)。就找路的问题来讲,咱们能够把H(x)定为离目标的直线距离,显然这样就能够知足上面的两个条件,保证找到的是最短路径。启发式搜索的好处是速度快并且占用空间很少。
显然,这个算法的关键有两点:一,如何快速地在大量节点中找到F(x)最小的节点;二,如何在节省空间的状况下快速判断一个节点是否已扩展过(其实这也是前面的DFS和BFS应用于寻路时须要解决的,靠一个数组Table[SY][SX]对空间占用过大,不过话又说会来,若是你的搜索限定在小范围内,例如几个屏幕,用数组的办法更好)。首先,咱们来看看要使用的数据结构:
#include
using namespace std;

int node_count = 0; //目前的待扩展节点数
int allnode_count = 0; //目前的节点数

#define SX 10 //地图宽
#define SY 10 //地图长
#define MAX_NODE 100 //容许同时存在多少待扩展节点
#define MAX_ALLNODE 1000 //容许节点数
#define MAX_HASH 1999 //Hash表大小,最好是质数

int tx = 9, ty = 9; //目标坐标

int dx[4] = {0,0,-1,1};//四种移动方向对x和y坐标的影响
int dy[4] = {-1,1,0,0};

char Block[SY][SX] = //障碍表
{{ 0,1,0,0,0,0,0,0,0,0 },
{ 0,1,1,0,1,1,1,0,0,0 },
{ 0,0,0,0,0,0,0,0,0,0 },
{ 1,1,1,0,1,0,0,0,1,0 },
{ 0,1,0,0,1,0,1,1,1,0 },
{ 0,1,0,0,1,1,1,1,1,0 },
{ 0,0,0,1,1,0,0,0,1,0 },
{ 0,1,0,0,0,0,1,0,1,0 },
{ 0,1,1,1,0,1,1,0,1,1 },
{ 0,0,0,0,0,0,1,0,0,0 }};

struct NODE //待扩展节点的资料
{
int x,y,f,level,n;
}
node[MAX_NODE];

struct //节点的资料
{
int act,father;
}
allnode[MAX_ALLNODE];

struct //Hash表,用来判断节点是否已访问过
{
int x,y;
}
Hash[MAX_HASH];

全部待扩展节点储存在一个数组node[ ]中。node[ ]的每个元素都是一个结构NODE{ int x,y,f,level,n; },其中x和y是节点坐标,f是节点的F函数值,level是节点位于何层,n是节点在allnode[ ]中的位置。node[1]存储的永远是F函数值最小且未扩展的节点,这是经过AddNode( )和GetNode( )实现的。最后还有一个node_count,存储目前节点的实际数量。
数组node[ ]的实际结构是相似这样的:

图8.2
有点像一棵树吧,这棵树的最大特色是父节点的F函数值永远比子节点的小,例如node[1].f node[x/2].f(x>1)。那么咱们如何将一个新节点加入才能保持这棵树的性质呢?首先,咱们要把node_count++,这时node[node_count](按照图8.x就是node[10],它是node[5]的子节点)空了出来。而后,咱们将这个新节点和它的父节点的F函数值比较。若是新节点的较小,就把它的父节点复制到新节点处(好比node[10]=node[5];),新节点的预备位置上移到父节点处,这样一直作下去;若是父节点的较小,子节点的位置就已可肯定。代码是这样的:
void AddNode(int x, int y, int act, int level, int father)
{
if ((x>=SX) || (x<0) || (y>=SY) || (y<0) ||
(Block[y][x]) || (CheckHash(x,y))) //CheckHash(x,y)可检查节点是否访问过
return;
node_count++;
int p = node_count, q;
int f = level + abs(x-tx) + abs(y-ty); //启发函数定义
while( p > 1 )
{
q = p >> 1;
if( f < node[q].f )
node[p] = node[q];
else
break;
p = q;
}
node[p].x = x;
node[p].y = y;
node[p].f = f;
node[p].level = level;
node[p].n = allnode_count;

allnode[allnode_count].act = act;
allnode[allnode_count].father = father;
allnode_count++;

Add2Hash(x, y); //加入Hash表
}

如何从node[ ]中取出node[1]呢?直接取出谁都会,问题是剩下的一堆子节点和子子节点如何处理。不过其实也不难,首先,咱们在node[1]的两个子节点中找一个F( )较小的,好比说node[2]吧,把它移上来;而后再在这个子节点的两个子子节点node[4]和node[5]中找一个F( )较小的,把它移到node[2]的位置。这样一直作下去,直到最后一层。那么在下面就空出了一个位置,咱们能够把node[node_count]移到那里,并把node_count--。
NODE GetNode()
{
NODE Top = node[1];
int p,q;
NODE a = node[node_count];
node_count--;
p = 1;
q = p * 2;
while( q <= node_count )
{
if( (node[q+1].f < node[q].f) && (q < node_count) )
q++;
if( node[q].f < a.f )
node[p] = node[q];
else
break;
p = q;
q = p * 2;
}
node[p] = a;
return Top;
}

接下来,咱们看看Add2Hash函数是如何把节点加入Hash表的:
void Add2Hash(int x, int y)
{
int f = (x * SY + y) % MAX_HASH; //Hash函数定义
for (;;)
{
if ((Hash[f].x) || (Hash[f].y)) //若x和y均为0则该Hash位置未被占用
{
f++; //如已占用就看下一个位置
}
else //找到一个未占用位置
{
Hash[f].x = x + 1; //加上1以防止与未占用Hash位置混淆
Hash[f].y = y + 1;
break;
}
}
}

那么CheckHash( )又是如何工做的呢:
int CheckHash(int x, int y)
{
int f = (x * SY + y) % MAX_HASH;
for (;;)
{
if ((Hash[f].x) || (Hash[f].y))
{
if ((Hash[f].x == x + 1) && (Hash[f].y == y + 1))
{
return 1; //找到了
}
else
f++; //找下一个位置
}
else
{
return 0; //找不到
}
}
}

最后,咱们来看看主程序:
void main()
{
int level = 0;
AddNode(0,0,-1,0,0);
for(;;)
{
NODE a = GetNode(); //取出一个节点
level = a.level + 1;
if ((a.x == tx) && (a.y == ty)) //是目标吗?
break;
for (int i = 0; i<4; i++)
{
AddNode(a.x + dx[i], a.y + dy[i], i, level, a.n); //扩展此节点
}
}
//开始输出节点
allnode_count--;
while(allnode[allnode_count].act != -1)
{
cout << allnode[allnode_count].act + 1;
allnode_count = allnode[allnode_count].father;
}
}

因为在上面的程序中人物的移动方向被限制在四个方向,咱们获得的路径必定是过度曲曲折折的(这也是DFS和BFS在游戏寻路方面的小遗憾),因此咱们下一步须要把它“拉直”。一种十分简单的拉直思路是:
while (!已所有拉直)
{
已所有拉直=true;
for (i=0到节点数)
{
for (j=i到节点数)
{
if (i到j的直线不经过任何障碍) //这是最须要优化的一步
{
去除i与j之间的节点;
已所有拉直=false;
}
}
}
}

应该如何最快速地实现这段伪代码呢?咱们能够先粗略地扫描几回,把最多见的弯路径拉直,这样节点数将大大减小,随便用任何方法判断i与j间有无障碍都将很快。

8.7 动态规划
动态规划是一种很是有趣的算法。它能够用极其简洁的语句漂亮地完成复杂的任务。
咱们先来看一张地图(线上的数字表明距离):

图8.3
为了求出A地到B地的最短距离,咱们能够使用动态规划,其程序是这样的:
#include
using namespace std;

const int n=6; //有多少个地方
//两地之间的距离,若为很大的数,如99999,表示两地间无道路
//本身到本身的距离应定为0
int dis[n][n] =
{{ 0, 5, 8, 99999, 99999, 99999 },
{ 5, 0, 2, 7, 99999, 99999 },
{ 8, 2, 0, 1, 6, 99999 },
{ 99999, 7, 1, 0, 99999, 4 },
{ 99999, 99999, 6, 99999, 0, 3 },
{ 99999, 99999, 99999, 4, 3, 0 }};
int d[n]; //某地离A地的距离

void main()
{
d[0]=0; //A地离A地的距离是0
for (int i=1;i
{
d[i]=99999; //除A地外,其它地方到A地的距离都估计为一个很大的数
}

for (i=0;i
{
for (int j=0;j
{
for (int k=0;k
{
if (d[j]>d[k]+dis[j][k]) //若是距离可改进
{
d[j]=d[k]+dis[j][k]; //改进之
}
}
}
}
cout<
}

其实这个程序有点大材小用,由于咱们只要稍加修改便可用一样的算法求出地图中任意两点间的最短距离。这个算法的主要缺点你们也许都看出来了,就是实在太慢了。只要节点数一多,三重循环就会耗去大量的时间。

8.8 神经网络
相信你们第一次玩WorldCraft3时会输给电脑吧,可是和电脑打得多了,摸透了它的战术以后就能够轻易取胜,由于人有学习的能力,电脑却否则。那么用什么办法才能使电脑的思考方式接近于人类,并拥有自我学习的能力呢?神经网络(Neural Networks)是一种目前很热门的实现方法。咱们知道,人类的神经系统是由神经元构成的,神经网络也不例外,只不过在神经网络中神经元的反应被大大地简化了。一个典型的神经网络的神经元由任意多个输入端和输出端组成,其输出等于1/(1+e-s),s为全部输入值*权重(每一个输入值都有一个相应的权重,待会训练时改变的就是权重的值)之和。
例如,若是一个神经元的输入分别为:x1=0.5,x2=0.8,x3=0;对应的权重为:w1=0.1,w2=0,w3=1,那么s=0.5*0.1+0.8*0+0*1=0.05,神经元的输出为1/(1+e-0.05)=0.512。
用多个神经元能够构建起复杂的网络,就像这样:

图8.4
若是咱们给x1输入1,x2输入0,那么位于左上的神经元会输出0.881,位于左下的神经元会输出0.500,这两个结果和1输入最后一个神经元中,最终结果f为0.655。
下面咱们来看看如何训练网络。仍是上面这个例子,若是咱们想用这个网络实现奇偶检验(若是x1与x2均为0或1,那么f=1;不然,即若是x1与x2一个为0一个为1的话,f=0),那么咱们能够这样训练此网络:
①计算最后一个神经元的偏差δ,它等于(d-f)*f*(1-f),其中d为指望输出,f为实际输出。在这个例子里,δ=(0-0.655)*0.655*(1-0.655)=-0.148。
②倒推上一层网络的每个神经元的偏差。举个例子,左上角的神经元只有一个输出端,咱们首先要计算其指向的神经元的δ与此路径的w之积(若是有多个输出端,将全部输出端的这个积加起来),在这里是-0.148*3=-0.444。这个神经元的输出是0.881,那么咱们将-0.444*0.881*(1-0.881),获得此神经元的δ为-0.047。一样的道理,左下角神经元的δ为0.074。
③对于每条路径,其新w=w+δx,δ为此路径指向的神经元的δ,x为此路径的输入值。这样,左上角神经元的w变成了1.953,-2和-0.047;左下角神经元的w变成了1.074,3和-0.926;最后一个神经元的w变成了2.870,-2.074和-1.148。
若是这时咱们再输入刚才的数据,会发现f变成了0.563,的确比训练前的0.655更接近指望值0。

8.9 遗传规划
遗传规划(Genetic Programming)又是一个天才的构想,它从进化论中吸收灵感,经过就像天然生物的"过分繁殖,生存斗争,遗传和变异"的方法实现算法的自我进化。仍是用一个实际问题来讲明GP的方法吧:如何使精灵自动找到附近的墙并永远绕着墙走?
在这里咱们假定精灵所在的世界是二维的,并且是一格格的,精灵有八个传感器:n, ne, e, se, s, sw, w, nw,当其对应的方向不能行走时传感器返回1不然返回0,精灵有四条指令:north,east,south,west,表明向相应的方向移动一格。那么这个程序能够实现咱们想要的功能:

图8.5
这里IF操做符的意义是:若是最左边的分支的运算结果为1那么执行中间的分支不然执行最右边的分支。事实上你还能够定义其它的操做符和指令,把任何程序都写成这种格式。
如今的问题是,怎样用GP的方法找到这样的程序?第一步是随机生成大批小程序,而后就开始对它们进行优化,你能够自由选择方法,一个典型的方法是:
①随机选出7个程序,测试哪一个的效果最好,将其加入下一代中。这样重复直到10%的程序被复制。
②其他90%的下一代程序是经过"杂交"造成的。程序的父母由再次进行7选1获得,杂交的方法是在父母程序上各随机选择一个杂交点,而后将杂交点及杂交点之下的整个分支交换,交换后的父程序或母程序进入下一代。
③有的时候能够进行"基因突变",方法是将7选1获得的程序树的某一个分支用随机生成的新分支代替。突变的几率能够定为1%,事实上天然界中突变的几率还要小得多。
第九章 向三维世界迈进

在DirectX 8.0中,Microsoft公司把原有的DirectDraw和Direct3D合并成为DirectX Graphics(但仍可以使用这些老部件),并进行了许多修改使其变得更强大和易于使用。DirectX 9.0又在DirectX 8.0的基础上作了许多改进。使用DirectX Graphics,咱们能够充分利用硬件资源编写出画面精美的3D游戏。

9.1 概述
咱们都知道,为了肯定空间中的一个点的位置,咱们须要使用三个坐标。在DXGraphics中,三根坐标轴的伸展方向如图9.1所示。
咱们还知道,目前的3D游戏中的物体实际上都是由一个个有着贴图的三角形构成的。事实上,在DXGraphics中的3D物体也能够由一个个点或一条条线组成(不过,这叫3D物体吗??)。而线和三角形又是由顶点(Vertex)构成的。不管是点、线仍是三角形,在DXGraphics中都叫Primitive。 下面再说一个概念:纹理(Texture),我想大多数人都知道这是什么吧。 图9.1
直接建立出来的3D物体是很不真实的,由于它的每一个三角形的表面都是一种颜色,不像平常生活中的东西。而纹理就像是三角形的皮肤同样,赋予它再也不单调的色彩。

图9.2
咱们会发现,原来的3D物体是白色的,但世界上显然有别的颜色的物体。为了代表物体在光线照射下呈现出的颜色,咱们还须要使用材质(Material)。用过3DMAX的人都知道,一个物体的材质由四个值决定,它们分别是:
①Diffuse
物体在普通光下漫反射显示的颜色。
②Ambient
物体在环境光照射下显示的颜色。
③Specular
物体的高光区域的颜色。
④Emissive
若是物体会发光,发出的光的颜色。

接下来还有一个重要的概念:Mipmap。在3D游戏中,有的物体离咱们远,有的物体离咱们近,一样的纹理会随着距离的不一样而显示出来的大小不一样(废话)。在物体离咱们很远时,咱们就不须要使用那么大的纹理,这样能够提升点显示速度并使图像看上去更舒服,这就是Mipmap技术(好像有点道理)。然而使用这种技术的一个后果是对内存或显存的消耗也大了,由于须要存储几张大小不一样的纹理。一个Mipmap序列是由一系列大小依次减半的纹理构成的,就像图9.3:

图9.3

最后咱们看看使用DXGraphics的游戏的主流程:
Main( )
{
初始化变量和指针;
初始化窗口;
初始化DirectX Graphics;
设置场景;
建立场景;
for(;;) //主循环
{
if (有消息)
{
if (为退出信息)
{
释放DirectX Graphics;
释放指针;
退出;
}
if (为用户有效输入)
{
处理;
}
else
{
调用缺省消息处理函数;
}
}
else if (程序激活)
{
清除场景; (Clear)
开始场景; (BeginScene)
渲染场景;
结束场景; (EndScene)
显示场景; (Present)
若是设备丢失,恢复之;
刷新游戏的其它部分;
}
else
{
等待消息;
}
}
}
基本概念就先讲到这里吧,让咱们先来看看如何初始化DirectX Graphics。

9.2 基本知识
9.2.1 初始化DXGraphics
就像初始化DirectDraw,在初始化DirectX Graphics前,咱们先要作一些准备工做:在程序开始前包含 等经常使用的头文件,并在工程中加入"d3d9.lib"和"d3dx9.lib"。接着咱们就能够执行像下面这样的程序进行初始化:

#define _FullScreen //在这里能够方便地设定程序的运行方式——窗口,仍是全屏幕
int InitGraph(HWND hwnd) //初始化DirectX Graphics须要窗口句柄
{
LPDIRECT3D9 pD3D; //D3D对象
LPDIRECT3DDEVICE9 pDev; //D3D设备
D3DDISPLAYMODE d3ddm; //D3D显示模式
D3DPRESENT_PARAMETERS d3dpp; //D3D图像显示方法
if(NULL == (pD3D = Direct3Dcreate9(D3D_SDK_VERSION)))
return -1; //建立D3D对象,D3D_SDK_VERSION为在d3d9.h中定义的版本号
if(FAILED(pD3D -> GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&d3ddm)))
return -1; //获得当前的显示模式
ZeroMemory(&d3dpp, sizeof(d3dpp)); //清空d3dpp,准备填充内容
#if defined(_Windowed) //若是须要程序在窗口模式运行…
d3dpp.Windowed = TRUE; //设定窗口模式
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; //设定换页效果为丢弃后台缓存
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; //设定后台缓存格式为未知
#endif
#if defined(_FullScreen) //若是须要程序在全屏幕模式运行…
d3dpp.Windowed=FALSE;
d3dpp.hDeviceWindow=hwnd; //窗口句柄
d3dpp.SwapEffect=D3DSWAPEFFECT_FLIP; //设定换页效果为翻页
d3dpp.BackBufferCount=2; //有2个后台缓存
d3dpp.BackBufferWidth=800; //屏幕宽为800像素
d3dpp.BackBufferHeight=600; //屏幕长为600像素
//使用当前设定的此显示模式下的刷新率
d3dpp.FullScreen_RefreshRateInHz=D3DPRESENT_RATE_DEFAULT;
//当即显示刷新后的图像
d3dpp.FullScreen_PresentationInterval=D3DPRESENT_INTERVAL_IMMEDIATE;
#endif
d3dpp.BackBufferFormat=d3ddm.Format; //色彩深度为桌面的色彩深度
//开启自动深度缓存,即自动按正确的遮盖关系显示图像
d3dpp.EnableAutoDepthStencil=TRUE;
d3dpp.AutoDepthStencilFormat=D3DFMT_D16; //16位自动深度缓存
//建立D3D设备
if(FAILED(pD3D->CreateDevice( D3DADAPTER_DEFAULT, //使用主显示设备
D3DDEVTYPE_HAL, //使用3D硬件,若为
//D3DDEVTYPE_REF则只使用CPU
hwnd, //窗口句柄
//下面的参数若为D3DCREATE_HARDWARE_VERTEXPROCESSING则使用硬件T&L
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, //上面填充的D3D图像显示方法
&pDev ) ) ) //要建立的D3D设备
return -1;
}

9.2.2 关闭DXGraphics
关闭DirectX Graphics是很是简单的,使用4.8节所说的SAFE_RELEASE释放D3D对象和D3D设备便可。

9.2.3 恢复DXGraphics设备
DirectX中很多设备都存在"丢失"现象(好比你按下Alt+TAB切换出DirectX程序时),DirectX Graphics设备也不例外。DirectX9中提供了一个Reset( )函数以帮助咱们恢复设备,其用法以下:
//Present在9.5节会介绍,这一句的意思是:若是设备丢失...
if (pDev->Present(NULL,NULL,NULL,NULL)==D3DERR_DEVICELOST)
{
//若是已经能够用Reset()恢复设备...
if (pDev->TestCooperativeLevel()==D3DERR_DEVICENOTRESET)
{
pDev->Reset(&d3dpp); //d3dpp是前面初始化时设置的图像显示方法
重设渲染状态; //见9.3.1节
重设矩阵; //见9.3.2节
}
}

9.3 设置场景
9.3.1 设置渲染状态
DXGraphics中能够设置的渲染状态实在很多,大大小小的有几十条,不过经常使用的并很少。设置渲染状态的方法是IDIRECT3DDEVICE9::SetRenderState( D3DRENDERSTATETYPE State, DWORD Value);,其中State是要设置的状态,Value是要设置成什么状态。下表列出了经常使用的渲染状态:
表9.1
State Value 含义
D3DRS_SHADEMODE D3DSHADE_FLAT 平坦光照模式
D3DSHADE_GOURAUD 较平滑的光照模式
D3DRS_LIGHTING TRUE 打开光照
FALSE 关闭光照
D3DRS_AMBIENT ARGB格式的环境光颜色,例如0xFF00FF00为绿光
D3DRS_CULLMODE D3DCULL_NONE 显示所有三角形
D3DCULL_CW 不显示三个顶点呈顺时针排列的三角形
D3DCULL_CCW 不显示三个顶点呈逆时针排列的三角形
D3DRS_FILLMODE D3DFILL_POINT 只显示顶点
D3DFILL_WIREFRAME 只显示由线构成的框架
D3DFILL_SOLID 所有显示
D3DRS_ZENABLE D3DZB_TRUE 打开Z缓存
D3DZB_FALSE 关闭深度缓存
你没必要每次刷新都从新设置一次渲染状态,由于一来会减慢速度,二来渲染状态设置了以后通常不会本身改变。
D3DRS_SHADEMODE的默认值是D3DSHADE_GOURAUD,在这种模式下物体的光照效果还能够。若是使用D3DSHADE_FLAT效果会比较糟糕,由于此时物体的一个面只能是一种颜色。
合理地设置D3DRS_CULLMODE,同时合理地建立3D物体,能够实现使被挡住的面部不被渲染,从而减小大约一半的显示工做量。然而要设置好这些东西是挺麻烦的,因此咱们通常使用D3DCULL_NONE。

9.3.2 设置矩阵
不知道你们有没有听过"矩阵"这个数学名词,在3D图形编程中咱们要大量地使用矩阵对3D物体的坐标进行转换。你能够把最经常使用的4阶矩阵看做16个数排成的4x4的方阵,就像这样:

图9.3
一个4x4的矩阵能够和一个空间中的坐标(x,y,z)进行乘法运算,得出一个新的坐标(x',y',z')。运算法则是这样的:
x'=(x*M11)+(y*M21)+(z*M31)+M41
y'=(x*M12)+(y*M22)+(z*M32)+M42
z'=(x*M13)+(y*M23)+(z*M33)+M43
在DXGraphics中,空间中的一个点要依次通过三个矩阵的转换才能正确地按要求显示在屏幕上,这三个矩阵分别是World,View和Projection。这三个矩阵均可以使三维形体平移、旋转和放缩,但通常说来World矩阵负责把形体放到场景中正确的位置,View矩阵负责根据观察者的位置和看的方向调整形体,而Projection矩阵负责设置视角和透视矫正(使物体近大远小)等。若是要手工填充这几个矩阵那就太麻烦了,DXGraphics固然提供了方便咱们建立这些矩阵的函数,下面咱们来看看例子:
D3DXMATRIX matWorld; //定义World矩阵
D3DXMatrixIdentity(&matWorld); //定义其为单位矩阵,即不改变场景
pDev->SetTransform(D3DTS_WORLD,&matWorld); //设置矩阵

D3DXMATRIX matView; //定义View矩阵
D3DXMatrixLookAtLH(&matView,
&D3DXVECTOR3(x, y, z), //眼睛所在的位置
&D3DXVECTOR3(ox, oy, oz), //眼睛向何处看去
&D3DXVECTOR3(0.0f,1.0f,0.0f)); //什么是"上"
pDev->SetTransform(D3DTS_VIEW,&matView); //设置矩阵

D3DXMATRIX matProj; //定义Projection矩阵
D3DXMatrixPerspectiveFovLH(&matProj,
D3DX_PI/2, //视角大小,这里设为π/2弧度,即90o
1.0f, //纵横比
0.01f, //眼睛能够看到多近
500.0f); //眼睛能够看到多远
pDev->SetTransform(D3DTS_PROJECTION,&matProj); //设置矩阵

须要解释一下的是D3DXMatrixLookAtLH函数最后一个参数的意义,你能够这样理解它:若是咱们站在坐标原点处,那么它就是指明咱们的头朝着哪个方向。通常来讲,咱们的头是向着y轴方向的,因此通常把它定为(0,1,0)。

9.4 建立场景
设置完场景后,咱们就须要建立一个场景以供显示。这一步有两种实现方法,第一种是本身把全部点、线和三角形的数据告诉DXGraphics,第二种是使用DXGraphics提供的函数直接调入用3DMAX等画好的3D场景。显然,第二种方法更为简便,但若是咱们要在屏幕上画一个2D的东西,好比准星时,该怎样作呢?这时能够像9.4.2节那样使用ID3DXSPRITE对象。

9.4.1 调入3D场景
先告诉你们一个坏消息:DXGraphics只能调入它本身定义的.x格式的文件!
接下来是一个好消息:你能够去http://www.milkshape3d.com下载一个超好用的MilkShape 3D,它不但能够实现将各类3D做图软件生成的文件转成.x格式,并且还能够把Quake2, Quake3,Counter Strike,Half-Life,Max Payne,Unreal,Serious Sam,The Sims等游戏中使用的3D文件格式转成.x文件!
然而还有一个坏消息:Milkshape 3D不注册只能使用一个月,并且咱们经常使用的改日期大法对它效果很差。那么怎么办呢?其实DirectX SDK中有一个插件,能够把用3DMax或Maya画好的场景输出为.x,本身在MICROSOFT的网站上找找DirectX SDK Extras吧。
下面转入正题:如何调入.x文件呢?其实很是简单:
LPD3DXMESH Mesh; //Mesh是网的意思,这里指3D场景
DWORD nMater; //场景中共有多少种Materials,即材质
D3DMATERIAL9* Mater; //存放场景中各类材质的数据的数组
LPDIRECT3DTEXTURE9* Tex; //存放场景中各类纹理的数据的数组
LPD3DXBUFFER pBuffer; //临时缓存
D3DXMATERIAL* tMater; //临时材质

D3DXLoadMeshFromX("mp5.x", //.x文件名
D3DXMESH_MANAGED, //说明Mesh由DXGraphics管理
pDev, //刚建立的D3D设备
NULL, //这里能够是一个LPD3DXBUFFER对象,保存场景中每一
//个三角形与哪三个三角形相联
&pBuffer, &nMater, &Mesh); //上面刚定义的那群东西
tMater = (D3DXMATERIAL*)pBuffer->GetBufferPointer( ); //获得材质的数据指针
Mater = new D3DMATERIAL8[nMater];
Tex = new LPDIRECT3DTEXTURE8[nMater];
for (DWORD i=0;i
{
Mater[i] = tMater[i].MatD3D; //tMater的做用是防止不断调用GetBufferPointer()
Mater[i].Ambient = Mater[i].Diffuse; //设置好Ambient材质,由于咱们的场景中一
//般只有环境光,而用做图软件画的物体通常
//没有设置此材质,结果会看不到东西
//建立纹理
//请保证场景中每个三角形都有纹理,不然会出错!
D3DXCreateTextureFromFile(pDev, tMater[i].pTextureFilename, &Tex[i]);
}
//释放临时对象
pBuffer->Release();
D3DXComputeNormals(Mesh); //计算各个顶点的朝向,为之后使用灯光做准备

调入场景后,咱们能够执行ID3DXMesh::OptimizeInplace( )对场景进行优化,这条指令能够提升很多显示速度。首先,咱们要根据Mesh中面的个数开一个数组储存Mesh中各个面的链接状况(就是D3DXLoadMeshFromX( )的第四个参数的意义):
DWORD *pAdj=new DWORD[Mesh->GetNumFaces()*3];
得到数据:
Mesh->GenerateAdjacency(0.0f,pAdj);
而后就能够优化Mesh:
Mesh->OptimizeInplace(D3DXMESHOPT_VERTEXCACHE, pAdj,NULL, NULL, NULL);

9.4.2 调入2D图像
即便是在3D游戏中,仍然少不了2D的东西。事实上,在很多游戏中一些看上去应该是3D的东西,例如树什么的,其实也就是一张张2D图像。调入和显示2D图像最简单的办法是使用ID3DXSPRITE对象。首先,咱们要把图像做为纹理调入内存:
LPDIRECT3DTEXTURE9 tex; //指向纹理的指针
LPD3DXSPRITE spr; //指向ID3DXSPRITE对象的指针
D3DXCreateTextureFromFileEx(pDev,"cross.png", //文件名
D3DX_DEFAULT, //文件宽,这里设为自动
D3DX_DEFAULT, //文件高,这里设为自动
D3DX_DEFAULT, //须要多少级mipmap,这里设为自动
0, //此纹理的用途
D3DFMT_UNKNOWN, //自动检测文件格式
D3DPOOL_MANAGED, //由DXGraphics管理
D3DX_DEFAULT, //纹理过滤方法,这里设为默认方法
D3DX_DEFAULT, //mipmap纹理过滤方法,这里设为默认方法
0xFFFFFFFF, //透明色颜色,ARGB格式,这里设为白色
NULL, //读出的图像格式存储在何变量中
NULL, //读出的调色板存储在何变量中
&tex); //要建立的纹理
而后即可建立ID3DXSPRITE对象:
D3DXCreateSprite(pDev,&spr);

9.5 刷新场景
在DXGraphics中,刷新一次场景须要五步。不过不用担忧,除了渲染场景这一步,其它几步都很是简单。首先,咱们要清除场景:
pDev->Clear(0, NULL,
D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0,0,0), 1.0f, 0);
这条指令能够只清除指定的几个矩形中的屏幕内容,第一个参数的意义就是要清除多少个矩形中的内容,而第二个参数指向一个装满了这些矩形的数组。第三个参数说明要清除那些内容,包括渲染后的场景 ( D3DCLEAR_TARGET ),Z Buffer ( D3DCLEAR_ZBUFFER ) 和Stencil Buffer ( D3DCLEAR_STENCIL )。第4、5、六个参数分别表示场景要被清为何颜色,Z Buffer要被清为何值,Stencil要被清为何值。
可是,Z Buffer和Stencil Buffer究竟是什么东东?你还记得咱们在9.2节中开启的所谓的自动深度缓存吗?那就是Z Buffer,它实际上就是一个数组,储存着物体离观察者眼睛的远近:0.0f是最近,1.0f是最远。咱们都知道近的东西能够挡住远的东西(除非它是透明的?),不过之前的显示卡是不懂这个道理的,一切都要你本身计算。如今好了,绝大多数显示卡都支持这项功能,不会再出现古怪的图像(什么?你喜欢之前的样子?faint)。为了实现不显示被挡住的点,在Z Buffer中要开辟一块区域,即Stencil Buffer,储存是否显示该点。好比说,在32位的Z Buffer中放置8位的Stencil Buffer。因此经过修改Stencil Buffer,咱们能够控制要显示哪些点,实现镂空、简单的淡入淡出(把一个个点改成图像的颜色或黑色)等效果。
清除干净场景后,咱们就应该调用IDirect3Ddevice9::BeginScene( )开始场景,为渲染场景作好准备。这个函数没有任何参数,很是简单。
下一步应该是渲染场景,因为这一步有几种实现方法,比较复杂一点,因此我把它放在9.6节单独讲述,这里先跳过去吧。
渲染好了场景就应该调用IDirect3Ddevice9::EndScene( )结束场景,这也是一个没有参数的函数。
最后,咱们能够调用IDirect3Ddevice9::Present( )函数将场景显示出来。这个函数的原型是这样的:HRESULT Present(CONST RECT* pSourceRect,
CONST RECT* pDestRect,
HWND hDestWindowOverride,
CONST RGNDATA* pDirtyRegion);
通常来讲,它的四个参数都应被设为NULL,也就是pDev->Present(NULL,NULL,NULL,NULL);便可。设备丢失时此函数会返回D3DERR_DEVICELOST。

9.6 渲染场景
9.6.1 渲染3D场景
对于用9.4.1节的方法调入的3D物体,咱们能够这样渲染:
for (unsigned long i=0;i
{
pDev->SetMaterial( &Mater[i] ); //设置材质
pDev->SetTexture( 0, Tex[i] ); //设置纹理
Mesh->DrawSubset( i ); //渲染是第i种材质的三角形
}

9.6.2 渲染2D图像
对于用9.4.2节的方法调入的2D图像,咱们能够这样渲染:
spr->Draw(tex, //前面建立的纹理
NULL, //源矩阵,就像DirectDraw中那个
NULL, //一个D3DXVECTOR2结构,代表在横竖方向分别扩大多少倍
NULL, //一个D3DXVECTOR2结构,绕何点旋转
0, //顺时针方向旋转多少弧度
&D3DXVECTOR2(397,297), //放到屏幕上何处
0xFFFFFFFF); //图像的颜色,ARGB格式,这里设为不透明白色
//若是设为0x7FFFFFFF等数值,用10.2节的办法便可实现半透明

9.7 改变场景
你们也许会注意到,渲染出来的3D物体的位置和方向、大小等彷佛是没法控制的。但办法老是有的,咱们能够借助World矩阵完成对3D物体的改变。方法很简单:
D3DXMATRIX matw,matt,mato;
pDev->GetTransform(D3DTS_WORLD,&matw); //获得原来的World矩阵
mato=matw;
D3DXMatrixTranslation(&matt, x,y,z); //将matt变成一个能将物体移动(x,y,z)距离的矩阵
D3DXMatrixMultiply(&matw, &matt, &matw); //matw=matt*matw,可将matw按matt
//在x/y/z轴方向上分别移动x,y,z

D3DXMatrixRotationYawPitchRoll(&matt, ry, rx, rz); //将matt变成一个能将物体旋转
//(rx,ry,rz)角度的矩阵
D3DXMatrixMultiply(&matw, &matt, &matw); //matw=matt*matw,可将matw按matt
//在x/y/z轴方向上分别旋转rx,ry,rz

D3DXMatrixScaling(&matt, sx, sy, sz); //将matt变成一个能将物体缩放(sx,sy,sz)倍的矩阵
D3DXMatrixMultiply(&matw, &matt, &matw); //matw=matt*matw,可将matw按matt
//在x/y/z轴方向上分别缩放sx,sy,sz倍

pDev->SetTransform(D3DTS_WORLD,&matw); //设定矩阵
for (unsigned long i=0;i
{
pDev->SetMaterial( &Mater[i] );
pDev->SetTexture( 0, Tex[i] );
Mesh->DrawSubset( i );
}
pDev->SetTransform(D3DTS_WORLD,&mato); //复原矩阵
接下去画第二个物体的时候能够依此类推:先改变World矩阵,再复原之。
另外还有一个重要问题是如何实如今3D场景中的漫游,这其实也很是简单,只要根据主角所在位置在每次刷新前设置好View矩阵便可。

9.8 显示文字
你们还记得在DirectDraw中咱们是如何使用HDC实现文字输出吗?在DXGraphics中可没有得到HDC的办法,咱们应该使用ID3DXFont类。首先咱们要建立一种字体:
HFONT font;
font = CreateFont(20, 0, 0, 0, FW_BOLD, 0, 0, 0, 0, 0, 0, 0, 0, "Tahoma");
这里咱们建立的是一个大小为20个像素的粗体Tahoma字体,CreateFont的第1、第五和最后一个参数是最值得咱们注意的。
建立完字体后,下一步就应该是根据它建立ID3DXFont对象:
LPD3DXFONT pFont;
D3DXCreateFont( pDev, font, &pFont);
接下去,用一个矩形肯定要将文字放到什么位置:
MakeRect(650,0,800,20); //MakeRect的定义在4.6节
最后就能够输出文字:
pFont->DrawText("Hello!", //要输出的文字
strlen("Hello!"), //文字长度
&rect, //上面建立的矩形
DT_RIGHT, //文字在矩形中的显示方式,还能够是DT_LEFT或DT_CENTER等等
D3DCOLOR_ARGB(128,255,0,0) ); //图像的颜色,这里设为半透明红色

9.9 程序实例

第十章 我没有想好名字

第九章所讲述的3D编程知识能够说是比较基本的,如何才能使咱们的游戏更逼真和华丽呢?这就须要咱们使用一系列比较高级的技术。不过放心,他们学起来并非很是复杂。若是你能很好地掌握这些技术,作出一个相似QuakeIII这样的游戏都将会是彻底可能的。
原本准备把这章一口气写完的,不过个人小猫还没下完整的DX9SDK,等下完看看和DX8有何区别和改进再写吧…
10.1 灯光
经常使用的在3D场景中实现灯光的方法有三种,第一种是利用DXGraphics提供的灯光对象简单地完成不太精确的光照效果,第二种方法是利用Lightmap技术,第三种办法是利用Pixel Shader。咱们仍是在一节中先看看最简单的第一种吧。
DXGraphics提供了三种光源:点光源POINT(例如灯泡)、平行光源DIRECTIONAL(例如太阳光)和聚光灯SPOT(DiabloII里面围绕着主角的一圈光就能够看做是这种光)。一个光源能够由一个D3DLIGHT9结构表示,这个结构的参数有:
Type:代表光源种类。能够是D3DLIGHT_POINT、D3DLIGHT_SPOT或 D3DLIGHT_DIRECTIONAL。
Diffuse / Specular / Ambient:光源射出的三种光的颜色,每个都是一个D3DCOLORVALUE结构,有a/r/g/b四个成员,取值范围都是0.0f到1.0f。
Position:光源的位置,对于平行光源此值无心义。
Direction:光源照射的方向,对于点光源此值无心义。
Range:光源能照射多远,最大容许值为sqrt(FLT_MAX),对于平行光源此值无心义,
Attenuation0 / Attenuation1 / Attenuation2:指定光线的衰减方式。通常来讲Attenuation0与Attenuation2都设为0,Attenuation1设为一个常数。若是你将Attenuation0设为1并将其他两个值设为0,光线将不随距离而衰减。对于平行光源此值无心义。
接下来的三个参数是聚光灯所特有的。咱们先来看看Diablo II中的聚光灯是怎样的:

图10.1
你们都会注意到,聚光灯的内圈的亮度不随距离而衰减,内圈与外圈之间则否则。下面三个参数就与这些有关:
Theta:内圈的角度(单位为弧度)。
Phi:外圈的角度(单位为弧度)。
Falloff:内圈与外圈之间的亮度的衰减率,通常设为1.0f。

填充完D3DLIGHT9结构后,下一步就是设置灯光。假设你刚才填充的D3DLIGHT9结构的变量的名字是d3dLight,那么这样作便可:
pDev->SetLight(0, &d3dLight); //设置为0号灯
pDev->LightEnable(0, TRUE); //打开0号灯
最后在渲染状态里打开灯光就完成了设置。
注意灯光对于2D图像不起做用(显然),那么如何实现DiabloII这样的2D光照呢?最简单的办法就是用3DMAX画一个2D平面而后把它当作3D场景调入,不过这显然太浪费了一点,用下面介绍的半透明和纹理混合却是个好主意。

10.2 半透明
半透明实在是很漂亮,如今的游戏使用它很是频繁。若是你想作的效果是使2D图像和文字半透明的话,实现方法难以置信地简单:
pDev->SetRenderState(D3DRS_ALPHABLENDENABLE,TRUE);
pDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
pDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

对,就设置这三句就好了。固然别忘了渲染2D图像和文字时设置颜色的ALPHA值为想要的透明度----若是你的图像不是已经包含了ALPHA通道。想一想当年咱们为了在DirectDraw中实现半透明要写多么复杂的代码,真是感慨万千啊?。
那么若是咱们要使3D物体半透明该怎么办呢?一样简单。首先,肯定一下你须要使整个面的ALPHA值相同(例如实现水的效果)仍是使用纹理的ALPHA通道。若是是第一种,只要在设计场景时把这个面的Diffuse颜色的a值设为透明度,而后在渲染前执行下面的语句:
pDev->SetTextureStageState(0,D3DTSS_ALPHAOP,D3DTOP_SELECTARG1);
pDev->SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_DIFFUSE);

若是你想用第二种实现方法,原本是不用执行任何语句的,由于系统默认状态就是使用ALPHA通道。可是若是你执行过上面那两条语句把半透明参考源改成Diffuse.a的话,那么要在渲染这种纹理前恢复它:
pDev->SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_TEXTURE);

哈哈,看来作出半透明效果确实很是简单,怪不得你们这么喜欢用它?。

10.3 纹理混合
纹理混合能够使咱们轻易地做出光照、弹痕等效果。其原理是给一个三角形安上两种纹理,而这两种纹理的混合方式由你指定。
首先,咱们要把第二种纹理调入内存:
D3DXCreateTextureFromFile(pDev, "light.bmp", &tex);
而后把它设为1号纹理(默认纹理为0号):
pDev->SetTexture(1, tex);
设定它如何与0号纹理混合:
//设定纹理混合源一为1号纹理的图像
pDev->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
//设定纹理混合源二为目前的图像
pDev->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT);
//设定纹理混合指令为相乘
pDev->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE);
纹理混合指令有不少种,下表列出了几种经常使用的指令的效果:
表10.1
D3DTOP_DISABLE 禁止纹理混合
D3DTOP_SELECTARG1 只显示Arg1
D3DTOP_SELECTARG2 只显示Arg2
D3DTOP_MODULATE Arg1 * Arg2
D3DTOP_MODULATE2X Arg1 * Arg2 *2
D3DTOP_MODULATE4X Arg1 * Arg2 *4
D3DTOP_ADD Arg1 + Arg2
D3DTOP_ADDSIGNED Arg1 + Arg2 – 0.5
D3DTOP_ADDSIGNED2X ( Arg1 + Arg2 – 0.5 ) * 2
D3DTOP_SUBTRACT Arg1 – Arg2
D3DTOP_ADDSMOOTH Arg1 + Arg2 – Arg1 * Arg2
为了正确地把第二种纹理显示出来,咱们要设定第二种纹理的纹理坐标。什么是纹理坐标呢?其实就是Mesh中的某个顶点对应着纹理中的哪一个点。举个例子,若是某个三角形要把纹理的右下那一半显示出来,那么咱们能够把三角形的一个顶点的纹理坐标设为( 0.0f, 1.0f ),即纹理的左下角;第二个顶点的纹理坐标设为( 1.0f, 1.0f ),即纹理的右下角;第三个顶点的纹理坐标设为( 1.0f, 0.0f ),即纹理的右上角。
因为用3D做图软件画出的Mesh通常只有一套纹理坐标,咱们首先要把它转换为拥有两套纹理坐标:
Mesh->CloneMeshFVF(D3DXMESH_MANAGED,
D3DFVF_XYZ | //有坐标信息
D3DFVF_NORMAL | //有顶点朝向信息
D3DFVF_DIFFUSE | //有DIFFUSE颜色信息
D3DFVF_TEX2 //有两套纹理坐标
, pDev, &Mesh);

而后根据咱们刚才的设定定义一个顶点数据类型:
strucet vertex
{
float x, y, z;
float nx, ny, nz;
D3DCOLOR diffuse;
float tu1, tv1; //第一套纹理坐标
float tu2, tv2; //第二套纹理坐标
};
vertex *v; //存储Mesh中顶点的信息

下一步是锁定Mesh并获得顶点信息:
int count = Mesh->GetNumVertices(); //存储Mesh中有多少顶点
Mesh->LockVertexBuffer(NULL,(BYTE**)&v);
//下面用拷贝第一套纹理坐标的办法生成第二套纹理坐标
//你能够任意改变这个算法
//好比说只给其中几个三角形设置纹理坐标
for (i=0;i
{
//事实上在这里能够任意更改Mesh中各个顶点的信息
v[i].tu2=v[i].tu1;
v[i].tv2=v[i].tv1;
}

最后要解锁Mesh:
Mesh->UnlockVertexBuffer();

10.4 雾
雾是一个能使场景更真实的效果,在不少游戏中都有应用。

图10.2 雾的效果
在DirectX中有两种雾:Vertex Fog和Pixel Fog,顾名思义,它们分别是基于顶点和像素。因此Pixel Fog的效果显然要好一些,而二者的速度没什么差异。下面咱们来看看Pixel Fog的设置方法:
float fogstart=0.01f,fogend=50.0f; //雾的开始范围和结束范围
pDev->SetRenderState(D3DRS_FOGENABLE, TRUE); //打开雾
pDev->SetRenderState(D3DRS_FOGCOLOR, 0x00000000); //设置雾的颜色
//设置雾的衰减方式为线性
pDev->SetRenderState(D3DRS_FOGTABLEMODE, D3DFOG_LINEAR );
//设定雾的开始范围和结束范围
//*((DWORD*)(&x))的做用是经过把指向x的指针转为指向DWORD类型的数据的指
//针来实现把x转为DWORD类型的目的(绕口…)
pDev->SetRenderState(D3DRS_FOGSTART, *((DWORD*)(&fogstart)));
pDev->SetRenderState(D3DRS_FOGEND, *((DWORD*)(&fogend)));

10.5 凹凸贴图与环境贴图
凹凸贴图和环境贴图对于渲染出更真实的物体是很是重要的,实现它们的方法也并不复杂。咱们先来看看凹凸贴图的实现方法吧。
图10.3 凹凸贴图

接下来让咱们看看环境贴图。
图10.4 环境贴图

10.6 粒子系统
使用粒子系统能够制造出火焰、瀑布等各类炫目的特效。
图10.5 粒子系统

10.7 骨骼动画
若是你玩过CS,也许会对这个游戏是如何制造出较为真实的走动、跳跃等动画有过很多猜测。事实上,它使用的是骨骼动画技术。这个技术要作起来但是比较麻烦的?。咱们仍是先来看看什么是骨骼动画技术。
在没有骨骼动画技术以前,三维动画是采用关键帧技术实现的----即只保存几个关键的mesh,而后经过在它们之间插值实现动画,就像下面这幅图所显示的那样:
图10.6 关键帧技术
这无疑是个挺好的技术,然而出于速度的考虑,插值通常是采用最简单和最粗略的线性插值,若是动画一复杂,效果将没法使人满意。就拿下图作例子,咱们使用线性插值将只能获得图2的效果,而不是图1的流畅旋转效果。
图10.7 关键帧技术的问题
该怎么办呢?联想到人的各类动做是如何实现的,咱们能够赋予mesh以骨骼,而三角形就象是皮肤,将跟随着骨骼一块儿运动----实际上就是用骨骼的转换矩阵转换各个顶点。因此要记住,骨骼自己是没有意义的,只有骨骼的转换矩阵才是重要的。

图10.8 骨骼示意图
如今想一想,若是咱们将上臂举起,并同时选转下臂,那么下臂的实际运动将会是怎样的呢?显然是两个运动的合成。因此,咱们须要创建起一个骨骼们的链接(或者说是继承)关系,而后子骨骼的实际转换矩阵将是子骨骼的转换矩阵与母骨骼的转换矩阵的乘积。这就是骨骼动画技术的精髓。好象不是很复杂的样子,hehe。
不过这种实现仍是有一个小问题,图10.6展现了它。
图10.9
左边的效果看起来不太漂亮,不过它就是上面所述的简单方法的结果。为了实现右边的效果,咱们须要使多根骨骼同时影响一个顶点。Mesh中的每一个顶点将被赋以多个weight(权重),它们决定了这个顶点被哪些骨骼以多大的程度影响。通常来讲,两个weight(实际上只需给出一跟骨骼的weight,另外一个weight通常来讲显然等于1-weight)已经能够给出很好的结果。而后,咱们将顶点分别以这些骨骼的矩阵转换,再把获得的这些结果按权重相加就是答案。
OK,骨骼动画的原理就讲完了,下一步是想一想该怎么实现它。

10.8 镜子
镜子的效果在游戏中很多见,水面、光滑的地板等均可以被认为是一种镜子。实现这种特效通常要靠Stencil Buffer技术。












图10.10 镜子效果

10.9 影子
也许你会对DXGraphics提供的灯光不能产生影子而感到困惑。DXGraphics不这样作的缘由是影子对程序速度的影响实在太大。举个例子,若是要显示一我的物的影子,咱们首先要
图10.11 影子效果

不过,在不少游戏(例如Counter Strike)中咱们只能看到建筑物的影子,而这些影子又都是固定的,因此要好作得多。咱们能够预先计算好这些阴影的范围,并把它们加入mesh,程序运行的时候只需判断人物是否在阴影中,若是是的话就把人物变暗便可。

第十一章 我没有想好名字

要说如今最热的,莫过于网络游戏,并且看来它会是大势所趋。那么想编游戏的咱们又怎么能错过潮流呢?由于如今的网络编程最经常使用的是Winsock,因此咱们下面就来看看如何使用它吧。

11.1 基本概念
Client, Server, Sync, Async…

11.2 程序流程
11.2.1 服务器端
在调用其余Winsock函数前,咱们首先要执行WSAStartup( )以初始化Winsock并通知Windows将Winsock的DLL调入内存。接下来,咱们须要调用socket( )建立服务器端的socket,并把它和服务器的一个端口用bind( )绑定。而后,咱们须要用WSAAsyncSelect设定网络事件,以便在窗口的消息处理函数中处理各类网络事件。最后调用listen( )开始监听各类网络事件。
当一个网络事件发生时,窗口的消息处理函数会接受到一条网络消息。根据消息的wParam参数,咱们能够了解消息的类型,从而对消息进行一些处理。好比说,若是wParam是FD_ACCEPT,说明有一个客户端请求与服务器链接,咱们能够调用accept( )接受之并得到客户端的socket;若是wParam是FD_READ,那么发生的网络事件是服务器收到了一条从客户端发来的信息,调用recv( )能够把信息收下来。
关闭服务器有两种方法,第一种是所谓的"优雅办法",它须要几个步骤:首先要调用shutdown( ),这会中止发送新信息并给客户端发送一个FD_CLOSE网络消息(客户端收到这则消息后,应该把还要发送的信息发出,而后依次调用shutdown( )和closesocket( ))。此时在服务器端,咱们应该在收到FD_READ时调用recv( )收完全部信息,而后在收到客户端发来的FD_CLOSE后才能 closesocket( )。最后能够调用WSACleanup( )把Winsock的DLL清理出内存。第二种办法是"暴力法",关闭速度够快可是有可能丢失最后的信息。这种方法用起来也简单,直接执行closesocket( )便可。

11.2.2 客户端
客户端的初始化过程与服务器端的过程有一些类似之处。首先也是WSAStartup( ),而后同样执行socket( )建立socket,但接下来就无需使用bind( )绑定接口了,由于没有人会去链接客户端。下一步的注册网络事件也有少量不一样:没有了FD_ACCEPT却有FD_CONNECT。最后固然少不了connect( )指令,用于链接服务器。
链接上服务器后,服务器会把一条FD_WRITE指令发给客户,表示能够发送信息了。这时客户端就能够使用send( )向服务器发送各类信息。
关闭客户端的步骤和关闭服务器的步骤彻底同样。

11.3 程序实例
首先,咱们固然要在程序中#include ,注意这条语句必须在#include 以前。而后,咱们要在工程中加入WS2_32.lib。下面咱们先来看看服务器端的代码(为了方便说明,没有进行错误处理)。
咱们预先定义一个类以方便处理:
class SERVER
{
public:
SERVER();
void Accept(SOCKET s);
void Close(SOCKET s);
void Receive(SOCKET s);
void Send(SOCKET s);
~SERVER();
SOCKET server, client[5]; //存储服务器及客户的socket
}Server;

SERVER::SERVER( ) //初始化
{
for (int i=0; i<5; i++)
client[i]=NULL;
WSADATA wData;
WSAStartup( MAKEWORD(2,2), &wData ); //初始化Winsock
server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //建立服务器socket
sockaddr_in localaddr; //存储本地地址
localaddr.sin_family = AF_INET; //类型为Internet地址
localaddr.sin_port = htons(8888); //设定端口为8888,能够为0-65535范围
localaddr.sin_addr.s_addr = INADDR_ANY; //可接收任何人的链接
bind(server, (struct sockaddr*)&localaddr, sizeof(sockaddr)); //绑定地址
WSAAsyncSelect(server, hwnd, WM_USER+2, //本身设定一个消息代号
FD_ACCEPT | FD_CLOSE | FD_READ | FD_WRITE); //设置事件
listen(server, 5); //开始监听,设置最多链接5个客户
}

既然咱们已经设定好了消息,在服务器窗口的消息处理函数中就能够接受到网络消息:
WinProc()
{
SOCKET s;
int iEvent;
switch(message)
{
case WM_USER+2: //处理网络消息
event = WSAGETSELECTEVENT( lParam ); //获得事件代号
s = (SOCKET) wParam;
switch(event)
{
case FD_ACCEPT: //有客户试图链接
Server->Accept(s);
break;
case FD_CLOSE: //任何一方试图关闭链接
Server ->Close(s);
break;
case FD_READ: //可接受信息
Server ->Receive(s);
break;
case FD_WRITE: //可发送信息
Server ->Send(s);
break;
}
return 0;

case XXXXX: //处理其它消息
……
//想关闭服务器时执行closesocket(server);
}
return DefWindowProc( hwnd, message, wParam, lParam );
}

void SERVER::Accept(SOCKET s) //接受链接
{
SOCKADDR_IN address;
int size = sizeof( SOCKADDR );
//将客户的socket存入client[]
for (int i=0; i<5; i++)
if (client[i]==NULL)
client[i] = accept( server, (LPSOCKADDR)&address, &size );
}

void SERVER::Receive(SOCKET s) //接收信息
{
char buffer[255]; //存储接收到的信息
recv(client,buffer,sizeof(buffer),0);
}

void SERVER::Close(SOCKET s) //关闭客户socket,用的是暴力法?
{
for (int i=0; i<5; i++)
if (client[i]==s)
client[i]=NULL;
closesocket(s);
}

接下来咱们看看客户端的代码:
class CLIENT
{
public:
CLIENT();
void Close(SOCKET s);
void Receive(SOCKET s);
void Send(SOCKET s);
~CLIENT();
SOCKET client;
}Client;

CLIENT::CLIENT() //初始化
{
WSADATA wData;
WSAStartup( MAKEWORD(2,2), &wData );
client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
WSAAsyncSelect(client, hwnd,
WM_USER+2, FD_CONNECT | FD_CLOSE | FD_READ | FD_WRITE);

sockaddr_in target;
target.sin_family = AF_INET;
target.sin_port = htons (8888); //服务器端口
target.sin_addr.s_addr = inet_addr ("127.0.0.1"); //服务器IP
connect(client, (struct sockaddr*)&target, sizeof(target)); //试图链接
}

CLIENT::Send(socket s)
{
char buffer[255];
strcpy(buffer,"haha, I am connected!");
send(client, buffer, sizeof(buffer),0);
}

CLIENT::Close(socket s)
{
closesocket(s);
}

客户端的消息处理函数是这样的:
WinProc()
{
SOCKET s;
int iEvent;
switch(message)
{
case WM_USER+2: //处理网络消息
event = WSAGETSELECTEVENT( lParam ); //获得事件代号
s = (SOCKET) wParam;
switch(event)
{
case FD_CONNECT: //链接服务器的结果已出来
//这里应该执行WSAGetLastError( ),具体方法见11.4节
break;
case FD_CLOSE: //任何一方试图关闭链接
Client ->Close(s);
break;
case FD_READ: //可接受信息
Client ->Receive(s);
break;
case FD_WRITE: //可发送信息
Client ->Send(s);
break;
}
return 0;

case XXXXX: //处理其它消息
……
//想关闭客户端时执行closesocket(client);
}
return DefWindowProc( hwnd, message, wParam, lParam );
}

11.4 错误处理
在Winsock中,发生各类错误的概率是比较大的,由于网络自己具备必定的不稳定性。发生了错误固然要处理,执行WSAGetLastError( )会返回上一个错误的代码,在附录四中能够查到这些代码的含义。那么咱们怎样知道有没有发生错误呢?固然是靠函数的返回值。
表11.1 经常使用Winsock函数返回值
函数名 成功时的返回值 失败时的返回值
WSAStartup( ) 0 一个socket错误代码
WSAAsyncSelect( ) 0 SOCKET_ERROR
WSACleanup( ) 0 SOCKET_ERROR
socket( ) 一个合法的socket INVALID_SOCKET
bind( ) 0 SOCKET_ERROR
listen( ) 0 SOCKET_ERROR
connect( ) 0 SOCKET_ERROR
accept( ) 一个合法的socket INVALID_SOCKET
recv( ) 收到了多少字节数据 SOCKET_ERROR
send( ) 发送了多少字节数据 SOCKET_ERROR
shutdown( ) 0 SOCKET_ERROR
closesocket( ) 0 SOCKET_ERROR
注意的是connect( )常常会返回用WSAGetLastError可得知错误代码为10035的SOCKET_ERROR,其缘由是connect( )须要一段时间才能完成。此时咱们应该继续调用connect( ),若是返回用WSAGetLastError可得知错误代码为10056的SOCKET_ERROR,说明已链接上。

11.5 显示IP地址
从上面的客户端程序能够看到,咱们链接时须要服务器端的IP地址。虽然咱们能够经过在服务器端运行winipcfg或ipconfig获得它,但若是能在服务器屏幕上直接把它显示出来就更好了。其方法是这样的:
void GetIP(int stat)
{
static char buf[MAXGETHOSTSTRUCT]; //存储服务器资料

if (stat==0) //若是是首次调用
{
char name[255]; //存储服务器的名字
gethostname(name,255); //获得服务器的名字
DWORD add = inet_addr( name ); //试图将服务器名转为Internet地址

if( add == INADDR_NONE ) //若是没法转换
{
WSAAsyncGetHostByName( hwnd, WM_USER+1,
name, buf, MAXGETHOSTSTRUCT); //经过服务器名获得IP
}
else
{
WSAAsyncGetHostByAddr( hwnd, WM_USER+1,
(const char *)&add, sizeof( IN_ADDR ), AF_INET,
buf, MAXGETHOSTSTRUCT ); //经过服务器地址获得IP
}
}
else //若是已收到WM_USER+1消息
{
char IP[64]; //存储IP地址,为xxx.xxx.xxx.xxx形式
LPHOSTENT lphostent = (LPHOSTENT)buf;
strcpy( IP, inet_ntoa( *(LPIN_ADDR)lphostent->h_addr ) );
//在这里能够放一些显示IP地址的语句
}
}

咱们在初始化完服务器后便可调用此函数,记住这时stat=0。它将会注册一个网络消息WM_USER+1,若是咱们在服务器窗口的消息循环中收到此消息,即说明已获得服务器资料,能够调用GetIP(1)把IP显示出来。

11.6 更有效地传送数据
前面所讲述的都只是怎样传送数据,但到底应该传送什么数据才能达到更高的效率呢?这显然要根据游戏类型而定。
第十二章 创造咱们的世界

由于我当初本身摸索的时候以为编游戏并非很难的事情,因此若是你以为这里要加什么内容就告诉我一声,谢谢!

12.1 程序流程
通常来讲,一个使用DirectX的游戏的主流程是这样的:
Main( )
{
初始化变量和指针;
初始化窗口;
初始化DirectX;
初始化Winsock;
for(;;) //主循环
{
if (有消息)
{
if (为退出信息)
{
释放DirectX;
释放指针;
退出;
}
else if (为网络消息)
{
处理;
}
else
{
调用缺省消息处理函数;
}
}
else if (程序激活)
{
经过DInput读取键盘和鼠标信息,处理之;
刷新图像;
刷新游戏的其它部分;
若是设备丢失,恢复之;
}
else
{
等待消息;
}
}
}

12.2 程序结构
图12.1
上面这幅图显示了一个典型的单机游戏的结构。
随便想的,望指正

12.3 基本方法
(1)先动笔设计,再编程。一个好的设计可让咱们事半功倍。
(2)按部就班,先写框架,慢慢扩充新特性。
(3)最好别偷懒用别人的框架,本身写的才是最好用的。
(4)作较大改变前先备份代码,保证永远有一个能够运行的备份,以防失去激情?。

12.4 SLG编程要点
12.4.1 电脑AI
和RTS的局部AI差很少

12.5 RPG & ARPG编程要点
12.5.1 迷宫的生成
我会写例程的,等一下吧

12.5.2 脚本技术
其实不是很复杂吧,只要稍微动笔设计一下就好了。不知道为何这么多人问

12.6 RTS编程要点
12.6.1 寻路
A*加避让?要写例程…

12.6.2 电脑AI
总体AI:通常是AI脚本技术。但彷佛效果不怎么好。如何才能有大局观?思考中…
局部AI:少血的兵日后退(补血或再上前线),集中火力解决血最少而攻击力较高的敌人(注
意也有可能集中火力得不偿失),等等。(抄袭WC3的微操?)

12.7 FPS编程要点
12.7.1 移动
实现3D场景中的移动能够很难也能够很简单,关键在于你想达到怎样的效果和你是否拥有清晰的思惟。咱们下面仍是说说一种在FPS中十分常见,并且效果很好的移动方式:用鼠标控制视线,用键盘控制先后左右的移动。
为了实现这种效果,固然必须存储玩家的所在坐标(假设为x,y,z)和眼睛看的位置(假设为tx,ty,tz)。首先在游戏的刷新函数中加上这几句(工做原理本身想一想吧,很是简单):
//假设mousex为鼠标的x坐标,mousey为鼠标的y坐标,且已按7.2节使用DInput
if (mousey>236) //防止mousey越界使视线的上下移动不正常
mousey=236; //236约等于π*150/2
if (mousey<-236) //防止mousey越界使视线的上下移动不正常
mousey=-236;
Player->ty=Player->y-tan((float)(mousey)/150); //150能够改成其它常数
Player->tx=Player->x+sin((float)(mousex)/150);
Player->tz=Player->z+cos((float)(mousex)/150);

而后在每次刷新画面时都像这样设置一次View矩阵:
D3DXMatrixLookAtLH(&matView,
&D3DXVECTOR3(x, y, z),
&D3DXVECTOR3(tx, ty, tz),
&D3DXVECTOR3(0.0f,1.0f,0.0f));

如今已经实现了用鼠标控制视线,下一步是加入移动函数,这其实也很是简单:
float dx=(tx-x)*time*7; //time为一次刷新所耗时间(秒)
float dz=(tz-z)*time*7; //你能够随便修改7这个数以实现你想要的速度
最后根据移动方向把x和z加上或减去dx和dz就完成了整个移动系统,很容易吧?

12.7.2 碰撞检测
不要把3D碰撞检测想得太难(虽然确实很难),由于体贴的Microsoft程序员已经想到了咱们的难处(真可贵啊)。在DXGraphics中提供了一个函数能够极大地方便咱们的编程,它就是D3DXIntersect( ),其做用是判断空间中的一条射线是否与指定的Mesh相交,若是相交的话还会返回距离。因此只要引一条从人物出发,到人物将要移动到的地方结束的射线,再判断一下与任何Mesh的距离是否都小于某值,便可完成几乎彻底精确的碰撞检测。惟一的问题是人物可能在其它方向上与物体相交,不过这种事发生的几率不大,有的话能够经过增长射线数解决----如今的CPU这么快,咱们能够懒一点?。
此函数的原型是:
D3DXIntersect(
LPD3DXBASEMESH pMesh, //要测试的Mesh
CONST D3DXVECTOR3* pRayPos, //射线从何处开始
CONST D3DXVECTOR3* pRayDir, //射线的方向
BOOL* pHit, //是否相交
DWORD* pFaceIndex, //在Mesh中的第几个三角形处相交
FLOAT* pU, //在三角形的何处相交
FLOAT* pV, //在三角形的何处相交
FLOAT* pDist, //距离
LPD3DXBUFFER* ppAllHits, //保存全部相交点的信息
DWORD* pCountOfHits //相交几回
);

要说明的只有一点:射线的方向应该是什么呢?很简单,假设起点为(x1,y1,z1),终点为(x2,y2,z2),方向就是(x2-x1,y2-y1,z2-z1)。
不过,若是场景中的多边形一多,这样作碰撞检测的效率可就比较低了,解决办法是使用Bounding Box & Bounding Sphere,即把物体看作是由一些长方体和球构成,而后借助它们实现碰撞检测。

12.8 游戏中的物理学
哈哈,我喜欢这一节?。这里将会向你们介绍一些物理基础知识。
附 录

附录一 Windows常见消息列表

WM_ACTIVATE Indicates a change in the activation state
WM_ACTIVATEAPP Notifies applications when a new task is activated

WM_CANCELMODE Notifies a window to cancel internal modes
WM_CHANGECBCHAIN Notifies clipboard viewer of removal from chain
WM_CHAR Passes keyboard events to focus window
WM_CHARTOITEM Provides list-box keystrokes to owner window
WM_CHILDACTIVATE Notifies a child window of activation
WM_CLEAR Clears an edit control or combo box

WM_CLOSE Signals a window or application to terminate
WM_COMMAND Specifies a command message
WM_COMMNOTIFY Notifies a window about the status of its queues
WM_COMPACTING Indicates a low memory condition
WM_COMPAREITEM Determines position of combo-box or list-box item
WM_COPY Copies a selection to the clipboard
WM_CREATE Indicates that a window is being created
WM_CTLCOLOR Indicates that a control is about to be drawn
WM_CUT Deletes a selection and copies it to the clipboard
WM_DEADCHAR Indicates when a dead key is pressed
WM_DELETEITEM Indicates owner-drawn item or control is altered

WM_DESTROY Indicates window is about to be destroyed
WM_DEVMODECHANGE Indicates when device-mode settings are changed
WM_DRAWCLIPBOARD Indicates when clipboard contents are changed
WM_DRAWITEM Indicates when owner-drawn control or menu changes
WM_DROPFILES Indicates when a file is dropped
WM_ENABLE Indicates when enable state of window is changing
WM_ENDSESSION Indicates whether the Windows session is ending
WM_ENTERIDLE Indicates a modal dialog box or menu is idle
WM_ERASEBKGND Indicates when background of window needs erasing

WM_FONTCHANGE Indicates a change in the font-resource pool
WM_GETDLGCODE Allows processing of control input
WM_GETFONT Retrieves the font that a control is using
WM_GETMINMAXINFO Retrieves minimum and maximum sizing information
WM_GETTEXT Copies the text that corresponds to a window
WM_GETTEXTLENGTH Determines length of text associated with a window
WM_HSCROLL Indicates a click in a horizontal scroll bar
WM_ICONERASEBKGND Notifies minimized window to fill icon background

WM_INITDIALOG Initializes a dialog box
WM_INITMENU Indicates when a menu is about to become active
WM_INITMENUPOPUP Indicates when a pop-up menu is being created
WM_KEYDOWN Indicates when a nonsystem key is pressed
WM_KEYUP Indicates when a nonsystem key is released
WM_KILLFOCUS Indicates window is about to lose input focus
WM_LBUTTONDBLCLK Indicates double-click of left mouse button
WM_LBUTTONDOWN Indicates when left mouse button is pressed
WM_LBUTTONUP Indicates when left mouse button is released
WM_MBUTTONDBLCLK Indicates double-click of middle mouse button

WM_MBUTTONDOWN Indicates when middle mouse button is pressed
WM_MBUTTONUP Indicates when middle mouse button is released
WM_MDIACTIVATE Activates a new MDI child window
WM_MDICASCADE Arranges MDI child windows in a cascade format
WM_MDICREATE Prompts an MDI client to create a child window
WM_MDIDESTROY Closes an MDI child window
WM_MDIGETACTIVE Retrieves data about the active MDI child window
WM_MDIICONARRANGE Arranges minimized MDI child windows
WM_MDIMAXIMIZE Maximizes an MDI child window

WM_MDINEXT Activates the next MDI child window
WM_MDIRESTORE Prompts an MDI client to restore a child window
WM_MDISETMENU Replaces the menu of a MDI frame window
WM_MDITILE Arranges MDI child windows in a tiled format
WM_MEASUREITEM Requests dimensions of owner-drawn control
WM_MENUCHAR Indicates when unknown menu mnemonic is pressed
WM_MENUSELECT Indicates when a menu item is selected
WM_MOUSEACTIVATE Indicates a mouse click in an inactive window
WM_MOUSEMOVE Indicates mouse-cursor movement

WM_MOVE Indicates the position of a window has changed
WM_NCACTIVATE Changes the active state of a nonclient area
WM_NCCALCSIZE Calculates the size of a window's client area
WM_NCCREATE Indicates a nonclient area is being created
WM_NCDESTROY Indicates when nonclient area is being destroyed
WM_NCHITTEST Indicates mouse-cursor movement
WM_NCLBUTTONDBLCLK Indicates non-client left button double-click
WM_NCLBUTTONDOWN Indicates left button pressed in nonclient area
WM_NCLBUTTONUP Indicates left button released in nonclient area
WM_NCMBUTTONDBLCLK Indicates middle button nonclient double-click

WM_NCMBUTTONDOWN Indicates middle button pressed in nonclient area
WM_NCMBUTTONUP Indicates middle button released in nonclient area
WM_NCMOUSEMOVE Indicates mouse-cursor movement in nonclient area
WM_NCPAINT Indicates a window's frame needs painting
WM_NCRBUTTONDBLCLK Indicates right button nonclient double-click
WM_NCRBUTTONDOWN Indicates right button pressed in nonclient area
WM_NCRBUTTONUP Indicates right button released in nonclient area
WM_NEXTDLGCTL Sets the focus to a different dialog box control
WM_PAINT Indicates a window frame needs painting
WM_PAINTCLIPBOARD Paints the specified portion of the window

WM_PALETTECHANGED Indicates focus-window has realized its palette
WM_PALETTEISCHANGING Informs windows about change to palette
WM_PARENTNOTIFY Notifies parent of child-window activity
WM_PASTE Inserts clipboard data into an edit control
WM_POWER Indicates the system is entering suspended mode
WM_QUERYDRAGICON Requests a cursor handle for a minimized window
WM_QUERYENDSESSION Requests that the Windows session be ended
WM_QUERYNEWPALETTE Allows a window to realize its logical palette

WM_QUERYOPEN Requests that a minimized window be restored
WM_QUEUESYNC Delimits CBT messages
WM_QUIT Requests that an application be terminated
WM_RBUTTONDBLCLK Indicates a double-click of right mouse button
WM_RBUTTONDOWN Indicates when the right mouse button is pressed
WM_RBUTTONUP Indicates when the right mouse button is released
WM_RENDERALLFORMATS Notifies owner to render all clipboard formats
WM_RENDERFORMAT Notifies owner to render particular clipboard data
WM_SETCURSOR Displays the appropriate mouse cursor shape

WM_SETFOCUS Indicates when a window has gained input focus
WM_SETFONT Sets the font for a control
WM_SETREDRAW Allows or prevents redrawing in a window
WM_SETTEXT Sets the text of a window
WM_SHOWWINDOW Indicates a window is about to be hidden or shown
WM_SIZE Indicates a change in window size
WM_SIZECLIPBOARD Indicates a change in clipboard size
WM_SPOOLERSTATUS Indicates when a print job is added or removed
WM_SYSCHAR Indicates when a System-menu key is pressed

WM_SYSCOLORCHANGE Indicates when a system color setting is changed
WM_SYSCOMMAND Indicates when a System-command is requested
WM_SYSDEADCHAR Indicates when a system dead key is pressed
WM_SYSKEYDOWN Indicates that ALT plus another key was pressed
WM_SYSKEYUP Indicates that ALT plus another key was released
WM_SYSTEMERROR Indicates that a system error has occurred
WM_TIMECHANGE Indicates that the system time has been set
WM_TIMER Indicates timeout interval for a timer has elapsed
WM_UNDO Undoes the last operation in an edit control

WM_USER Indicates a range of message values
WM_VKEYTOITEM Provides list-box keystrokes to owner window
WM_VSCROLL Indicates a click in a vertical scroll bar
WM_VSCROLLCLIPBOARD Prompts the owner to scroll clipboard contents
WM_WINDOWPOSCHANGED Notifies a window of a size or position change
WM_WINDOWPOSCHANGING Notifies a window of a new size or position
附录二 虚拟键列表
Windows消息中的虚拟键

VK_LBUTTON 鼠标左键 0x01
VK_RBUTTON 鼠标右键 0x02
VK_CANCEL Ctrl + Break 0x03
VK_MBUTTON 鼠标中键 0x04

VK_BACK Backspace 键 0x08
VK_TAB Tab 键 0x09

VK_RETURN 回车键 0x0D

VK_SHIFT Shift 键 0x10
VK_CONTROL Ctrl 键 0x11
VK_MENU Alt 键 0x12
VK_PAUSE Pause 键 0x13
VK_CAPITAL Caps Lock 键 0x14

VK_ESCAPE Esc 键 0x1B

VK_SPACE 空格键 0x20
VK_PRIOR Page Up 键 0x21
VK_NEXT Page Down 键 0x22
VK_END End 键 0x23
VK_HOME Home 键 0x24
VK_LEFT 左箭头键 0x25
VK_UP 上箭头键 0x26
VK_RIGHT 右箭头键 0x27
VK_DOWN 下箭头键 0x28
VK_SNAPSHOT Print Screen 键 0x2C
VK_INSERT Insert 键 0x2D
VK_DELETE Delete 键 0x2E

'0' – '9' 数字 0 - 9 0x30 - 0x39
'A' – 'Z' 字母 A - Z 0x41 - 0x5A

VK_LWIN 左WinKey(104键盘才有) 0x5B
VK_RWIN 右WinKey(104键盘才有) 0x5C
VK_APPS AppsKey(104键盘才有) 0x5D

VK_NUMPAD0 小键盘 0 键 0x60
VK_NUMPAD1 小键盘 1 键 0x61
VK_NUMPAD2 小键盘 2 键 0x62
VK_NUMPAD3 小键盘 3 键 0x63
VK_NUMPAD4 小键盘 4 键 0x64
VK_NUMPAD5 小键盘 5 键 0x65
VK_NUMPAD6 小键盘 6 键 0x66
VK_NUMPAD7 小键盘 7 键 0x67
VK_NUMPAD8 小键盘 8 键 0x68
VK_NUMPAD9 小键盘 9 键 0x69

VK_F1 - VK_F24 功能键F1 – F24 0x70 - 0x87

VK_NUMLOCK Num Lock 键 0x90
VK_SCROLL Scroll Lock 键 0x91


DirectInput中的虚拟键

DIK_0 – DIK_9 数字 0 - 9
DIK_A – DIK_Z 字母 A - Z
DIK_F1 – DIK_F12 功能键F1 – F12
DIK_BACK Backspace 键
DIK_TAB Tab 键
DIK_RETURN 回车键

DIK_LSHIFT 左Shift 键
DIK_RSHIFT 右Shift 键
DIK_LCONTROL 左Ctrl 键
DIK_RCONTROL 右Ctrl 键
DIK_LMENU 左Alt 键
DIK_RMENU 右Alt 键
DIK_PAUSE Pause 键
DIK_CAPITAL Caps Lock 键

DIK_ESCAPE Esc 键

DIK_SPACE 空格键
DIK_PRIOR Page Up 键
DIK_NEXT Page Down 键
DIK_END End 键
DIK_HOME Home 键
DIK_LEFT 左箭头键
DIK_UP 上箭头键
DIK_RIGHT 右箭头键
DIK_DOWN 下箭头键
DIK_SYSRQ SysRq键
DIK_INSERT Insert 键
DIK_DELETE Delete 键


DIK_LWIN 左WinKey(104键盘才有)
DIK_RWIN 右WinKey(104键盘才有)
DIK_APPS AppsKey(104键盘才有)

DIK_NUMPAD0 – DIK_NUMPAD0 小键盘 0 – 9 键

DIK_NUMLOCK Num Lock 键
DIK_SCROLL Scroll Lock 键
附录三 DirectX函数返回值列表
DirectDraw部分

DD_OK
The request completed successfully.
DDERR_ALREADYINITIALIZED
The object has already been initialized.
DDERR_BLTFASTCANTCLIP
A DirectDrawClipper object is attached to a source surface that has passed into a call to the IDirectDrawSurface::BltFast method.
DDERR_CANNOTATTACHSURFACE
A surface cannot be attached to another requested surface.
DDERR_CANNOTDETACHSURFACE
A surface cannot be detached from another requested surface.
DDERR_CANTCREATEDC
Windows can not create any more device contexts (DCs), or a DC was requested for a palette-indexed surface when the surface had no palette and the display mode was not palette-indexed (in this case DirectDraw cannot select a proper palette into the DC).
DDERR_CANTDUPLICATE
Primary and 3-D surfaces, or surfaces that are implicitly created, cannot be duplicated.
DDERR_CANTLOCKSURFACE
Access to this surface is refused because an attempt was made to lock the primary surface without DCI support.
DDERR_CANTPAGELOCK
An attempt to page lock a surface failed. Page lock will not work on a display-memory surface or an emulated primary surface.
DDERR_CANTPAGEUNLOCK
An attempt to page unlock a surface failed. Page unlock will not work on a display-memory surface or an emulated primary surface.
DDERR_CLIPPERISUSINGHWND
An attempt was made to set a clip list for a DirectDrawClipper object that is already monitoring a window handle.
DDERR_COLORKEYNOTSET
No source color key is specified for this operation.
DDERR_CURRENTLYNOTAVAIL
No support is currently available.
DDERR_DCALREADYCREATED
A device context (DC) has already been returned for this surface. Only one DC can be retrieved for each surface.
DDERR_DEVICEDOESNTOWNSURFACE
Surfaces created by one DirectDraw device cannot be used directly by another DirectDraw device.
DDERR_DIRECTDRAWALREADYCREATED
A DirectDraw object representing this driver has already been created for this process.
DDERR_EXCEPTION
An exception was encountered while performing the requested operation.
DDERR_EXCLUSIVEMODEALREADYSET
An attempt was made to set the cooperative level when it was already set to exclusive.
DDERR_EXPIRED
The data has expired and is therefore no longer valid.
DDERR_GENERIC
There is an undefined error condition.
DDERR_HEIGHTALIGN
The height of the provided rectangle is not a multiple of the required alignment.
DDERR_HWNDALREADYSET
The DirectDraw cooperative level window handle has already been set. It cannot be reset while the process has surfaces or palettes created.
DDERR_HWNDSUBCLASSED
DirectDraw is prevented from restoring state because the DirectDraw cooperative level window handle has been subclassed.
DDERR_IMPLICITLYCREATED
The surface cannot be restored because it is an implicitly created surface.
DDERR_INCOMPATIBLEPRIMARY
The primary surface creation request does not match with the existing primary surface.
DDERR_INVALIDCAPS
One or more of the capability bits passed to the callback function are incorrect.
DDERR_INVALIDCLIPLIST
DirectDraw does not support the provided clip list.
DDERR_INVALIDDIRECTDRAWGUID
The globally unique identifier (GUID) passed to the DirectDrawCreate function is not a valid DirectDraw driver identifier.
DDERR_INVALIDMODE
DirectDraw does not support the requested mode.
DDERR_INVALIDOBJECT
DirectDraw received a pointer that was an invalid DirectDraw object.
DDERR_INVALIDPARAMS
One or more of the parameters passed to the method are incorrect.
DDERR_INVALIDPIXELFORMAT
The pixel format was invalid as specified.
DDERR_INVALIDPOSITION
The position of the overlay on the destination is no longer legal.
DDERR_INVALIDRECT
The provided rectangle was invalid.
DDERR_INVALIDSTREAM
The specified stream contains invalid data.
DDERR_INVALIDSURFACETYPE
The requested operation could not be performed because the surface was of the wrong type.
DDERR_LOCKEDSURFACES
One or more surfaces are locked, causing the failure of the requested operation.
DDERR_MOREDATA
There is more data available than the specified buffer size can hold.
DDERR_NO3D
No 3-D hardware or emulation is present.
DDERR_NOALPHAHW
No alpha acceleration hardware is present or available, causing the failure of the requested operation.
DDERR_NOBLTHW
No blitter hardware is present.
DDERR_NOCLIPLIST
No clip list is available.
DDERR_NOCLIPPERATTACHED
No DirectDrawClipper object is attached to the surface object.
DDERR_NOCOLORCONVHW
The operation cannot be carried out because no color-conversion hardware is present or available.
DDERR_NOCOLORKEY
The surface does not currently have a color key.
DDERR_NOCOLORKEYHW
The operation cannot be carried out because there is no hardware support for the destination color key.
DDERR_NOCOOPERATIVELEVELSET
A create function is called without the IDirectDraw7::SetCooperativeLevel method being called.
DDERR_NODC
No DC has ever been created for this surface.
DDERR_NODDROPSHW
No DirectDraw raster operation (ROP) hardware is available.
DDERR_NODIRECTDRAWHW
Hardware-only DirectDraw object creation is not possible; the driver does not support any hardware.
DDERR_NODIRECTDRAWSUPPORT
DirectDraw support is not possible with the current display driver.
DDERR_NOEMULATION
Software emulation is not available.
DDERR_NOEXCLUSIVEMODE
The operation requires the application to have exclusive mode, but the application does not have exclusive mode.
DDERR_NOFLIPHW
Flipping visible surfaces is not supported.
DDERR_NOFOCUSWINDOW
An attempt was made to create or set a device window without first setting the focus window.
DDERR_NOGDI
No GDI is present.
DDERR_NOHWND
Clipper notification requires a window handle, or no window handle has been previously set as the cooperative level window handle.
DDERR_NOMIPMAPHW
The operation cannot be carried out because no mipmap capable texture mapping hardware is present or available.
DDERR_NOMIRRORHW
The operation cannot be carried out because no mirroring hardware is present or available.
DDERR_NONONLOCALVIDMEM
An attempt was made to allocate non-local video memory from a device that does not support non-local video memory.
DDERR_NOOPTIMIZEHW
The device does not support optimized surfaces.
DDERR_NOOVERLAYDEST
The IDirectDrawSurface::GetOverlayPosition method is called on an overlay that the IDirectDrawSurface::UpdateOverlay method has not been called on to establish a destination.
DDERR_NOOVERLAYHW
The operation cannot be carried out because no overlay hardware is present or available.
DDERR_NOPALETTEATTACHED
No palette object is attached to this surface.
DDERR_NOPALETTEHW
There is no hardware support for 16- or 256-color palettes.
DDERR_NORASTEROPHW
The operation cannot be carried out because no appropriate raster operation hardware is present or available.
DDERR_NOROTATIONHW
The operation cannot be carried out because no rotation hardware is present or available.
DDERR_NOSTRETCHHW
The operation cannot be carried out because there is no hardware support for stretching.
DDERR_NOT4BITCOLOR
The DirectDrawSurface object is not using a 4-bit color palette and the requested operation requires a 4-bit color palette.
DDERR_NOT4BITCOLORINDEX
The DirectDrawSurface object is not using a 4-bit color index palette and the requested operation requires a 4-bit color index palette.
DDERR_NOT8BITCOLOR
The DirectDrawSurface object is not using an 8-bit color palette and the requested operation requires an 8-bit color palette.
DDERR_NOTAOVERLAYSURFACE
An overlay component is called for a non-overlay surface.
DDERR_NOTEXTUREHW
The operation cannot be carried out because no texture-mapping hardware is present or available.
DDERR_NOTFLIPPABLE
An attempt has been made to flip a surface that cannot be flipped.
DDERR_NOTFOUND
The requested item was not found.
DDERR_NOTINITIALIZED
An attempt was made to call an interface method of a DirectDraw object created by CoCreateInstance before the object was initialized.
DDERR_NOTLOADED
The surface is an optimized surface, but it has not yet been allocated any memory.
DDERR_NOTLOCKED
An attempt is made to unlock a surface that was not locked.
DDERR_NOTPAGELOCKED
An attempt is made to page unlock a surface with no outstanding page locks.
DDERR_NOTPALETTIZED
The surface being used is not a palette-based surface.
DDERR_NOVSYNCHW
The operation cannot be carried out because there is no hardware support for vertical blank synchronized operations.
DDERR_NOZBUFFERHW
The operation to create a z-buffer in display memory or to perform a blit using a z-buffer cannot be carried out because there is no hardware support for z-buffers.
DDERR_NOZOVERLAYHW
The overlay surfaces cannot be z-layered based on the z-order because the hardware does not support z-ordering of overlays.
DDERR_OUTOFCAPS
The hardware needed for the requested operation has already been allocated.
DDERR_OUTOFMEMORY
DirectDraw does not have enough memory to perform the operation.
DDERR_OUTOFVIDEOMEMORY
DirectDraw does not have enough display memory to perform the operation.
DDERR_OVERLAPPINGRECTS
Operation could not be carried out because the source and destination rectangles are on the same surface and overlap each other.
DDERR_OVERLAYCANTCLIP
The hardware does not support clipped overlays.
DDERR_OVERLAYCOLORKEYONLYONEACTIVE
An attempt was made to have more than one color key active on an overlay.
DDERR_OVERLAYNOTVISIBLE
The IDirectDrawSurface4::GetOverlayPosition method is called on a hidden overlay.
DDERR_PALETTEBUSY
Access to this palette is refused because the palette is locked by another thread.
DDERR_PRIMARYSURFACEALREADYEXISTS
This process has already created a primary surface.
DDERR_REGIONTOOSMALL
The region passed to the IDirectDrawClipper::GetClipList method is too small.
DDERR_SURFACEALREADYATTACHED
An attempt was made to attach a surface to another surface to which it is already attached.
DDERR_SURFACEALREADYDEPENDENT
An attempt was made to make a surface a dependency of another surface to which it is already dependent.
DDERR_SURFACEBUSY
Access to the surface is refused because the surface is locked by another thread.
DDERR_SURFACEISOBSCURED
Access to the surface is refused because the surface is obscured.
DDERR_SURFACELOST
Access to the surface is refused because the surface memory is gone. Call the IDirectDrawSurface::Restore method on this surface to restore the memory associated with it.
DDERR_SURFACENOTATTACHED
The requested surface is not attached.
DDERR_TOOBIGHEIGHT
The height requested by DirectDraw is too large.
DDERR_TOOBIGSIZE
The size requested by DirectDraw is too large. However, the individual height and width are valid sizes.
DDERR_TOOBIGWIDTH
The width requested by DirectDraw is too large.
DDERR_UNSUPPORTED
The operation is not supported.
DDERR_UNSUPPORTEDFORMAT
The pixel format requested is not supported by DirectDraw.
DDERR_UNSUPPORTEDMASK
The bitmask in the pixel format requested is not supported by DirectDraw.
DDERR_UNSUPPORTEDMODE
The display is currently in an unsupported mode.
DDERR_VERTICALBLANKINPROGRESS
A vertical blank is in progress.
DDERR_VIDEONOTACTIVE
The video port is not active.
DDERR_WASSTILLDRAWING
The previous blit operation that is transferring information to or from this surface is incomplete.
DDERR_WRONGMODE
This surface cannot be restored because it was created in a different mode.
DDERR_XALIGN
The provided rectangle was not horizontally aligned on a required boundary.


Direct3D部分

D3D_OK
No error occurred.
D3DERR_CONFLICTINGRENDERSTATE
The currently set render states cannot be used together.
D3DERR_CONFLICTINGTEXTUREFILTER
The current texture filters cannot be used together.
D3DERR_CONFLICTINGTEXTUREPALETTE
The current textures cannot be used simultaneously. This generally occurs when a multitexture device requires that all palletized textures simultaneously enabled also share the same palette.
D3DERR_DEVICELOST
The device is lost and cannot be restored at the current time, so rendering is not possible.
D3DERR_DEVICENOTRESET
The device cannot be reset.
D3DERR_DRIVERINTERNALERROR
Internal driver error.
D3DERR_INVALIDCALL
The method call is invalid. For example, a method's parameter may have an invalid value.
D3DERR_INVALIDDEVICE
The requested device type is not valid.
D3DERR_MOREDATA
There is more data available than the specified buffer size can hold.
D3DERR_NOTAVAILABLE
This device does not support the queried technique.
D3DERR_NOTFOUND
The requested item was not found.
D3DERR_OUTOFVIDEOMEMORY
Direct3D does not have enough display memory to perform the operation.
D3DERR_TOOMANYOPERATIONS
The application is requesting more texture-filtering operations than the device supports.
D3DERR_UNSUPPORTEDALPHAARG
The device does not support a specified texture-blending argument for the alpha channel.
D3DERR_UNSUPPORTEDALPHAOPERATION
The device does not support a specified texture-blending operation for the alpha channel.
D3DERR_UNSUPPORTEDCOLORARG
The device does not support a specified texture-blending argument for color values.
D3DERR_UNSUPPORTEDCOLOROPERATION
The device does not support a specified texture-blending operation for color values.
D3DERR_UNSUPPORTEDFACTORVALUE
The device does not support the specified texture factor value.
D3DERR_UNSUPPORTEDTEXTUREFILTER
The device does not support the specified texture filter.
D3DERR_WRONGTEXTUREFORMAT
The pixel format of the texture surface is not valid.
E_FAIL
An undetermined error occurred inside the Direct3D subsystem.
E_INVALIDARG
An invalid parameter was passed to the returning function
E_INVALIDCALL
The method call is invalid. For example, a method's parameter may have an invalid value.
E_OUTOFMEMORY
Direct3D could not allocate sufficient memory to complete the call.
S_OK
No error occurred.
附录四 Winsock函数返回值列表

WSAEINTR (10004)
Interrupted Function Call -- A blocking operation was cancelled.
WSAEACCESS (10013)
Permission Denied -- An attempt to access a socket was forbidden by its access permissions.
WSAEFAULT (10014)
Bad Address -- An invalid pointer address was specified in a function call.
WSAEINVAL (10022)
Invalid Argument -- An invalid argument was passed to a function.
WSAEMFILE (10024)
Too Many Open Files -- There are too many open sockets.
WSAEWOULDBLOCK (10035)
Resource Temporarily Unavailable -- The specified socket operation cannot be completed immediately, but the operation should be retried later.
WSAEINPROGRESS (10036)
Operation Now in Progress -- A blocking operation is currently executing.
WSAEALREADY (10037)
Operation Already in Progress -- An operation was attempted on a non-binding socket that already had an operation in progress.
WSAENOTSOCK (10038)
Socket Operation on Non-Socket -- An operation was attempted on something that is not a socket.
WSAEDESTADDRREQ (10039)
Destination Address Required -- A required address was omitted from a socket operation.
WSAEMSGSIZE (10040)
Message Too Long -- A message was sent on a datagram socket that exceeds the internal message buffer or some other limit.
WSAEPROTOTYPE (10041)
Protocol Wrong Type for Socket -- A protocol was specified that is not supported by the target socket.
WSAENOPROTOOPT (10042)
Bad Protocol Option -- An unknown, invalid, or unsupported protocol option or leel was specified.
WSAEPROTONOSUPPORT (10043)
Protocol Not Supported -- The specified protocol is not supported or is not implemented.
WSAESOCKTNOSUPPORT (10044)
Socket Type Not Supported -- The specified socket type is not supported in the address family.
WSAEOPNOTSUPP (10045)
Operation Not Supported -- The specified operation is not supported by the referenced object.
WSAEPFNOSUPPORT (10046)
Protocol Family Not Supported -- The specified protocol family is not supported or is not implemented.
WSAEAFNOSUPPORT (10047)
Address Family Not Supported by Protocol Family -- An address incompatible with the requested network protocol was used.
WSAEADDRINUSE (10048)
Address Already in Use -- An attempt to use the same IP address and port with two different sockets simultaneously was made.
WSAEADDRNOTAVAIL (10049)
Cannot Assign Requested Address -- The requested address is not valid (given the context of the function).
WSAENETDOWN (10050)
Network is Down -- A socket operation encountered a network that is down.
WSAENETUNREACH (10051)
Network is Unreachable -- A socket operation encountered an unreachable network.
WSAENETRESET (10052)
Network Dropped Connection on Reset -- A connection was broken due to "keep-alive" activity detecting a failure.
WSAECONNABORTED (10053)
Software Caused Connection Abort -- A connection was aborted by software on the host computer.
WSAECONNRESET (10054)
Connection Reset by Peer -- A connection was forcibly closed by the remote host.
WSAENOBUFS (10055)
No Buffer Space Available -- A socket operation could not be performed because the system ran out of buffer space or the queue was full.
WSAEISCONN (10056)
Socket is Already Connected -- A connect request was made on a socket that is already connected.
WSAENOTCONN (10057)
Socket is Not Connected -- An attempt to send or receive data failed because the socket is not connected.
WSAESHUTDOWN (10058)
Cannot Send After Socket Shutdown -- An attempt to send or receive data failed because the socket has already been shut down.
WSAETIMEDOUT (10060)
Connection Timed Out -- The remote host failed to respond within the timeout period.
WSAECONNREFUSED (10061)
Connection Refused -- The target machine actively refused the attempt to connect to it.
WSAEHOSTDOWN (10064)
Host is Down -- The destination host is down.
WSAEHOSTUNREACH (10065)
No Route to Host -- The destination host is unreachable.
WSAEPROCLIM (10067)
Too Many Processes -- The Winsock implementation has exceeded the number of applications that can use it simultaneously.
WSASYSNOTREADY (10091)
Network Subsystem is Unavailable -- The underlying system to provide network services is unavailable.
WSAVERNOTSUPPORTED (10092)
winsock.dll Version Out of Range -- The Winsock implementation does not support the requested Winsock version.
WSANOTINITIALIZED (10093)
Successful WSAStartup Not Yet Performed -- The calling application has not successfully called WSAStartup to initiate a Winsock session.
WSAEDISCON (10094)
Graceful Shutdown in Progress -- The remote party has initiated a graceful shutdown sequence.
WSATYPE_NOT_FOUND (10109)
Class Type Not Found -- The specified class was not found.
WSAHOST_NOT_FOUND (11001)
Host Not Found -- No network host matching the hostname or address was found.
WSATRY_AGAIN (11002)
Non-Authoritative Host Not Found -- A temporary error while resolving a hostname occured, and should be retried later.
WSANO_RECOVERY (11003)
This is a Non-Recoverable Error -- Some sort of non-recoverable error occured during a database lookup.
WSANO_DATA (11004)
Valid Name, No Data Record of Requested Type -- The requested name is valid and was found, but does not have the associated data requested.
附录五 游戏编程经常使用网址

新浪网游戏制做论坛
http://newbbs2.sina.com.cn/index.shtml?games:gamedesign
17173游戏联盟论坛
http://bbs.17173.com.cn/default.asp
CSDN专家门诊
http://www.csdn.net/Expert/Forum.asp?roomid=12&typenum=2
中国游戏开发者
http://mays.soage.com/
中国游戏开发技术资源网
http://www.gameres.com/
云风工做室
http://linux.163.com/cloudwu/2000/index.html
Imagic工做室
http://www.imagic3d.com/cindex.html
Game1st
http://www.game1st.com/cindex.htm
何苦作游戏
http://www.npc6.com/index.htm
金点时空
http://www.gpgame.com/
GameDev (英文)
http://www.gamedev.net
FlipCode (英文)
http://www.flipcode.com
附录六 中英文名词对照

Back Buffer 后台缓存
Buffer 缓存
Class 类
Color Key 关键色,透明色
Cooperative Level 控制级,协做级
Off-screen Surface 离屏页面
Material 材质,材料
Primary Surface 主页面
Sprite 精灵
Surface 页面,表面
Template 模板
Texture 纹理,贴图
Union 联合
Vertex 顶点
附录七 常见问题及解决办法

1. 程序编译时出现"Warning"
说了是"Warning"(警告)就不会是什么大问题,你能够忽略它们。不过有时程序的运行异常就是由这些Warning引发,特别是类型转换警告。

2. "Cannot Execute Program"
退出VC.net再从新启动它,也能够试试Build---Rebuild Solution。若是还不行,检查程序是否正在运行中。再不行,到工程属性处看看是否设错了exe输出路径。

3. "Unresolved External Symbol"
若是出现"Main"或是"WinMain"的字样,你必定是选错了工程类型:究竟是Win32 Application,仍是Win32 Console Application?若是出现了类或函数的名字,那么要么是你没有在工程中加入全部应加入的lib,要么是你定义的extern变量没有无extern形式与之相对应。请参阅1.1节和1.8节。

4. 运行时出错
95%以上的运行时错误是由指针引发的。因此如今马上进入Debug模式,而后按F5执行代码,看看究竟是在哪一行代码处出错吧。特别注意此行的指针是否未初始化和是否越界。

5. 你们还有什么问题,能够告诉我
<