摘要 本文介绍使用新的Windows PResentation Foundation提供的3D图形和可用于Workflow Foundation(经由DmRules)中的函数库来开发一个简单的猜单词游戏。本文意在向读者展示.Net 3.0开发交互式3D应用程序的简易性。注:图1显示了本游戏的一个运行快照。
图1.猜字游戏运行过程中
一、 简介
.Net 3.0为程序员提供了大量的新工具。其实,其最终目的是使得所有程序员都能够轻松地开发3D应用程序。Vista把图形处理功能交给了视频卡来完成(大多数情况下,我们很可能已经购买了这样的视频卡)。这使得我们不必自己处理以前非常昂贵的计算问题而且不必担心因此减弱性能。
首先确定我必须确定我开发的DmRules库能否应用于程序中。游戏的原始界面很简单,是以Windows表单实现的,这足以满足游戏的需要了。而且,这个界面还可以向你展示DmRules库的工作原理。我决定使用WPF来实现一些3D内容;为运行本游戏,你需要安装.Net 3.0。
二、 游戏规则
这个游戏很简单。在Yahoo Games中有一个我很喜欢玩的称作"Text Twist"的游戏,这里描述的游戏使用的规则与之很类似。基本上是首先提供给你一个由6个字母组成的单词。最终,你必须猜测这个单词。你还可以通过查找其它多于两个字母长度的单词得分,只要它们使用的字母出现在原始单词中就行。
你可以使用很多种方法来编写这个游戏的规则。在这个游戏中,每个人都有一组规则。例如,一个人可以决定,假如你设法找到不是原始单词的10个单词,那么你仍应该赢得这一回合。其他玩家在猜测中提供多少字母方面可能更看重得分。得分,游戏规则,单词列表,时间限制……所有这些都可以使用规则来进行控制。这也正是为什么我选择这个游戏作为使用DmRules的一个例子。
DmRules库答应你在App.config文件中编写规则。这些规则是针对每一种类型设计的。这影响到我在该游戏中设计这些类的方式。规则被应用于两个方面:用户作的猜测类(SingleGuess)和当前游戏类(Game)。
(一) SingleGuess类
一个猜测是用户选择的一个字母序列。在一次猜测中,我需要决定两个方面:猜测正确吗?假如猜测是正确的,它值多少点(得多少分)?
为了确定猜测的正确性,我编写了一组规则。每一条规则都是使用App.config文件(如下代码所示)实现的。你已经看出,你可以把这些表达式编写成实际的xml。如此我们可以在以后很轻易地改变这些规则而不需要重新进行编译。
1. 假如猜测有不到3个字母,那么它是不正确的。
<DmRule cond="this._GuessText.Length < 3" name="More than 2 letters"
haltAfterThen="true" priority="1000">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._IsCorrect" right="false" />
<DmCdStmt xsi:type="Assignment" left="this._ErrorText"
right=""Word must have at least 3 letters"" />
</ThenStmts>
</DmRule>
2. 假如猜测使用不是在系统提供字母列表中的字母,那么它是不正确的。
<DmRule cond="!this._Game.HasLetters(this._GuessText)"
name="Has correct letters" haltAfterThen="true" priority="990">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._IsCorrect" right="false" />
<DmCdStmt xsi:type="Assignment" left="this._ErrorText"
right=""Letters not in scrambled word"" />
</ThenStmts>
</DmRule>
3. 假如猜测已经完成,那么它是不正确的。
<DmRule cond="this._Game.GuessesMade.Contains(this._GuessText)"
name="Already guessed" haltAfterThen="true" priority="980">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._IsCorrect" right="false" />
<DmCdStmt xsi:type="Assignment" left="this._ErrorText"
right=""You've already guessed that word"" />
</ThenStmts>
</DmRule>
4. 假如猜测的单词在字典中,那么它是正确的。否则,不正确。
<DmRule cond="DictUtil.IsWordInList(this._GuessText)" name="Is in dictionary"
haltAfterElse="true" priority="970">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._IsCorrect" right="true" />
</ThenStmts>
<ElseStmts>
<DmCdStmt xsi:type="Assignment" left="this._IsCorrect" right="false" />
<DmCdStmt xsi:type="Assignment" left="this._ErrorText"
right=""Word is not in dictionary"" />
</ElseStmts>
</DmRule>
你可能已经注重到,上面规则中的优先权(priority)属性。必须把它添加到DmRules上,因为Workflow Foundation的规则系统并不能保证规则被以任何特定顺序执行,除非明确地指定一种优先权。相应的数字越高,将越早执行该规则。优先权也可以是负数。
另外,还有haltAfterThen和haltAfterElse属性。有时,根据一种特定规则的计算方式,或者因为运行任何其它的规则可能效率不高或者因为其它规则能够以一种你不想使用的方式修改状态,你可能想停止运行规则。在给定上面说明的规则优先权的情况下,一旦猜测被确定是不正确的,那么应该立即停止运行相应的规则。Workflow Foundation实际上提供了一种规则语句,其中有一条能够被插入到一个规则列表的任何位置的暂停(halt)命令。但是,把一条halt语句插入到上面配置文件中间对系统不无副影响,因为在一条规则中不答应存在循环或条件。因此,我决定仅使用属性并且把halt语句添加到规则列表的最后。
SingleGuess类中还有更多的规则。这些规则必须处理一个正确猜测的得分方式。得分是基于在该单词中有多少个字母以及是否猜测匹配原始单词。为了进行下一个回合的游戏,我决定假如你正确猜测了一个6个字母的单词,那么你已经过了这一关。如你所见,这是轻易改变的:
<DmRule cond="this._GuessText == this._Game.OriginalWord" name="Guessed original word">
<ThenStmts>
<DmCdStmt xsi:type="EXPrStmt" expr="this._Game.Complete()"/>
<DmCdStmt xsi:type="Assignment" left="this._Score" right="40"/>
</ThenStmts>
<ElseStmts/>
</DmRule>
<DmRule
cond="this._GuessText != this._Game.OriginalWord && this._GuessText.Length == 6"
name="Guessed six-letter word">
<ThenStmts>
<DmCdStmt xsi:type="ExprStmt" expr="this._Game.Complete()"/>
<DmCdStmt xsi:type="Assignment" left="this._Score" right="25"/>
</ThenStmts>
<ElseStmts/>
</DmRule>
<DmRule cond="this._GuessText.Length == 3" name="Score 3-letter">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._Score" right="10"/>
</ThenStmts>
<ElseStmts/>
</DmRule>
<DmRule cond="this._GuessText.Length == 4" name="Score 4-letter">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._Score" right="15"/>
</ThenStmts>
<ElseStmts/>
</DmRule>
<DmRule cond="this._GuessText.Length == 5" name="Score 5-letter">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._Score" right="20"/>
</ThenStmts>
<ElseStmts/>
</DmRule>
(二) Game类
这个游戏类负责决定是否用户应该移动到下一级还是失败了。我决定使这个游戏基于时间进行设计。我在前三级中建立了一个单词列表,这些单词比较轻易猜出。此后,我仅随机地从字典中选择一个由6个字母组成的单词。猜测这些单词可能很困难,特定是当其中的一半我从未听说过时。你可能决定根据需要改变这些规则。添加更多级别的单词列表,改变答应的时间数,答应用户在不同环境下进入到下一级等。
我在这个游戏中创建的规则列举如下:
1. 假如原始单词是一个空字符串(我用于指示当前级别已经完成)而且已经完成的游戏级别小于3,那么选取一个新单词,清除猜测列表,把时限设置为30秒,并且把级别设置为未完成。
<DmRule
cond="this._OriginalWord == "" && this._GamesPlayed < 3"
name="Unassigned original word, level one" priority="1000">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._OriginalWord"
right="DictUtil.FindGuessWord(6, Level.One)"/>
<DmCdStmt xsi:type="ExprStmt" expr="this.Scramble()"/>
<DmCdStmt xsi:type="ExprStmt" expr="this._GuessesMade.Clear()"/>
<DmCdStmt xsi:type="Assignment" left="this._TimeLeft"
right="TimeSpan.FromSeconds(30)"/>
<DmCdStmt xsi:type="Assignment" left="this._IsComplete" right="false"/>
</ThenStmts>
<ElseStmts />
</DmRule>
2. 假如原始单词是一个空字符串并且当游戏相应的字母数大于或等于3时,那么从字典中随机选取一个单词,清除创建的猜测列表,把时间设置为30秒,并且把级别设置为未完成。
<DmRule
cond="this._OriginalWord == "" && this._GamesPlayed >= 3"
name="Unassigned original word, above level one" priority="990">
<ThenStmts>
<DmCdStmt xsi:type="Assignment" left="this._OriginalWord"
right="DictUtil.FindGuessWord(6, Level.Zero)"/>
<DmCdStmt xsi:type="ExprStmt" expr="this.Scramble()"/>
<DmCdStmt xsi:type="ExprStmt" expr="this._GuessesMade.Clear()"/>
<DmCdStmt xsi:type="Assignment" left="this._TimeLeft"
right="TimeSpan.FromSeconds(30)"/>
<DmCdStmt xsi:type="Assignment" left="this._IsComplete" right="false"/>
</ThenStmts>
<ElseStmts />
</DmRule>
3. 假如时间已到并且游戏完成,那么应该发出信号以指示该用户已经顺利完成这一回合。
<DmRule cond="this._TimeLeft.TotalSeconds == 0 && this._IsComplete"
name="Time ran out, game complete" priority="800">
<ThenStmts>
<DmCdStmt xsi:type="ExprStmt" expr="this.SUCcess()"/>
</ThenStmts>
<ElseStmts />
</DmRule>
4. 假如时间已到并且游戏未完成,那么应该发出信号以指示该用户这一回合失败。
<DmRule cond="this._TimeLeft.TotalSeconds == 0 && !this._IsComplete"
name="Time ran out, game not complete" priority="790">
<ThenStmts>
<DmCdStmt xsi:type="ExprStmt" expr="this.Failure()"/>
</ThenStmts>
<ElseStmts />
</DmRule>
Success()和Failure()方法都以简单地激活Game类中的一个事件的方式结束。UI必须订阅该事件以进行响应。
三、 原始Windows表单接口
图2.最初设计的表单界面
假如你认为图2中的这个3D界面太丑陋,那么你只注重一下这个接口好了——至少它能够实现我们的目的。我们在此看到的内容有:一个已经猜测出的单词列表,得分,待猜测的单词,这一回合中剩下的时间,一个指示器告诉我们是否我们已经过了这一回合,一个要输入猜测内容的文本框,一个按钮用于改变要猜测单词,还有一个按钮用于提交猜测单词。
这个接口也包括在本文相应的源码中。当时间一到,弹出一个消息框指示你可以点击进入到下一回合或告诉你要猜测的单词,并且让你开始一次新游戏。四、 WPF中的3D图形
在本文开始的图像已经向你展示了3D接口看上去的感觉。被猜测的单词对应一组特定的点位置,你可以点击这些字母来选择它们。选择的字母将翻到它在用户构建的单词中的相应位置上。还有一个按钮用于清除当前猜测内容,另外一个按钮用于输入猜测。当猜测结束或被清除后,字母翻转回它们在被猜测单词中的原来位置。还有一个时钟指示剩余的时间,还有一个已经猜测的单词的列表,并且指示用户的得分情况。
在下一节中,我将讨论我的实现中碰到的一些有趣的事情。由于WPF仅仅提供一种3D环境而没有任何现成的原型可以使用;因此,相应的编程工作自然(与其它的3D编程)有些不同。
(一) 创建字母
每个字母实质上是一个挤压的立方体。我使其看起来象一个乱写的字母。为此,我只是取得一种随机的木纹理并且使用一个工具程序把字母绘制到其上。这个工具程序名为CreateTextures,也一起包含到本文相应的源码中。该工具基本上负责打开树根纹理并且把一个字母绘制到该纹理上。你必须设想使字母包装起一个正方体。因此,下面图3是字母"P"相应的纹理看上去的样子:
图3.相应于字母"P"的纹理
其中,一个P是颠倒的,另一个是朝后的。理解这些的最轻易的方式是,把纹理图像想像为一张礼品包装纸。设想你正在使用它包装一本书。把书的顶部放到这两个P之间,然后把书竖起来。然后,拿起包装纸,把书包起来。
现在,我的包装纸实际上应该被翻转过来了,但是这应该不难理解。对一个3D对象实现纹理包装是很简单的。就象设计对象本身一样简单。你只需考虑对象的3D坐标,当然这有时也有一定的难度。
我使用一个LetterFactory类创建这些字母。在XAML中,你要为你的三维网格物体定义点,三角指数,法线和纹理坐标。假如你看过任何的微软的3D编程示例,你会注重到它们通常以XAML形式创建这些三维网格物体。但是,我喜欢以编码方式来实现。读取一个三维网格物体非常困难,因为要把所有的相关信息"打包"到一个组XML属性中。无论如何,假如你对这些三维网格物体创建方式感爱好的话,请认真分析相应的源码。
(二) 决定是否用户点击了一个字母
创建完字母后,我现在需要了解何时用户点击了它。这是我在实现任何其它内容之前必须要实现的。为此,我很轻松地在MSDN中找到一个实现这种操作的例子。WPF的确使实现用户点击了哪个三维网格物体的实现非常轻易。首先,把下列代码添加到你的表单的构造函数中:
this.myViewport.MouseLeftButtonDown+=new MouseButtonEventHandler(HitTest);
这里的myViewport是你的窗口中的3D视图。下面是HitTest方法相应的代码:
public void HitTest(object sender, System.Windows.Input.MouseButtonEventArgs args) {
Point mouseposition = args.GetPosition(myViewport);
PointHitTestParameters pointparams = new PointHitTestParameters(mouseposition);
VisualTreeHelper.HitTest(myViewport, null, HTResult, pointparams);
}
HitTest方法使用一个代理处理实际结果。HTResult方法按如下方式工作:
public HitTestResultBehavior HTResult(System.Windows.Media.HitTestResult rawresult) {
RayHitTestResult rayResult = rawresult as RayHitTestResult;
if (rayResult != null) {
RayMeshGeometry3DHitTestResult rayMeshResult = rayResult as
RayMeshGeometry3DHitTestResult;
if (rayMeshResult != null) {
GeometryModel3D hitgeo = rayMeshResult.ModelHit as GeometryModel3D;
...
}
}
return HitTestResultBehavior.Continue;
}
上面的变量hitgeo是被点击的模型。此代码是很轻易使用的;直接使用这些现成的代码,你就能够确定用户点击了哪个三维网格物体而不用担心在3D空间和2D空间之间的坐标翻译问题并且不需要进行任何数学矢量计算。WPF早已考虑到你的需要了。事实上,你在上面的代码中看到的HitTestResult还可以用于实现用户交互以外的内容。例如,它可以用于进行碰撞检测或决定一个用户的瞄准线。
(三) 实现字母动画
动画是Windows Presentation Foundation提供的另一种酷特征。你可以非常轻易地实现简单的动画效果。但是,你首先应该了解一些基本的3D概念。变换是3D游戏编程中常用操作。旋转,平移,投映和缩放都属于变换。我们在这个应用程序中不使用任何缩放,而由WPF负责处理投影问题。因此,我们可只关注旋转和平移的问题。使用这些技术,我们就可以实现我们的字母的动画效果:从一个位置翻转到另一个位置。
我的基本要求是,检测何时用户点击了被猜测单词中的一个字母。当这发生时,我想把这个字母移动到用户正在构造的单词上。为了充分利用3D效果,我想让字母翻转360度。你会看到实现这些是多么轻易。
我们一共要实现三种变换:旋转360度,沿Y轴平移和沿X轴平移。其中,最轻易的是沿Y轴平移,因为这个值是静态的。
TranslateTransform3D tt3d = new TranslateTransform3D(new Vector3D(0, 0, 0));
DoubleAnimation da = new DoubleAnimation(-4, new Duration(TimeSpan.FromSeconds(1)));
tt3d.BeginAnimation(TranslateTransform3D.OffsetYProperty, da);
我们使用一个0矢量创建一个平移。动画在经过一段时间后会改变平移;所以,我们不需要把该矢量设置为其它非0值。实际动画使用一个值和一个时限(TimeSpan)值。在这种情况中,我想每秒移动-4单位。BeginAnimation调用指示,我想沿哪个轴移动这-4单位-在此是沿着Y轴。实际实现的是,把字母往下移动4个单位,这将耗费一秒钟。
下一个变换是沿着X轴的一个平移变换。根据字母来源于该被猜测单词中的位置以及它要移动到猜测单词的特定位置,可以改变这个平移。
double oldX = double.Parse(str[1]);
double newX = (_CurrGuess.Length + 1) * -2.5;
TranslateTransform3D tt3d2 = new TranslateTransform3D(new Vector3D(0, 0, 0));
da = new DoubleAnimation(newX - oldX, new Duration(TimeSpan.FromSeconds(1)));
tt3d2.BeginAnimation(TranslateTransform3D.OffsetXProperty, da);
这里的执行非常相似于上面情况,只是要确定要移动多少单位和沿什么轴移动。因此,让我们继续讨论更令人感爱好的旋转变换:
RotateTransform3D myRotateTransform = new RotateTransform3D(
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 1));
DoubleAnimation myAnimation = new DoubleAnimation();
myAnimation.From = 0;
myAnimation.To = 360;
myAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
myAnimation.RepeatBehavior = new RepeatBehavior(1);
myRotateTransform.Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty,
myAnimation);
我们沿一个轴实现旋转。上面的矢量指示,它位于X轴上。AxisAngleRotation3D构造函数的第二个参数是一个角度值(为1)。该动画每次运动1度,这很轻易理解。我使动画从第0帧(相应于0度)移动到第360帧(相应于360度)。我还想使其它一切都在一秒内完成。
现在,最后一步是把所有变换施加到hitgeo对象:
(hitgeo.Transform as Transform3DGroup).Children.Insert(1, myRotateTransform);
(hitgeo.Transform as Transform3DGroup).Children.Add(tt3d);
(hitgeo.Transform as Transform3DGroup).Children.Add(tt3d2);
代码看上去有点希奇,但是为了实现多种变换,你必须把字母的Transform属性赋值为一个Transform3DGroup(其实,这是相应于一组变换)。我是在创建字母时实现这一点的,我想保留对字母施加的原始变换并且再添加上一些新的变换。
必须以特定顺序来实现变换操作。例如,我们想在平移之前实现旋转。为了理解这一点,你可以设想一下太空中的月亮。地球能够绕自己的轴进行自转(旋转周期为24小时),月亮绕着地球旋转。这两种旋转的区别是:月亮首先平移到地球外的某个位置处,然后再进行旋转;而沿着地球轴的旋转仍然发生,也就是月亮绕着地球转。
无论如何,最终结果是,必须把旋转插入到一组变换中的一个特定位置。在本例中,只需把平移添加到变换的最后。最终,我们得到一个翻动其位置的字母("T")(见图4):
图4.翻动字母("T")的动画效果
(四) 制作3D按钮
按钮是任何接口的一个基本组成部分。在我们的3D环境中,我们没有现成的工具可用。因此,我决定自己制作按钮。我的前提是,我需要在我点击鼠标时能够"按"的某种东西。由于实现动画平移很简单,所以,我们可以使用这一技术来实现实际的"按"动作。
我的第一个考虑是,用户怎么能够知道按钮被"按下"了呢?在一般的Windows UI实现中,因为阴影的变化效果而使按钮看上去被"按下"了。假如我在3D环境的左上方加入一个光源,也许我能够实现这一效果。但是,为此,我也需要对此作一些变化以使按钮看上去好象"弹起"。这也意味着,我需要为该按钮预备一个平面。所以,我想使用另一种更为轻易些的方案。
我使用的方案是,制造另外一个挤压了的立方体。一方面,我要绘制该按钮的文本。当被点击时,该按钮将"推入",然后再恢复到其原始位置。作为一种参考实现,我创建了一个很大的灰色多边形用作背景。按钮位于这个背景多边形上面。当"按下"该按钮时,实际上是"按下"该多边形。我想,WPF应该默认地使用了Z缓冲区技术,因为最终结果是,当按下按钮时,该按钮看上去被"压入"了多边形里面。
但是,这样以来问题出现了:我该如何把文本放到按钮上面呢?办法是,得到一个图像,然后把文本绘制到该图像上去,并且把该图像包到按钮作为一个纹理。因此,首先,我创建一个文本:
FormattedText ft = new FormattedText(text,
new CultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface(new FontFamily("Arial"), FontStyles.Normal,
FontWeights.Normal, new FontStretch()),
24D,
Brushes.Black);
注重,这里的text变量中存储着我想编写的文本。我选择使用黑色Arial,24pt字体。这基本上确定了一个文本框,并且能够相应于文本大小来调整该文本框大小。我可以使用这个尺寸来决定我想如何缩放我的按钮的尺寸。你会在ButtonFactory的实现代码中看到类似如下的内容:
buttonModel.Geometry = CreateButtonMesh((ft.Width + 4) / (ft.Height));
当我创建这个三维网格物体时,我改变它的宽度以匹配在文本中的宽高比。这防止字母看起来有点拉长。接下来,我们要创建一个DrawingVisual对象,用它来实现纹理效果。我们将绘制一个浅蓝色的矩形来表示按钮的背景颜色,然后绘制文本。
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawRectangle(Brushes.LightGray, new Pen(Brushes.LightGray, 1),
new Rect(0, 0, ft.Width + 4, ft.Height * 4));
drawingContext.DrawText(ft, new Point(2, ft.Height * 1.5));
drawingContext.Close();
现在,我们只需使该图像可见,并且把该图像作为一个材质应用到我们的三维网格上即可。
RenderTargetBitmap bmp = new RenderTargetBitmap((int)ft.Width + 4,
(int)(ft.Height * 4), 0, 0, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
buttonModel.Material = new DiffuseMaterial(new ImageBrush(bmp));
更多的请看:http://www.QQread.com/windows/2003/index.Html
(五) 得分板
下一个问题是,怎么显示当前的得分。既然我能够把格式化的文本放到一个图像上面,并且能够把它伸展到一个按钮三维网格物体上,那么我考虑到,我应该能够使用一个常规的多边形并且使用当前得分把一个纹理绘制到它上面。这是一个相当简单的过程。我以很类似于创建按钮的方式创建该得分板,除了该多边形具有一个固定大小外。纹理正相应于改变的内容。因此,在每次得分改变时,我都使用下面的方法来更新它:
private void UpdateScore() {
if (bmpScore != null) {
FormattedText ft = new FormattedText(_CurrGame.Score.ToString(),
new CultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface(new FontFamily("Arial"), FontStyles.Normal,
FontWeights.Normal, new FontStretch()),
16D,
Brushes.DarkRed);
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawRectangle(Brushes.LightGray,
new Pen(Brushes.LightGray, 1), new Rect(0, 0, ft.Width + 4, ft.Height * 4));
drawingContext.DrawText(ft, new Point(120 - ft.Width - 2, 2));
drawingContext.Close();
bmpScore = new RenderTargetBitmap(120, 25, 0, 0, PixelFormats.Pbgra32);
bmpScore.Render(drawingVisual);
scoreBoard.Material = new DiffuseMaterial(new ImageBrush(bmpScore));
}
}
我想,对于改变得分来说,也许仅改变纹理位图本身就足够了。然而,这看起来却无任何效果。既然得分不是在一秒内改变许多次,我想,这就足够了:使用变化的纹理为该三维网格物体创建一种新材质。
(六) 显示时间
实现用户接口的另一个要害因素是向用户显示他猜测单词还剩多少时间。我想使用我实现得分板一样的思路来显示该回合中还剩下多少分钟多少秒。但是,这看起来并不那么有趣。我想,也许我可以模拟一个数字显示,但是这可能过于复杂。最后,我决定显示一个模拟时钟。在这种时钟上,可以很轻易地向你显示你在本游戏中还剩下多少时间。而且,它的执行很简单,因为我们仅需要使该时钟的钟针动起来即可。
我的第一项任务是创建一只钟。现在,我可以很轻易地仅创建一个矩形并且在其上加上时钟纹理。但是,我想,我应该使用三维网格物体制作一个真正的圆圈。运用数学知识来解决这个问题是很有意思的,也许我以后还会再给它加上一些动画效果。无论如何,我使用正弦和余弦函数创造了一个具有适当的纹理坐标的圆圈三维网格物体。
MeshGeometry3D mg3d = new MeshGeometry3D();
mg3d.Positions.Add(new Point3D(0, 0, 0));
mg3d.TextureCoordinates.Add(new Point(0.5, 0.5));
for (double d = 0; d <= 360; d += 5) {
double x = 4.0 * Math.Sin(d / 180.0 * Math.PI);
double y = 4.0 * Math.Cos(d / 180.0 * Math.PI);
mg3d.Positions.Add(new Point3D(x, y, 0));
x = x / 8.0 + 0.5;
y = y / 8.0 + 0.5;
mg3d.TextureCoordinates.Add(new Point(x, y));
}
for (int i = 1; i <= 360 / 5 + 1; i++) {
mg3d.TriangleIndices.Add(0);
mg3d.TriangleIndices.Add(i);
mg3d.TriangleIndices.Add(i + 1);
}
作为一名喜欢进行优化的程序员,这个例程的优化使我大大为难。但是,由于仅调用一次,所以,我决定不费心再使其运行稍快些。我创建的第一个点位于圆形的中心,其它的点组成沿着各个方向的三角形。如你所见,创建一个圆圈三维网格物体非常轻易。
图5.时钟动画效果
接下来,我们要实现时钟中间的表针。我创建了一个很简单的三维网格物体,通过使用一个三角形并且在其上施加一个旋转动画。我仅需要绕着Z轴旋转它,这是非常轻易的,在此不再赘述。最终动画效果见图5。
五、 总结
在本文开始,我已说明在本游戏中我使用了我的DmRules库。然后,我们通过一个具体例子介绍了如何使用Windows Presentation Foundation进行3D编程。
最后,本文中的小游戏向你介绍了进行Windows Vista编程非常有意义的内容。