【翻译】Managed DirectX(第六章)

Managed DirectX9.0中文教程 Managed DirectX9.0中文教程 有需要的就下吧 立即下载
第六章 使用Managed DirectX编写游戏

翻译:clayman

clayman_joe@yahoo.com.cn

仅供个人学习之用,勿用于任何商业用途,转载请注明作者^_^


选择游戏
      虽然很多关于3D游戏编程的高级主题还没有讨论,但我们已经有足够的背景知识来写一个简单游戏了。这一章,我们将使用至今学过的知识,再加上一点点新的东西来创建游戏。
     真正开始写游戏之前,最好先拟一份计划。我们需要确定写什么类型的游戏,它将有哪些最基本的特性,等等。考虑到目前的技术限制,自然不能写太复杂的游戏。这将是一个简单的游戏。在MS-DOS环境下,曾经有一个叫做“Donkey”的游戏,玩家控制着车不能撞到路上的donkey。听起来足够简单吧,我们将创建一个三维版本,并且用普通的障碍物来代替donkey。这个游戏叫做“躲避者(Dodger)”。
     开始编码之前,需要花一点时间来策划和设计游戏。我们需要怎样的游戏,玩的时候来控制。Well,显然,要有一个Car类来控制交通工具。接下来,使用另一个类来控制障碍物将会很不错。除此之外,主要的游戏引擎类必须完成所有的渲染操作并把所有对象组织起来。
     如果尝试商业游戏,那么大部分时间将会花在游戏创意上。游戏创意将会写成详细的文档,包括了游戏主题和特性的各种细节。本书的着重于讨论游戏的实际开发工作,而不是游戏发行和创意,所以我们将略过这一步。
     通常开之发写还必须写完整的技术文档(technical specification)(简称为spec)。它包以适当的细节列出了所以类,以及需要实现的各种方法、属性。通常还包括表示对象之间关系的UML图。这份文档的目的是让你在编码前坐下来认真考虑程序的设计。由于本书聚焦于代码的编写,我们同样略过这一步。需要说明的是,强烈建议你在写任何代码前花点时间撰写技术文档。
 
编写游戏
     现在可以打开VS创建项目了。创建一个名为Dodger的windows应用程序。使用DodgerGame代替代码中所有出现Form1的地方。添加对DirectX程序集的引用。创建私有的device成员,如下修改构造函数:
      public DodgerGame()
     {
         this.Size = new Size(800,600);
         this.Text = “Dodger Game”;
         this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque,true);
}
这将会把窗口设置为800×600(注:实际代码中我将会创建一个全屏的游戏,另外如果现在运行程序,会发现我们创建了一个透明的窗口),设置窗口标题和样式(style),这样渲染代码才会正常工作。接下来修改程序的入口点:
static void Main() {详见源码}
这个应该很熟悉了吧,基本上就是之前每一章用来启动程序的代码。创建窗体、初始化图形引擎,运行窗体。在initializeGraphics内做如下改动:
private void InitializeGraphics() {详见源码};
创建了presentation parameters结构之后,确保有它有深度缓冲。这里有什么新内容呢?首先,保存了默认的适配器的序数号,接下来保存了creation flags,并把它的默认值设为software vertex processing。但是,现代图形卡都在硬件层实现了vertex processing。何必把宝贵的CPU资源用在显卡可以完成的任务上呢?答案是不需要这样做,但你不知道是否真的支持这种特性,于是有了接下来的代码。在真正创建device之前,需要先保存显卡的功能(capabilities,简称Caps),这样可以用来决定使用那一种flags创建device。因为你只是创建一个硬件设备,所以只储存这几个Caps就可以了。关于检查适配器所有Caps的内容回忆一下第二章吧。
还记得使用顶点缓冲时需要在重置设备之后重建缓冲吗?我们为device订阅了created事件。当device重置之后,设定device的所有默认状态,添加如下代码:
private void OnDeviceReset(object sender,EventArgs e) {详见源码};
(注意:类似于这里的代码,你可能会使用一个层(layer)来检查支持的灯光。这种情况下,先检查是否支持一盏灯,如果可以,则创建它。然后再用类似的方法检测是否支持第二盏灯。这样即使最差的情况你也能获得一盏灯光)
这里和前面学过的代码也很类似,通过projection fransform和view transform来设置摄像机。对于这个游戏来说,我们的摄像机不需要移动,所以只需要在重置设备之后设置一次就可以了(与设备相关的状态都会在重值之后丢失)。
环境光不是最好的选择,我们已经知道他不能产生真实的光影效果,所以方向光将是不错的选择。但并不能确定设备是否支持这种光源。创建了设备之后,就不需要再使用先前的Caps结构了,device会为你保留着这些信息。如果device支持方向光,而且支持一盏以上的灯光,你应该使用它;否则,使用默认的环境光。它虽然不真实,但总比黑色的场景要好吧。最后,重载OnPaint方法,:
protected override void OnPaint(PaintEventArgs e){详见源码};
这里没有什么新内容,当然你可以把背景改为任何你喜欢的颜色。现在已经为加载模型做好了准备。创建变量来储存.X文件中的赛道模型吧。
     private Mesh roadMesh = null;
     private Material[] roadMaterials = null;
     private Texture[] roadTextures = null;
