站内搜索

用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。


  TDXDraw DirectDraw 和 Direct3D 组件 
  TDXDIB 容纳DIB(设备无关位图,Device Independent Bitmap)的组件 
  TDXImageList 图片列表组件 
  TDX3D Direct3D 组件 (和TDXDraw一起使用) 
  TDXSound DirectSound 组件 
  TDXWave 容纳 Wave(波形音频文件)的组件 
  TDXWaveList Wave文件列表组件 
  TDXInput 输入组件,包括键盘和手柄输入 
  TDXPlay 通讯组件,用于网络游戏开发 
  TDXSpriteEngine 精灵引擎,用于管理游戏中产生的精灵(Sprite)详见游戏开发过程 
  TDXTimer 高速定时器,比 TTimer 要更快,更准确 
  TDXPaintBox TpaintBox 的 DIB 版本 
  TDXForm 优化过的 TForm,专门用于游戏开发 

  动画原理

  在介绍动画的原理之前,我们先来介绍几个DirectX的术语。

  表面(Surfaces)

  这里我们暂且称作表面吧。在 DirectX 中,显存中被用来存储各种位图和视频缓冲的部分均被称作表面。而当前被显示在屏幕上的表面被称作主表面(primary surface)。在实验动画的过程中,我们通常要预先生成至少一个不显示的表面,称作后台表面(offscreen surface)。在需要产生动画的时候,我们可以直接将后台表面中的内容显示到屏幕上,而将原来主表面翻转到后台而不再显示。这么说可能大家更容易明白,我们大家在小的时候都玩过在书上画小人的游戏吧。在每页书的下方空白处画上一个个小人,每页上的内容逐渐发生变化,然后快速翻动整本书,就可以看到有小人在跳动。我们现在的主表面就是你现在正看到那页书,后面的那些还没有翻到的页面就是后台表面,通过翻动实现了动画效果。在运行一些大型游戏的时候,在显存已经用完的情况下,游戏将自动调用系统内存。目前而言,想要流畅地运行一个 DirectX 游戏,非 3D 类的最好有 4M 以上的显存,3D 类的则显存越大越好。

  每秒帧数(Frames per Second)

  通常简称 FPS,用我们刚才举的例子来描述,也就是指我们每秒所翻过的书的页数。当帧数达到24帧每秒时,人眼已经无法分辨,从而认为画面是连续的。电影采用的24帧每秒,而电视采用的是25帧每秒。实际上,在日本的动画制作中有一拍二(12帧每秒)甚至一拍三(8帧每秒)的做法,同样可做出欺骗肉眼的效果,这个我们暂且不作讨论。在游戏中,我们将采用24帧或25帧每秒的速度以达到欺骗肉眼的效果。

  位图和精灵(Bitmaps and Sprites)

  精灵是指在游戏中用来实现动画效果的一组图片,这些图片通常采用位图格式,并且有用特定颜色填充的透明区域。如下图,就是一个精灵。



  图中的用黑色填充部分就是预先所定义的透明区域,在游戏中,我们按照白色线条可以将整个图片分别切开成9块,每块的大小均为80×50。我们先从左向右,然后在从上往下对起进行编号为1~9。如果我们将这九张图片依次循环显示出来,并且设置播放的速度为每秒24张,我们就可以得到一个飞船在旋转的动画,也就是一个飞船精灵。
  
  开发过程

   说了许多废话以后,下面我们脱离纸上谈兵,开始正式的开发。在本例中,我们主要实现用鼠标来控制精灵往八个方向行走。所有图片均来自大宇公司《轩辕剑叁――天之痕》,其中精灵采用故事中陈靖仇的形象特此致谢。同时,请各位读者勿把这些图片用于商业用途,否则后果自负。


   打开 Delphi 并新建一个应用程序,依次选中 DelphiX 组件栏的 TDXDraw、TDXImageList、TDXInput、TDXTimer、TDXSpriteEngine 组件,添加到用户区,分别命名为 DXDraw、DXImageList、DXInput、DXTimer、DXSpriteEngine,按照下表设置其各项属性。对于 DXImageList,点击 Object Inspector 中的 Items,在其中加入两张位图(background.bmp和player.bmp),分别命名为 background 和 player,设置 player 的 PatternHeight 和 PatternWidth 均为120象素,设置其 transparentcolor 为粉红色(clFuchsia)。

  控件 属性 值 

  DXDraw Align alClient 
  Display.Width 800 
  Display.Height 600 
  Display.BitCount 24 
  Options [doAllowReboot, doWaitVBlank, doCenter, doFlip] 
  AutoInitialize True 
  DXTimer Enabled True 

  Interval 0 
  DXInput Mouse.BindInputState True 
  Mouse.Enabled 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 TPlayerSprite.DoCollision(Sprite: TSprite; var Done: Boolean);
  begin
  Done:=False; {已经侦测到碰撞,不再重复检测碰撞}
  {检测游戏人物是否与其它精灵发生了碰撞,此处可以扩展为对话等情节}
  end;

  procedure TPlayerSprite.DoMove(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)-MainForm.AnchorX>0) then l:=true else r:=true;
  if (Trunc(Y)-MainForm.AnchorY>0) then u:=true else d:=true;absX:=abs(Trunc(X)-MainForm.AnchorX);
  absY:=abs(Trunc(Y)-MainForm.AnchorY);

  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+Engine.Width div 2 - Width div 2;
  Engine.Y := -Y+Engine.Height div 2 - Height div 2;
  {移动引擎,从而是游戏人物处于舞台的正中央}
  end;

  procedure TMainForm.DXTimerTimer(Sender: TObject; LagCount: Integer);
  begin
  if not DXDraw.CanDraw then exit;
  {检测DXDraw是否可以画,否则退出}
  DXInput.Update;
  {捕捉各类设备输入,这里我们用来检测鼠标的输入}

  LagCount := 1000 div 60;
  {用来控制整个游戏运行速度的参数}
  DXSpriteEngine.Move(LagCount);
  DXSpriteEngine.Dead;

  DXDraw.Surface.Fill(0);
  {将整个屏幕填充为黑色}
  DXSpriteEngine.Draw;

  with DXDraw.Surface.Canvas do
  begin
  brush.style:=bsclear;
  pen.style:=psclear;
  pen.color:=clwhite;
  Font.Color:=clWhite;
  Font.Size:=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(MainForm.MouseX)+'Y: ' +IntToStr(MainForm.MouseY));
  {鼠标当前位置,相对于窗口左上角,未转换为游戏世界坐标}
  Release;
  end;
  {在字母上输出相应参数,用于程序调试}
  DXDraw.Flip;
  {将内存中的后台表面翻转到当前并且显示}
  end;

  procedure TMainForm.DXDrawFinalize(Sender: TObject);
  begin
  DXTimer.Enabled := False;
  {关闭定时器}
  end;

  procedure TMainForm.DXDrawInitialize(Sender: TObject);
  begin
  DXTimer.Enabled := True;
  {启动定时器}
  end;

  procedure TMainForm.FormCreate(Sender: TObject);
  begin
  Steps:=0;
  AnchorX:=0;
  AnchorY:=0;

  MouseX:=320;
  MouseY:=240;
  {默认使鼠标处于屏幕的中央}

  ImageList.Items.MakeColorTable;

  DXDraw.ColorTable := ImageList.Items.ColorTable;
  DXDraw.DefColorTable := ImageList.Items.ColorTable;
  DXDraw.UpdatePalette;
  {更新系统调色板}

  BackSprite:=TBackgroundSprite.Create(DXSpriteEngine.Engine);
  with TBackgroundSprite(BackSprite) do
  begin
  SetMapSize(1, 1);{设定背景显示样式为1×1}
  Image := ImageList.Items.Find('background'); {载入背景图片}
  Z := -2; {设定背景层次}
  Tile := True; {设定背景填充样式为平铺}
  end;

  PlayerSprite := TPlayerSprite.Create(DXSpriteEngine.Engine);
  with TPlayerSprite(PlayerSprite) do
  begin
  Image := ImageList.Items.Find('player');
  Z := 2;
  Width := Image.Width;
  Height := Image.Height;
  end;
  {载入游戏人物}
  end;

  procedure TMainForm.FormKeyDown(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
  DXDraw.Finalize;

  if doFullScreen in DXDraw.Options then
  begin
  RestoreWindow;

  DXDraw.Cursor := crNone;
  BorderStyle := bsSizeable;
  DXDraw.Options := DXDraw.Options - [doFullScreen];
  end else
  begin
  StoreWindow;

  DXDraw.Cursor := crNone;
  BorderStyle := bsNone;
  DXDraw.Options := DXDraw.Options + [doFullScreen];
  end;

  DXDraw.Initialize;
  end;
  end;

  procedure TMainForm.DXDrawMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
  begin
  AnchorX := x + Trunc(PlayerSprite.x)-320;
  AnchorY := y + Trunc(PlayerSprite.y)-240;
  {将鼠标在屏幕上点击的位置转换到游戏世界中}
  PlayerSprite.CanMove:=True;
  {此参数允许鼠标拖动}
  end;

  procedure TMainForm.DXDrawMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
  begin
  if PlayerSprite.CanMove then
  begin
  AnchorX := x + Trunc(PlayerSprite.x)-320;
  AnchorY := y + Trunc(PlayerSprite.y)-240;
  {在鼠标拖动过程中将鼠标在屏幕上点击的位置转换到游戏世界中}
  end;

  MouseX:=X;
  MouseY:=Y;
  {鼠标当前位置}
  end;

  procedure TMainForm.DXDrawMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
  begin
  PlayerSprite.CanMove:=False;
  end;

  procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
  begin
  DXSpriteEngine.Free;
  end;

  procedure TPlayerSprite.MoveTo(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 游戏的权威资料很少,如果这篇讲解可以起到抛砖引玉的效果的话,我将再高兴不过了。本文部分内容借鉴了国外一些游戏开发网站的技术文档,在此一并致谢。如果各位有什么问题需要和我交流,请发信至eagle_california@yahoo.com。

  • 上一篇:Delphi一点通:如何将源代码学好
  • 下一篇:Delphi与Word之间的融合技术