先说说如果用Delphi进行游戏编程要些什么,要注意什么。 1、到网上查找下载 DirectX 7.0 for Delphi 声明档或更高版本(本人源码用的是7.0)。查找时最好用DirectDraw.pas,否则DelphiX控件信息会占满你前100页的搜索结果。 2、如果你是用D7或更高版本,DirectX 7.0 for Delphi 声明档的 DirectDraw.pas 第145行要改为: {$IFDEF VER150} 否则你无法编译。8.0版要更改更多地方。 3、如果是全屏模式,千万不要用单步执行的方式运行。否则会死的很难看。 4、推荐一本书《Delphi Graphics and Game Programming Exposed with DirectX 7.0》,网上能找到英文版下载。 5、中文翻译表达不准备。如表面、界面等。源码很多是参看推荐书中,但也有很多改进。 先来二段最简单的代码。 实现功能一:将一张图片用全屏模式显示出来(更多注解请看后面笔记) uese DirectDraw; var FDirectDraw: IDirectDraw7; {代表显示器} FPrimarySurface: IDirectDrawSurface7; {代表主页面} FBitmap:IDirectDrawSurface7; {用于存放BMP} procedure TForm1.FormCreate(Sender: TObject); var TempDirectDraw: IDirectDraw; DDSurface: TDDSurfaceDesc2; begin DirectDrawCreate(Nil,TempDirectDraw,Nil); try TempDirectDraw.QueryInterface(IID_IDirectDraw4, FDirectDraw); finally TempDirectDraw := nil; end; FDirectDraw.SetCooperativeLevel(handle,DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN); FDirectDraw.SetDisplayMode(800,600,32,0,0); FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_CAPS; DDSurface.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE; if FDirectDraw.CreateSurface(DDSurface,FPrimarySurface,Nil)<>DD_OK then Close; FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH; DDSurface.dwWidth := 800; DDSurface.dwHeight := 600; DDSurface.ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN; FDirectDraw.CreateSurface(DDSurface, FBitmap, nil); end; procedure TForm1.FormShow(Sender: TObject); var BMP:TBitMap; SrcRect:TRect; begin BMP:=TBitMap.Create; BMP.loadfromfile('./desk.bmp'); if FBitmap.GetDC(l_DC)<>DD_OK then close; BitBlt(l_DC, 0,0 , 800, 600, BMP.Canvas.Handle, 0, 0, SrcCopy); SrcRect := Rect(0, 0, 800, 600); FBitmap.ReleaseDC(l_DC); FPrimarySurface.BltFast(0, 0, FBitmap, @SrcRect, DDBLTFAST_NOCOLORKEY OR DDBLTFAST_WAIT); BMP.Free; end; 实现功能二:继上面显示图形后,显示几个文字; procedure TForm1.FormShow(Sender: TObject); var BMP:TBitMap; SrcRect:TRect; TempCanvas: TCanvas; SrfcDC: HDC; begin BMP:=TBitMap.Create; BMP.loadfromfile('./desk.bmp'); if FBitmap.GetDC(l_DC)<>DD_OK then close; BitBlt(l_DC, 0,0 , 800, 600, BMP.Canvas.Handle, 0, 0, SrcCopy); SrcRect := Rect(0, 0, 800, 600); FBitmap.ReleaseDC(l_DC); FPrimarySurface.BltFast(0, 0, FBitmap, @SrcRect, DDBLTFAST_NOCOLORKEY OR DDBLTFAST_WAIT); BMP.Free; TempCanvas := TCanvas.Create; FPrimarySurface.GetDC(SrfcDC); with TempCanvas do begin Handle := SrfcDC; Brush.Color := clBlack; FillRect(Rect(0, 0, 640, 480)); Font.Color := clLime; TextOut(100, 100, '显示文字') end; TempCanvas.Handle := 0; FPrimarySurface.ReleaseDC(SrfcDC); TempCanvas.Free; end; 游戏的的最基本要素:图形文字,用两段简单的化码就实现了。接下来是游戏最得要的要素了:动起来。 不过,千成不要急。这里我们还不能急于求成。因为这里还有很多东西要补充,结出说明。如果你运行了前两段代码,就会发现,这是全屏方式的,并且,如果弹出桌面后再返回程序,图形就完全变了。即使把画图写字的代码放到OnActive事件中也一样。如果显卡不支持800 X 600 X 32色模式,这程序就弹出出去了。所以先解释(一)中的代码。 var FDirectDraw: IDirectDraw7; {代表显示器} FPrimarySurface: IDirectDrawSurface7; {代表主页面} 有时也会说IDirectDrawSurface7代表的是显存,不一定正确,每个人理解不一样。 FBitmap:IDirectDrawSurface7; {用于存放BMP}离屏表面。这个变量不是多余的,虽然也是IDirectDrawSurface7,也可以看成是显存,但它的创建和上面那个变量不一样。可以看成是缓冲。 OnCreate事件中代码: DirectDrawCreate(nil, TempDirectDraw, nil); 创建一个临时的 DirectDraw 对象。第一、三个参用nil,将游戏界面创建到主显示器上。如果有多个显示器,并且要创建到非主显示器上,你要先查找到所有显示器,然后将相应GUID的指针值,代替第一个参数。这里不讨论。(具体可参看DirectX控件源码是如何实现的) TempDirectDraw.QueryInterface(IID_IDirectDraw7, FDirectDraw); 我们只能通过查询 DirectDraw 对象界面的方法,来取得一个 DirectDraw7 界面 TempDirectDraw := nil; 现在我们有了 DirectDraw7 界面,临时 DirectDraw 对象不再需要它了 DX对象会在程序关闭时自动释放,也可以不赋nil值。前面例子中IDirectDraw7、IDirectDrawSurface7 在程序关闭时都未处理。没有必要。 FDirectDraw.SetCooperativeLevel(handle,DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN); 设置合作水平:全屏模式。第二参数可以是多种组合,具体意义网上能找到,不重复。 FDirectDraw.SetDisplayMode(800,600,32,0,0); 设置屏幕大小、颜色数:800 X 600 ,32真彩色。现在显卡一般都支持:8 bit、16 bit、32 bit,而24色比较特殊,很多显卡都不支持了。我主要讲的是16色和32色。24色除了占的字节数和32色不同之外,其它的都和32色一样(目前如此)。8 bit(256色)在《Delphi Graphics and Game Programming Exposed with DirectX 7.0》书中有详细说明。希望你已下载到此书。 如果想以此入门转而进入3D游戏编程的朋友,应主攻16色。 FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_CAPS; DDSurface.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE; FDirectDraw.CreateSurface(DDSurface,FPrimarySurface,Nil)<>DD_OK then Close; 这一段代码是创建主页面。这也DX的一大特点,创建对象时是跟据一个数据结构的要求来创建的。因此首先要初始化数据结构,然后再调用DX相应的函数。 FillChar:先对数据结构清零; DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); 这也是DX的一个特点,每个数据结构都有数据结构的大小,网上说是为了区别DX版本。这里只要按这种方式写就可以了 DDSurface.dwFlags := DDSD_CAPS; 标志,网上有所以参数说明,不重复 DDSurface.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE;设为主页面(PRIMARYSURFACE) if FDirectDraw.CreateSurface(DDSurface,FPrimarySurface,Nil)<>DD_OK then Close; 创建主页面,如果创建失败,就关闭程序。 接下来的代码和上面的创建过程一样,就是初始化数据结构时几个参数不同 DDSurface.dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH; DDSurface.ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN; 告诉程序,下面创建的是一个离屏表面,即不显示的页面。 OnShow 代码 这里三个函数要说一下: GetDC、ReleaseDC,这两个没什么好说的,只要成双出现就行了。 BltFast。这是DX图形拷贝最快的一个函数。说是这么说,但好角Blt速度也差不了多少。 显示文字的代码就不用多说了,参看TCanvas就行了。 ---------------------------------------------------------------- 以上对DX代码作了简单说明总结。下面再说一下跟图形有关的东东,和几个非常简单特效。 一、抠图。 这个是非常实用,并且用的非常多。 实现起来也非常简单。 **接(一)程序** var ColorKey: TDDColorKey; //这也是一个数据结构 (*接在最后面*) BMP:=TBitMap.Create; BMP.loadfromfile('./aaa.bmp'); if FBitmap.GetDC(l_DC)<>DD_OK then close; BitBlt(l_DC, 0,0 , bmp.width, bmp.heigh, BMP.Canvas.Handle, 0, 0, SrcCopy); FBitmap.ReleaseDC(l_DC); //装入 图形,这里BMP重建,是因为前面FREE了 ColorKey.dwColorSpaceLowValue := 0; ColorKey.dwColorSpaceHighValue := 0; //将黑色作为透明色 FBitmap.SetColorKey(DDCKEY_SRCBLT, @ColorKey); SrcRect := Rect(0, 0, bmp.width, bmp.heigh); FPrimarySurface.BltFast(200, 100, FBitmap2, @SrcRect, DDBLTFAST_SRCCOLORKEY OR DDBLTFAST_WAIT); BMP.Free; end; 注意最后一个参数:DDBLTFAST_SRCCOLORKEY ,(一)中的是DDBLTFAST_NOCOLORKEY。 还应意,DX的透明色一般都用纯黑色。这是因为黑色,在8bit、16bit、24bit、32bit中的值都是零,而其他的颜色就不一定了。如:RGB(0,0,255)在16bit就不是纯蓝了。 二、BLT函数和它实现的几个特效 var MaskRect:TRect; PDD:TDDBltFX; //Blt特效数据结构 (** BLT 填冲色块 **) MaskRect := Rect(100,100,200,200); FillChar(PDD, SizeOf(TDDBltFX), 0); PDD.dwSize := Sizeof(TDDBltFX) ; PDD.dwFillColor:=RGB(200,200,255); FPrimarySurface.Blt(@MaskRect,nil,@MaskRect,DDBLT_COLORFILL or DDBLT_WAIT,@PDD); (** FX 境像 **) MaskRect := Rect(200,200,220,216); FillChar(PDD, SizeOf(TDDBltFX), 0); PDD.dwSize := Sizeof(TDDBltFX); PDD.dwDDFX := DDBLTFX_MIRRORUPDOWN; FPrimarySurface.Blt(@MaskRect, FBitmap, @SrcRect, DDBLT_DDFX or DDBLT_WAIT,@PDD); 注:Blt通过两个TRect就可以直接实现缩放。更多特效可参看TDDBltFX的数据结构中dwDDFX的变量值 ------------------------------------------------------------ 虽然现在几乎找不到不支持800 X 600 X 32 的显卡。但还是有必要给出下面这段代码: uses DirectDraw; var FDirectDraw: IDirectDraw4; implementation function EnumModesCallback(const lpDDSurfaceDesc: TDDSurfaceDesc2; lpContext: Pointer): HResult; stdcall; begin TStringList(lpContext).Add(IntToStr(lpDDSurfaceDesc.dwWidth)+' X '+ IntToStr(lpDDSurfaceDesc.dwHeight)+', '+ IntToStr(lpDDSurfaceDesc.ddpfPixelFormat.dwRGBBitCount)+ 'bits/pixel'); Result := DDENUMRET_OK; end; procedure TForm1.FormCreate(Sender: TObject); var TempDirectDraw: IDirectDraw; begin DirectDrawCreate(nil, TempDirectDraw, nil); try TempDirectDraw.QueryInterface(IID_IDirectDraw4, FDirectDraw); finally TempDirectDraw := nil; end; {列举显示模式} FDirectDraw.EnumDisplayModes(0,nil, ListBox1.Items, EnumModesCallback); end; 跟据这段代码,你就可以在显示模式不被支持时,让用户选择另一个模式,或给出一个提示对话框,而不用象我前面给出的例子,强制退出了。 在前面代码中,除列举显式模式程序之外,都是在全屏模式下实现的。如何实现窗口模式呢?又或者,还有更高的要求,只显式在一个Panle控件中呢?这个问题《Delphi Graphics and Game Programming Exposed with DirectX 7.0》中没有给出答案。在解决非全屏模式的同时我们可以解决另一个问题,前面的显示文字图形的代码,是不能显示控件的。你放的任何控件都会被DX覆盖掉,这给调试带来一定麻烦,特别是全屏模式。 下面将给出非全模式、显示在控件中的方法。之后给出改进(英文书提供)的游戏框架代码。 一、窗口模式 运用窗口模式,首先设置合作水平时必须指明是窗口模式: FDirectDraw.SetCooperativeLevel(handle,DDSCL_NORMAL); DDSCL_NORMAL 普通模式 然后建立Clipper——修剪者。指定DX显示范围。并依附在主表面上。如果不指定,你会看到一个很怪异的图中图的情形。 var Clipper:IDirectDrawClipper; FDirectDraw.CreateClipper(0,Clipper,nil); Clipper.SetHWnd(0,Handle); FPrimarySurface.SetClipper(Clipper) ; //依附在主表面上 Clipper.SetHWnd(0,Handle); 这里是指定的是窗口handle。如果你想DX只画在一个控件上,而窗口上其它位置留作它用,如画在panel1上,那么,这一句必须这么写: Clipper.SetHWnd(0,panel1.Handle); 好了。下面全出一个比较全的代码,实现非全屏模式。 Uses DirectDraw; const WM_DIRECTXACTIVATE = WM_USER + 200; //自定义消息 private //自定义过程,说明在后 procedure DrawSurfaces; procedure FlipSurfaces; procedure AppIdle(Sender: TObject; var Done: Boolean); procedure AppMessage(var Msg: TMsg; var Handled: Boolean); procedure RestoreSurfaces; var FDirectDraw: IDirectDraw7; FPrimarySurface: IDirectDrawSurface7; oneSurface: IDirectDrawSurface7; procedure TForm1.FormActivate(Sender: TObject); var TempDirectDraw: IDirectDraw; DDSurface: TDDSurfaceDesc2; DDSCaps: TDDSCaps2; Clipper:IDirectDrawClipper; L_DC:HDC; BMP:TBitMap; begin DirectDrawCreate(nil,TempDirectDraw,Nil); //在主显示器上创建IDirectDraw对象 try TempDirectDraw.QueryInterface(IID_IDirectDraw7, FDirectDraw); finally TempDirectDraw := nil; end; Application.OnMessage := AppMessage; //处理程序消息 FDirectDraw.SetCooperativeLevel(handle,DDSCL_NORMAL); //设置合作水平 // FDirectDraw.SetDisplayMode(800,600,32,0,0); //显示模式不用设置 FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_CAPS; DDSurface.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE; DDSurface.dwBackBufferCount := 1; //后台反转表面数一个 if FDirectDraw.CreateSurface(DDSurface,FPrimarySurface,Nil)<>DD_OK then Close; //建立主表面 FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags:=DDSD_WIDTH or DDSD_HEIGHT or DDSD_CAPS; DDSurface.dwWidth := 800; DDSurface.dwHeight := 600; DDSurface.ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN ; FDirectDraw.CreateSurface(DDSurface,oneSurface,nil); //建立离屏表面 FDirectDraw.CreateClipper(0,Clipper,nil); Clipper.SetHWnd(0,Handle); FPrimarySurface.SetClipper(Clipper) ; PostMessage(Handle, WM_ACTIVATEAPP, 1, 0); //发送消息,开始循环 end; procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean); begin case Msg.Message of WM_ACTIVATEAPP: {如果窗口不是活动窗口进,停止动画} if not Boolean(Msg.wParam) then Application.OnIdle := nil else {如果变成活动窗口,发送自定义消息} PostMessage(Application.Handle, WM_DIRECTXACTIVATE, 0, 0); WM_DIRECTXACTIVATE: begin {收到自定义消息,重装所有界面(跟据需要重装他们的内存),重连 OnIdle 事件, 重画所有表面 } RestoreSurfaces; Application.OnIdle := AppIdle; // DrawSurfaces; end; WM_SYSCOMMAND: begin {关闭屏保} Handled := (Msg.wParam = SC_SCREENSAVE); end; end; end; procedure TForm1.AppIdle(Sender: TObject; var Done: Boolean); begin //如果程序有空,就做下面两件事 DrawSurfaces; FlipSurfaces; end; procedure TForm1.DrawSurfaces; //离屏表面装入图片,如果控制好按顺序装入不同的图形, var //并控制好时间,就实现了动画,具本可看DirectX 7.0 for Delphi //声明档例子 BMP:TBitmap; L_DC:HDC; begin BMP:=TBitMap.Create; BMP.LoadFromFile('Desk.bmp'); oneSurface.GetDC(L_DC); BitBlt(l_DC, 0,0 , 800,600, BMP.Canvas.Handle, 0, 0, SrcCopy); oneSurface.ReleaseDC(L_DC); end; procedure TForm1.FlipSurfaces; //把离屏表面的图形画到主表面上去 var rcSrc,rcDest:TRECT; P:TPoint; begin P.X:=0;P.y:=0; P:=ClientToScreen(P); //如果你想用控件,这里是:P:=Panel1.ClientToScreen(P); rcDest:=GetClientRect; OffsetRect(rcDest,P.x,P.y); //计算出窗口客户区RECT在屏幕中的绝对位置, //计算不正确,呵呵……自己试试 SetRect(rcSrc,0,0,800,600); FPrimarySurface.Blt(@rcDest, oneSurface, @rcSrc, DDBLT_WAIT, nil); end; procedure TForm1.RestoreSurfaces; //表面恢复 begin FPrimarySurface._Restore; oneSurface._Restore; end; 好了,这时你放几个控件到窗口上,如:按钮、Panel、Memo,这时。它们是显示的DX图形上面了。 如果你没有设置窗口的BorderStyle:=bsSizeable; 那你可以试试改变窗口的大小,看看Blt的自动缩放动能。 如果看过DirectX 7.0 for Delphi声明档例子,你这时会产生一个疑惑:因为它用的方法是:FPrimarySurface.Flip(nil, 0); 非全屏模式下,不能用Flip。虽然FLip很快,非常快。但窗口模式不能用,这也是为什么很多游戏不能在在非全屏模式下运行的原因。我的一个程序,在窗口模式下只有7~8个FPS,而全屏模式下能达到20多。鱼和熊掌不能兼得。 关于表面恢复: 这也是全屏模式下的概念,非全屏模式下不会产生这个(我是没发现,正确与否还得论证)。在全屏模式是,DX会把所有的显存空出来给你的程序。当你程序最小化,显示出桌面时,内存中的数据就被破坏了,回到你的程序时,就必须进行恢复。恢复很简单,把丢失了的表面_Restore一下就行了。要命的是内存的的图片数据,你得一个个的恢复。你会说我上面怎么没有恢复图片的代码呀?你认真看一下就知道了,每次Blt之前都装入一次(DrawSurfaces事件),所以在表面恢复后,就没有必要。 玩过全屏游戏的朋友都知道,弹出桌后再回到游戏,100%的游戏都不能立刻显示游戏画面,都是黑屏,然后听到硬盘一阵狂响,这是在装入图片数据。然后才能让你继续游戏。 二、游戏框架代码 上面的代码已经有一点框架的雏形了。下面给出的是比较全面的框架,包括全屏和非全屏模式,还有比较详细的说明和心得体会。说明是翻译那本英文书上的,不是很准确,请多包涵。全屏模式下,是采用Blt、还是Flip,自己修改相应代码。看完这个框架,你对DX游戏编程就会有更深的理解了。 这只是一个框架,千万别试着运行它,在你还没有加入必要的代码之前。否则会死得很难看。 uses DirectDraw; const CooperAtiveLevel = DDSCL_FULLSCREEN or DDSCL_ALLOWREBOOT or DDSCL_ALLOWMODEX or DDSCL_EXCLUSIVE; SurfaceType = DDSCAPS_COMPLEX or DDSCAPS_FLIP or DDSCAPS_PRIMARYSURFACE; const {自定义消息} WM_DIRECTXACTIVATE = WM_USER + 200; type TForm_main = class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormActivate(Sender: TObject); procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); private {反转一新DGI 表面 用于显示错误} procedure ExceptionHandler(Sender: TObject; ExceptionObj: Exception); {主循环} procedure AppIdle(Sender: TObject; var Done: Boolean); {画表面} procedure DrawSurfaces; {反转 DirectDraw 表面} procedure FlipSurfaces; {重装丢失的表面} procedure RestoreSurfaces; { 截取消息功能} procedure AppMessage(var Msg: TMsg; var Handled: Boolean); { 初始DX为全屏模式 } procedure InitDirectDraw; { 初始DX为窗口模式 } procedure InitDirectDrawWindows; public { Public declarations } end; var Form_main: TForm_main; DXWidth:integer = 800; {DX 显示宽度} DXHeight:integer = 600; {DX 显示高度} DXCOLORDEPTH:integer = 8; {DX 显示颜色数 8为256色} BufferCount:integer = 1; {反转表面个数} WindowMode:boolean = false; {DX 是用窗口模式,还是全屏模式} FDirectDraw : IDirectDraw7; {DirectDraw 主界面} MainSurface : IDirectDrawSurface7; {原始表面} FlipSurFace : IDirectDrawSurface7; { 反转表面、离屏表面 } implementation {$R *.dfm} { TForm1 } { – 用回调函数保证选择的图形支持 DirectX – } function EnumModesCallback(const EnumSurfaceDesc: TDDSurfaceDesc2; Information: Pointer): HResult; stdcall; begin {如果宽、高和颜色深度与指定的常量中一样 ,那么显示模式可以被支持} if (EnumSurfaceDesc.dwHeight = DXHEIGHT) and (EnumSurfaceDesc.dwWidth = DXWIDTH) and (EnumSurfaceDesc.ddpfPixelFormat.dwRGBBitCount = DXCOLORDEPTH) then Boolean(Information^) := TRUE; Result := DDENUMRET_OK; end; { – 当出现异常指令时,这个事件实现简单的背景反转 DGI 表面,这样例外对话框才能被看到 - } procedure TForm_main.ExceptionHandler(Sender: TObject; ExceptionObj: Exception); begin { 断开 OnIdle 事件,停止the rendering loop} Application.OnIdle := nil; {如果 DirectDraw 对象已经建立,反转GDI表面 } if Assigned(FDirectDraw) then FDirectDraw.FlipToGDISurface; {显示异常消息} MessageDlg(ExceptionObj.Message, mtError, [mbOK], 0); {重新连接 OnIdle 事件到 rendering loop} Application.OnIdle := AppIdle; end; procedure TForm_main.AppIdle(Sender: TObject; var Done: Boolean); begin { 表明程序将连续不断的调用些事件} Done := FALSE; {如果 DirectDraw 没有初始化, 退出} if not Assigned(FDirectDraw) then Exit; {注: 这个小游戏的逻辑可以插入控制,如小精灵移动,碰撞,被发现} {画表面内容,反转表面} DrawSurfaces; FlipSurfaces; end; procedure TForm_main.DrawSurfaces; begin { – 此方法当表面要被画时调用 – – 它将连续不断的被 AppIdle 事件调用,所有场景,动画都在这个方法里面 - } {这里将会是程序主要部份} end; { – 此方法将反转表面 - } procedure TForm_main.FlipSurfaces; var DXResult: HResult; rcSrc,rcDest:TRECT; P:TPoint; begin { 执行页面反转。 注 DDFLIP_WAIT 标记被用了,表明这个函数将不返回, 只到页面反转已经执行,这将能使程序执行其它的进程, 只到页面反转函数返回,然而,程序必不断的调用反转函数,保证 反转成功 } if WindowMode then begin //窗口模式 P.X:=0;P.y:=0; P:=ClientToScreen(P); rcDest:=GetClientRect; OffsetRect(rcDest,P.x,P.y); SetRect(rcSrc,0,0,DXWidth,DXHeight); DXResult := MainSurface.Blt(@rcDest, FlipSurFace, @rcSrc, DDBLT_WAIT, nil); end else //全屏模式 DXResult := MainSurface.Flip(nil, DDFLIP_WAIT); { 如果表面丢失,重装它们,其他错误,则引发异常 这主要是针对全屏模式,窗口模式很少发生丢失情况 } if DXResult = DDERR_SURFACELOST then RestoreSurfaces end; { – 此方法当表面内存丢失时被调用 – – 并且必须重载。 表面在显示内存中包含了Bitmaps就必须重新初始化 – } procedure TForm_main.RestoreSurfaces; begin MainSurface._Restore; FlipSurFace._Restore; (* 接下来代码是将相应的图形装入到 MainSurface 和 FlipSurFace 中*) end; { – 消息事件 – } procedure TForm_main.AppMessage(var Msg: TMsg; var Handled: Boolean); begin case Msg.Message of WM_ACTIVATEAPP: {如果窗口不是活动窗口进,停止动画} if not Boolean(Msg.wParam) then Application.OnIdle := nil else {如果变成活动窗口,发送自定义消息} PostMessage(Application.Handle, WM_DIRECTXACTIVATE, 0, 0); WM_DIRECTXACTIVATE: begin {收到自定义消息,重装所有界面(跟据需要重装他们的内存),重连 OnIdle 事件, 重画所有表面 } RestoreSurfaces; Application.OnIdle := AppIdle; DrawSurfaces; end; WM_SYSCOMMAND: begin {关闭屏保} Handled := (Msg.wParam = SC_SCREENSAVE); end; end; end; { - 创建,初始化 DX 对象为全屏模式 -} procedure TForm_main.InitDirectDraw; var {我们只能从DirectDraw 界面创建DirectDraw7,因些我们要一个临时的界面} TempDirectDraw: IDirectDraw; {创建DirectDraw7所需的数据结构} DDSurface: TDDSurfaceDesc2; DDSCaps: TDDSCaps2; {标志,用于决定图形模式是否支持} SupportedMode: Boolean; begin {如果 DirectDraw 已经初始化,退出} if Assigned(FDirectDraw) then exit; {创建临时的 DirectDraw 对象。这将用来创建需要的 DirectDraw7 对象 } DirectDrawCreate(nil, TempDirectDraw, nil); try {我们只能通过查询 DirectDraw 对象界面的方法来取得 DirectDraw7 界面 } TempDirectDraw.QueryInterface(IID_IDirectDraw4, FDirectDraw); finally {现在我们有了 DirectDraw7 对象, 临时 DirectDraw 对象就不在需要了 } TempDirectDraw := nil; end; {调用 EnumDisplayModes 回调函数,检查显示模式是否支持 } FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_HEIGHT or DDSD_WIDTH or DDSD_PIXELFORMAT; DDSurface.dwHeight := DXHEIGHT; DDSurface.dwWidth := DXWIDTH; DDSurface.ddpfPixelFormat.dwSize := SizeOf(TDDPixelFormat_DX6); DDSurface.ddpfPixelFormat.dwRGBBitCount := DXCOLORDEPTH; SupportedMode := FALSE; FDirectDraw.EnumDisplayModes(0, @DDSurface, @SupportedMode, EnumModesCallback ); {如果需要的显示模式不被DirectX支持,显示一个错误消息,并结束程序} if not SupportedMode then begin MessageBox(Handle, PChar('The installed DirectX drivers do not support a '+ 'display mode of: '+IntToStr(DXWIDTH)+' X '+ IntToStr(DXHEIGHT)+', '+IntToStr(DXCOLORDEPTH)+' bit color'), 'Unsupported Display Mode Error', MB_ICONERROR or MB_OK); Close; Exit; end; {设置合作水平,定义在常量中,为全屏幕式 } FDirectDraw.SetCooperativeLevel(Handle, COOPERATIVELEVEL); {设置显示模式,常量中定义 } FDirectDraw.SetDisplayMode(DXWIDTH, DXHEIGHT, DXCOLORDEPTH, 0, 0); {初始化 DDSurface 结构, 指示我们将建立一个复杂的反转表面, 并带一个后台缓冲表面 backbuffer } FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT; DDSurface.ddsCaps.dwCaps := SURFACETYPE; DDSurface.dwBackBufferCount := BUFFERCOUNT; {创建主表面对象} FDirectDraw.CreateSurface(DDSurface, MainSurface, nil); {指出我们想建立一个后台缓冲表面(表面立刻到主表面反转链的后面)} FillChar(DDSCaps, SizeOf(TDDSCaps2), 0); DDSCaps.dwCaps := DDSCAPS_BACKBUFFER; {取反转表面} MainSurface.GetAttachedSurface(DDSCaps, FlipSurFace) ; {在这一刻, 画面以外的缓冲器和其他表面将被建立。其他 DirectDraw 对象也将立刻建立 如:调色板(palettes)。画面以外的缓冲器的内容也同时被初始化 } end; { - 创建,初始化 DX 对象为窗口模式 下面注解只标注和全屏不同的地方,其它注解参看全屏模式 -} procedure TForm_main.InitDirectDrawWindows; var TempDirectDraw: IDirectDraw; DDSurface: TDDSurfaceDesc2; Clipper:IDirectDrawClipper; //窗口模式下的Clipper:修剪 SupportedMode: Boolean; begin if Assigned(FDirectDraw) then exit; DirectDrawCreate(nil, TempDirectDraw, nil); try TempDirectDraw.QueryInterface(IID_IDirectDraw4, FDirectDraw); finally TempDirectDraw := nil; end; FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_HEIGHT or DDSD_WIDTH or DDSD_PIXELFORMAT; DDSurface.dwHeight := DXHEIGHT; DDSurface.dwWidth := DXWIDTH; DDSurface.ddpfPixelFormat.dwSize := SizeOf(TDDPixelFormat_DX6); DDSurface.ddpfPixelFormat.dwRGBBitCount := DXCOLORDEPTH; SupportedMode := FALSE; FDirectDraw.EnumDisplayModes(0, @DDSurface, @SupportedMode, EnumModesCallback ); {如果需要的显示模式不被DirectX支持,显示一个错误消息,并结束程序} if not SupportedMode then begin MessageBox(Handle, PChar('The installed DirectX drivers do not support a '+ 'display mode of: '+IntToStr(DXWIDTH)+' X '+ IntToStr(DXHEIGHT)+', '+IntToStr(DXCOLORDEPTH)+' bit color'), 'Unsupported Display Mode Error', MB_ICONERROR or MB_OK); Close; Exit; end; FDirectDraw.SetCooperativeLevel(Handle, DDSCL_NORMAL); { 窗口模式不需要设置显示模式,如果你一定要设置也行,但不是好事 } // FDirectDraw.SetDisplayMode(DXWIDTH, DXHEIGHT, DXCOLORDEPTH, 0, 0); FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags := DDSD_CAPS; DDSurface.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE; FDirectDraw.CreateSurface(DDSurface,MainSurface,Nil); FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0); DDSurface.dwSize := SizeOf(TDDSurfaceDesc2); DDSurface.dwFlags:=DDSD_WIDTH or DDSD_HEIGHT or DDSD_CAPS; DDSurface.dwWidth := 800; DDSurface.dwHeight := 600; DDSurface.ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN ; FDirectDraw.CreateSurface(DDSurface,FlipSurFace,nil); {在窗口模式下,必须建立 Clipper ,作用是为了让 DX不要画出界( 别人如是说 ) 并使之依附在主表面上 } FDirectDraw.CreateClipper(0,Clipper,nil); { 这里的 Handle 可以是控件 Handle,使用控件Handle 和窗体Handle 在 FlipSurfaces 事件中将会有不同的操作 } Clipper.SetHWnd(0,Handle); MainSurface.SetClipper(Clipper) ; end; { – 初始化窗口属性 – } procedure TForm_main.FormCreate(Sender: TObject); begin {设置程序的异常处理} Application.OnException := ExceptionHandler; { 初始化窗口属性,全屏模式和窗口模式要求不一样 } if WindowMode then begin BorderStyle := bsSingle; BorderIcons := [biSystemMenu,biMinimize]; end else begin BorderStyle := bsNone; ShowCursor(FALSE); { 不显示鼠标. 是否显式鼠标跟据你程序要求作决定 } FormStyle := fsStayOnTop; { 注:全屏模式 FormStyle属性必须是:fsStayOnTop} Cursor := crNone; BorderIcons := []; end; end; procedure TForm_main.FormDestroy(Sender: TObject); begin {释放异常处理的handler} Application.OnException := nil; {显示鼠标} ShowCursor(TRUE); {记住, 我们没有明确的释放 DirectX 对象, 因为当他们失去上下文连接时, 他们会自动释放,如当程序关闭时 } end; procedure TForm_main.FormActivate(Sender: TObject); begin if WindowMode then InitDirectDrawWindows else InitDirectDraw; Application.OnMessage := AppMessage; //连接程序消息事件 PostMessage(Handle, WM_ACTIVATEAPP, 1, 0); {发送一个消息,连接 OnIdle 事件,开始主循环 } end; procedure TForm_main.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin Case Key of VK_ESCAPE:Close; //ESC end; end; end. 至此,你可以尝试写一点小小的动画了,如DX7.0例子中的小动画。 另,如果你想在全屏幕式下显示控件,比如按钮什么的,那么,你必须进行以下处理: 建立Clipper,依附在主表面上;然后改Flip为Blt。用了Clipper后,就不能用Flip了。 对于图形编程,就应该精通到对每一个象素点的操作。并且可以对每一个象素的R,G,B颜色分量进行操作。DX也应如何。 下面要说的就是,DX如何可以做到对每一个象素点的R、G、B分量进行操作。 var SourSf: IDirectDrawSurface7; SourSrfcInfo: TDDSurfaceDesc2; Sour:pointer; SourSrfcInfo.dwSize := SizeOf(TDDSurfaceDesc2); SourSf.Lock(nil, SourSrfcInfo, DDLOCK_SURFACEMEMORYPTR or DDLOCK_WAIT, 0); //第一参数放nil,锁整个表面,也可以放@sRect 那么锁定的是 sRect 这个矩形了 Sour:=SourSrfcInfo.lpSurface; SourSf.Unlock(nil); //解锁 Sour就是SourSf的颜色值的起始地址。如果锁定的是矩形,返回的是矩形第一个点的地址。暂时放下这个Sour,先说说SourSrfcInfo结构。 这个数据结构在Lock之后,取得当前屏幕各种返回值,有些返回值非常重要。 ddpfPixelFormat.dwRGBBitCount 返回当前屏幕的颜色数,如8、16、24、32。这对于同时使用全屏非全屏的游戏来说很重要,因为非全屏时,你设置的屏幕大小颜色数是没用的,颜色数是跟据桌面的颜色数来定的,只有通这个值,你才能确定颜色数是多少。 ddpfPixelFormat.dwRBitMask ddpfPixelFormat.dwGBitMask ddpfPixelFormat.dwBBitMask 这三个值是返回如何取得颜色RGB的分量。颜色值 and 上面其中一个值就可以取得其中一个分量。也许你会认为这是多此一举。象32bit: 00 RR GG BB,这分的很清楚呀。但对于16bit来说,它就有565和555两种方法表示。虽然现在的显卡几乎100%是565,但是作为程序员不能心存侥幸,用返回值来处理是100%正确(对于图形格式555和565不在讨论范围,请到网上找资料) 。《轩辕剑5》在联想的某种机型上采用窗口模式时不能正确抠图,其中原由未深究,程序员心存侥幸是一定的了。 这个数据结构还有很多值,请自己去查资料、慢慢体会了。 回过头来讲Sour这个变量。取得了首址,我们就可计算得到任何一点的地址值。DX中用的最多的是TRECT,一般也是跟据TRECT来找对应的地址,然后读取它的值。 为了使各种颜色数能通用。这里要增加一个变量: RGBByteCount:= SourSrfcInfo.ddpfPixelFormat.dwRGBBitCount div 8; 这是因为颜色数为 8 bit 时,颜色值占一个 Byte,16 bit 占2个Byte,24 bit 占3个byte, 32bit占4个Byte。而指针加1只加一个Byte。 下面是锁定整个屏幕后,矩形srRect的首址计算方法。 var Sour,SrMem:pointer; Sour:=SourSrfcInfo.lpSurface; SrMem:= Pointer(Longint(Sour)+ srRect.Left *RGBByteCount+ SrRect.Top *SourSrfcInfo.lPitch ); 第Y行的第一个象素的地址值:Sour := Pointer(Longint(SrMem)+(SourSrfcInfo.lPitch )*Y ); 讲了一大堆,取出来的还是地址,颜色值还是没有取出来,那颜色值怎么取呢?可以参照《Delphi Graphics and Game Programming Exposed with DirectX 7.0》中256色的读法: TBytePtr = array[0..0] of Byte; PBytePtr = ^TBytePtr; var BackMem: PBytePtr; begin BackMem := PBytePtr(Sour); //第Y行的第一个象素的值 注,256色读取的不是颜色值,是索引值。要跟据索引值和颜色表找出对应RGB值。以下不再讨论256色。 同样对于16色 : TWordPtr = array[0..0] of word; PWordPtr = ^TWordPtr; 16色每个象素点占2个Byte,而word正好是2 Byte; 32色每个象素占4个Byte,用Dword: TDWordPtr = array[0..0] of Dword; PDWordPtr = ^TDWordPtr; 麻烦来了,24色每个象素占3 Byte,而没有变量对应它。先定义一个3byte的数据结构,然后再如上定义。我想应该可以,但没做过试验。现在很多显卡不支持24色,可能也是这个原因吧。 另一种方法,用汇编通杀。强烈建议使用这种方法。 asm push esi mov esi,Sour mov eax,[esi] …… end; 这时,eax的值就是Sour指针对应的值。注意:这时读取的是一个Dword,然后再分吧,1个,2个,3个。用汇编能读多少就读多少,不要因为是16色,而写成这样 movzx edx,word ptr [esi],如果用MMX能用 movq mm0,[esi] 一次读入一个8byte,就不要用movd mm0,[esi] 读入4byte。边界例外,要注意别读出界哦~~~(更多MMX请看我的另一篇笔记,网上有更多相关资料可供你查阅)。原因很简单,我在回答“我爱Pascal”的一个问题回答过:mov eax,[esi] 这样的东东是耗时大户,能少读一次是一次,而汇编其他的代码耗时很少,读入后再细分,代码是长点,但耗时绝对少。 值读出来了,进行一系列操作之后,要怎么写回去?这个应该不是问题了。只接把值写回到地址就行了。 BackMem:=$0000FFFF; 对于汇编 asm …… mov [esi],eax …… end; 给出一个旋转图形特效代码,原理请看那本英文书或网上找资料。因为采用的反算法,每次只能读取计算一个点,因此无法运用上面说的能读几个点读几个点的美好想法。 var SinTable: array[0..359] of single; //旋转 CosTable: array[0..359] of single; function ReadColor(Sour:pointer;ByteCount:integer):integer; //读取一个象素点的颜色值 begin case ByteCount of 2:asm //16bit mov eax,sour xor edx,edx //edx清零 mov dx,[eax] mov @result,edx end; 3:asm //24bit,24就是麻烦 mov eax,sour xor edx,edx movzx dx,byte ptr [eax+2] shl edx,16 mov dx,word ptr [eax] mov @result,edx end; 4:asm //32 bit mov eax,sour mov edx,[eax] mov @result,edx end; end; end; procedure writeColor(Dest:pointer;sour:integer;ByteCount:integer);//写象素点 begin case ByteCount of 2:asm mov eax,Dest mov edx,sour mov [eax],dx end; 3:asm mov eax,Dest mov edx,sour mov word ptr [eax],dx bswap edx mov byte ptr [eax+2],dh end; 4:asm mov eax,Dest mov edx,sour mov [eax],edx end; end; end; function RotateDraw(DestSf: IDirectDrawSurface7; DsRect:TRect; //目标区域大小、位置 SourSf: IDirectDrawSurface7; SrRect:TRect; //源区域大小、位置 ColorKey:integer; UseColorKey:boolean; Theta:integer ):HResult; //角度 用于Sin函数 var DestSrfcInfo, SourSrfcInfo: TDDSurfaceDesc2; DestX, DestY, SrcX, SrcY: Integer; SinTheta, CosTheta: Single; CenterX, CenterY: Single; Dest, Sour, DsMem, SrMem:pointer; Width,Height:integer; aa:integer; begin if Theta<0 then Theta:=360+(Theta mod 360); Theta:=Theta mod 360; SinTheta := SinTable[Theta]; CosTheta := CosTable[Theta]; try SourSrfcInfo.dwSize := SizeOf(TDDSurfaceDesc2); Result:=SourSf.Lock(nil, SourSrfcInfo, DDLOCK_SURFACEMEMORYPTR or DDLOCK_WAIT, 0); DestSrfcInfo.dwSize := SizeOf(TDDSurfaceDesc2); Result:=DestSf.Lock(@DsRect, DestSrfcInfo, DDLOCK_SURFACEMEMORYPTR or DDLOCK_WAIT, 0); Sour:= pointer(Longint(SourSrfcInfo.lpSurface) + srRect.Left * RGBByteCount //RGBByteCount 是公用变量,调用前要先计算好 + SrRect.Top * SourSrfcInfo.lPitch ); Dest:= DestSrfcInfo.lpSurface; Width:=SrRect.Right-srRect.Left; Height:=SrRect.Bottom-SrRect.Top; CenterX := (Width / 2); //中心点 CenterY := (Height / 2); for DestY := 0 to Height-1 do begin for DestX := 0 to Width-1 do begin {通过目标坐标计算出对应的源坐标点, 判断如果这点是在源点上,那么就画出来} SrcX := Trunc(CenterX + (DestX - CenterX)*CosTheta - (DestY - CenterY)*SinTheta); SrcY := Trunc(CenterY + (DestX - CenterX)*SinTheta + (DestY - CenterY)*CosTheta); {如果这点在源图形中} if (SrcX > 0) and (SrcX < Width) and (SrcY > 0) and (SrcY < Height) then begin {拷贝这点到目标点上} SrMem := pointer(Longint(Sour)+ SrcY * SourSrfcInfo.lPitch + SrcX * RGBByteCount); aa:=ReadColor(SrMem,RGBByteCount); DsMem := pointer(Longint(Dest)+ DestY * DestSrfcInfo.lPitch + DestX * RGBByteCount); if UseColorKey then begin //是否抠图 if aa<>ColorKey then writeColor(DsMem,aa,RGBByteCount); end else writeColor(DsMem,aa,RGBByteCount); end; end; end; finally DestSf.Unlock(nil); SourSf.Unlock(nil); end; end; initialization //在程序一开始运行时先计算好旋转要用的数组 for iAngle := 0 to 359 do begin SinTable[iAngle] := Sin(iAngle*(PI/180)); CosTable[iAngle] := Cos(iAngle*(PI/180)); end; end. 调用:借用(三)中的框架代码,这里不再重复。注意恢复表面时,要重载图形。 var Theta:integer = 0; TickCount:Dword; procedure TForm_main.DrawSurfaces; var SrcRect:TRect; sRect,aRect:TRect; thisTickCount : DWORD; begin thisTickCount := GetTickCount; if (thisTickCount - TickCount) > 50 then begin TickCount:=thisTickCount; dec(Theta,2); end; sRect:=Rect(0,0,110,110); //源矩形大小,这里别照抄,要跟据你图形大小定 SrcRect:=Rect(0,0,110,110); offsetRect(SrcRect,260,30); //目标矩形大小,移到(260,30)位置 RotateDraw(FlipSurFace,SrcRect ,TempSurFace,sRect,0,true,Theta); end; 最后说说FPS,这是一个很重要的东西,编写游戏的水平就看这个指标,每秒画多少帧画面。 放一个TTimer控件在窗体上,Interval 设为 100。 var FrameRate:integer; FOldTime2:Dword; FPS:integer; procedure TForm_main.Timer1Timer(Sender: TObject); var t: DWORD; i:integer; begin t := TimeGetTime; i := Max(t-FOldTime2, 1); if i>1000 then begin FPS := Round(FrameRate*1000/i); FrameRate:=0; FOldTime2 := t; end; end; 在DrawSurfaces中用文字写出来FPS 值就行了。 ----------------------------------------------------- 对于游戏来说,还有声音,输入设备,网络连接,力反馈等等。这些东西就不多讲了。请见谅。 声音处理跟图象处理差不多,建立过程也相似;键盘、鼠标的操作也不是难事。知道DX原理之后,就一通百通。力反馈没有研究,因为没有条件。至于网络,没写过即时战斗游戏也就用不上IPX连网了,也没研究,无权发言。以上所写仅给致力于游戏编程而苦于无法入门的朋友。 只是游戏开发远没有这么简单。首先美工关不是我们程序员能处理的,声音资源也不是我们能把握的。我们能处理的只是代码。就代码而言:1、至少要知道汇编,否则你无法优化你的程序。可以参看日本人写的DelphiX控件里面的汇编,你会受益非浅;写个简短的程序,然后调出CPU窗口,只接看汇编代码。2、能看懂C语言,必竟游戏是C的天下,源代码N多;3、还必须知道一些计算机图形原理,这是因为要进行图形特效处理,你不可能指望美工把各种可能的图形都画好供你调用,甚至不用抠图。FASTBMP控件是个很好的参考,它带有许多图形特效。如果你不想用DX写游戏,甚至可以用它来开发出不错的游戏。而代码竟然跟DX也差不多(初始化除外)。