转载:夜深人静做游戏

Spine换装系统深度优化:从局部换肤到动态合批的进阶之路 本文深入探讨了Spine换装系统的性能优化策略,从局部换肤到动态合批的进阶技术。通过分析传统方案的性能瓶颈,提出动态合批、GPU Instancing等高级优化方法,显著降低Draw Call和内存占用,提升游戏性能。适用于MMO和大型游戏开发,帮助技术美术和工程师突破大规模换装场景的性能限制。 阅读详情
 

声     明

再写下后面的东西以前,我觉得有必要专门花一页的篇幅进行下面的申明.在我的眼里它的必要性不亚于在吃中餐前,先阅读中国筷子的使用说明书.在以后的文字中,我都准备尽可能以一种轻松,略带调侃的语气来完成,但是下面的文字我却会写得很正式,因为谁也不想笑着看完筷子使用说明书后,在吃饭时被筷子卡住喉咙,那样的话就一点都不好笑了.

    其一,如果你希望看到一篇教条的文字,那么它并不适合你.因为这篇文字以及后来你们将要看到的程序从一开始就并不是你们想象中的那种循规蹈矩的风格.

    其二,如果你仅仅是一个游戏爱好者,那么这篇文章的主人一定会成为你的好朋友;如果你是一个游戏开发者,那么这篇文章的主人很愿意和你结交成一个良师益友,并且十分愿意和你共同探讨游戏编程的众多的技术问题,一起学习,一起进步;如果你是一个希望进入游戏开发的朋友,首先,我要提醒你,你要考虑清楚了,不要被现在游戏业红火的表面现象所迷惑,如果在慎重考虑以后,你仍然义无反顾(你死定了!),那么这篇文字非常适合你进入游戏开发领域,它将会是从基础的游戏的构架一直讲到游戏的开发(再次强调,进入这行后,后悔了可不要怪我哦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.1 编译器
      我们以后用的开发工具全是基于 visual studio.net2003, 所以我先来说说这个编译器的一些基本使用方法(不要BS我^_^).首先启动vc.net(然后等,无休止的等,微软的东西慢的很^_^), 会看见一个"起始页",点击"新建项目"一栏,并在出现的窗口 选择"Visual C++ 项目"一栏中的"Win32 控制台项目",填好"名称"和"位置",按"确定",这时会出现一个"Win32 应用程序向导"。此时需要在"附加选项"一栏把"空项目"前的方框勾上,以防VC.net在工程中加入一些无意义的垃圾. 最后按"Finish"就完成了工程的创建。
    然后大家就建立完一个工程了,这个时候编译环境实际上已经达建好了,如果你是一个老鸟,你还可以修改一些你自己习惯的设置,让它用起来更顺手. 大家会注意到现在工程还没有文件,所以接下去我们需要学习如何新建一个文件,如果你想新建的文件是C++程序文件(.cpp),那么应该在"源文件"上按右键,选择"添加" --- "添加新项",在出现的窗口中选择"C++ 文件",定好名字,再按"开始"即可(假如你加入的文件叫"star.cpp",将象下图所示);如果你想新建的文件是头文件(现在先不要管头文件是什么),在"头文件"按右键,也选择"添加" --- "添加新项",在出现的窗口中选择"头文件",也定好名字并按"开始"就行了。
    最后我们来看看一个重要的操作,即如何把LIB文件(现在也不要管LIB文件是什么…)加入工程中:首先在"解决方案"窗口中找到工程名,然后在上面按右键并选择"属性",在出现的窗口中选择"连接器" --- "输入" --- "添加依附项",最后填上要加入的LIB文件名即可。在这里,由于我们主要是研究游戏的编程,所以为了告诉编译器我们需要使用DirectDraw,我们要在程序文件中加入#include <ddraw.h>,并把"ddraw.lib"和"dxguid.lib"加入工程,记住,做完了这些工作后DirectDraw程序才能被正常编译。

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程序主要使用顺序的,过程驱动的程序设计方法。顺序的,过程驱动的程序有一个明显的开始,明显的过程及一个明显的结束,因此程序能直接控制程序事件或过程的顺序。虽然在顺序的过程驱动的程序中也有很多处理异常的方法,但这样的异常处理也仍然是顺序的,过程驱动的结构。

而Windows的驱动方式是事件驱动,就是不由事件的顺序来控制,而是由事件的发生来控制,所有的事件是无序的,所为一个程序员,在你编写程序时,你并不知道用户先按哪个按纽,也不知道程序先触发哪个消息。你的任务就是对正在开发的应用程序要发出或要接收的消息进行排序和管理。事件驱动程序设计是密切围绕消息的产生与处理而展开的,一条消息是关于发生的事件的消息。

1.2.2窗体的注册和初始化


    了解了消息机制以后,我们可以创建窗体了.创建窗体前我们看看windows程序的流程,大致流程图如下:

在这之后,我们可以创建Windows窗体了

//*********************************************************

//函数:WinMain( )

//功能:Windows程序入口函数。创建主窗口,处理消息循环

//*********************************************************

int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{

 MSG msg;                                     //消息结构

 

 InitWindow(hInstance,nCmdShow);              //初始化窗体

        while(1)                                    // 消息循环
    {

 if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))

 {

   if (msg.message==WM_QUIT)                  //退出消息循环

    break;

   TranslateMessage(&msg);                    //得到消息,处理回调函数

   DispatchMessage(&msg);

 }

 }

 return msg.wParam;

} WinMain( ) 函数的原型如下:
  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的常量,在程序中可组合使用这些常量.