接下来修改一下前一章里的load mesh方法。最大的改变是将把它改为静态方法,因为不止一个类会调用它,同样把所有的材质和纹理作为参数来传递,而不是作为类成员来访问。添加如下代码:
public static Mesh LoadMesh(Device device,string file,ref Material[] meshMaterials,ref Texture[] meshTextures){详见源码};
这个方法前面已经深入讨论过了。使用这个方法来加载赛道模型,还需要在重置设备的事件里添加它,在OnDeviceReset最后加上如下代码:
roadMesh = LoadMesh(device,@"../../road.x",ref roadMaterials,ref roadTextures);
确定你已经把赛道模型和纹理文件复制到了源文件的目录下。这段代码将会加载模型以及纹理,并储存纹理、裁制以及模型。每一帧道路mesh都需要渲染很多次,因该创建一个方法来完成渲染工作。添加如下代码:
private void DrawRoad(float x, float y ,float z) {详见源码};
你应该还记得这个方法吧,它和我们之前使用的方法如此类似。把mesh变换为正确的位置然后渲染每一个子集。我们需要每次渲染两段赛道mesh:一段是赛车现在行驶的赛道,一段是即将行驶到的赛道。实际上我们的赛车并没与移动,而是赛道在移动。这样做的原因有两个:如果每一帧都移动赛车,那么还必须同时移动摄像机来跟上它。这些而外的计算实际上是不必要的。还有一个更重要的原因:如果赛车向前移动,而且玩家很厉害,那么赛车的位置可能会超出浮点值的范围,甚至导致溢出。因为我们的游戏世界并没有边界(游戏不会有终点),所以让赛车停留在原地,移动赛道。
自然,需要一些变量来控制赛道。添加如下代码:
public const float RoadLocationLeft = 2.5f;
     public const float RoadLocationRight = -2.5f;
     private const float RoadSize = 100.0f;
     private const float MaxRoadSpeed = 250.0f;
     private const float RoadSpeedIncrement = 0.5f;
     private float RoadDepth0 = 0.0f;
     private float RoadDepth1 = -100.0f;
private float RoadSpeed = 30.0f;
作为mesh的赛道模型是已知的,长宽各为100个单位。RoadSize常量就是赛道的长度,两个location常量标记了赛道两边的中点。最后两个常量用来控制游戏操作。最大速度让游戏每秒移动250个单位,每次加速多移动0.5个单位。
最后,设置两段赛道的深度。把地一段赛道设置为0,第二段紧跟着上一段赛道。添加绘制赛道的代码,使用这几个变量来绘制赛道。在BeginScene方法之后添加如下代码:
DrawRoad(0.0f,0.0f,RoadDepth0);
DrawRoad(0.0f,0.0f,RoadDepth1);
现在运行程序,可以看到已经正确的绘制了赛道,但是这条沥青的赛道看起来极度可怕。这种结果是由Direct3渲染计算像素的方式引起的。当一个texel要覆盖屏幕中的多个像素时,这些像素需要通过一个放大过滤器来补偿(magnify filter to compensate)。当几个texel需要被绘制为一个像素时,他们会通过一个缩小过滤器。两种情况下的默认过滤器是一个名为Point的过滤器,它将会使用texel最接近的颜色作为像素的颜色,因此导致了这种情况。
有很多种方法来过滤纹理,但是,device不一定支持。我们只需要一个可以在texel之间插值计算,让赛道纹理看起来比较平滑的过滤器就可以了。在OnDeviceReset方法里添加如下代码:
详见private void OnDeviceReset(object sender,EventArgs e)中的代码
如你所见,先检查设备在放大(magnification)和缩小(minification)上是否支持各向异性(anisotropic)的过滤器。如果可以,就使用它。不行的话,再检测是否支持线性(linear)过滤器。如果两者都不可用,那么只能什么都不作,保留这种粗糙的效果。假设你的图形卡能支持其中一种过滤器,那么现在可以看到效果要好多了。
赛道以及处在了屏幕的中间,但还没有移动。还需要一个方法来更新游戏状态,完成移动赛道,进行碰撞检测。应该再OnPaint方法一开始就调用这个方法(再clear方法前):
OnFrameUpdate();
以下则是这个方法的代码:
private void OnFrameUpdate(){详见源码}
整个游戏编写完之后会有很庞大的代码,但现在,我们所需的只是让路动起来而已。先忽略elapsedTime,这段代码所作的只是移动路面而已。最后还需要添加一个变量:
private float elapsedTime = 0.0f;
 
特别提示:
为什么需要使用时间呢?为了方便讨论,假设我们每一帧都把赛道移动相同的距离。也许在你的电脑上它运行的很完美,但在其他的系统上呢?找一台比你的系统配置低的系统运行看看吧,赛道看起来会运行的相当缓慢。同样换到配置较高的系统上,赛道又会移动的快很多。原因在于你的计算是基于帧速率(frame rate)。假设在你的系统上,每秒可以跑60帧,那么所有的计算过程都是依赖于这个静态的帧速率而来的。因此,在每秒可以跑40帧或80帧的系统中,自然会得到不同的计算结果。让你的程序在每一个系统下运行都得到同样的结果是我们的基本目标之一,因此无论如何都应该避免基于帧速率的计算。
解决这个问题一个比较好的方法就是根据时间来计算位移。比如,赛道的最大速度定义为每秒250个单位。首先,我们需要获得自上一次“更新”过后过去的时间间隔。.net运行时内建的一个属性(tick count)可以用来获得系统的tick count。但它它并不完美:这个计时器的精度太低。它的值大约每15毫秒才更新一次,因此,在一个高帧速率的系统中(每秒60帧以上),赛道的移动将是不连续的,因为所用的时间不是平滑的。
如果你的系统支持的话,在DirectX SDK包含了一个高精度(通常精度为1毫秒)的计时器类DirectXTimer。但如何你的系统不支持,那么则只能使用tick count了。本书都将使用这个计时器来计算时间。(注:这里的DirectXTimer实际上是作者通过P/Invoke自己实现的一个计时器,代码在Utility.cs文件中,书上没有具体讲解实现方法,但大家应该都能看明白吧^_^)
 
