先说说如果用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也差不多(初始化除外)。
转载于:https://www.cnblogs.com/keycode/archive/2010/10/17/1853708.html