admin 管理员组文章数量: 1086019
2024年1月10日发(作者:border bottom width)
DelphiX 游戏教程
当我开始编程的时候为的就是开发游戏。但一段时间以后我放弃了,因为用DELPHI做开发实在找不到合适的游戏API来支持。我曾考虑改用C/C++,因为它们有那么多的API支持,例如DirectX和OpenGL。但是对我来说使用那么多API来开发C/C++程序实在我人恐惧。
后来我碰巧遇见 DelphiX。它把API封装的简单易用,相当迅速、强大。我非常喜欢它并使用至今,现在我愿意和大家一起分享。
这里并不介绍如何安装DelphiX ,也不会介绍DirectX,因为这会占用太多的文章篇幅。感兴趣想了解这方面知识的人,我推荐你们看看Dominique Louis的文章以及其它Borland 开发社区的文章(1,
2,
3)。
这篇系列教程的基本目标是创建一个基本的游戏引擎。将包括这些主题:建立一个等尺寸的贴图引擎;活动单元和贴图;路径寻找、基本的游戏AI、碰撞检测。
这一章将创建一个基本的等角投影贴图引擎,用来从磁盘读取地图。与此相关的编辑器可以从这里下载downloaded here。
开始
首先预览一下下面文章中会遇到的特殊词汇。
TILE:一个具有规则形状的小图片。一个tile不能表现什么,但是大量的tile可以组成某些东西。一般用来生成游戏中的地形。Tile可以有不同的形状。最常用的有菱形、方形和六边性,由于这是一个等投影tile引擎,所以我们使用菱形tile。
TILING ENGINE. 将tile绘制到各自位置的有序代码(一般是过程或者部分过程)。它也要决定拿一个渲染帧被显示。
TILING:设置所有tile到正确位置的过程。也就是tiling 引擎要做的事。
制作一个等投影的地形
这个技巧制作一个等投影的环境,将所有的tile的边相连。此例使用菱形的tile。如果我们简单的将它们一行一行的边边相连放置在一起效果如下所示:
正如你所见,我们创建的只能叫西洋跳棋。图片的透明部分形成巨大的漏洞。边缘没有很好的相连,为什么?这就是我些这篇文章要解决的。
通常的方法是将第一行画好,下一行的起始不是在32象素(tile的高度)而是16象素。 也要向左侧偏移32个象素开始。然后每个新行都向上移动16个象素并左移(右移)32个象素。
但这不是全部,你玩Age of empires或AoE2 时一定注意到地图的边缘不是锯齿型,这个地图的形状是菱形。解决的方法是第一行只画一个tile,然后画2个、3个等等, 到某一个点为止。然后开始逐行减少直到为零。这时候你不需要左右来回切换偏移方向,只要在增加的时候向左偏移,减少的时候向右偏移即可。如果做一个3x3 的tile地图看上去如下:
你想为地图增加草和水吗?如果你想知道答案就看这里。如果在地图中增加变化(策略游戏) ,我们就不得不从某些地方将地图加载进来。一个2维的整型数组可以解决这个问题。. 在数组中检测当前的tile是否绘制。可以使用多张地图,在运行期不要重定义。我们将它们存储为文件。在代码中你会看到这是一件简单的事。这篇文章有一个地图编辑器,这是一个非常简单的地图编辑期,用来保持地形数组。你必须下载它并自行编译才能使用。
理论听烦了,下面看看实践吧!
建立一个TILING 引擎
首先,建立一个文件夹用来存放所有的图片和地图文件,以防丢失。
第二步下载图片here。如果你选择使用自己的图片,记住图片一定要是64 pixels宽32 pixels高。
然后下载编辑器,确定安装了DelphiX 后编译编辑器。喝点东西,找张舒适的沙发,我们开始吧。
建立一个新的Delphi应用程序。把它保持到图片的目录中。命名为 和 。
首先建立一个窗体,将窗体申明的类从TForm 改为TDXForm。在对象查看器中设置如下属性。
BorderStyle: bsNone
Caption: Whatever you want
Name: MainForm
现在需要一个绘画用的surface。选择TDXDraw 组件,将它拖放在窗口上并设置如下:
Align alClient
Display 640x480x8
Name DXDraw
Options
doFullScreen True
doAllowReboot True
doFullScreen True
下面拖放一个TDXImageList组件用来放置图片,并设置如下:
DXDraw DXDraw
Name TileMaps
增加图片的Items属性, (You will figure out how, it's self-explanatory.) ,添加有4个图片,编号0-3。
然后我们需要一个Timer告诉计算机如何绘图。不要使用Delphi自带的Timer,对于游戏来说,它太慢了。DXTimer很快,一次我用两个时钟定时,一个每秒走了19次,一个每秒1200次。拖放一个DXTimer
并设置属性:
Enabled False
Interval 0
Name DXTimer
下一步我们增加滚屏功能,这一次使用DXInput 组件,属性设置如下:
Name DXInput
Joystick
Enabled False
Mouse
BindInputState True
Enabled True
现在放置一个panel到form上。清除它的 caption 字 "Panel.",设置Align属性为上面防止一个button。我们将用它来关闭应用程序。
现在拷贝粘贴程序吧!
"alBottom." ,在
拷贝粘贴前
现在转到代码窗口并加入如下申明到private 段:
procedure DrawTiles(StartX, StartY : Integer);
这个过程用来绘制tiles,是tiling engine。你可以看到,它需要两个值确定从什么地方开始绘制tiles。建立两个如下的类型申明:
Type TTile = Record
TileImage : Integer;
end;
Type TTileMap = Record
Map : array[1..50,1..50] of TTile;
end;
全局变量:
Map : TTileMap;
StartX, StartY : Integer;
这里创建了地图信息,尺寸是 50x50 tiles的。我听见你问为什么不建立一个整型数组来代替它?可以,我把它定义成记录,这样以后可以扩展它而不需要改变很多代码。这样增加了系统的灵活性。
在 button的onClick 事件写:
Close;
用来退出应用程序。在全屏模式时,我们将不能看到系统菜单和系统按钮。
现在实现我们前面申明的DrawTiles 过程:
procedure les(StartX, StartY : Integer);
var PosX, PosY, LoopX, LoopY, XTile, YTile, NumOfTilesY,
TileImage : Integer;
begin
PosY := 0;
PosX := 1;
NumOfTilesY := 1;
for LoopY := 1 to 50 do
begin
XTile := 1;
YTile := LoopY;
for LoopX := 1 to NumOfTilesY do
begin
TileImage := [XTile, YTile].TileImage;
[TileImage].Draw(DXDraw. Surface,
StartX + PosX + LoopX * 64, StartY + LoopY * 16, 0);
XTile := XTile + 1;
YTile := YTile - 1;
end;
NumOfTilesY := NumOfTilesY + 1;
PosX := PosX - 32;
PosY := PosY + 16;
end;
NumOfTilesY := NumOfTilesY - 2;
PosX := PosX + 64;
for LoopY := 1 to 49 do
begin
XTile := LoopY;
YTile := 3;
for LoopX := 1 to NumOfTilesY do
begin
TileImage := [XTile, YTile].TileImage;
[TileImage].Draw(DXDraw. Surface, StartX +
PosX + LoopX * 64, StartY + PosY + LoopY * 16, 0);
XTile := XTile + 1;
YTile := YTile - 1;
end;
NumOfTilesY := NumOfTilesY - 1;
PosX := PosX + 32;
end;
end;
这个过程从StartX, StartY坐标开始绘制tiles。 TileImage控制当前的tile 进行绘制。它重新计算每一个tile,可以看到,访问这个过程我们需要在DXTimer的: onTimer事件中实现:
if not w then Exit;
(0);
n := 'FPS:' + InttoStr(ate);
DrawTiles(StartX, StartY);
;
;
If isLeft in then StartX := StartX + 16;
If isRight in then StartX := StartX - 16;
If isUp in then StartY := StartY + 8;
If isDown in then StartY := StartY - 8;
第一个过程检测是否能够绘制。然后全黑,清除当前的帧。帧率通过Caption of the panel显示。然后调用DrawTiles 过程,以StartX 和StartY为起始位置进行绘制。然后flips到屏幕。之后更新输入状态,测试按钮状态 ,调整地图的移动。确保在onCreate 事件for the form中增加如下代码:
StartX := 1;
StartY := 1;
d := True;
支持自定义地图
这就是全部的地图显示。如果要显示自定义地图怎么办?请增加如下的申明:
MapFile : file of TTileMap;
在panel上增加名为"Open."的按钮,再增加一个 OpenDialog组件。设置它的过虑文件明为
MapFile(*.map) | *.map。在onClick event of the Open button增加代码:
if e then
begin
AssignFile(MapFile, me);
Reset(MapFile);
Read(MapFile,Map);
end;
读写并绘制地图。
好了,现在你已经得到了等角投影的贴图引擎并且能够运行它。在下一章里我们看看活动贴图和鼠标的世界坐标。
DelphiX game tutorial2:动画基础
大家好,上次我们学习了如何创建等投影的tiled世界。这次我们将看看如何确定光标和tile的位置。我们也将继续进行基本的动画贴图。为此建立了一套新的图片,你可以在此下到新的图片和转换器
CodeCentral。如果你没有上一篇文章的源码,你可以从这里下载
here。这些代码是这篇文章所需要的,好了,现在开始这个主题。
理论概述
动画
在DelphiX中让图片动起来非常简单。这篇文章用到的图片在外观上彼此略有不同。首先我们给这些tiles号码扩展到16。这些图片将使整个环境更加不同, 总之,前三个tile将用于动画。如果你打开图片,你看到没一帧如图所示:
当然它们并没有红色的边框和号码。当你加载这些图片到DXImageList 组件时,有两个名为PatternHeight 和 PatternWidth。这是用来设置每一帧的高度和宽度的。现在我们可以按照号码顺序决定显示哪一帧,第一帧编号为0,第二帧编号为1,依此类推。通过每次改变一个变量让地图重画。我们可以获得一帧接一帧的绘制效果,如图:
这就是基本的动画,当然当你不同的动作执行不同的动画时会更复杂,但所有的技术就是这么多了。
世界方位WORLD POSITIONING
这是这部分的一个技巧。在屏幕坐标系中这个很容易,看上去是这样的:
但是这个坐标系统不能在等投影的贴图中很好的使用。通过旋转和相差,我们得到合适的坐标系统,如图:
在Delphi中所有的位置都由系统的屏幕坐标决定。但是tiles使用投影坐标系统,就像第二张图。难点在于如何在两种坐标系之间进行转换。
这里要利用到数学知识。最终,公式非常有用, A row of tiles is placed along, or follows, the function
f(X)=0.46875X or f(X)=0.46875X depending on which axis. (Why all the decimals? Remember in the last
article we used the tile height of 30, which divided by 64 is 0.46875. If we use 0.5 instead, which doesn't
seem to be too much different, we will get a slight deviation in the positioning system. 光标将选择它旁边的tiles,in some cases as many as three tiles in the wrong direction.) The X in these function refers to the
screen coordinate system. These two functions look like this:
The shaded area is a value above and below the function that the tile is within. Where two shaded
areas cross each other we have the shape of a tile. What we have to do in order to figure out which tile the
cursor is in is to determine when the tile is inside the shaded area. So, if the Y value of the cursor is
between -0.46875X+14 and -0.46875X-14 then the cursor is on the same WorldY as the tile. If the Y value
is between 0.46875X+14 and 0.46875X-14 then the cursor is on the same WorldX as the tile. If both these
happen at the same time then the cursor is on the tile (where the shaded areas cross). This is just for one
tile. What about 100x100 tiles? This picture might give you an idea of how to solve this.
First of all, every row is offset one tile height (30 pixels) from the one above. So, what we do is
enumerate through all WorldX rows until we strike the right one. Then we take the number of that row and
place it in the TileX Integer. Next, we enumerate through all the WorldY rows and place the right number
in the TileY Integer. Finally, we return the two values to the main program. Now, the main program can
use these values to get tile info, draw the marker, etc. Ok? Let's implement it in our tiling engine.
实现
动画
首先,从图片列表中删除所有的旧图片添加新的图片,不要添加 ""。每个图片属性设置如下:
PatternHeight = 32
PatternWidth = 64
放置一个新的DXImageList到窗口上,命名为Markers。加入,然后加入如下的全局声明:
Frame: Integer;
TileX, TileY: Integer;
在OnCreate event for the form 加入:
Frame = 0;
在OnTimer event of the DXTimer 加入:
if (Frame = 5) then
Frame=0
else
Frame = Frame + 1;
end;
在DrawTiles 过程中将Frame清零,调用Draw 过程,如下:
[TileImage].Draw(e,
StartX + PosX + LoopX * 64, StartY + LoopY * 16, Frame);
记得在DrawTiles过程中将上面声明两次,记得都要改变,否则你将得到半个地图而没有动画,现在保存并测试你的程序。你不能使用编辑器,因为文件的结构改变了。你可以下载转换器在这CodeCentral。这个转换器能转四位的位图文件为.map 文件。 你可以用它创建50x50 pixel 的地图并保存为4位 (16 色)位图,然后用tiling engine将它转换。如果你的帧率太高,动画看上去非常的糟糕,你可以调整Timer时间间隔为33 。这将形成30的帧率。这就是动画的所有东东。下面看看难点吧!
世界方位WORLD POSITIONING
首先进行如下的声明,将此加入TTile的声明:
Marked: Boolean;
再声明这个过程:
procedure CheckTiles(X, Y: Integer; var XTile, YTile: Integer);
加入过程代码:
procedure iles(X, Y: Integer; var XTile, YTile:
Integer);
var LoopX, LoopY: Integer;
begin
for LoopY := 1 to 50 do
begin
if (Y - ((0.46875) * (X - StartX) + StartY + (LoopY * 30)) < 15)
and (Y - ((0.46875) * (X - StartX) + StartY + (LoopY * 30)) > (-15))
then
YTile := LoopY;
end;
for LoopX := 1 to 50 do
begin
if (-(0.46875) * (X - StartX) + StartY + (LoopX * 30) - Y < 15) and
((-0.46875) * (X - StartX) + StartY + (LoopX * 30)- Y > (-15)) then
XTile := LoopX;
end;
end;
在OnMouse move event of the DXDraw 增加:
[TileX, TileY].Marked := False;
CheckTiles(X - 96, Y, TileX, TileY);
[TileX, TileY].Marked := True;
在DrawTiles 过程中加入Draw 函数。取消旧tile的标记,增加新的tile,增加如下新的代码,确保写两份,否则只有半个地图。
if [XTile, YTile].Marked then
begin
[0].Draw(e, StartX + PosX + LoopX * 64,
StartY + PosY + LoopY * 15 - 2, 0);
end;
给标记的tiles绘制一个标号。 OnTimer event:加入下面代码:
n := 'FPS: ' + IntToStr(ate);
to:
n := 'FPS: ' + IntToStr(ate) + ' TileX: '
+ IntToStr(TileX) + 'TileY: ' + IntToStr(TileY);
这就是光标的世界坐标。使用世界坐标,在后面可以用来提取tile、单元、建筑等等的信息。
DirectInput 键盘编程入门
游戏编程可不仅仅是图形程序的开发工作,实际上包含了许多方面,本文所要讲述的就是关于如何使用 DirectInput 来对键盘编程的问题。
在 DOS 时代,我们一般都习惯于接管键盘中断来加入自己的处理代码。但这一套生存方式在万恶的 Windows 社会下是行不通的,我们只能靠领 API 或者 DirectInput 的救济金过活。
在 Windows 的 API 中,有一个 GetAsyncKeyState() 的函数可以返回一个指定键的当前状态是按下还是松开。这个函数还能返回该指定键在上次调用 GetAsyncKeyState() 函数以后,是否被按下过。虽然这个函数听上去很不错,但现在领这种救济金的程序员是越来越少了。原因无它,只因为 DirectInput 的救济金比这丰厚,而且看上去似乎更专业?
为了早日成为职业的救济金用户,我们就从学习 DirectInput 的键盘编程开始吧。
DIRECTINPUT 的初始化
前面讲 DirectDraw 时,曾经提到,微软是按 COM 来设计DirectX的,所以就有了一个 DIRECTINPUT 对象来表示输入设备,而某个具体的设备由 DIRECTINPUTDEVICE 对象来表示。
实际的建立过程是先创建一个 DIRECTINPUT 对象,然后在通过此对象的 CreateDevice 方法来创建 DIRECTINPUTDEVICE 对象。
示例如下:
#include
#define DINPUT_BUFFERSIZE 16
LPDIRECTINPUT lpDirectInput; // DirectInput object
LPDIRECTINPUTDEVICE lpKeyboard; // DirectInput device
BOOL InitDInput(HWND hWnd)
{
HRESULT hr;
// 创建一个 DIRECTINPUT 对象
hr = DirectInputCreate(hInstanceCopy, DIRECTINPUT_VERSION, &lpDirectInput, NULL);
if FAILED(hr)
{
// 失败
return FALSE;
}
// 创建一个 DIRECTINPUTDEVICE 界面
hr = lpDirectInput->CreateDevice(GUID_SysKeyboard, &lpKeyboard, NULL);
if FAILED(hr)
{
// 失败
return FALSE;
}
// 设定为通过一个 256 字节的数组返回查询状态值
hr = lpKeyboard->SetDataFormat(&c_dfDIKeyboard);
if FAILED(hr)
{
// 失败
return FALSE;
}
// 设定协作模式
hr = lpKeyboard->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
if FAILED(hr)
{
// 失败
return FALSE;
}
// 设定缓冲区大小
// 如果不设定,缓冲区大小默认值为 0,程序就只能按立即模式工作
// 如果要用缓冲模式工作,必须使缓冲区大小超过 0
DIPROPDWORD property;
= sizeof(DIPROPDWORD);
erSize = sizeof(DIPROPHEADER);
= 0;
= DIPH_DEVICE;
= DINPUT_BUFFERSIZE;
hr = lpKeyboard->SetProperty(DIPROP_BUFFERSIZE, &);
if FAILED(hr)
{
// 失败
return FALSE;
}
hr = lpKeyboard->Acquire();
if FAILED(hr)
{
// 失败
return FALSE;
}
return TRUE;
}
在这段代码中,我们首先定义了 lpDirectInput 和 lpKeyboard 两个指针,前者用来指向 DIRECTINPUT 对象,后者指向一个 DIRECTINPUTDEVICE 界面。
通过 DirectInputCreate(), 我们为 lpDirectInput 创建了一个 DIRECTINPUT 对象。然后我们调用 CreateDevice 来建立一个 DIRECTINPUTDEVICE 界面。参数 GUID_SysKeyboard 指明了建立的是键盘对象。
接下来 SetDataFormat 设定数据格式,SetCooperativeLevel 设定协作模式,SetProperty 设定缓冲区模式。因为这些函数方法的参数很多,我就不逐个去详细解释其作用了,请直接查看 DirectX 的帮助信息,那里面写得非常清楚。
完成这些工作以后,我们便调用 DIRECTINPUTDEVICE 对象的 Acquire 方法来激活对设备的访问权限。在此要特别说明一点,任何一个 DIRECTINPUT 设备,如果未经 Acquire,是无法进行访问的。还有,当系统切换到别的进程时,必须用 Unacquire 方法来释放访问权限,在系统切换回本进程时再调用 Acquire 来重新获得访问权限。
所以,我们通常要在 WindowProc 中做如下处理:
long FAR PASCAL WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_ACTIVATEAPP:
if(bActive)
{
if(lpKeyboard) lpKeyboard->Acquire();
}
else
{
if(lpKeyboard) lpKeyboard->Unacquire();
}
break;
...
}
哦,对了,前一段例程中还提到了立即模式和缓冲模式。在 DirectINPUT 中,这两种工作模式是有区别的。
如果使用立即模式的话,在查询数据时,只能返回查询时的设备状态。而缓冲模式则将记录所有设备状态变化过程。就个人喜好而言,笔者偏好后者,因为这样一般不会丢失任何按键信息。对应的,如果在使用前者时的查询频度太低,则很难保证采集数据的完整性。
DIRECTINPUT 的数据查询
立即模式的数据查询比较简单,请看下面的示例:
BYTE diks[256]; // DirectInput keyboard state buffer 键盘状态数据缓冲区
HRESULT UpdateInputState(void)
{
if(lpKeyboard != NULL) // 如果 lpKeyboard 对象界面存在
{
HRESULT hr;
hr = DIERR_INPUTLOST; // 为循环检测做准备
// if input is lost then acquire and keep trying
while(hr == DIERR_INPUTLOST)
{
// 读取输入设备状态值到状态数据缓冲区
hr = lpKeyboard->GetDeviceState(sizeof(diks), &diks);
if(hr == DIERR_INPUTLOST)
{
// DirectInput 报告输入流被中断
// 必须先重新调用 Acquire 方法,然后再试一次
hr = lpKeyboard->Acquire();
if(FAILED(hr))
return hr;
}
}
if(FAILED(hr))
return hr;
}
return S_OK;
}
在上面的示例中,关键处就是使用 GetDeviceState 方法来读取输入设备状态值以及对异常情况的处理。通过使用 GetDeviceState 方法,我们把输入设备的状态值放在了一个 256 字节的数组里。如果该数组中某个数组元素的最高位为 1,则表示相应编码的那个键此时正被按下。例如,如果 diks[1]&0x80>0,那么就表示 ESC 键正被按下。
学会了立即模式的数据查询以后,下面我们开始研究缓冲模式的情况:
HRESULT UpdateInputState(void)
{
DWORD i;
if(lpKeyboard != NULL)
{
DIDEVICEOBJECTDATA didod[DINPUT_BUFFERSIZE]; // Receives buffered data
DWORD dwElements;
HRESULT hr;
hr = DIERR_INPUTLOST;
while(hr != DI_OK)
{
dwElements = DINPUT_BUFFERSIZE;
hr = lpKeyboard->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), didod, &dwElements, 0);
if (hr != DI_OK)
{
// 发生了一个错误
// 这个错误有可能是 DI_BUFFEROVERFLOW 缓冲区溢出错误
// 但不管是哪种错误,都意味着同输入设备的联系被丢失了
// 这种错误引起的最严重的后果就是如果你按下一个键后还未松开时
// 发生了错误,就会丢失后面松开该键的消息。这样一来,你的程序
// 就可能以为该键尚未被松开,从而发生一些意想不到的情况
// 现在这段代码并未处理该错误
// 解决该问题的一个办法是,在出现这种错误时,就去调用一次
// GetDeviceState(),然后把结果同程序最后所记录的状态进行
// 比较,从而修正可能发生的错误
hr = lpKeyboard->Acquire();
if(FAILED(hr))
return hr;
}
}
if(FAILED(hr))
return hr;
}
// GetDeviceData() 同 GetDeviceState() 不一样,调用它之后,
// dwElements 将指明此次调用共读取到了几条缓冲区记录
// 我们再用一个循环来处理每条记录
for(int i=0; i { // 此处放入处理代码 // didod[i].dwOfs 表示那个键被按下或松开 // didod[i].dwData 记录此键的状态,低字节最高位是 1 表示按下,0 表示松开 // 一般用 didod[i].dwData&0x80 来测试 } return S_OK; } 其实,每条记录还有 dwTimeStamp 和 dwSequence 两个字段来记录消息发生的时间和序列编号,以便作更复杂的处理。本文是针对初学者写的,就不打算去谈论这些内容了。 DIRECTINPUT 的结束处理 我们在使用 DIRECTINPUT 时,还要注意的一件事就是当程序结束时,必须要进行释放处理,其演示代码如下: void ReleaseDInput(void) { if (lpDirectInput) { if(lpKeyboard) { // Always unacquire the device before calling Release(). lpKeyboard->Unacquire(); lpKeyboard->Release(); lpKeyboard = NULL; } lpDirectInput->Release(); lpDirectInput = NULL; } } 这段代码很简单,就是对 DIRECTINPUT 的各个对象去调用 Release 方法来释放资源。这种过程同使用 DIRECTX 的其它部分时是基本上相同的。 用Delphi + DirectX开发简单RPG游戏 提到 RPG (角色扮演游戏,Role Play Game),在座各位恐怕没有不熟悉的。从古老经典的 DOS 版《仙剑奇侠传》到新潮花哨的《轩辕剑》系列,无不以曲折优美的故事情节,美丽可人的主角,悦耳动情的背景音乐,震撼了每一个玩家的心灵。而说到 RPG,就不能不提 DirectX,因为 PC 上大部分的 RPG 都是用这个冬冬开发的。早在《轩辕剑叁外传——天之痕》推出的时候,我就曾想过用 DirectX 写一个自己的 RPG,自己来安排故事情节的发展,却总是因为这样或那样的事情,一直没有能够实现这个心愿。在耗费了宝贵的几年青春,搞定了诸如考试、恋爱、出国等琐碎杂事之后,我终于可以在这个 SARS 肆虐的时代,坐在陪伴了我整个大学生涯的电脑前,听着颓废而又声嘶力竭的不知名歌曲,写一些一直想写却没有写的东西。 DirectX 简介 DirectX 对于大多数游戏爱好者来说都不陌生(当然,那些只在DOS下艰苦作战的朋友例外),在安装一个游戏前,系统总是会提示你是否需要同时升级 DirectX。简单地说,DirectX 就是一系列的 DLL (动态连接库),通过这些 DLL,开发者可以在无视于设备差异的情况下访问底层的硬件。DirectX 封装了一些 COM(Component Object Model)对象,这些 COM 对象为访问系统硬件提供了一个主要的接口。首先,我们先来看一下 DirectX 的结构: 图1:DirectX 基本结构 DirectX 目前主要由以下七个主要部分组成: DirectDraw – 为程序直接访问显存提供接口,同时和其它的Windows应用程序保持兼容。 Direct3D – 为访问3D加速设备提供接口。 DirectInput – 为各种输入设备提供接口,比如鼠标,键盘,力反馈游戏手柄和操纵杆等。 DirectPlay – 为游戏提供网络功能接口,比如支持通过 TCP/I、IPX 等协议进行游戏中的数据传输。 DirectSound – 为访问声卡提供接口,支持WAV、MIDI 等文件的直接播放。 DirectSound3D –通过此接口,可以模拟出某一个声音在三维空间中任何一个位置的播放所产生的效果,从而达到逼真的环绕立体声。 DirectMusic – 此接口主要是生成一系列的原始声音采样反馈给相应的用户事件。 开发工具(Delphi & DelphiX) 下一步,我们来介绍开发工具。我们通常所安装的其实只有 DirectX 的运行库(一系列封装好的DLL文件),其内部函数结构非常复杂,所以我们还需要 DirectX 的开发工具。所谓工欲善其事,必先利其器,虽然微软公布了 DirectX SDK,但是由于所有的头文件都是用 C/C++ 写成的,作为 Delphi 的热情拥护者,我们还是无从下手。把 C/C++ 写成的代码转换成 Pascal 可不是一件容易的事,但是不必担心,这项工作已经有人做好了。日本人 Hiroyuki Hori 为 Delphi 写了一个免费的组件包,称作 DelphiX。这些组件可以使得开发者可以轻松地访问 DirectX 的 DirectDraw、Direct3D、DirectSound、DirectInput(支持力反馈手柄)和 DirectPlay 对象。目前的 DelphiX 包支持 Borland Delphi 3/4/5/6/7 和 DirectX 7.0 以上版本(见图2)。安装了 DelphiX 之后,我们将不需要再安装微软的 DirectX SDK。在这篇文章里我们将使用的就是 DelphiX。 提到 RPG (角色扮演游戏,Role Play Game),在座各位恐怕没有不熟悉的。从古老经典的 DOS 版《仙剑奇侠传》到新潮花哨的《轩辕剑》系列,无不以曲折优美的故事情节,美丽可人的主角,悦耳动情的背景音乐,震撼了每一个玩家的心灵。而说到 RPG,就不能不提 DirectX,因为 PC 上大部分的 RPG 都是用这个冬冬开发的。早在《轩辕剑叁外传——天之痕》推出的时候,我就曾想过用 DirectX 写一个自己的 RPG,自己来安排故事情节的发展,却总是因为这样或那样的事情,一直没有能够实现这个心愿。在耗费了宝贵的几年青春,搞定了诸如考试、恋爱、出国等琐碎杂事之后,我终于可以在这个 SARS 肆虐的时代,坐在陪伴了我整个大学生涯的电脑前,听着颓废而又声嘶力竭的不知名歌曲,写一些一直想写却没有写的东西。 DirectX 简介 DirectX 对于大多数游戏爱好者来说都不陌生(当然,那些只在DOS下艰苦作战的朋友例外),在安装一个游戏前,系统总是会提示你是否需要同时升级 DirectX。简单地说,DirectX 就是一系列的 DLL (动态连接库),通过这些 DLL,开发者可以在无视于设备差异的情况下访问底层的硬件。DirectX 封装了一些 COM(Component Object Model)对象,这些 COM 对象为访问系统硬件提供了一个主要的接口。首先,我们先来看一下 DirectX 的结构: 图1:DirectX 基本结构 DirectX 目前主要由以下七个主要部分组成: DirectDraw – 为程序直接访问显存提供接口,同时和其它的Windows应用程序保持兼容。 Direct3D – 为访问3D加速设备提供接口。 DirectInput – 为各种输入设备提供接口,比如鼠标,键盘,力反馈游戏手柄和操纵杆等。 DirectPlay – 为游戏提供网络功能接口,比如支持通过 TCP/I、IPX 等协议进行游戏中的数据传输。 DirectSound – 为访问声卡提供接口,支持WAV、MIDI 等文件的直接播放。 DirectSound3D –通过此接口,可以模拟出某一个声音在三维空间中任何一个位置的播放所产生的效果,从而达到逼真的环绕立体声。 DirectMusic – 此接口主要是生成一系列的原始声音采样反馈给相应的用户事件。 开发工具(Delphi & DelphiX) 下一步,我们来介绍开发工具。我们通常所安装的其实只有 DirectX 的运行库(一系列封装好的DLL文件),其内部函数结构非常复杂,所以我们还需要 DirectX 的开发工具。所谓工欲善其事,必先利其器,虽然微软公布了 DirectX SDK,但是由于所有的头文件都是用 C/C++ 写成的,作为 Delphi 的热情拥护者,我们还是无从下手。把 C/C++ 写成的代码转换成 Pascal 可不是一件容易的事,但是不必担心,这项工作已经有人做好了。日本人 Hiroyuki Hori 为 Delphi 写了一个免费的组件包,称作 DelphiX。这些组件可以使得开发者可以轻松地访问 DirectX 的 DirectDraw、Direct3D、DirectSound、DirectInput(支持力反馈手柄)和 DirectPlay 对象。目前的 DelphiX 包支持 Borland Delphi 3/4/5/6/7 和 DirectX 7.0 以上版本(见图2)。安装了 DelphiX 之后,我们将不需要再安装微软的 DirectX SDK。在这篇文章里我们将使用的就是 DelphiX。 图中的用黑色填充部分就是预先所定义的透明区域,在游戏中,我们按照白色线条可以将整个图片分别切开成9块,每块的大小均为80×50。我们先从左向右,然后在从上往下对起进行编号为1~9。如果我们将这九张图片依次循环显示出来,并且设置播放的速度为每秒24张,我们就可以得到一个飞船在旋转的动画,也就是一个飞船精灵。 开发过程 说了许多废话以后,下面我们脱离纸上谈兵,开始正式的开发。在本例中,我们主要实现用鼠标来控制精灵往八个方向行走。所有图片均来自大宇公司《轩辕剑叁——天之痕》,其中精灵采用故事中陈靖仇的形象特此致谢。同时,请各位读者勿把这些图片用于商业用途,否则后果自负。[page] 打开 Delphi 并新建一个应用程序,依次选中 DelphiX 组件栏的 TDXDraw、TDXImageList、TDXInput、TDXTimer、TDXSpriteEngine 组件,添加到用户区,分别命名为 DXDraw、DXImageList、DXInput、DXTimer、DXSpriteEngine,按照下表设置其各项属性。对于 DXImageList,点击 Object Inspector 中的 Items,在其中加入两张位图(和),分别命名为 background 和 player,设置 player 的 PatternHeight 和 PatternWidth 均为120象素,设置其 transparentcolor 为粉红色 (clFuchsia)。 控件 属性 值 DXDraw Align alClient 800 600 nt 24 Options [doAllowReboot, doWaitVBlank, doCenter, doFlip] AutoInitialize True DXTimer Enabled True [page] Interval 0 DXInput putState True d True DXSpriteEngine DXDraw DXDraw 下面就是全部的源程序,请先在 Delphi 中产生相应事件然后填入代码,最后按下F9运行就可以运行程序了。用鼠标点击你的目的地,陈靖仇就会自动跑到指定地点。尝试一下开发一些简单的游戏吧,用 DelphiX 这把牛刀!所有程序在 Delphi 4.0 + DirectX 8.0 环境下测试通过。本文所需控件可以在 这里 下载。 Unit Main; Interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Menus, DXClass, DXSprite, DXInput, DXDraws; type TDirection = (DrUp, DrDown, DrLeft, DrRight, DrUpLeft, DrUpRight, DrDownLeft, DrDownRight); {自定义游戏中所用到的方向} TPlayerSprite = class (TImageSprite) CanMove: Boolean; protected procedure DoMove(MoveCount: Integer); override; procedure MoveTo(MoveCount:Integer; Direction: TDirection); procedure DoCollision(Sprite: TSprite; var Done: Boolean); override; end; TMainForm = class(TDXForm) {此处使用优化过的TDXForm来代替TForm} DXTimer: TDXTimer; DXDraw: TDXDraw; DXSpriteEngine: TDXSpriteEngine; DXInput: TDXInput; ImageList: TDXImageList; procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure DXDrawFinalize(Sender: TObject); procedure DXDrawInitialize(Sender: TObject); procedure FormCreate(Sender: TObject); procedure DXTimerTimer(Sender: TObject; LagCount: Integer); procedure DXDrawMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure DXDrawMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); procedure DXDrawMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure FormClose(Sender: TObject; var Action: TCloseAction); private AnchorX: Integer; AnchorY: Integer; {鼠标点击发生的位置} MouseX: Integer; MouseY: Integer; {鼠标当前位置} PlayerSprite: TPlayerSprite; {游戏中我们所用鼠标控制的人物} BackSprite: TBackGroundSprite; {游戏的背景图} end; const speed=5; {游戏人物向各个方向运动时的动画播放速度} var MainForm: TMainForm; Steps: Integer; {用于控制切换精灵动画图片的参数} implementation {$R *.DFM} procedure ision(Sprite: TSprite; var Done: Boolean); begin Done:=False; {已经侦测到碰撞,不再重复检测碰撞} {检测游戏人物是否与其它精灵发生了碰撞,此处可以扩展为对话等情节} end; procedure (MoveCount: Integer); var l,r,d,u: Boolean; absX,absY: Integer; {游戏人物的当前位置与目的地的绝对距离} begin inherited DoMove(MoveCount); MoveCount:=Trunc(MoveCount*1.5); l:=false; r:=false; u:=false; d:=false; if (Trunc(X)-X>0) then l:=true else r:=true; if (Trunc(Y)-Y>0) then u:=true else d:=true; absX:=abs(Trunc(X)-X); absY:=abs(Trunc(Y)-Y); if absX<4 then begin l:=false; r:=false; end; if absY<4 then begin u:=false; d:=false; end; {如果绝对距离已经小于四个象素,则认为已经到达目的地} if u and l and not d and not r then MoveTo(MoveCount,DrUpLeft); if u and r and not l and not d then MoveTo(MoveCount,DrUpRight); if d and l and not r and not u then MoveTo(MoveCount,DrDownLeft); if d and r and not u and not l then MoveTo(MoveCount,DrDownRight); if d and not l and not r and not u then MoveTo(MoveCount,DrDown); if u and not l and not r and not d then MoveTo(MoveCount,DrUp); if l and not u and not r and not d then MoveTo(MoveCount,DrLeft); if r and not l and not u and not d then MoveTo(MoveCount,DrRight); {根据目的地来判断运动的方向,从而播放相应方向运动的动画} Collision; {检测碰撞} Engine.X := -X+ div 2 - Width div 2; Engine.Y := -Y+ div 2 - Height div 2; {移动引擎,从而是游戏人物处于舞台的正中央} end; procedure rTimer(Sender: TObject; LagCount: Integer); begin if not w then exit; {检测DXDraw是否可以画,否则退出} ; {捕捉各类设备输入,这里我们用来检测鼠标的输入} LagCount := 1000 div 60; {用来控制整个游戏运行速度的参数} (LagCount); ; (0); {将整个屏幕填充为黑色} ; with do begin :=bsclear; :=psclear; :=clwhite; :=clWhite; :=10; textout(10,10,'Press ESC to Quit'); textout(100,100,'X: '+IntToStr(AnchorX)+'Y: '+IntToStr(AnchorY)); {鼠标点击的位置经转换后在游戏世界中的坐标} textout(100,200,'Sprit x:'+IntToStr(Trunc(PlayerSprite.x))+'Y: ' +IntToStr(Trunc(PlayerSprite.y))); {精灵在游戏世界中的坐标} textout(100,300,'Relative x:'+IntToStr(AnchorX-Trunc(PlayerSprite.x))+'Y: ' +IntToStr(AnchorY-Trunc (PlayerSprite.y))); {精灵当前位置与目的地之间的绝对距离} textout(200,100,'Mouse x:'+IntToStr()+'Y: ' +IntToStr()); {鼠标当前位置,相对于窗口左上角,未转换为游戏世界坐标} Release; end; {在字母上输出相应参数,用于程序调试} ; {将内存中的后台表面翻转到当前并且显示} end; procedure Finalize(Sender: TObject); begin d := False; {关闭定时器} end; procedure Initialize(Sender: TObject); begin d := True; {启动定时器} end; procedure eate(Sender: TObject); begin Steps:=0; AnchorX:=0; AnchorY:=0; MouseX:=320; MouseY:=240; {默认使鼠标处于屏幕的中央} lorTable; able := able; orTable := able; Palette; {更新系统调色板} BackSprite:=(); with TBackgroundSprite(BackSprite) do begin SetMapSize(1, 1);{设定背景显示样式为1×1} Image := ('background'); {载入背景图片} Z := -2; {设定背景层次} Tile := True; {设定背景填充样式为平铺} end; PlayerSprite := (); with TPlayerSprite(PlayerSprite) do begin Image := ('player'); Z := 2; Width := ; Height := ; end; {载入游戏人物} end; procedure yDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin {如果按了Esc,则退出} if Key=VK_ESCAPE then Close; {全屏模式和窗口模式的切换} if (ssAlt in Shift) and (Key=VK_RETURN) then begin ze; if doFullScreen in s then begin RestoreWindow; := crNone; BorderStyle := bsSizeable; s := s - [doFullScreen]; end else begin StoreWindow; := crNone; BorderStyle := bsNone; s := s + [doFullScreen]; end; lize; end; end; procedure MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin AnchorX := x + Trunc(PlayerSprite.x)-320; AnchorY := y + Trunc(PlayerSprite.y)-240; {将鼠标在屏幕上点击的位置转换到游戏世界中} e:=True; {此参数允许鼠标拖动} end; procedure MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if e then begin AnchorX := x + Trunc(PlayerSprite.x)-320; AnchorY := y + Trunc(PlayerSprite.y)-240; {在鼠标拖动过程中将鼠标在屏幕上点击的位置转换到游戏世界中} end; MouseX:=X; MouseY:=Y; {鼠标当前位置} end; procedure MouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin e:=False; end; procedure ose(Sender: TObject; var Action: TCloseAction); begin ; end; procedure (MoveCount: Integer; Direction: TDirection); begin {控制精灵往各个方向移动} case Direction of DrUp: begin Y := Y-(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+20+1; {当前动画中播放的图片序号} if steps>4*speed-2 then steps:=0; end; DrDown: begin Y := Y+(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+1; if steps> 4*speed-2 then steps:=0; end; DrLeft: begin X := X-(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+10+1; if steps>4*speed-2 then steps:=0; end; DrRight: begin X := X+(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+30+1; if steps>4*speed-2 then steps:=0; end; DrUpLeft: begin X := X-(150/1000)*MoveCount; Y := Y-(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+15+1; if steps>4*speed-2 then steps:=0; end; DrUpRight: begin X := X+(150/1000)*MoveCount; Y := Y-(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+25+1; if steps>4*speed-2 then steps:=0; end; DrDownLeft: begin X := X-(150/1000)*MoveCount; Y := Y+(150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+5+1; if steps>4*speed-2 then steps:=0; end; DrDownRight: begin X := X + (150/1000)*MoveCount; Y := Y + (150/1000)*MoveCount; Inc(steps); AnimPos:=steps div speed+35+1; if steps>4*speed-2 then steps:=0; end; end; end; end. 后记 通过以上的讲解和例子,相信大家已经对 Delphi 下的 DirectX 游戏开发有了初步的概念。国内讲解开发 DirectX 游戏的权威资料很少. Delphi作为一种方便的可视化程序设计语言,一 直非常受大家喜爱。但它在图形处理、3D表现等方面 不很让人满意。如果说你要开发一个Windows95下的 3D游戏,你会用什么工具呢?DirectX!不少人会不加 思索地答道。然而接触过DirectX的朋友们都知道它 内部的结构复杂,一般来说结合VC开发是一个理想 的组合。而要在Delphi中利用DirectX SDK真是难上 加难。但现在一切都好啦,有了本文介绍的DelphiX 组件,你终于可以用你熟悉的Delphi来开发漂亮的图 形程序了。 DelphiX是由日本人Hiroyuki Hori开发的使Di rectX5.0在Delphi中更容易使用的一套控件,从网上 下载时叫,837KB。解开后在bin目录下 运行install_for?(根据你的Delphi版本号,支持3.0和 4.0),DelphiX会自动将控件安装到你的Delphi中,帮 助文件也自动融合到Delphi的帮助里,真是好用极 了! DelphiX包括的控件有如下这些: TDXDraw 最重要的控件,是DirectDraw和Direct3D的基础; TDXDIB 一个代表DIB图像的控件; TDXImageList 代表一组 TPicture; TDX3D Direct3D控件,要与TDXDraw共同使用; TDXSound DirectSound控件; TDXWave 一个代表波形Wave的控件; TDXWaveList 一组Wave; TDXInput 输入控制控件(操纵键盘和摇杆要靠它); TDXPlay 通讯控件; TDXSpriteEngine “精灵”引擎; TDXTimer 高速时间控件; TDXPaintbox TDXForm 专为DelphiX优化过的Form。 DirectDraw中重要的对象有: TDirectDraw对象 DirectDraw应用程序的核心,它是你创建的第一个对象。创建了DirectDraw对 象后,可以在它的基础上创建其它所有相关的对象。 在DelphiX中的属性即是一个Tdi rectDraw对象。 TDirectDrawSurface对象 表征了一块内存区 域,在该区域的数据将作为图像显示在屏幕上或移动 到其它表面上。 TDirectDrawPalette对象 表征了一个用于表面 的16色或256色的索引调色板,它包含了一系列描述 同表面相关的RGB颜色索引值。 TDirectDrawClipper对象 帮助你禁止向表面的 某一位置或超出表面的位置块写数据。 TSprite对象 代表了“精灵”,在许多视频游戏都 使用了精灵。从最基本的意义上来讲,一个精灵就是 在屏幕上移动的图像。精灵画在一个表面上,覆盖在 已有的背景上,合成后的图像被送到屏幕上显示出 来,在DelphiX中通过TspriteEngine实现对Tsprite的 控制。 TDirectDrawSurfaceCanvas对象 提供方便的 访问机制,你可以像访问一般Canvas对象一样访问 它。即是这样一个对象。 TdirectDrawDisplay对象 控制着DirectDraw的 显示模式,y是这样一个对象。 DelphiX基本上严格按照Microsoft DirectX SDK 开发包来将其功能在Delphi中实现出来。所以,如果 你对DirectX SDK比较熟悉的话,你会发现大多数程 序从C移植到Delphi是很容易的事。DelphiX中没有 提供帮助的地方,你可以在DirectX SDK中获得答 案。 下面通过一个简单的例子对如何利用DelphiX编 程作一介绍,只涉及Ddraw二维的一小部分。 unit Unit1; interface uses es, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DXClass, DXDraws, DIB; type TForm1=class(TDXForm) DXDraw1:TDXDraw; DXTimer1:TDXTimer; DXDIB1:TDXDIB; procedure DXDrawlFinalize(Sender:TObject); procedure DXDraw1Initialize(Sender:TObject); procedure DXDraw1RestoreSurfase(Sender:TObject); procedure DXTimer1Timer(Sendsr:TObject;LagCount:Integer); procedure FormActivate(Sender:TObject); private {Private declarations} public {Public declarations} private Fsurface:TDirectDrawSurface; end; var Form1:TForm1; x,y:integer; implementation {$R *.DFM} procedure lInitialize(Sender:TObject); begin FSurface:=(); {创建抽象图像表面} end; procedure 1Finalize(Sender:TObject); begin ; FSurface: = nil; {释放表面} end; procedure 1RestoreSurfare(Sender:TObject); begin omGraphic();{将位图送入抽象表面} end; procedure r1Timer(t;LagCount integer); begin if not w then Exit;{在绘制之前检验是否允许} (0); x: = x + 1; y: = y + 1; (X, Y, Rect,FSur face, True); ; {将内存中的表面再将映射入实际显存} end; procedure tivate(Sender:TObject); begin X: =0; y: =0; end; end. 首先要在程序开头包含DXClass、DXDraws两个 Unit(TDXtimer、TDXForm在DXClass中定义,TDX Draw在DXDraws中定义)。程序用的Form不要从一 般的TForm派生,而应从TDXForm中派生。在Form 中放入TDXDraw控件和TDXTimer控件,将其inter val值设为0。对DXDraw1控件改变其属性,使其与 Form大小一致。再放人一个TDXDIB控件,在其属性 中的DIB一项调人一个位图,如Windows下的bub 。定义一个DirectDraw表面Fsurface,这是一 个抽象内存表面,以后会将它映射到DXDraw对象的 Surface上。在DXDraw的Events一栏中编写OnIni tialize、OnFinalize、OnRestoreSurface三个过程,之后便 可以根据需要对表面进行操纵。 另外,要完成上面程序的功能,还有其他不少办 法。例如利用TDXImageList对象,可以用它的方法 [N].draw来完成同样功能。 总之,DirectX博大精深,希望本文能为大家掀开 冰山的一角。DelphiX可以在pigprince的个人主页上 找到,网址是Http:///~pigprins
版权声明:本文标题:DelphiX游戏教程 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1704892848a465908.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论