为场景添加一辆可移动的赛车吧
好了,现在已经有了渲染好的、并且可以沿着场景移动的赛道了,接下来应该添加实际与玩家交互的对象了:一辆赛车。可以简单的再添加赛道的那个主要的类里加上一些关于赛车的变量和常量就可以了,但这样的代码将不是模块化的。你应该把关于赛车的代码分离出来,成为一个独立的类。为工程添加一个名为“Car”的新类吧。
Car类应该完成些什么任务呢?因为当其他物体移动的使用它仍然是静止不动的,不需要向前,也不需要向后。但为了让赛车能躲避路上的障碍物,它应该能够左右移动,同样,它还需要能渲染自身。好了,有了这些信息,就可以为类添加成员了:
 详见源码
这些变量已经足够用于控制赛车了。Height和depth都为静态的常量。赛车向两旁的移动速度的增量也是常量。使用最后一个常量的原因是赛车模型的大小刚好比赛道大,所以需要把它缩小一点点。
其他的成员基本上一看名字就知道它的用途了。有赛车当前的位置数据,默认情况下赛车位于赛道的左边。赛车的直径(Diameter),稍后会使用它来进行碰撞检测。有赛车的侧滑速度。当然,还有用来检测赛车在向哪个方向移动的两个布尔变量。最后,是有关mesh的变量。
Car类的构造函数需要完成两个任务:创建mesh对象(包括与它相关的结构)以及计算赛车的直径。添加一下构造函数:
public Car(){详见源码}
创建car mesh的方法和创建road mesh的方法基本上一样。接下来计算直径的新的代码则是比较有趣的。这里实际上是在计算赛车的边界球体(bounding sphere,mesh的所有的顶点都包含在这个球体内)。Geometry类包含了这个方法,只要把需要计算边界的顶点作为参数传给这个方法就可以了。
这里所需的就是从mesh获得顶点。你已经知道顶点保存在顶点缓冲内的,因此直接使用这块顶点缓冲。为了读取顶点缓冲中的数据,必须调用lock方法。在下一章中,会学到更多来自于VertexBuffer类的lock方法重载。现在,只需要知道这个方法会使用一个流返回所有顶点数据。还可以使用ComputeBoundingSphere方法获得这个mesh的“中心”以及边界球体的半径。因为我们并不需要关心mesh的中心,所以只需要把半径乘2获得直径就可以了。但是,模型经过了缩放,所以直径也需要缩放同样的比例。最后(在必不可少的finally块中),确定解锁并且释放了顶点缓冲。
接下来,添加绘制赛车的方法。Car类已经保存了赛车的位置,只需要获得device对象就可以绘图了。这个方法几乎和DrawRoad方法一样,区别在于变量不同以及在变换前需要缩放mesh,添加如下代码:
public void DrawCar(Device device) {详见源码}
在使用Car类之前,还需要让外部可以访问类的私有成员,添加如下公共属性:
{详见源码}
现在,应该在主要的游戏引擎类里添加成员来使用Car类了。在DogerGame类里添加如下代码:
private Car car = null;
由于car类的构造函数需要device作为变量才能初始化,所以只有在创建了device之后才能调用它。在OnDeviceReset方法里创建car是个不错的主意,在创建了road mesh之后添加如下代码:
car = new Car(device);
创建了赛车之后,就可以更新渲染部分的代码了。在OnPaint中两个DrawRoad方法之后添加以下代码:
car.Draw(device);
可以看到,已经在路上正确的渲染了赛车。可是,如何才能控制赛车左右移动呢?先忽略鼠标的存在,假设玩家拥有键盘,并且将使用键盘来控制游戏。使用键盘上的4个方法键来控制游戏是不错的选择。重载OnKeyDown方法:
protected override void OnKyeDown(KeyEventArgs e) {详见源码}
这里没有什么特别的内容。如果按下了ESC则游戏结束同时关闭窗口。按下左键或者右键,则把相应的moving变量设置为true,另一个则设为false。现在运行程序,按下按键可以正确更新赛车的两个moving变量。但赛车本身并不会移动,还需要为赛车添加一个函数更新它的位置:
public void Update(float elapsedTime)
这个方法接受逝去的时间值作为参数,所以无论在任何系统上,都会得到相同的结果。这个方法本身很简单,哪一个moving变量的值为true,则向那个方向移动移动相应的距离(根据所经过的时间长短)接下利检查是否已经移动到了边界,如果是的话则完成移动。但是,这个方法是不会自己调用自己的,还需要更新OnFrameUpdate方法,加入以下代码:
car.Update(elapsedTime);
(注:如果你是按着教程一步一步来,没有偷看最后源码的话,会发现此时赛车根本不会移动,郁闷吧,呵呵,原因是根本没有启动计时器。在初始化图形设备的InitializeGraphics()方法中加上如下代码吧 Utility.Timer(DirectXTimer.Start); )
 
添加障碍物
恭喜,这就是你创建的第一个3D互动程序了。已经完成了模拟赛车的移动。虽然实际上是赛道在移动,但显出的效果确实是赛车在移动。至此,游戏已经完成大半。接下来是添加障碍物的时候了。与添加Car类一样,添加一个名为Obstacle的类。
我们将使用不同颜色形状的mesh作为障碍物。通过mesh类创建stock对象可以改变mesh的类型,同时,使用材质来改变障碍物的颜色。添加如下的变量和常量:
{详见源码}
第一个常量表示将会有5种不同类型的mesh(球体、立方体、圆环、圆柱以及茶壶)。其中大多数的物体都有一个长度或半径的参数。我们希望所有障碍物都有同样的尺寸,所以应该把这些参数都设置为常量。很多种mesh类型都有一个而外的参数可以控制mesh中的三角形数量(stacks,slices,rings等等)。最后一个常量就是用来控制这些参数的。可以增大或减小这个参数来控制mesh的细节。
接下来的color数组用来控制mesh的颜色。我只是随即的选择了一些颜色而已,也可以把它们改为任何你喜欢的颜色。应该注意到这个类里既没有任何的材质数组,也没有纹理数组。你应该知道默认的mesh类型只包含了一个没有材质和纹理的子集,因此,额外的信息是不需要的。
由于障碍物需要放置在路面之上,并且实际上是路在移动,所以必须保证它们是和路面同时移动的。需要position属性来保证在路面移动时障碍物会同时更新。最后由于在创建茶壶时不能控制它的大小,需要检查创建的是否为茶壶,平且对它进行相应的缩放。为Obstacle类添加如下构造函数:
     注意到这里我们使用了来自utility的Rnd属性。它的具体实现非常简单位于utitity.cs文件中,只是用来返回一个随即的时间而已。Obstacle默认的构造函数保存了障碍物的默认位置,而且默认的为一个“非茶壶的”mesh。接下来选择创建某个类型的mesh。最后,选择一个随机的颜色作为材质颜色。
     在把障碍物添加到游戏引擎之前,还有一些额外的工作需要完成。首先,添加一个方法来和赛道同步更新障碍物的位置。,添加如下代码:
     public void Update(float elapsedTime,float speed)
     再一次使用elapsed time作为参数来保证程序在任何系统都能正常工作。同时,把当前赛道的速度也作为参数,这样物体就好像是“放置”在赛道上一样。接下来,还需要一个方法渲染障碍物:
