永无止境的记忆游戏






4.67/5 (4投票s)
学习如何使用 XBap 编写一个小游戏

引言
2009年初,微软举办了一场编程竞赛,旨在激发大家对WPF/XAML技术的兴趣。挑战很简单:用10k代码创造出令人惊叹的东西。程序必须以ClickOnce、Silverlight或XBap的形式利用WPF。我一直在研究XBaps,并认为这种模式听起来很酷,所以我选择尝试为比赛创建一个小型XBap应用程序。
背景
如果您不熟悉,XBap是XAML浏览器应用程序的缩写。它是ClickOnce部署的下一个伟大飞跃;用户只需点击网页上的超链接即可启动应用程序。此链接指向已部署到托管应用程序的Web服务器上的.xbap文件。这与ClickOnce部署的发生方式完全相同,但对于XBap,客户端浏览器会下载应用程序并在浏览器空间内启动它。
Silverlight应用程序也托管在浏览器中。虽然XBap和Silverlight应用程序都在沙箱中运行,但XBap提供完整的WPF编程模型,而Silverlight只能使用.NET Framework的子集。下面是一个简短的表格来比较两者
XBap | Silverlight | |
编程 | 完整的.NET/WPF框架 | 框架和WPF的子集 |
客户端计算机 | 需要完整的.NET Framework | 无需安装框架,只需Silverlight客户端(约5MB下载) |
平台 | 客户端计算机必须是Windows计算机或模拟的Windows | 可以在支持Silverlight客户端的任何平台上运行(Windows、MacOS、即将推出Linux) |
浏览器 | 可以在Internet Explorer或Firefox中运行 | 任何支持Silverlight客户端的浏览器 |
需要的客户端软件 | .NET 2.0/3.5可再发行组件包(约30 MB) | Silverlight运行时(约5 MB) |
沙盒 | 两者都在沙箱中运行;即保护客户端计算机免受本地磁盘访问、注册表访问等。两者都可以使用服务(XBap必须使用源服务器) |
游戏
我为比赛构思的游戏是创建一个记忆匹配游戏。这是一个简单的游戏,玩家“翻开”棋盘上的卡片,翻开第二张卡片后,检查玩家是否选择了匹配的卡片。如果没有,卡片会再次翻开以进行下一轮。诀窍在于记住错过的匹配,如果玩家能记住原始匹配的位置,则在其他地方找到匹配的卡片。
这款游戏的有趣之处在于它理论上永无止境。原始棋盘有4张卡片,需要玩家找到2个匹配项。然后玩家进入下一关,进行4个匹配项,然后是8个,然后是12个,以此类推,直到方块变得太小而无法点击!;o)
Using the Code
有两个源代码zip文件:一个是比赛的原始提交版本,另一个是“未压缩”版本。未压缩版本已重新格式化,并更改了一些对象和变量名称,以使代码更清晰。请记住,这是一项10k挑战,因此每个空格都计入总字节数。如果您查看压缩版本(为比赛提交的原始版本),您会发现我非常小心地利用了每一个字节!从现在开始,我将引用未压缩版本源代码中的变量和方法名称。
棋盘布局和卡片构建
整个游戏是使用一个Grid
创建的,该Grid
根据当前级别动态生成行和列。如果Grid
包含ColumnDefinitions
和RowDefinitions
,但没有为它们分配高度或宽度,WPF的布局引擎会自动将它们均匀分布。发现这个小技巧就是我开始编写整个程序的原因。
主级别变量(_currentLevel
)跟踪玩家在游戏中的进度,并用作每个棋盘的主要驱动值。棋盘本身是通过BuildGrid()
方法构建的,该方法将行和列添加到Grid
中。
下一步是使用BuildCards()
方法创建“卡片”。有一个名为PlayerCard
的自定义对象,它保存了我们将在本文稍后介绍的各种信息。
class PlayerCard
{
public int myXCoordinate { get; set; }
public int myYCoordinate { get; set; }
public int partnerXCoordinate { get; set; }
public int partnerYCoordinate { get; set; }
public bool firstGuess { get; set; }
public bool secondGuess { get; set; }
}
BuildCards()
遵循以下基本算法
- 计算填充当前级别所需的卡片数量
- 每隔一个级别有一个奇数个空格,所以这些级别会有一个空白格
- 创建一张卡片
- 随机选择棋盘上的一个方格
- 如果此方格尚未被卡片填充,则用当前卡片填充它
- 如果此方格已填充,则随机选择棋盘上的另一个方格,直到找到一个空方格
- 这可能看起来并不那么优化。确实不是。此代码的重点是最小化。但我很惊讶C#能处理得这么好……在开发过程中,我可以观看它构建级别34、35……65、66……而速度没有太大的减慢!;o)
- 创建下一张卡片(这将是第一张卡片的匹配项)
- 遵循与第一张卡片相同的算法
- 找到一个方格并用这张卡片填充后,将这张卡片的网格坐标分配给第一张卡片
- 回到创建的第一张卡片,并将第二张卡片的坐标分配给它
- 这就是
PlayerCard
的……坐标变量的用途
- 这就是
- 重复此过程,直到所有卡片都创建完毕
每张“卡片”实际上只是一个Border
对象。在所有Border
对象创建完成后,每对都会使用RadialGradientBrush
和BackerGradient()
方法分配一个随机化的背景。此方法会打乱GradientStop
对象的颜色和偏移量,为每对创建不同的图案。作为一个“永无止境”的记忆游戏,随着玩家在级别中越来越高,这种区分效果越来越小。但这正是游戏的挑战所在!
算法最后为每张卡片创建一个Button
对象。每个按钮的大小与Border
完全相同,并位于Border
之上。这些按钮都分配了CardClickCheckForMatch(...)
来处理单击,以及CardMouseMoveAnimation(...)
来处理漂亮的鼠标悬停淡入效果。因此,“翻卡”动作是通过在单击卡片时将按钮的Opacity
动画设置为零来实现的。然后,如果没有找到匹配项,则通过将按钮的Opacity
重新动画化回100来“翻回”两张卡片。
匹配检测和计分
通过使用匹配状态变量(_turnCount
)和CardClickCheckForMatch(...)
方法来实现匹配检测。如果当前单击是匹配检查中的第一次单击,则该方法会根据匹配的网格坐标在卡片集合(_PlayerCards
)中查找被单击的卡片,并将其firstGuess
成员标记为true并返回。如果这是本轮的第二次单击,则在网格中找到第二张卡片,并将其secondGuess
设置为true
。然后调用MatchCheck(...)
方法。
MatchCheck(...)
方法通过首先使用LINQ to Objects提取firstGuess
和secondGuess
已标记的卡片来确定是否找到匹配项。如果第一张卡片的myXCoordinate
/ myYCoordinate
与第二张卡片的partnerXCoordinate
/ partnerYCoordinate
相等,那么我们就找到了匹配项!
然后使用ReportScore(...)
方法计算得分。分数由以下因素决定
- 当前级别编号
- 自上次匹配以来或自级别开始以来的已用时间
- 匹配越快,分数越高
- 连击序列
- 每一次连续匹配而不失误都会使分数乘以10
- 游戏开始时选择的难度设置
如果这是一次失误,则根据当前级别编号和难度设置,以类似的方式扣除分数。无论计算出多少分数,都会添加到总游戏分数中,然后在屏幕上显示在第二次单击的卡片上方,并带有漂亮的淡入淡出效果和TranslateTransform
,以增加游戏的“吸引力”。
ScorePoint(...)
(其中包含对ReportScore(...)
的调用)还会确定这是否是游戏中的最终匹配,并奖励玩家他们的表现统计数据。然后,玩家有机会进入下一级别并继续享受乐趣!
难度设置
初次启动游戏时,玩家可以使用Slider
选择难度设置。难度会以以下方式影响游戏……
- 向易 ←
- 错过的匹配翻得更慢,以便有更多时间记住错过的卡片模式
- 找到匹配项的计算中获得的奖励分数较低
- 失误时扣除的分数较少
- 向难 →
- 失误时卡片翻得更快
- 匹配时获得的奖励分数较高
- 失误时扣除的分数较多
关注点
这是一次关于学习游戏编程和WPF/XAML的有趣冒险。我学到了很多WPF动画的新技巧。
历史
- 2009 年 4 月 30 日:初始发布