(2)第二个参数:lpfnWndProc,给它消息处理函数的函数名称即可,必要时应该进行强制类型转换,将其转换成WNDPROC型。
(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: 用来表明窗口的风格,如有无最大化,最小化按纽啊什么的.
在DirectX编程中,我们一般使用的是WS_POPUP | WS_MAXIMIZE,用这个标志创建的窗口没有标题栏和系统菜单且窗口为最大化,可以充分满足DirectX编程的需要。
参数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); //调用缺省消息处理过程

}

看明白没有啊?case后面就是触发的消息,当不同的击键产生的时候会触发不同的消息,于是就通过这个回调函数结束前面的循环,产生消息事件,事件完成以后再回到消息循环里面.(哦,天啊,多么怀念当年的vb编程啊,IDE为我们封装了所有的事件和方法,只是调用就是了),同时我们更加应该感到庆幸,因为我们终于明白了windows的运行机制.

2.3 编后语

win32编程是一个博大精深的知识,我这里只是肤浅的介绍了一下我对于基础的消息机制的运行原理的理解,再次建议想要进入程序员行业的朋友一定要看看我开始推荐的两本书,(绝对不是书托呀^_^).他里面除了包含最基础的消息机制以外,还有包含所以基础类和派生类的原理,以及com的构架,以及Interface的运作机制,还有更为重要的就是当前流行的.net框架的基础Framework的运行机制和构架设计.这才是这两年编程的命脉,象医生一样,只有把握了命脉,才能把握编程环境的大方向,我们初学者才可以有地放失的学习对自己有用的技术,学习起来效果将会事半功倍.从下一章开始,我们就完全进入游戏编程的介绍了.

最后用候捷老师的话作为这章的结束语,顺便作为给大家一个忠告∶"勿在浮沙筑高台!",

 
第二章 将游戏进行到底
从这一章开始我们正式进入游戏编程的部分,正确的说法是开始一起研究和学习 Direct 编程,有了上面win32编程的基础上,我们实际上就已经建立了容纳我们这个游戏的空间,这个空间里面我们就可以实现D irect 编程,通过调用Direct里面微软给我们封装好的API函数,我就可以达到对硬件的操作,从而制作出自己的游戏世界.

2.1开天辟地