public void Draw(Device device) {详见源码}
因为茶壶没有经过正确的缩放,因此如果渲染的是茶壶,那么应该先对他进行缩放,再移动到正确的位置。之后,设置材质颜色,把纹理设置为null,绘制mesh。
显然,同一时间赛道上需要多个障碍物。你需要一个方法来简单的在游戏引擎里添加或者移除障碍物。使用数组是一个可行的方法,却不是最好的:数组不能重置大小。集合是一个不错的选择,为obstacles添加一个集合来储存障碍物:
public class Obstacles : Ienumerable {详见源码}
当然,别忘了对System.Collections名称空间的引用。这个类包含了可以直接访问集合成员的索引器,可以让foreach方法正确工作的迭代器,以及三个值得注意的方法:add,remove以及clear。obstacle文件有了这些基本的方法之后,可以为游戏引擎添加障碍物了。
首先,需要一个变量来储存当前场景里的障碍物。为DodgerGame类添加如下变量:
private Obstacles obstacles;
接下来,需要一个方法用新的障碍物填充即将出现的一段赛道,添加如下代码:
private void AddObstacles(float minDepth) {详见源码}
这个方法是把障碍物添加到游戏的起点。首先,计算需要添加到这段赛道的障碍物数量。同时,还必须保证在障碍物之间有足够的距离让赛车躲避,否则,对玩家而言很不公平。接下来,就把障碍物随机的添加到路上。同时,把它添加到当前的obstacles集合中。注意到在创建障碍物时使用了一个名为ObstaclesHeight的常量,以下是它的声明:
 
在障碍物出现在场景之前,还有3件事要做:you need to add a call into our obstacle addition method somewhere,你需要取保为场景中的每一个障碍物都调用了update方法,你最后还需要渲染障碍物。因为在开始游戏前,需要把所有成员变量都重置为默认状态。是添加一个新方法的时候了,使用这个方法来初始化AddObstacles。添加如下代码:
private void LoadDefaultFameOptions()
{
         RoadDepth0 = 0.0f;
         RoadDepth1 = -100.0f;
         RoadSpeed = 30.0f;
         car.Location = RoadLocationLeft;
         car.Speed = 10.0f;
         car.IsMovingLeft = false;
         car.IsMovingRight = false;
         foreach(Obstacle o in obstacles)
         {
              o.Dispose();
         }
          obstacles.Clear();
         AddObstaxles(RoadDepth1);
         Utility.Timer(DirectXTimer.Start);
}
这个方法重置了大量我们关心的成员变量。同时依次对集合中的对象进行dispose操作,并且在重新填充集合之前删除所有元素。最后,启动计时器。应该在InitializeGraphics方法中创建了device之后的地方调用这个方法。千万不要把这个方法添加到OnDeviceReset方法中;只需要在每次游戏开始的时候调用一次就足够了。
LoadDefaultGameOptions();
现在需要在OnFrameUpdate方法中添加一个方法来更新障碍物。因为每一帧都需要更新所有障碍物,所以应该迭代他们。在OnFrameUpdate方法中,car.Update(elapsedTime)之前,添加如下代码:
foreach(Obstacle o in obstacles)
     {
         o.Update(elapsedTime,roadSpeed);
}
把障碍物添加到游戏引擎的一步就是渲染他们了。在OnPaint方法中,紧跟在绘制赛车的代码之后,添加我们熟悉的方法来渲染障碍物:
foreach(Obstacle o in obstacles)
     {
         o.Draw(device);
}
尝试着运行程序吧!(注:哈哈,程序抛出异常了吧,记住,还要在OnDeviceReset的最后添加 obstacles = new Obstacles();)。当你沿赛道行驶了一段距离,避开了几个障碍物之后,发生了什么?看起来在避开了最初的几个障碍物之后,就不在有新的障碍物出现了。回想一下先前的代码,你只是在第一段赛道添加了障碍物,之后,不停的移动这段赛道。但对于新的“赛道段”,你并没有调用任何方法填充障碍物。在添加新赛道段的地方添加如下代码吧:
if(roadDepth0 > 75.0f)
     {
         roadDepth0 = roadDepth1 - 100.0f;
         AddObstacles(roadDepth0);
     }
     if(roadDepth1 > 75.0f)
     {
         roadDepth1 = roadDepth0 - 100.0f;
         AddObstacles(roadDepth1);
}
不错,现在看起来好多了。你获得了一辆在赛道上缓缓行驶并且可以穿越(至少现在可以)障碍物的赛车。障碍物看起来有些呆板。应该让他动起来,让障碍物在赛车经过的时候旋转起来。首先,需要添加一些新的成员变量让obstacle类能控制旋转:
private float rotation = 0;
     private float rotationspeed = 0.0f;
     private Vector3 rotationVector;
