【IT168专稿】在本系列文章中,你将学习编写一个基于Silverlight 3的纸牌游戏。你会注意到,这款游戏酷似各种版本的Windows中自带的纸牌游戏。我们写这个游戏的主要目的是为了探讨Siverlight 3中对于游戏开发技术特别是鼠标拖放功能的支持。
注意,本文纸牌游戏应用程序的开发及测试环境如下:
? Windows XP Professional (SP3)
? .NET 3.5(SP1)
? Visual Studio 2008 Professional (SP1)
? Microsoft Expression Blend 3
? Microsoft Silverlight Tools for Visual Studio 2008 SP1
相信每一位Windows读者肯定会熟悉其自带的纸牌游戏。如今,Silverlight技术日益流行,成为基于微软技术开发下一代富客户端Web应用的主要平台。本系列文章将尝试使用Silverlight 3技术来开发类似Windows提供的纸牌游戏。在这一系列文章中,你除了会学习到开发这款游戏软件所涉及的主要实现思想外,还会学习到基本的Silverlight 3编程技术,特别是鼠标编程技巧。
在本篇中,我们将主要讨论有关开发纸牌游戏的基本知识。之后,我们将简要介绍纸牌游戏的基本规则。最后,讨论纸牌游戏开发所涉及的重要数据结构和基本的实现代码。
一、创建自己的鼠标拖放方案
老实说,在开发纸牌游戏的开始,我几乎搜遍了整个互联网,试图找到某种合适的基于Silverlight且能够尽快投入到纸牌游戏中使用的鼠标拖动解决方案。为此,我研究了许多专家在解决Silverlight问题时提供到多种鼠标拖放解决方案,例如TranslateTransform解决方案,Expression Blend 3 MouseDragElementBehavior解决方案,类似于Flex的DragManager解决方案 ,CodePlex开源网站提供的MouseClickManager解决方案,等等。然而,每一款都有自己的瑕疵,无法直接应用于纸牌游戏开发中。最后,我决定使用最基本的Silverlight鼠标拖放解决方案—尽管琐碎了一些,但也具有最大的灵活性。
在下面几节中,我们将讨论在开发我们自己的鼠标拖放和双击解决方案中所涉及到的一些重要概念。
二、处理扑克控件的坐标问题
Silverlight 3对于其各种容器控件,如Grid、Canvas和Stack等,提供了不同的坐标管理方案。为了简化纸牌游戏的设计,我们选择使用Canvas控件作为所有扑克控件的容器控件。因此,我们可以借助下列函数来处理扑克控件对应的坐标问题:
{
public static readonly DependencyProperty LeftProperty;
public static readonly DependencyProperty TopProperty;
public static double GetLeft(UIElement element);
public static double GetTop(UIElement element);
public static void SetLeft(UIElement element, double length);
public static void SetTop(UIElement element, double length);
//省略其他……
}
在我们的游戏中,我们直接使用上面的方法GetLeft和GetTop获得扑克控件的左上角坐标信息(注意:这些坐标是相对于父控件Canvas的)。另一方面,我们使用另外两个方法SetLeft和SetTop指定扑克牌控件左上角坐标位置。这样一来,我们便可以在画布上根据事先设计随意地控制扑克牌的位置。
三、处理扑克控件的zIndex属性
在开发纸牌游戏的过程中,另一个有趣但非常重要的问题是每张扑克牌的zIndex属性值的特征。我们知道,默认情况下,父控件中所有子控件都具有相同的zIndex属性值,即0。但是,由于Silverlight的设计特点,具有相同的zIndex属性值的各子控件在屏幕上呈现时仍表现出不同层的效果(如果位置相重叠,则稍后呈现的子控件看上去出现在较前呈现的子控件的顶部)。
另一方面,当我们拖动扑克时一种合乎逻辑的拖动外观应当是,被拖放扑克位于所有其它扑克的上部。因此,我们必须人为地干预被拖扑克控件的zIndex属性值。此外,考虑到我们的游戏中总共有52张扑克和zIndex属性值的有效范围,我们可以采取这样的措施:每次对被拖动的扑克的zIndex属性的值加1—从而使其看上去一定位于所有其他扑克的上面。如果被拖动的扑克可以投放到目标位置,那么这个新的zIndex属性值不再变动;否则,我们需要再次更改当前拖动扑克的zIndex属性值,从而使其恢复拖放前的zIndex属性值—使其与其他扑克控件的相对层位置保持不改变。
类似于上面,我们仍然给出用于控制扑克的zIndex属性值的主要方法。
{
public static readonly DependencyProperty ZIndexProperty;
public static int GetZIndex(UIElement element);
public static void SetZIndex(UIElement element, int value);
//others omitted…
}
显然,取得zIndex属性值使用GetZIndex方法,而设置zIndex属性值使用SetZIndex方法。对于扑克的zIndex属性值问题,我们不再作过多的解释。
四、探讨Silverlight 3的鼠标双击支持
在纸牌游戏中,双击鼠标左键(以下简称“双击鼠标”)的使用非常频繁。例如,当在左上部过渡区或下部有一张正面的扑克,而此时在目标扑克片堆叠中恰好有一个与之相匹配的扑克。此时,通过双击当前扑克,我们可以将其更迅速地移动到目标扑克片堆叠中。
众所周知,目前Silverlight已提供对于单击鼠标的充分支持。但是,它却没有提供直接的双击鼠标支持,虽然我们可以通过间接的方法来做到这一点。为此,许多博客文章中争相展示各自的解决方案。在这个游戏中,我使用的是由迈克斯诺提供的解决方案。他的实现原理很容易理解:每当收到一次鼠标左键单击事件都要启动一个计时器DispatcherTimer。如果在双击的时间间隔内另一次鼠标单击事件被截获,那么说明发生了鼠标双击事件。这个时间间隔通常设置为200毫秒左右。200毫秒过后停止并禁用相应的定时器,直到收到下一次单击鼠标事件。
【注意】虽然许多高手都建议使用200毫秒双击解决方案,但是在本纸牌游戏中并没有获得令人满意的效果。此外,本人已经测试过另外两个数字—300和400,但效果仍然不理想—有时你可能需要双击鼠标左键三次才能够获得双击效果。根据我的经验,在玩这款游戏时,如果你觉得双击有时会出问题,您可以更改为拖动的方式。
接下来,让我们来关注如何在Silverlight 3中创建自定义的鼠标双击事件支持方案。
下面是创建定时器控件DispatcherTimer实例并设置相应时间间隔和订阅其Tick事件的概要代码。
//省略其他……
doubleclickTimer = new DispatcherTimer();
doubleclickTimer.Interval = new TimeSpan(0, 0, 0, 0, INTERVAL);
doubleclickTimer.Tick += new EventHandler(doubleclickTimer_Tick);
//省略其他……
}
下面是定时器控件Tick事件触发后执行的代码内容。
void doubleclickTimer_Tick(object sender, EventArgs e){
doubleclickTimer.Stop();
}
下面是用户按下鼠标左键后判断其是否为双击操作以及根据是单击还是双击执行各自相应代码的概要描述。
//如果第二次的单击前计时器结束,说明这是一个双击动作
if (doubleclickTimer.IsEnabled) {
doubleclickTimer.Stop();
//扑克双击处理代码……(略)
}
else {
//如果不是双击,启动计时器
doubleclickTimer.Start();
//扑克单击处理代码……(略)
}
}
在上面代码中,首先创建一个名为doubleclickTimer的DispatcherTimer控件并对其Tick事件进行侦听。在这里,请注意,通过调用启动计时器doubleclickTimer的Start方法,其属性IsEnabled属性值自动变为true。显然,在游戏的一开始,属性IsEnabled属性值被设置为false。
如果在双击的200毫秒时间间隔内另一个鼠标单击事件被截获,那么说明发生了鼠标双击事件。200毫秒后定时器控件的Tick事件将被触发,计时器会停止并禁用,直到收到另一次鼠标单击事件。这样一来,在上面代码的两个注释处,我们便可以分别插入我们的自定义鼠标双击和单击编程逻辑。
五、捕获鼠标操作及订阅相关事件
说实话,如何捕获鼠标事件是我初步想开发纸牌游戏时要面对的第一个难题。是针对每一张扑克都设置鼠标捕获,只针对当前扑克进行鼠标捕获,或是针对父控件Canvas进行鼠标捕获?最后,我决定订阅父控件(名为cardContainer的Canvas)的鼠标事件,而仅针对当前扑克设置鼠标捕获。这样一来,我们可以很容易地获取所有移动当前扑克所需的有关信息,并相应地获得其他可能一起移动的扑克的相关信息,最终实现共同移动所有这些扑克。由于这部分编程比较复杂,我们在后面文章中继续探讨这些问题