有了框架,我们就可以在里面动手画游戏了, DirectDraw是DirectX的重要组成部分,它就像一支画笔,主要负责各种把各种图像显示在屏幕上,对Windows环境中的游戏非常重要。

首先我们来定义这只笔,说得专业一点就是初始化DirectDraw.

我们先写一个初始化函数(在我的demo里面你不能找到下面这个函数,那是因为我把初始化写在了地图初始化里面,具体就是void map::init(),其实你仔细看他们是完成同样的功能).

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,必须创建一个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。
设置控制级和显示模式

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。

DWORD dwFlags

);

dwWidth and dwHeight 用来设置显示模式的宽度和高度。

dwBPP用来设置显示模式的颜色位数。

dwRefreshRate设置屏幕的刷新率,0为使用默认值。

dwFlags现在唯一有效的值是DDSDM_STANDARDVGAMODE。

DWORD dwFlags

);

dwWidth and dwHeight 用来设置显示模式的宽度和高度。

dwBPP用来设置显示模式的颜色位数。

dwRefreshRate设置屏幕的刷新率,0为使用默认值。

dwFlags现在唯一有效的值是DDSDM_STANDARDVGAMODE。

 

DWORD dwFlags

);

dwWidth and dwHeight 用来设置显示模式的宽度和高度。

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;

       }

    }
一旦完成了对话,人物移动和地图切换,实际上就完成了一个demo需要的最最基本的要求,虽然一个游戏还包含很多东西,不过能这样已经可以做成一个有剧情的单线RPG了,呵呵,是不是觉得特有成就感啊,是啊,当时我能做的这种地步的时候也是觉得特别高兴,有点仙剑之父的意思(大言不惭,BS!).

 

 

 

 

 

 

 

 

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 编后语

到这里,我想讲述的游戏编程的东东就基本上完了,但是必须要提醒大家的是,这只是沧海一粟,它本身是一个博大精深的东西,里面包含了无数前辈的智慧的结晶.随便找一本游戏编程的书来看看,你就知道,书中还提到:人工智能,游戏中的物理动作设计,动画制作, DirectSound,DirectInput,DirectPlay …… , 就是在图形显示部分都还有包含粒子应用,碰撞检测,地图卷轴技术……,举不胜举,这里特别有关于人工智能的一点要拿出来说一下:记得曾经一个老游戏程序员告诉我一句话,现在我转告给你们:"一个AI代表一个程序员的水平,程序员越聪明,就越能写出高明的计算机AI.",没有其他的意思,我比较喜欢这句话,所以就说给你们听.说了这么半天废话,我想要说的还是那句话,游戏编程是一个博大精深的领域,如果本文的阅读者有志于在这方面有所发展的话,强烈建议你买一本好书(怎么这个人没说几句话就又要买书啊,莫非真的是托?),花点时间好好看一看,动手写写code,那样的话,无论是你的编程水平,还是你做游戏的水平都会有一个质的提高.

 

此文章转载于 http://blog.youkuaiyun.com/ronaldo17/archive/2005/10/10/499122.aspx

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
智慧加油站小程序数据库设计文档 【代码】智慧加油站小程序数据库设计文档。 阅读详情

相关推荐

Zemax 中的 LED 阵列模型

这些光线是所有光线的子集,用于布局可视化,而不是详细分析。X 和 Y 散度(αx 和 αy)以度为单位,以不同的方式设置为 6 度和 12 度。每个 LED 都以朗伯模式(广角分布)发光,尽管有些 LED 具有内置光学元件,用于更窄或更聚焦的光束。,具体取决于这些类别中使用的半导体材料。LED 阵列由多个发光二极管 (LED) 组成,这些二极管以特定模式或配置排列,以实现均匀照明、更高强度或特定照明特性。如果数据的存在更改为“辐射强度”,则它以弧度为单位显示每个立体角的功率,作为探测器上入射角的函数。