障碍物的旋转速度和转轴都应该是随机的,这样他们才会看起来与众不同。在obstacle类构造函数的最后添加以下两行代码就可以实现这个功能:
rotationspeed = (float)Utility.Rnd.NextDouble() * (float)Math.PI;
     rotationVector = new Vector3((float)Utility.Rnd.NextDouble(),(float)Utility.Rnd.NextDouble(),(float)Utility.Rnd.NextDouble());
为了使障碍物旋转起来,还有两个需要修改的地方。首先,需要把实现旋转的代码添加到Update函数中:
Rotation += (rotationspeed * elapsedTime);
这里没什么特别的,只是根据时间和随机的旋转速度来增加旋转角度而已。最后,真正改变通过world transform来实现旋转,这样,渲染出来的物体才是旋转的。更新以下代码:
if(isTeapot)
     {
         device.Transform.World = Matrix.RotationAxis(rotationVector,rotation)
         *Matrix.Scaling(ObjectRadius,ObjectRadius,ObjectRadius)*Matrix.Translation(position);
     }
     else
     {
         device.Transform.World = Matrix.RotationAxis(rotationVector,rotation) * Matrix.Translation(position);
}
再次运行程序,可以看到当赛车经过的时候,障碍物已经在随机旋转了。下一步该干什么了呢?Well,让赛车真正的能和障碍物碰撞将会是很酷的,同时可以记录下总分来,看看你到底开了多远。当添加计分系统的同时,你也应该实时的记录游戏状态。在游戏引擎的DodgerGame类中,添加如下变量:
private bool isGameOver = true;
     private int fameOverTick = 0;
     private bool hasGameStarted = false;
private int score = 0;
所有关于游戏的信息都储存在这里。你可以知道游戏是否结束了,是否是第一次开始游戏,上一次和当前的游戏得分。这些都是不错的特性,不过怎么才能实现他呢?首先从计分系统开始吧,毕竟这才是玩家最关心的内容。玩家每经过了一个障碍物,就获得一定的分数。当然,你也希望游戏更具挑战性:加快赛道的速度,这样障碍物也会来到更快。另外,很重要的一点是:在LoadDefaultGameOptions方法中添加一行代码来重置总分,这样在新游戏开始的时候玩家才不会获得额外的分数。
sorce = 0;
接下来,在OnFrameUpdate方法中,在移动障碍物之前,添加如下代码:
Obstacles removeObstacles = new Obstacles();
     foreach(Obstacle o in obstacles)
     {
         if(o.Depth > car.Diameter - (Car.Depth * 2))
         {
              removeObstacles.Add(o);
              roadSpeed += RoadSpeedIncrement;
              if(roadSpeed >= MaxRoadSpeed)
              {
                   roadSpeed = MaxRoadSpeed;
              }
              car.IncrementSpeed(); 
score += (int)(roadSpeed * (roadSpeed / car.Speed));
              }
         }
         foreach(Obstacle o in removeObstacles)
         {
              obstacles.Remove(o);
              o.Dispose(); 
}
     removeObstacles.Clear();
使用这段代码获得了玩家已经经过的障碍物的列表(这个“表”同一时间只应该包含一个元素)。列表中每次增加一个障碍物,就相应的增加总分,增加赛道的速度提升难度,增加赛车的速度(虽然赛车的速度没有赛道增加的快)。完成了这些操作之后,把障碍物从列表中移除。注意观察,我们还使用了一个公式来根据赛道的速度计算得分,因此,开的越远,得分就越多。同时你可能已经注意到我们使用了一个还没有实现的方法来增加赛车的速度。不用多说,添加代码:
public void IncrementSpeed()
     {
         carSpeed += SpeedIncrement;
}
现在,需要添加一个新的方法来判断赛车是否撞到了障碍物。在obstacle类中,添加如下代码:
public bool IsHittingCar(float carLocation,float carDiameter)
     {
         if(position.Z > (Car.Depth - (carDiameter / 2.0f)))
         {
              if((carLocation < 0) && (position.X < 0))
                   return true;
              if((carLocation > 0) && (position.X > 0))
                   return true;
         }
         return false;
}
这里都是些很简单的东西;首先检查赛车和障碍物的深度(depth)是否相同,能否发生碰撞,如果相同,并且位于路的同一边,那么返回ture,否则赛车和障碍物不会碰撞,,返回false。有了这些代码,就可以在游戏引擎中实现碰撞检测了。更新OnFrameUpdate方法:
foreach(Obstacle o in obstacles)
     {
         o.Update(elapsedTime,roadSpeed);
         if(o.IsHittingCar(car.Location,car.Diameter))
         {
              isGameOver = true;
              gameOverTick = System.Environment.TickCount;
              Utility.Timer(DirectXTimer.Stop);
         }
}
每次更新了障碍物之后,检查是否发生了碰撞。如果发生了,那么游戏结束。设置游戏状态,并且停止计时器。
 
最后一步
至今为止,我们还没有使用过那些状态变量。你应该先完成游戏的逻辑设计。你将要求玩家通过按下任意键来启动游戏。游戏结束之后,将会有一瞬间停顿(大约一秒左右),接下来再次按下任意键就可以重新启动游戏。你首先要确定的是一旦游戏结束了,其它状态就不应该再更新了。因此,OnFrameUpdate方法的第一行应该是这样的:
if((isGameOver) || (!hasGameStarted))
     return;
接下来解决通过按下任意键启动游戏,在OnKeyDown方法重载的最后一行,添加如下的逻辑部分:
if(isGameOver)
     {
         LoadDefaultGameOptions();
     }
     isGameOver = false;
