深入Managed DirectX9(十)
(接 Managed DirectX9 九)
添加障碍物
恭喜,这就是你创建的第一个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);
}
再花一点时间来复习一下所完成的工作吧,这是你的第一个游戏。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
至此,第一大部分初级图形技术的内容就全部完了
附上的第一部分的所有PDF文档及源代码,下载。
添加障碍物
恭喜,这就是你创建的第一个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);
}
再花一点时间来复习一下所完成的工作吧,这是你的第一个游戏。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
至此,第一大部分初级图形技术的内容就全部完了
附上的第一部分的所有PDF文档及源代码,下载。