jkh920184196的博客 1595

DirectX错误号及其原因

/*  dxerr.c - DirectX 8 & 9 Error Functions  Written by Filip Navara  This library is distributed in the hope that it will be useful,  but WITHOUT ANY WARRANTY; without even the implied warr

沧海一粟 8617

一文告诉你:如何在trae中编译运行STM32程序,并让AI帮你修改代码

本文详细介绍了如何将STM32程序从Keil迁移到国产免费工具Trae,并利用AI进行代码修改的全过程。

水滴爬呀爬 4886

关于 Direct Blt 0x887601AE (DDERR_SURFACEBUSY) 和函数指针

0x887601ae(0x887601ae, "DDERR_SURFACEBUSY", "Access to this surface is being refused because the surface is already locked by another thread.")

空嗔冥泪 1976

从零开始的 AI Infra 学习之路

AI Infra(AI 基础设施)是指为上层的 AI 算法应用提供支持的 AI 全栈底层技术,通过合理利用计算机体系结构,可以实现 AI 计算的加速和部署。AI Infra 主要包括以下内容:AI 训练框架 & 推理引擎;AI 编译 & 计算架构;AI 硬件 & 体系结构。参考资料:;。

AI Infra / LLM / 软件开发 3万+

数据分析—用户行为数据分析

一般价值用户(R高F高M低):用户处于购买活跃期,感兴趣的商品多,购买次数少 → 针对该部分客户,目的就是要让他们购买,所以可以和运营部门沟通,降低利润,给出有力折扣促进购买,进而转为重要价值用户。重要价值用户(R高F高M高)维护:用户处于购买活跃期,感兴趣的商品多,购买次数也多 → 针对兴趣产品,保持一定频率的触达,持续维护。

m0_74091312的博客 1419

DirectX错误号及其原因。

/* dxerr.c - DirectX 8 & 9 Error Functions Written by Filip Navara  This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of

peng6662001的专栏 2万+

CANoe.Diva实战:基于CDD的UDS诊断自动化测试全流程解析

本文详细解析了使用CANoe.Diva进行基于CDD的UDS诊断自动化测试的全流程,包括环境搭建、CDD文件配置、测试范围设置、用例生成与验证、CANoe集成测试及报告分析。通过实战经验分享,帮助工程师快速掌握诊断自动化测试技巧,提升测试效率与质量。

weixin_29258297的博客 218

《学姐教我写代码(四)》学姐说要教我做游戏

学姐为了助我脱坑游戏,竟然要求教我做游戏

英雄哪里出来 7884

损失函数大汇总(六十一)(Signed Distance Map Loss附公式推导与代码)

摘要 本文介绍了Signed Distance Map (SDM) Loss,一种将形状先验直接融入分割任务的方法。SDM将每个像素表示为到目标边界的带符号距离(内部为负,外部为正),相比二值mask能更连续地表达形状几何信息。SDM Loss的核心思想是让网络学习这个隐式形状场,通过可微的Heaviside函数近似将SDM转换为分割结果。文中重点分析了一种product-based regression loss,它能同时关注距离数值和内外符号的正确性,对边界区域尤为敏感。SDM Loss通常与传统的分割

悦学共鸣,温柔以待,汇聚光芒,共同成长 188

基于 STM32 的便携式肌电信号采集与疲劳度评估系统设计与实现

本系统以 STM32 单片机为核心控制器,通过肌电电极采集人体骨骼肌表面的 sEMG 信号,经仪表放大、滤波、AD 采样后送入 MCU 进行时域 / 频域特征提取,利用 RMS(均方根)、MPF(中位频率)、MF(平均频率)等指标实时评估肌肉疲劳状态,最终通过 OLED 显示疲劳等级,并支持蓝牙上传至上位机。

weixin_42300449的博客 105

转载:盘点分享编程项目的五大好处

这是在知乎上看到的一篇文章。我把其中的主要内容转载过来,和大家一起分享。 加薪升职跑不了,高端工作轻松找随着 IT 行业发展日新月异,大家会发现:所谓的高薪职位越来越难找了。从前些年的 iOS、安卓开发的浪潮,到最近几年 HTML5、Javascript 的流行令前端开发异常火爆,再到现在的人工智能和大数据在业界呼风唤雨,大公司在招聘时所要求的知识储备和技能掌握越来越严格。倘若您没有独特的闪光点,...

cherrychen0327的博客 694

运放实现阶梯波发生器-总结报告

实验目的: 学习、理解、掌握方波电路、限幅电路积分累加电路、反馈电路 课程条件: 电脑仿真软件Multisim 原理框图和实验思路: 方波电路通过微分电路可以形成正负脉冲,在通过限幅电路后变为单脉冲,最后在积分电路中形成阶梯波。微分电路存在的意义就是为了防止方波频率过大,从而导致在经过限幅电路的方波,在一个周期内将电容的电充满,这样就不会产生阶梯波。如果方波的频率比较小,可以近似认为是正负脉冲,这样就可以跳过微分电路。 ...............

0 1万+

当你不能再拥有,唯一能做的,就是令自己不要忘记

“开始总是分分钟都妙不可言,谁都以为热情它永不会减” 我喜欢听李宗盛,是因为只有林忆莲最懂他,最能把他给她的情书唱成经典来表达互相的情感,每次都能从他们的歌词中感受到那种浓浓的爱慕之情。他们的结合没有得到人的祝福,严重的讲违背道德,但是他们还是坚持的走到了一起,我喜欢他们,因为他们能为爱情放弃一切,奋不顾身,虽然仍是曲终人散,但我不相信是因为感情淡了,没有新鲜感了,或者认为当时是因为一时...

weixin_30732825的博客 534

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

如果没事儿 我应做些什么?

如果没事儿 或者 在我可自由支配的时光我应做些什么 如果是在上班时间可以整理下当前所做的工作可以参看下周边的同事的相关处理可以总结下之前所做的可以准备下之后要做的可以学习下本行业的相关知识 如新的流行技术等可以考虑考虑工作的相关运做 是否可再改进 再协调 再提高 如果是在家里可以看看家人有什么其他的需要可以一起散散步 逛逛街 做顿想吃的可以聊聊天 说说工作上的事 说说身边亲朋的事可以做做家务可...

weixin_30846599的博客 93

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

做人(转载)

1:能不抽烟最好不抽,它或许可以帮助你吸引一些女生,但不抽绝不会招来厌烦,表现男子气概的途径有很多,没必要拿健康做赌注。 2:给自己定目标,一年,两年,五年,也许你出生不如别人好,通过努力,往往可以改变70%的命运。破罐子破摔只能和懦弱做朋友。 3:找女朋友外表是第一关,但要了解她的品行之后再做打算也不迟。 4:不要在乎小钱,工作的人都后悔从前对自己的GF不够好。记住你们的重要日子,你们的谈话,女

slm007的专栏 724

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

做一个出格的人

  昨天晚上在已经写好日记的时候,忽然想到应该在某一行插入几个字,但这样的话会让日记看起来不整齐。我之前记日记都是记得很整齐的,有时候写错了字都要用胶带粘掉。正在纠结要不要插入那几个字时,忽然疑问来了,是谁规定写日记一定要写的整整齐齐的,难道乱点就不是日记了吗?然后果断加上了那几个字,又在上面乱画了几笔。   晚上一直被这个问题困扰。从小到大一直被教育不要这样不要那样的我们已经习惯了这种循规蹈矩...

weixin_33957648的博客 103

语录_内部论坛转载

心里有座坟,葬着未亡人[@more@]>老子不打你,你不知道我文武双全。 >我那么喜欢你,你喜欢我一下会死啊。 >心里有座坟,葬着未亡人 >我允许你走进我的世界,但不许你在我的世界里走来走去.. >...

congloulong3135的博客 100

linux图形栈pipewire是什么,linux 图形栈现状 (转载)

发信人: Dieken (风催草低 - 明月何尝不照人), 信区: ITExpress标题: Re: linux 图形栈现状发信站: 水木社区 (Wed Apr 24 23:21:32 2019), 站内[ 用户 Dieken 在转载时对文章内容进行了编辑 ]// 吐槽帖。这东西要知足,真的是 Xwindow 阻止我们用 Linux 当桌面吗?看片不流畅吗?上网、办公不流畅吗?打游戏不流畅吗?...

weixin_34405084的博客 383

听书,怅然若失

今天,一本书。 听完,两周。 精品不是,金手指。 我的歌后女友,叶落、楚墨儿、宋嫣。 名或有误,听的心里。 音乐娱乐,顺带歌曲,喜欢。 剧寻常,听立体。 听,浮想联翩。 三百集,可乐糖。 听,无心视频、游戏。 习惯睡觉,放着。 听,夜深人静。 听完,挂念宋嫣。 不圆满,才使人深刻。 听,更深。 怅然若失。 转载于:https://www.cnblogs.com/lov...

weixin_30376083的博客 95

生活中最重要的是什么?

经常会被问及这个问题 生活中最重要的是什么? 很多人会毫不犹豫的说是快乐! 是的 不可否认,快乐是人们生活中扮演着越来越重要的角色,君不见现在的游戏有多么的赚钱,君不见现在的娱乐节目收视率多高,君不见现在的歌星有多么的受人追捧,我们且不说这是不是因为人们日益枯燥的生活和残酷的现实所导致,但就这势不可挡的趋势,我们应该明白,在中国,这是不可阻挡的,那么我们应该这样理解并适应这种现象呢?...

weixin_30861459的博客 159

《全栈技术资料》你值得拥有 | 寻找C站宝藏

全栈技术资料,你值得拥有

英雄哪里出来 1万+

年轻白领生活正被“公司化”(转载)

   下班的时间越来越晚,回家的欲望越来越少。公司里的人影越来越多,心里的压力越来越大。在广州,在深圳,在上海,在北京,在每一个经济高速发展的城市,一群忙碌于各个写字楼之间的都市职业人,开始越来越多地把公司当作自己的家。     这是一个年轻的群落,他们的年龄大多在25~45岁之间,从事着诸如IT研发、广告创意、媒体传播、职业培训、企业管理等等工作。在普通的打工者眼中,他们是时尚的都市白领,拿...

dcj3sjt126com的专栏 236

写给三十岁的自己

写程序,用尽满腹经纶; 聊人生,烦恼无处开口。 有些人有些事,当我们懂得的时候已不再年轻。                                  ----写给三十岁的自己 三十岁的自己已不再年轻,少了年轻时的鲁莽和冲动,多了些成熟和稳重。年轻的时候,给我一把刀,我能砍出一片天,现在给我一把枪,我立马去上交。 三十岁的自己喜欢上了加班,喜欢在安安静静的晚上自己一个人...

dbc68367的博客 239

那些年 一场游戏 一场梦

多年以前,当我们还是孩童的时候,曾是那么的渴望着长大。长大以后,懵懵懂懂的,我们又希望能够早些成熟。而当我们都成熟之后才明白,原来,成熟真的不是一件幸福的事情,我们为此付出了太大的代价。生活的种种无奈要面对,人生的诸多曲折要经历,所以我们不得不抛却内心的纯真,挂上虚伪的笑容,冷漠着自己的视线,用伪装将自己层层包裹,时间久了,最真实的自我也就忘了。   纤尘风染,俗世纷争。多...

weixin_34319640的博客 119
上一篇: 转载:WIN32 SDK编程拾遗-系统托盘图标
下一篇: 转载:开发外挂的一些原理
bbdg
博客等级 码龄21年 6粉丝 0原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值