haGameStarted = true;
好了,这就是我们所要求的行为。当游戏结束了,玩家按下任意键,一个恢复为默认设置的新游戏就开始了。如果你愿意,可以从InitializeGraphics方法中删除LoadDefaultGameOptions的调用了,因为在每次按下任意键启动游戏的时候就会调用它。但是,我们还没有添加碰撞后让画面短暂停留一瞬间的代码。同样也在OnKeyDown方法中来实现;而且因该在检查是否按下了ESC键之后的添加代码:
这将会在游戏结束后的一秒之内忽略所有击键(除了可以按下ESC退出游戏)。现在可以尝试着玩玩我们的游戏了!虽然它们并不完整。我们记录了得分,却并没有显式的把这个得分告诉玩家。现在来完善这一步吧。Direct3D名称空间下有一个称为Font的类可以用来绘制文本。注意,在System.Drawing名称空间里也有一个Font类,而且如果没有前缀修饰的情况下使用“Font”,那个这两个类会发生冲突。幸运的是,可以使用using语句来做如下申明:
Using Driect3D = Microsoft.DirectX.Direct3D;
你所创建的每个字可以是不同的颜色,但最好使用相同的大小以及字体。对这个游戏来说,你需要两种不同的文本类型,自然,也需要2种不同的字体。为DodgerGame类添加如下变量:
private Direct3D.Font scoreFont = null;
private Direct3D.Font gameFont = null;
你只有在创建了device之后才能初始化这些变量。在创建了device之后的地方添加代码。它们并不需要在OnDeviceReset事件中初始化,应为这些类会自动处理重置设备时的事件。在InitializeGraphics的最后一行添加如下代码:
scoreFont = new Microsoft.DirectX.Direct3D.Font(device, new System.Drawing.Font("Arial",12.0f,FontStyle.Bold));
     gameFont = new Microsoft.DirectX.Direct3D.Font(device, new System.Drawing.Font("Arial",36.0f,FontStyle.Bold | FontStyle.Italic));
     这里创建了两种不同大小的Arial字体。之后,更新进行渲染的方法来绘制字体。字体是最后才需要绘制的内容,因此在绘制赛车的代码之后添加如下代码:
     if(hasGameStarted)
     {
         scoreFont.DrawText(null,string.Format("Current score:{0}",score),
         new Rectangle(5,5,0,0), DrawTextFormat.NoClip,Color.Yellow);
     }
if(isGameOver)
     {
         if(hasGameStarted)
         {
              gameFont.DrawText(null,"You crashed. The game is over.",
              new Rectangle(25,45,0,0),DrawTextFormat.NoClip,Color.Pink);
         }
         if((System.Environment.TickCount - gameOverTick) >= 1000)
         {
              gameFont.DrawText(null,"Press any key to begin.", new Rectangle(25,100,0,0),
              DrawTextFormat.NoClip, Color.WhiteSmoke);
         }
     }
     我们将在后面的章节里详细讨论DrawText方法。现在只需要知道它能完成他的名字所表示的功能。好了,现在可以看到当游戏已开始,就可以看到当前的分数了。除此而外,当游戏结束之后,你告诉玩家他撞车了。最后,游戏结束了一秒钟之后,提醒玩家按任意键可以可以重新开始游戏。
     Wow,至今为止,你已经完成了一个完整的游戏了。试玩一下吧。还有什么遗漏的吗?把最高分保存起来将是不错的尝试^_^。
     (注:以下部分作者演示了如何把最高分和玩家的姓名作为一个结构保存到注册表中,由于大部分是代码,且这部分内容基本与图形无关,就不翻译了)
     First off, we will need a place to store the information for the high scores. We will only really care about the name of the player as well as the score they achieved, so we can create a simple structure for this. Add this into your main games namespace:
public struct HighScore
{
          private int realScore;
         private string playerName;
         public int Score { get { return realScore; } set { realScore = value; } }
         public string Name { get { return playerName; } set {
         playerName = value; } }
}
 
Now we will also need to maintain the list of high scores in our game engine. We will only maintain the top three scores, so we can use an array to store these. Add the following declarations to our game engine:
Private HighScore[] highScores = new HighScore[3];
private string defaultHighScoreName = string.Empty;
All we need now is three separate functions. The first will check the current score to see if it qualifies for inclusion into the high score list. The next one will save the high score information into the registry, while the last one will load it back from the registry. Add these methods to the game engine:
private void CheckHighScore()
{
         int index = -1;
         for (int i = highScores.Length - 1; i >= 0; i--)
         {
              if (score >= highScores[i].Score) // We beat this score
        {
            index = i;
        }
      }
 
    // We beat the score if index is greater than 0
    if (index >= 0)
    {
        for (int i = highScores.Length - 1; i > index ; i--)
        {
            // Move each existing score down one
            highScores[i] = highScores[i-1];
        }
        highScores[index].Score = score;
        highScores[index].Name = Input.InputBox("You got a highscore!!","Please enter your name.", defaultHighScoreName);
    }
}
 
