深入Managed DirectX9(九)
特别提示:
为什么需要使用时间呢?为了方便讨论,假设我们每一帧都把赛道移动相同的距离。也许在你的电脑上它运行的很完美,但在其他的系统上呢?找一台比你的系统配置低的系统运行看看吧,赛道看起来会运行的相当缓慢。同样换到配置较高的系统上,赛道又会移动的快很多。原因在于你的计算是基于帧速率(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)
~~~~~~~~~~~~~~未完待续~~~~~~~~~~~~~·
完整的代码已经在《深入Managed DirectX9(八 )》中给出了,这里附上step by step的代码。
为什么需要使用时间呢?为了方便讨论,假设我们每一帧都把赛道移动相同的距离。也许在你的电脑上它运行的很完美,但在其他的系统上呢?找一台比你的系统配置低的系统运行看看吧,赛道看起来会运行的相当缓慢。同样换到配置较高的系统上,赛道又会移动的快很多。原因在于你的计算是基于帧速率(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)
~~~~~~~~~~~~~~未完待续~~~~~~~~~~~~~·
完整的代码已经在《深入Managed DirectX9(八 )》中给出了,这里附上step by step的代码。