声 明
再写下后面的东西以前,我觉得有必要专门花一页的篇幅进行下面的申明.在我的眼里它的必要性不亚于在吃中餐前,先阅读中国筷子的使用说明书.在以后的文字中,我都准备尽可能以一种轻松,略带调侃的语气来完成,但是下面的文字我却会写得很正式,因为谁也不想笑着看完筷子使用说明书后,在吃饭时被筷子卡住喉咙,那样的话就一点都不好笑了.
其一,如果你希望看到一篇教条的文字,那么它并不适合你.因为这篇文字以及后来你们将要看到的程序从一开始就并不是你们想象中的那种循规蹈矩的风格.
其二,如果你仅仅是一个游戏爱好者,那么这篇文章的主人一定会成为你的好朋友;如果你是一个游戏开发者,那么这篇文章的主人很愿意和你结交成一个良师益友,并且十分愿意和你共同探讨游戏编程的众多的技术问题,一起学习,一起进步;如果你是一个希望进入游戏开发的朋友,首先,我要提醒你,你要考虑清楚了,不要被现在游戏业红火的表面现象所迷惑,如果在慎重考虑以后,你仍然义无反顾(你死定了!),那么这篇文字非常适合你进入游戏开发领域,它将会是从基础的游戏的构架一直讲到游戏的开发(再次强调,进入这行后,后悔了可不要怪我哦J).如果你不是一个游戏爱好者,甚至没有进入这行的打算,那么我要说得是真遗憾,早点进来吧,游戏的大门永远为你敞开.
其三,这篇文字以及其后讲到的所有源程序绝对是原创, 因为这篇文字的主人崇尚"将原创进行到底"的主义.如果你对文字有什么不满意的地方,或者你对里面讲的东东有什么不理解的地方;或者你认为这本身就是一篇很垃圾的东西.都欢迎你来信BT我.
其四,闷炮工作室这样做是为了鼓励更多的人加入游戏制作的行列,只希望以本文为"中华游戏之崛起"而贡献一点微薄之力!
其五,对于我提供的代码和文字,你可以使用和学习,但本人不承担责任。不能用于商业用途,如有需要,请与本人联系。在保留作者的版权申明的条件下,你可以自由传播这些代码。
其六,一点点个人的民族气节哈^_^,同时,你还需要遵守以下条款:
1: 不能喜欢S H E.
2: 支持一个中国,BS台独分子.
3: 尽量抵制日货物.需有明显的反日倾向.
一旦你使用了我的代码和资料.表示你同意以上条款.
其他,……还没有想好.
感觉象在写《南京条约》,那个又愿意写这样的文字呢,不过没有规矩不成方圆,所以赖着头皮写下上面的东东,希望大家理解.
石磬著
By Shi Qing
Email: shiqingstudio@yahoo.com.cn
QQ: 16398943
第一章 离开游戏说编程
我为什么会用这个名字呢?那是因为我要告诉大家,首先游戏编程不是这个世界上编程的唯一东西,而且还要忠告所有玩家,游戏也不是这个世界上的唯一,如果有时间多做些户外运动,游戏切记适可而止;另外,更重要的是要告诉大家,就目前微软统制的这个时代,几乎所有的编程都属于win32编程,自从上个世纪,微软成就了盖茨以来,同时面向对象编程和基于窗体的win32编程也同时成就了今天的软件行业.所以基础的win32编程就成为了不可不说的话题.
1.1编程环境
这里要说的编程环境不是我们的生活环境,不是象论坛里面每天讨论的程序员的工资,也不是政府给软件业的大环境,更不是世界软件的何去何从,这里要说的是我们所用的开发工具和软件接口.这里所有讲到的编译器和图形接口都是用得微软的东东.也就是我将向大家讲述的所有游戏编程内容都是基于visual studio.net2003和direct7.
1.1.2图形接口
什么是图形接口?DirectX就是一个图形接口,DirectX是Microsoft开发的基于Windows平台的一组API,它是为高速的实时动画渲染、交互式音乐和环境音效等高要求应用开发服务的;Direct X是微软公司专为PC游戏开发的API(应用程序接口),特点是:比较容易控制,可令显卡发挥不同的功能,与Windows 95和Windows NT兼容性较好。在Direct X 5.0中共分六个部分:DirectDraw—管理游戏的视频输出、Direct 3D—管理游戏的3D图形、DirectPlay—管理游戏的网络通讯、DirectSound—管理游戏的声音输出、DirectInput—管理游戏摇杆控制、Direct Setup—管理游戏的安装。以上这些都是微软给我们的解释,简单的说,它终究都是一些微软给我们封装好了的API函数,我们通过对这些函数的调用和操作可以达到直接对硬件的操作,包括加速,显卡的显示,游戏手柄的使用,声卡的使用………,既然可以对显卡和声卡以及手柄进行操作,自然而然我们就可以编写游戏.所以我们游戏的开发都是基于direct编程.
1.2 win32编程
现在的所以编程都是面相对象编程,在大学里面头发花白的老头就不断的给我们灌输这个思想,什么是对象呢,所有的窗体,组件,包括封装的类,他们都是对象,然而现在我们家庭软件绝大多数都是windows的窗体程序,所以可以说我们现在的编程都是基于窗体的win32编程,就是windows下的32位编程(听说下一代64位机有望在今年面试,不过走入家庭不知道又是什么时候了),自然容纳我们的游戏的容器就是windows窗体,所以这一章的重点就是下面的窗体的建立,只有建立了这个容器,我们才能在上面编制其他的程序,无论是游戏或者其他的什么东西.
1.2.1消息机制
win32编程的核心思想就是消息机制,每一个程序员都必须了解它,它也是最低层的东西了,不知道看本文的朋友有没有看过候捷大师的《深入浅出MFC》和李维老师的《VCL构架剖析》(没有听说过?你不会是火星来的吧^_^),如果没有,强烈建议你去买来看看,两本书讲的不是同一个编译器,(一个是micosoft的,一个是borland),不是同一种语言(一个是c++,一个是pascl),前一本出来的时间比较早,现在俨然成为华人地区程序员的圣经,后一本书是昨年才出来的,但是两本书讲的东西都是一样的,想学好编程一定要看看这两本书,他们旁博引证,引经论典,丝丝入扣,给我的启发颇深,看来也是显而易懂(怎么觉得想在给两个老师作宣传J),看完以后你就知道我们平时看着简单的程序到底是怎么运行的,他们在后台是怎么样实现的.由于我的表达能力有限,所以在这里只能尽量把我的理解大概讲讲,讲的不好,不要拿砖头砸我啊.
我想先谈一下Dos与Windows驱动机制的区别:
DOS程序主要使用顺序的,过程驱动的程序设计方法。顺序的,过程驱动的程序有一个明显的开始,明显的过程及一个明显的结束,因此程序能直接控制程序事件或过程的顺序。虽然在顺序的过程驱动的程序中也有很多处理异常的方法,但这样的异常处理也仍然是顺序的,过程驱动的结构。
1.2.2窗体的注册和初始化
了解了消息机制以后,我们可以创建窗体了.创建窗体前我们看看windows程序的流程,大致流程图如下:
//*********************************************************
//函数:WinMain( )
//功能:Windows程序入口函数。创建主窗口,处理消息循环
//*********************************************************
int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg; //消息结构
InitWindow(hInstance,nCmdShow); //初始化窗体
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message==WM_QUIT) //退出消息循环
break;
TranslateMessage(&msg); //得到消息,处理回调函数
DispatchMessage(&msg);
}
}
return msg.wParam;
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
第一个参数hInstance是标识该应用程序的句柄。不过句柄又是什么呢?其实就是一个指向该程序所占据的内存区域的指针,它唯一地代表了该应用程序,Windows使用它管理内存中的各种对象。
第二个参数是hPrevInstance,应用程序的前一个实例句柄,别管它,对于Win32位而言,它一般是NULL.
第三个参数是lpCmdLine,是指向应用程序命令行参数字符串的指针。比如说我们运行"test hello",则此参数指向的字符串为"hello"。
最后一个参数是nCmdShow,是一个用来指定窗口显示方式的整数。它告诉应用程序如何初始化窗口,如最大化,最小化等状态。
//函数: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 = NULL; //用来着色窗口背景的刷子
wc.lpszMenuName = NULL; //指向菜单资源名的指针
wc.lpszClassName = "menpao_RPG_DEMO";// 指向窗口类名的指针
RegisterClass(&wc); //注册窗口
hwnd=CreateWindow("menpao_RPG_DEMO","menpao_RPG_DEMO",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;
}
(1)第一个参数:成员style控制窗口样式的某些重要特性,在WINDOWS.H中定义了一些前缀为CS的常量,在程序中可组合使用这些常量.
(3)第三,四个参数:cbWndExtra域指定用本窗口类建立的所有窗口结构分配的额外字节数。当有两个以上的窗口属于同一窗口类时,如果想将不同的数据和每个窗口分别相对应。则使用该域很有用。这般来讲,你只要把它们设为0就行了,不必过多考虑。
(4)第五个参数:hInstance成员,给它的值是窗口所对应的应用程序的句柄,表明该窗口与此应用程序是相关联的。
(5)第六个参数:成员hIcon被设置成应用程序所使用图标的句柄,图标是将应用程序最小化时出现在任务栏里的的图标,用以表示程序仍驻留在内存中。Windows提供了一些默认图标,我们也可定义自己的图标,VC里面专有一个制作图标的工具。
(6)第七个参数: hCursor域定义该窗口产生的光标形状。LoadCursor可返回固有光标句柄或者应用程序定义的光标句柄。IDC_ARROW表示箭头光标.
(7)第八个参数:hbrBackground成员用来定义窗口的背景色。这里设为NULL。
(8)第九个参数:lpszMenuName用来指定菜单名,本程序中没有定义菜单,所以为NULL。
(9)第十个参数:lpszClassName指定了本窗口的类名。本程序命名为“menpao_RPG_DEMO”。
当对WNDCLASS结构域一一赋值后,就可注册窗口类了,在创建窗口之前,是必须要注册窗口类的,注册窗口类用的API函数是RegisterClass,注册失败的话,就会出现一个对话框如程序所示,函数RegisterClass返回0值,也只能返回0值,因为注册不成功,程序已经不能再进行下去了。
注册完了以后,就是创建该窗体,一般我们一时调用API函数中的CreatWindows()函数完成的
以上面注册的这个窗体为例
hwnd = CreateWindow(
"menpao_RPG_DEMO", //创建的这个窗体类的名称
"menpao_RPG_DEMO", //窗口标题
WS_POPUP|WS_MAXIMIZE, //窗口风格,全部风格见后表
0, //窗口位置x坐标
0, //窗口位置y坐标
GetSystemMetrics(SM_CXSCREEN ), //窗口高度,这里是全屏
GetSystemMetrics( SM_CYSCREEN ),//窗口高度,这里是全屏
NULL, //父窗口句柄
NULL, //菜单句柄
hInstance, //应用程序句柄
NULL); //最后一个参数是附加数据,一般都是0
参数1:登记的窗口类名,这个类名刚才咱们在注册窗口时已经定义过了。
参数2:用来表明窗口的标题。可以和第一个参数一样。
参数3: 用来表明窗口的风格,如有无最大化,最小化按纽啊什么的.
参数4,5: 用来表明程序运行后窗口在屏幕中的坐标值。
参数6,7: 用来表明窗口初始化时(即程序初运行时)窗口的大小,即长度与宽度。
参数8: 在创建窗口时可以指定其父窗口,这里没有父窗口则参数值为0。
参数9: 用以指明窗口的菜单,菜单以后会讲,这里暂时为0。
最后一个参数是附加数据,一般都是0。
如果窗口创建成功,CreateWindow( )返回新窗口的句柄,否则返回NULL。
不要以为创建和注册完了以后就大功告成,这样的话你是在屏幕上什么也看不见,我们必须要调用另外一个API函数才能看见窗体就是ShowWindow,他的原型是:
ShowWindow (hwnd, iCmdShow)
其第一个参数是窗口句柄,告诉ShowWindow()显示哪一个窗口,而第二个参数则告诉它如何显示这个窗口.
WinMain()调用完ShowWindow后,还需要调用函数UpdateWindow,最终把窗口显示了出来。调用函数UpdateWindow将产生一个WM_PAINT消息,这个消息将使窗口重画,即使窗口得到更新.
2.2.3回调函数
看了上面这么多内容以后,你一定很疑惑了(我们知道了windows程序是怎么样运行的了,也知道了窗体是怎么样建立的,可是我们怎么通过消息让窗体触发事件的呢),这个问题很关键,对,我们就是通过回调函数来实现用消息触发事件的.下面我们来看看什么是回调函数.
windows就是一个大的消息循环程序,当遇到外部有其他命令时,就会暂停这个大循环,而进行外部给的命令的事件,而外部命令的这个函数就是回调函数(winproc()),他是通过句柄(hwnd)来确定这个命令触发的窗体。同样我们先写回调函数的原型。在这个函数中,不同的消息将被switch语句分配到不同的处理程序中去。Windows的消息处理函数的原型是这样定义的:
LRESULT CALLBACK WindowProc(
HWND hwnd, //接收消息窗口的句柄
UINT uMsg, //主消息值
WPARAM wParam, //副消息值1
LPARAM lParam //副消息值2
);
消息处理函数必须按照上面的这个样式来定义,当然函数名称可以随便取。
举个例说明一下,我们要实现分别点击鼠标左右键在窗体里面分别弹出"left button!"和"right button!"的功能.
//************************************************************
//函数:WinProc( )
//功能:处理主窗口消息
//************************************************************
LRESULT CALLBACK q( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
switch( message )
{
case WM_KEYDOWN://击键消息
switch( wParam )
{
case VK_ESCAPE:
MessageBox(hWnd,"quit!","Keyboard",MB_OK);
PostQuitMessage( 0 );//退出
break;
}
return 0;
case WM_LBUTTONDOWN:
MessageBox(hWnd,"left button!","Mouse",MB_OK);
return 0;
case WM_RBUTTONDOWN:
MessageBox(hwnd,"right button!","mouse",MB_OK);
return 0;
case WM_MBUTTONDBLCLK:
MessageBox(hwnd,"the other!","mouse",MB_OK);
return 0;
case WM_DESTROY:
PostQuitMessage( 0 );
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam); //调用缺省消息处理过程
}
2.3 编后语
win32编程是一个博大精深的知识,我这里只是肤浅的介绍了一下我对于基础的消息机制的运行原理的理解,再次建议想要进入程序员行业的朋友一定要看看我开始推荐的两本书,(绝对不是书托呀^_^).他里面除了包含最基础的消息机制以外,还有包含所以基础类和派生类的原理,以及com的构架,以及Interface的运作机制,还有更为重要的就是当前流行的.net框架的基础Framework的运行机制和构架设计.这才是这两年编程的命脉,象医生一样,只有把握了命脉,才能把握编程环境的大方向,我们初学者才可以有地放失的学习对自己有用的技术,学习起来效果将会事半功倍.从下一章开始,我们就完全进入游戏编程的介绍了.
最后用候捷老师的话作为这章的结束语,顺便作为给大家一个忠告∶"勿在浮沙筑高台!",
2.1开天辟地
有了框架,我们就可以在里面动手画游戏了, DirectDraw是DirectX的重要组成部分,它就像一支画笔,主要负责各种把各种图像显示在屏幕上,对Windows环境中的游戏非常重要。
首先我们来定义这只笔,说得专业一点就是初始化DirectDraw.
我们先写一个初始化函数(在我的demo里面你不能找到下面这个函数,那是因为我把初始化写在了地图初始化里面,具体就是void map::init(),其实你仔细看他们是完成同样的功能).
LPDIRECTDRAW7 lpDD;// DirectDraw对象的指针
LPDIRECTDRAWSURFACE7 lpDDSPrimary; // DirectDraw主页面的指针
LPDIRECTDRAWSURFACE7 lpDDSBuffer; // DirectDraw后台缓存的指针
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;
}
呵呵,看不懂了吧,看不懂就对了,我还没有讲你怎么能看得懂呢,那下面我一个一个函数的讲讲吧.
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。
DDERR_DIRECTDRAWALREADYCREATED
DDERR_OUTOFMEMORY
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的函数,只是无法改变显示模式。
DWORD dwHeight,
DWORD dwBPP,
DWORD dwRefreshRate,
DWORD dwFlags
);
dwBPP用来设置显示模式的颜色位数。
dwRefreshRate设置屏幕的刷新率,0为使用默认值。
dwFlags现在唯一有效的值是DDSDM_STANDARDVGAMODE。
DWORD dwFlags
);
dwBPP用来设置显示模式的颜色位数。
dwRefreshRate设置屏幕的刷新率,0为使用默认值。
dwFlags现在唯一有效的值是DDSDM_STANDARDVGAMODE。
DWORD dwFlags
);
dwBPP用来设置显示模式的颜色位数。
dwRefreshRate设置屏幕的刷新率,0为使用默认值。
dwFlags现在唯一有效的值是DDSDM_STANDARDVGAMODE。
DWORD dwFlags
);
dwBPP用来设置显示模式的颜色位数。
dwRefreshRate设置屏幕的刷新率,0为使用默认值。
dwFlags现在唯一有效的值是DDSDM_STANDARDVGAMODE。
下一步是创建一个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; ok,初始化完成以后,一个Directdraw对象我们就建立起来了,然后我们可以更具上面的各个参数的不同定义修改其中的内容,达到不同的我们想要的显示效果。如果你觉得上面讲的太繁琐,建议你看着我的源代码里面的地图初始化void map::init()部分,比较着来看上面的内容你就明白是怎么回事了.
2.2 我来了
按照不同的显示模式和要求我们设置好了画笔以后,就该轮到主脚登场了,于是下一步我们就要让我们这个游戏的主脚闷炮和他的小伙伴们(NPC)上场了.事实上我们要完成的就是贴图,由于完成了Directdraw的初始化,贴图就相当简单了.在讲贴图以前我们先一定要介绍一个在游戏编程里面占有很重要地位的两个函数:图像传送函数IDirectDrawSurface7::Bltfast( )
先说图像传送函数.
IDirectDrawSurface7::Bltfast( )
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来剪裁,速度比这种方法慢)。
我们还必须在初始化的时候加载所有要显示的人物图片的指针和装载进图片.也就是说在初始化里面我们必须加入以下部分(而具体到底要加载什么人物图片就是通过上面介绍的那个函数就可以完成了).
要将位图(*.bmp)调入页面是非常简单的,对于普通的真彩(24位)图像我们只需用DDReLoadBitmap(页面,"图像名.bmp")即可调入图像。这个函数在ddutil.cpp中,你需要把它和ddutil.h拷贝到你的程序的目录下,将其加入你的Project并在主程序的开头#include "ddutil.h"(在Chapter IV.zip中你可以找到我修改过以适应DirectDraw7的ddutil.h和ddutil.cpp)。注意,DDReLoadBitmap( )函数会自动缩放图像以适应页面的大小。调入图片后,还要设置图片的透明色,不然图片不会正常显示.
DDBLTFX ddBltFx;//创建页面对象
ddBltFx.dwSize=sizeof(DDBLTFX);//申请空间
ddBltFx.dwFillColor=0;
lpDDSPrimary->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);//创建主页面
lpDDSBuffer->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
lpDDSMap->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
lpDDSOut->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
lpDDSOther->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
lpDDSRoom->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
lpDDSEnd->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
lpDDSSprite->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&ddBltFx);
DDReLoadBitmap(lpDDSMap,"inn.BMP");//装载背景图片
DDReLoadBitmap(lpDDSOut,"city.BMP");
DDReLoadBitmap(lpDDSRoom,"Room.bmp");
DDReLoadBitmap(lpDDSEnd,"end.bmp");
DDReLoadBitmap(lpDDSSprite,"Sprite.BMP");//装载人物图片
DDSetColorKey(lpDDSMap,RGB(0,255,0));//设置地图透明色
DDSetColorKey(lpDDSOut,RGB(0,255,0));
DDSetColorKey(lpDDSRoom,RGB(0,255,0));
DDSetColorKey(lpDDSOther,RGB(0,255,0));
DDSetColorKey(lpDDSEnd,RGB(0,255,0));
DDSetColorKey(lpDDSSprite,RGB(8,33,82));
MakeRect(0,0,640,480);
lpDDSOther->BltFast(0,0,lpDDSMap,&r,NoKey);
MakeRect(32,64);
lpDDSOther->BltFast(275,175,lpDDSSprite,&r,SrcKey);
MakeRect(672,256);
lpDDSOther->BltFast(223,313,lpDDSSprite,&r,SrcKey);
MakeRect(608,96); lpDDSOther->BltFast(445,320,lpDDSSprite,&r,SrcKey);
2.3 让我动吧
经过我们的一番努力,我们的男主脚终于在我们的世界诞生了,可是他还是不能行动啊,那多么不方便啊,不能移动怎么去冒险啊,所以接下来我们就说说怎么样让他动起来.
先还是说说人物移动的原理吧: 后台缓存和换页对形成无闪烁的动画至关重要。举一个例子,要显示一个物体在一张图片上运动,我们需要在a时刻先画物体,在b时刻把一开始被物体遮住的背景画好,最后在c时刻把物体画在新位置上。但这些操作需要一定时间,如果我们直接改主页面,那么b时刻用户就会看到画面上没有物体,但a和c时刻画面上又有物体,用户就会觉得画面有些闪烁。如何解决这个问题呢? DirectDraw 中的换页就可以帮我们这个忙。首先,我们得设置好一个换页链结构,它由一组 DirectDraw 页面组成,每一个页面都可以被轮流换页至显示屏幕。当前正好位于显示屏幕的页面叫主页面。等待换页至屏幕的页面叫后台缓存。应用程序在后台缓存上进行绘图操作,然后将此页面换页成为主页面,原来的后台缓存就显示在屏幕上了,而原来的主页面就成为了后台缓存,在一般情况下我们只需改变后台缓存的内容。所以,我们在完成了b、c两个步骤后再换页即可避免闪烁现象。
换页所使用的的函数是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);
这部分内容可以在我的Publicfan.cpp里面找到,里面我自定义了一个页面置换函数void Flip().
再说了换页函数后并不是人物就可以动了,只是达成了人物移动的一个最基本条件,然后我们还要从逻辑上真正去实现人物的移动.逻辑上人物的移动其实很好理解,当人物向左走的时候,贴图的y坐标不变,而x坐标递减;向右移东的时候,则反之.于是我写了一个man.cpp,里面自定义一个人物移动的函数
Void man::move(int where)
switch(where)
{
case 0: //人物向左移动
if(man1.tx<0)
man1.tx=0;
if(man1.tx>640)
man1.tx=640;
if(man1.ty<0)
man1.ty=0;
if(man1.ty>480)
man1.ty=480;
MakeRect(544,352);
man1.tx-=10;
break;
case 1: //人物向下移动
if(man1.tx<0)
man1.tx=0;
if(man1.tx>640)
man1.tx=640;
if(man1.ty<0)
man1.ty=0;
if(man1.ty>480)
man1.ty=480;
MakeRect(544,320);
man1.ty+=10;
break;
case 2: //人物向右移动
if(man1.tx<0)
man1.tx=0;
if(man1.tx>640)
man1.tx=640;
if(man1.ty<0)
man1.ty=0;
if(man1.ty>480)
man1.ty=480;
MakeRect(544,288);
man1.tx+=10;
break;
case 3: //人物向上移动
if(man1.tx<0)
man1.tx=0;
if(man1.tx>640)
man1.tx=640;
if(man1.ty<0)
man1.ty=0;
if(man1.ty>480)
man1.ty=480;
MakeRect(544,256);
man1.ty-=10;
break;
case 4: //人物朝向不同选择贴不同的face
if(man1.tx<0)
man1.tx=0;
if(man1.tx>640)
man1.tx=640;
if(man1.ty<0)
man1.ty=0;
if(man1.ty>480)
man1.ty=480;
if(
{
MakeRect(544,256);
}
if(
{
MakeRect(544,320);
}
if(
{
MakeRect(544,352);
}
if(
{
MakeRect(544,288);
}
break;
}
}
四个case语句代表了选择四个不同方向的移动,case里面就有坐标递加或者递减的操作,来改变人物的移动坐标,最后一个case解决了人物朝向不同方向移动的时候,贴出不同的face.最后把前面我们说到的消息循环结合起来就可以了.
case VK_UP: //按下键盘上键,触发人物向上移动
enable=1;
break;
case VK_DOWN: //按下键盘上键,触发人物向下移动
enable=2;
break;
case VK_LEFT: //按左键盘上键,触发人物向左移动
enable=3;
break;
case VK_RIGHT: //按右键盘上键,触发人物向右移动
enable=4;
break;
按下键盘上不同的方向键的时候人物就向响应的方向移动.到此,我们的主脚就可以在屏幕内按照我们的要求随便移动了.
2.4 走四方
人物虽然能移动了,可是只能在这个屋子里面也好像不是太好,从小我就是一个好动的孩子,所以我们游戏里面的主脚自然也不能是一个安静的人罗,我们要走出这片天地,到外面的世界去看看.
所以,我们要走四方,简单的说就是要能地图切换.我们就来解决地图切换的问题.在开始装载图片的小节里面,我们知道了如何装载图片,而且在那段程序里面我们加载了所有我们要用的图片资源,这里也包含不同的地图资源,所以,这里我们不用再加载一次了,我们可以直接进行地图切换了.
我们把地图切换写在刷新页面的函数里面.原理是,我们要在原图中设置一些坐标作为地图的切入点,一旦人物移动到我们提前设置好的位置,就把页面置换过去,换上新地图的指针.程序如下:
void refresh()
{
MakeRect(0,0,640,480);
lpDDSBuffer->BltFast(0,0,lpDDSOther,&r,NoKey);//始终显示buffer页面
man1.show();
if((man1.tx <= 330) && (man1.tx >= 310) && (man1.ty <= 460) && (man1.ty >= 430)) //到达这个坐标时进入城市
{
MakeRect(0,0,640,480);
lpDDSOther->BltFast(0,0,lpDDSOut,&r,NoKey);
lpDDSBuffer->Flip( NULL , DDFLIP_WAIT );
MakeRect(736,192);
lpDDSOther->BltFast(300,350,lpDDSSprite,&r,SrcKey);
MakeRect(736,480);
lpDDSOther->BltFast(370 ,150,lpDDSSprite,&r,SrcKey);
man1.tx=170;
man1.ty=50;
}
else if((man1.tx<=470) && (man1.tx>=450) && (man1.ty<=310) && (man1.ty>=290) ) //到达这个坐标时进入最后一个房间
{
MakeRect(0,0,640,480);
lpDDSOther->BltFast(0,0,lpDDSRoom,&r,NoKey);
lpDDSBuffer->Flip( NULL , DDFLIP_WAIT );
MakeRect(352,96);
lpDDSOther->BltFast(405 ,252,lpDDSSprite,&r,SrcKey);
man1.tx=100;
man1.ty=310;
}
else if((man1.tx<=70) && (man1.tx>=50) && (man1.ty<=450) && (man1.ty>=430) ) //到达这个坐标时进入游戏结束画面
{
MakeRect(0,0,639,479);
lpDDSOther->BltFast(0,0,lpDDSEnd,&r,NoKey);
lpDDSBuffer->Flip( NULL , DDFLIP_WAIT );
}
Flip();
}
完成上面的代码以后,我们的主脚就可以随心所欲的在这个我们为他创造的世界里面自由活动了.
2.5 我不想成为哑巴
除了走路以外,主脚总不能是个哑巴啊,至少我还没有看到过不能说话的游戏,哑巴怎么能完成我们交给他的人物嘛,而且,我们总要通过游戏向玩家传递一些信息嘛,所以说话或者说文字的输出那是一个必然的而且基础的功能.
下面我们来完成对话,还是先说原理吧:说白了,对话其实就是在Direct下输出文字,主脚和不同的NPC交互的输出文字,就形成了对话.
输出文字其实很简单,为了向页面输出文字,我们首先要获得页面的HDC(设备描述句柄),然后调用Windows GDI 函数向页面输出文字。由于获得句柄后将不能使用DirectDraw函数改动页面,所以输出完文字后要立刻释放句柄。
HDC hdc;
if (lpDDSXXX->GetDC(&hdc) == DD_OK) //拿句柄
{
SetBkColor(hdc, RGB(0, 0, 255)); //设置文字背景色
SetTextColor(hdc, RGB(255, 255, 0)); //设置文字颜色
TextOut(hdc,100,400, text, strlen(text)); //句柄, 左上角X, 左上角Y,
//文字(char *), 文字长度
lpDDSXXX->ReleaseDC(hdc); //释放句柄
}
只要完成上面的功能就可以在页面输出文字了,在我的这个demo里面我把他封装在void man::say(char words[255])里面,于是只要调用这个函数就可以在页面输出文字.那么怎么和其他的NPC对话呢,那其实已经很简单了.我又写了个说话的函数,在里面进行的坐标的判断,通过不同的坐标达到和不同的NPC进行对话,不过这个办法并不科学,一般现在使用的方法是碰撞检测,由于碰撞检测又是一个很庞大的技术,所以这里不再累赘了,有兴趣的朋友可以找一些专业的游戏编程的书来看看.
下面我们罗列出部分,完整的程序你可以自己看我的demo.
void man::speak()
{
if ((man1.tx<=255)&&(man1.tx>=223)&&(man1.ty<=345)&&(man1.ty>=313))//当主脚在这个坐标触发消息的时候和这个NPC对话
{
switch(man1.i)
{
case 0: //通过case的递加来实现交互对话
MakeRect(672,320);
lpDDSOther->BltFast(50,400,lpDDSSprite,&r,SrcKey);
man1.say("小鸟:闷炮,你今天怎么米有去上课啊,外语老师又点名了呀.");
break;
case 1:
MakeRect(544,320);
lpDDSOther->BltFast(50,400,lpDDSSprite,&r,SrcKey);
man1.say("闷炮:去与不去都一样,反正他都是照着书念的,你去还不是没有听课啊");
break;
case 2:
MakeRect(672,320);
lpDDSOther->BltFast(50,400,lpDDSSprite,&r,SrcKey);
man1.say("小鸟:听说我们这个外语老师是个杀手哟,以前有很多兄弟都死在他手下的啊.");
break;
case 3:
MakeRect(544,320);
lpDDSOther->BltFast(50,400,lpDDSSprite,&r,SrcKey);
man1.say("闷炮:米有关系,考试要放的");
break;
case 4:
MakeRect(672,320);
lpDDSOther->BltFast(50,400,lpDDSSprite,&r,SrcKey);
man1.say("小鸟:不会哟,上学期就把老子杀了,靠!◎#¥%……×");
break;
case 5:
MakeRect(544,320);
lpDDSOther->BltFast(50,400,lpDDSSprite,&r,SrcKey);
man1.say("闷炮:那就是长相问题啦∶)");
break;
case 6: //每个NPC对话完以后要清空对话框,另外吧对话变量赋值为1,为了和其他的NPC对话的时候能从头开始
CleanDialog();
man1.i=0;
break;
}
2.6 来点音乐
一般游戏为了解决枯燥的问题,通常还要加点音乐,好,我们也来加点音乐,其实并不是那么难,想其他所有的编程一样,无非是调用微软的API函数就可以完成了,书归正传
我们可以使用MCI来简易地实现在程序中播放MIDI和WAV等声音。使用它需要预先声明,我们需要在文件头#include <mmsystem.h>,并在工程中加入"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。
还是结合我们的demo来讲比较清楚一点,在我们的demo里面,我采用了仙剑奇侠传里面的一个比较经典的那个音乐,你听了一定会知道是哪一个(希望大宇公司不要告我侵权呀,借来用用而已呀^_^).我在地图初始化的时候就加载这段音乐,所以我写了一个void map::sound()函数.
void map::sound()
{
MCI_OPEN_PARMS OpenParms;
OpenParms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_SEQUENCER;
OpenParms.lpstrElementName = (LPCSTR) "0311.mid";//要播放的音乐文件名
OpenParms.wDeviceID = 0;
mciSendCommand (NULL, MCI_OPEN, MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT,(DWORD)(LPVOID) &OpenParms);
MCI_PLAY_PARMS PlayParms;
PlayParms.dwFrom = 0;
mciSendCommand (OpenParms.wDeviceID, MCI_PLAY,MCI_FROM, (DWORD)(LPVOID)&PlayParms);
}
其他的函数和语句前面介绍过了,不再累赘了.完成这些以后你就能在我的demo里面听到《仙剑奇侠传》的背景音乐了.
2.7 冒险去了
结合以上讲述的方法,再写一个好点的剧本,那么就可以冒险去了,于是我就有了现在的《理工冒险记》,虽然还有很多问题.但是勉强也算是一个单线情节的RPG,至于个中滋味,就要各位玩家自己亲身去体会了.这一节纯粹是游戏的宣传,没有一点技术含量,希望大家不要拿砖头砸我呀.
2.8 编后语
此文章转载于 http://blog.csdn.net/ronaldo17/archive/2005/10/10/499122.aspx