private void LoadHighScores()
{
         Microsoft.Win32.RegistryKey key =Microsoft.Win32.Registry.LocalMachine.CreateSubKey("Software//MDXBoox//Dodger");
         try
         {
              for(int i = 0; i < highScores.Length; i++)
              {
                   highScores[i].Name = (string)key.GetValue(string.Format("Player{0}", i),  string.Empty);
highScores[i].Score = (int)key.GetValue(string.Format("Score{0}", i),  0);
              }
              defaultHighScoreName = (string)key.GetValue("PlayerName", System.Environment.UserName);
         }
         finally
         {
              if (key != null)
              {
                   key.Close(); // Make sure to close the key
             
         }
}
 
/// <summary>
/// Save all the high score information to the registry
/// </summary>
public void SaveHighScores()
{
         Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey("Software//MDXBoox//Dodger");
try
         {
              for(int i = 0; i < highScores.Length; i++)
              {
                   key.SetValue(string.Format("Player{0}", i),highScores[i].Name);
                    key.SetValue(string.Format("Score{0}", i),highScores[i].Score);
              }
              key.SetValue("PlayerName", defaultHighScoreName);
}
         finally
         {
              if (key != null)
                 key.Close(); // Make sure to close the key
           }
}
 
I won't delve too much into these functions since they deal mainly with built-in .NET classes and have really nothing to do with the Managed DirectX code. However, it is important to show where these methods get called from our game engine.
The check for the high scores should happen as soon as the game is over. Replace the code in OnFrameUpdate that checks if the car hits an obstacle with the following:
if (o.IsHittingCar(car.Location, car.Diameter))
{
isGameOver = true;
gameOverTick = System.Environment.TickCount;
         Utility.Timer(DirectXTimer.Stop);
         CheckHighScore();
}
You can load the high scores at the end of the constructor for the main game engine. You might notice that the save method is public (while the others were private). This is because we will call this method in our main method. Replace the main method with the following code:
using (DodgerGame frm = new DodgerGame())
{  
         frm.Show();
         frm.InitializeGraphics();
         Application.Run(frm);
        frm.SaveHighScores();
}
 
The last thing we need to do is actually show the player the list of high scores. We will add this into our rendering method. Right before we call the end method on our game font, add this section of code to render our high scores:
gameFont.DrawText(null, "High Scores: ", new Rectangle(25,155,0,0),
     DrawTextFormat.NoClip, Color.CornflowerBlue);
for (int i = 0; i < highScores.Length; i++)
{
         gameFont.DrawText(null, string.Format("Player: {0} : {1}",highScores[i].Name, highScores[i].Score), new Rectangle(25,210 + (i * 55),0,0), DrawTextFormat.NoClip,
               Color.CornflowerBlue);
}
   再花一点时间来复习一下所完成的工作吧,这可是你的第一个游戏
可以到这个地方去下载源码http://bbs.gameres.com/showthread.asp?threadid=32844
DirectX9中文帮助手册(解密版) Managed DirectX 9.0 SDK Summer 2004 中文文档 译者:Wu Jie & Liu Kang 创建时间:2006-10-23 (可打印) 立即下载

相关推荐

Managed_DirectX9入门教程

关于managed_directx9与c#混合入编程,初学者的必读教材。

深入Managed DirectX9(转)作者calyman(超级好的翻译。建议初学者仔细看)

作者 calymanclayman_joe@yahoo.com.cn:【翻译】深入Managed DirectX9(一) Device类是DirectX里的所有绘图操作所必须的。可以把这个类假想为真实的图形卡。场景里所有其他图形对象都依赖于device。你的计算机里可以有一个到几个device,在MnagedDirctX3D里,你可以控制任意多个device。Devic...

weixin_30338461的博客 444

directx9中文文档(C#版)

不知道有没有,发一个上来。内容比较全,可惜实例少了些,需要参考SDK包中实例.

用c#进行directX开发的一个简单例子

这几天一直在研究c#下进行directX的3D开发,颇有些成绩。先贴出一个小例子和大家分享,我做的是一个旋转的4棱锥。如果有人对这方面感兴趣不知道如何学习的话,我建议看两个文档&lt;Managed DirectX 9图形和游戏编程简略中文文档&gt;,&lt;Managed DirectX 9 SDK 中文文档&gt;。另外最好下载个DirectX SDK (August 2007).rar。里...

weixin_34336292的博客 531

Directx9.0教程(中文版)、3D数学基础图形与游戏开发

directx9.0教程中文版以及源码;3D数学基础图形与游戏开发

Managed DirectX 9(中文版)

Managed DirectX 9(中文版)是受欢迎的directx教材,此版为中文版

Managed DirectX(第五章)

定义Mesh    虽然有很多时候,你需要手动创建顶点和索引数据,但更普遍的情况是从外部的资源加载已有的顶点数据,比如从一个文件。通常我们使用.X文件来保存这些信息。在前一章里,代码的大部分都用来创建几何体了。对于简单的三角形和立方体来说这似乎是完全可行的,但设想假如用相同的方式来创建拥有上万个顶点的物体将,所花费的时间和努力都将是很可怕的。     幸运的是,Managed DirectX里有一

sunkangta的专栏 1208

深入managed directx9

一个职业游戏制作者关于DirectX9 的深入探讨,个性视角,印象深刻

【翻译】Managed DirectX(第五章)

Rendering with Meshes翻译:claymanclayman_joe@yahoo.com.cn 定义Mesh    虽然有很多时候,你需要手动创建顶点和索引数据,但更普遍的情况是从外部的资源加载已有的顶点数据,比如从一个文件。通常我们使用.X文件来保存这些信息。在前一章里,代码的大部分都用来创建几何体了。对于简单的三角形和立方体来说这似乎是完全可行的,但设想假如用相同的方式来创建拥

Clayman的专栏 3645

英飞凌TC3xx DSADC旋变软解码实战:从示波器波形到VX1000数据,手把手教你避坑

本文详细介绍了英飞凌TC3xx系列微控制器的DSADC模块在电机旋变软解码中的实战应用。从示波器波形分析到VX1000数据采集,提供了一套完整的调试方法论,帮助工程师快速定位和解决常见问题,提升解码精度和系统性能。

weixin_30598225的博客 517

如何在 Ubuntu 24.04 或 22.04 上安装和使用 GDebi

本文介绍了在Ubuntu 24.04上安装和使用GDebi的方法。GDebi是DPKG的轻量级替代品,能够自动解析并安装.deb包的依赖项。文章详细说明了系统更新、安装GDebi的步骤,并演示了通过GUI和CLI两种方式安装.deb包的操作流程。此外,还提供了验证安装和卸载包的方法。GDebi虽然非必需,但为用户提供了处理依赖项的另一种选择,适合需要安装本地Debian包的用户。

山岚的运维笔记 1976

关于Managed DirectX kick start的源码

                  这本书是我很早以前就翻译的了,现在源码光盘 并不在身边。现在需要源码的同志可以到zbuffer去下载,至于其中的资源文件,除第六章的以外,大部分都可以在DirectX SDK里找到。另外,你也可以到gameres上去搜索我原来发帖子,标题是《深入Managed DirectX9》,每一章后面都带了完整代码和资源。            其实个人觉得既然X

Clayman的专栏 3833

开源生成式推荐框架MiniOneRec实践:从SID构建到RL微调全解析

在推荐系统领域,从协同过滤到深度学习,核心目标始终是精准预测用户兴趣。其技术原理经历了从矩阵分解到深度神经网络的演进,旨在从海量用户行为数据中挖掘复杂模式。随着大语言模型(LLM)的兴起,生成式推荐作为一种新兴范式,将推荐问题重构为序列生成任务,展现出更强的泛化能力和端到端优化潜力,尤其适合处理动态、多模态的推荐场景。本文聚焦于首个完全开源的生成式推荐框架MiniOneRec,它通过引入语义商品ID(SID)解决物品离散化难题,并采用监督微调(SFT)与强化学习(RL)结合的完整训练流程,为工程师和研究者提

weixin_42573647的博客 534

Managed Directx 9 kick start,第五章 纹理

Rendering with Meshes 我们又向前迈进了一大步,这可比总看着立方体旋转要有趣多了。下一章我们将使用Managed DirectX来写一个真正的游戏了,最然它可能看起来很简单,但毕竟是我们的第一个三维游戏^_^,第六章内容比较,大概3、4次才能翻译完^_^。  定义Mesh    虽然有很多时候,你需要手动创建顶

bbwolfcool的专栏 1181

从‘炼丹’到‘讲理’:物理信息神经网络(PINN)的硬约束 vs 软约束,我该选哪个?

本文深入探讨了物理信息神经网络(PINN)中硬约束与软约束的选择策略,帮助研究者在深度学习中有效融入物理先验知识。通过对比两种约束方法的实现复杂度、数据效率和计算性能,提供了针对材料科学和流体动力学等领域的实用决策指南,并介绍了自适应约束和混合约束等前沿技术。

weixin_42535703的博客 526

STM32按键中断切换红绿蓝灯

通过以上步骤,我们实现了使用STM32的按键中断来切换红绿蓝LED灯的功能。在实际应用中,可能还需要考虑更多的细节,如按键消抖、更精确的延时实现等。但上述代码提供了一个基本的框架和思路,可以根据具体需求进行扩展和优化。希望这篇博客对您有所帮助!

2301_79865940的博客 952

使用Managed DirectX编写游戏

转自 http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9Game_01.htm http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9Game_02.htm http://dev.gameres.com/Program/Vis...

weixin_30270561的博客 186

告别版本过低提示!微信3.9登录修复与聊天记录无缝迁移全攻略

本文详细解析了微信3.9版本过低提示的根源,并提供了完整的修复方案和聊天记录迁移指南。通过专用修复工具绕过版本检测,以及新旧版本聊天记录目录对比,帮助用户实现无缝迁移。特别提醒备份重要数据,确保操作安全。

weixin_42535461的博客 3131

Direct 3D和XNA游戏开发基础(C#语言版)(奋斗的小鸟)_PDF 电子书

下载地址:   内容简介   使用C#语言可以编写基于DirectX和微软最新游戏开发平台XNA(仅支持C#语言)的3D图形和3D游戏程序,其运行速度已接近于C++代码的运行速度,一些商业游戏已经使用C#语言创建。用C#语言编写3D图形和3D游戏程序可以降低学习和开发难度,提高开发效率,使开发人员能写出更安全的代码。本书目的是使具有C语言基础的读者通过本书学习,掌握用C#语言开发基于Di

奋斗的小鸟之博客 3122

【WorkBuddy专栏39】零基础也能做小程序——WorkBuddy微信小程序开发完全指南

【WorkBuddy专栏39】零基础也能做小程序——WorkBuddy微信小程序开发完全指南

AI人人懂 3145

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Managed DirectX(第十三章)

第十三章 渲染骨骼动画(Rendering Skeletal Animation)       本文版权归原作者所有,仅供个人学习使用,请勿转载,勿用于任何商业用途。由于本人水平有限,难免出错,不清楚的地方请大家以原著为准。欢迎大家和我多多交流。翻译:claymanBlog:http://blog.youkuaiyun.com/soilworkclayman_joe@y

Clayman的专栏 4148

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

向大家推荐一个在.Net下使用C#语言和Managed DirectX 9开发游戏的视频教程

视频教程:3D游戏开发步步高系列课程(微软课堂) 。美中不足的是视频的声音和画面不太对应。专心的听声音,听老师讲解吧。 PPT和源码下载:3D游戏开发步步高系列课程-PPT和源码 网址链接:3D游戏开发步步高系列课程(视频课程讲师:付仲恺)...

weixin_34192816的博客 317

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

【Unity知识速递】第7期 Unity 6.6 深度解析:WebGPU 正式版与 WebAssembly64,Web 游戏的内存革命

如何启用 Wasm64?需要注意的是,Wasm64 需要浏览器支持 Memory64 提案,目前主流浏览器的最新版本都已支持。一句话总结: Unity 6.6 用 WebGPU 转正、Wasm64 16GB 内存和渐进式加载三连击,将 Web 游戏的体验天花板又往上推了一大截——Web 平台不再是"妥协版",正在成为真正的一等公民。Unity 6.6 新增了 WebAssembly64(Wasm64) 构建选项,这可能是本次更新中对 Web 游戏开发者影响最大的一项升级。

crystal_WJT的博客 417

我给 DeepSeek 大肥鱼做了一款马里奥式闯关游戏

当全网都在把大肥鱼做成直播虚拟主播的时候,我决定给她做一款横版闯关游戏。

2301_79028807的博客 888
上一篇: 【翻译】Managed DirectX(第五章)
下一篇: 【翻译】Managed DirectX(第七章)
soilwork
博客等级 码龄23年 290粉丝 116原创
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值