【IT168 专稿】在上一篇中,我们重点讨论了开发纸牌游戏中实现鼠标拖放编程所涉及的一些基本技术。在本篇中,我们将讨论纸牌游戏开发中的菜单编程技巧,并结合Silverlight编程环境总结纸牌游戏规则,最后初步涉及游戏开发的基础内容。
一、Silverlight菜单控件简介
为了顺便探讨Silverlight编程中的菜单有关问题,我特意引入了开源站点CodePlex上的一个Silverlight菜单控件(http://slmenu.codeplex.com/WorkItem/View.aspx?WorkItemId=23119)来控制整个游戏的逻辑。如此以来,在菜单控制之下,整个纸牌游戏软件的操作就像桌面应用十分类似。尽管如此,至少还有两点值得你注意:
(1)你应该改变菜单控件的zIndex属性为一个较大的整数(在我们的游戏中设置为1000就足够了)。
(2)当前的发行版中可能存在一个小错误—在您单击菜单项后你必须再单击菜单范围外的某处才能够开始你的实际工作。
事实上,还有第三个也是最重要的一点你应该注意。我们留待后面再行讨论。
接下来,我们将结合纸牌游戏开发中的数据结构简要分析一下玩这款游戏时涉及到的一些基本规则。
二、纸牌游戏规则分析
现在,让我们来介绍一下纸牌游戏的游戏规则。考虑到了这些规则后,我们就可以更好地寻找控制鼠标拖放操作所应用的方案。正如你所知,纸牌游戏的目标是利用左上角牌叠中所有的牌,在右上角组成以A打头,从A到K顺序排列的四套花色牌叠。图1给出了这个游戏的一个运行时快照。

▲图1:纸牌游戏某运行时刻快照
相信大多数读者都相当熟悉Windows版本的纸牌游戏。在本游戏中,我们尽可能提供全力支持。因此,在后面我们将向你介绍这款游戏中所有可能有效的拖放操作。
(1)关于单击鼠标左键(后面简称“单击鼠标”)
首先,你可以单击这个游戏屏幕中的任意位置。但是,如果没有单击扑克或单击了一张正面向上的扑克牌(无论是在左上方过渡区,在下部行栈,或在右上方目标栈区),什么都不会发生。但是,如果你单击了一张正面朝下的扑克牌,无论这张扑克牌是在左上角的过渡区或是底部行栈区,此扑克都将被翻转过来。注意,在图2中我们给出了单击鼠标对应的有效区域。其中,有效地区使用红色圆圈圈出来。

▲图2:单击鼠标对应的有效区域
(2)关于双击鼠标左键(后面简称“双击鼠标”)
接下来,让我们考虑双击鼠标对应的有效区域。游戏规则表明,通过双击某一张正面向上的扑克牌,无论这张扑克牌是在左上方过渡区或在下部行栈上,如果其他条件也得到满足,那么这张扑克将自动飞行到右上角目标栈的一个位置。在当前扑克牌飞往目标区后,其紧邻下方可能存在的另一张扑克将会显示为正面向下,为下一次鼠标单击作好准备。
(3)关于按住鼠标左键拖动(后面简称“拖动鼠标”)
最后,我们来讨论一下玩纸牌游戏过程中最频繁使用的鼠标拖动情况。显然,根据游戏规则,只有当前扑克正面向上时,才能拖动它在其他扑克上方移动。再者,如果其他有效的条件得到满足,那么当前扑克就可以被拖动到右上角目标栈的一个位置。此外,更一般的鼠标拖动情况是拖动一叠皆是正面向上的扑克牌到另一张相匹配的扑克上—我们将在后面讨论鼠标拖放编程时再详细讨论。
三、游戏设计
在了解了纸牌游戏的一些重要规则后,现在我们来探讨一些基本的编程技术。为了更好地了解纸牌游戏的开发规则,让我们先看看游戏画面上的不同分区,如图3所示。
注意:在图3中我们特意使用了开发本游戏中使用的一些数据结构名称。接下来,我们将给出解释。
为简化起见,我们只列出在我们的游戏中使用的最重要的数据结构,如下所示。
List Board;
List Deck;
List[] SuitStack ;
List[] RowStack;
List PlaceHolder;
在上面的代码中,变量CurrentCard用来保存当前扑克。当你单击屏幕上的任何扑克之一,该变量的值即刻决定下来。
第二个List
为了方便确定扑克的位置,我们还推出一个List
PlaceHolder = new List(13);
PlaceHolder.Add(new Rect(0 + PADDING, 0 + PADDING, WIDTH, HEIGHT1));//topleft
PlaceHolder.Add(new Rect(89 + PADDING, 0 + PADDING, 2 * WIDTH, HEIGHT1));//swap area
PlaceHolder.Add(new Rect(267 + PADDING, 0 + PADDING, WIDTH, HEIGHT1));//topright1
PlaceHolder.Add(new Rect(355 + PADDING, 0 + PADDING, WIDTH, HEIGHT1));//topright2
PlaceHolder.Add(new Rect(443 + PADDING, 0 + PADDING, WIDTH, HEIGHT1));//topright3
PlaceHolder.Add(new Rect(532 + PADDING, 0 + PADDING, WIDTH, HEIGHT1));//topright4
PlaceHolder.Add(new Rect(0 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom1
PlaceHolder.Add(new Rect(89 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom2
PlaceHolder.Add(new Rect(180 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom3
PlaceHolder.Add(new Rect(267 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom4
PlaceHolder.Add(new Rect(355 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom5
PlaceHolder.Add(new Rect(443 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom6
PlaceHolder.Add(new Rect(532 + PADDING, 114 + PADDING, WIDTH, HEIGHT2));//bottom7
}
从前面的图3中你也不难看到屏幕上共对应着13个扑克占位符。为此,我们定义了一个可以存储13个元素的List

四、定义扑克控件Card
屏幕上存在那么多的扑克,我们是如何管理的呢?答案是:在本游戏中,我们把每张扑克定义为一个独立的Silverlight用户控件—Card。
基于这个想法,我们需要定义扑克控件Card的正面朝上还是朝下,扑克点数,扑克花色,这些皆定义为控件的公共属性。但是,如何处理扑克图像还是一个问题。在这一点,不同的开发者可能有自己的实现方案。我的想法是,让扑克控件维护一个Uri数组,并使每个Uri指向相应的扑克图像文件(.png格式,这也是Silverlight开发中被广泛应用的图像格式)。下面的清单给出了扑克控件Card的定义。
public Face CurrentFace { get; set; } //扑克正面朝上还是朝下
public int Number { get; set; } //扑克点数
public Suits CurrentSuit { get; set; } //扑克花色
private Uri[] uriPokerImages = {
new Uri("/Poker_MVP;Component/images/cards/cl1.PNG", UriKind.RelativeOrAbsolute),
new Uri("/Poker_MVP;Component/images/cards/cl2.PNG", UriKind.RelativeOrAbsolute),
//省略其他……
new Uri("/Poker_MVP;Component/images/cards/spq.PNG", UriKind.RelativeOrAbsolute),
new Uri("/Poker_MVP;Component/images/cards/spk.PNG", UriKind.RelativeOrAbsolute),
new Uri("/Poker_MVP;Component/images/cards/background.PNG", UriKind.RelativeOrAbsolute),
new Uri("/Poker_MVP;Component/images/cards/go.PNG", UriKind.RelativeOrAbsolute),
new Uri("/Poker_MVP;Component/images/cards/placeholder.PNG", UriKind.RelativeOrAbsolute),
};
public Uri this[int nIndex] {
get {
return uriPokerImages[nIndex];}
set{
uriPokerImages[nIndex]=value;}
}
public Card(){
InitializeComponent();
}
public Card(Face face, int number, Suits currSuit ) {
InitializeComponent();
this.CurrentFace = face;
this.Number = number;
this.CurrentSuit = currSuit;
if(face==Face.faceup)
this.imageHolder.Source = new BitmapImage(this[(int)currSuit * 13 + (number - 1)]);
else if (face == Face.facedown) //反面图像
this.imageHolder.Source =new BitmapImage(this[52]);
else if (face == Face.empty)
this.imageHolder.Source = new BitmapImage(this[53]);//空牌图像
else
this.imageHolder.Source = new BitmapImage(this[54]);//占位符图像
}
public void SetToFaceUp(){
this.CurrentFace = Face.faceup;
this.imageHolder.Source = new BitmapImage(
this[(int)(CurrentSuit) * 13 + (Number - 1)]);
}
public void SetToFaceDown(){
this.CurrentFace = Face.facedown;
this.imageHolder.Source = new BitmapImage(this[52]);
}
}
注意:上面代码中,我们共定义了除大王和小王以外的55幅扑克图像。其中,最后面的三幅特别的图像用于特殊的用途。而且,为了检索相关的图像,我们定义了一个索引属性Uri,用于作为指向此图像Uri的指针。此外,为了方便创建扑克控件的实例,我们还重载了Card控件的构造器函数。至于其他两个方法—SetToFaceUp和SetToFaceDown,如其自的名称所暗示的,分别用来改变扑克的状态,即设置扑克的正面朝上与朝下。
从下一篇开始,我们将讨论如何使用上面定义的数据结构来生成扑克和发牌问题。