65.9K
CodeProject 正在变化。 阅读更多。
Home

使用泛型构建益智游戏

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.93/5 (7投票s)

2004 年 9 月 9 日

10分钟阅读

viewsIcon

39297

downloadIcon

979

本文介绍如何使用双向链表构建一个简单的游戏。

引言

我第一次需要构建一个双向链表的时候,我还在用Intel汇编进行编程。说实话,当时我对这种结构的工作原理理解得很吃力。后来,我不得不在C和C++中做同样的事情。尽管C#语言比它们更易用,并且拥有优秀的类库支持,但我相信一些开发者仍然可能难以理解双向链表的工作原理。基于此,我决定使用Windows Form和C#创建一个这个小游戏,以直观地展示在编程具有通用用途的双向链表时,`Namespace Generic` 中类所提供的功能。理解这些概念将使在C#或其他.Net平台语言的程序实现中更容易使用它们。要运行此程序,您需要使用Visual Studio C# Express或Visual Studio 2005 Beta1。您可以在 http://lab.msdn.microsoft.com 下载这两者。

背景

是否有任何背景信息对本文有所帮助,例如对所介绍的基本概念的介绍?

游戏如何工作

通过这个游戏学习非常简单。图1显示了初始游戏界面,它有两个面板和一个按钮组。

头部面板或源面板存储需要移动到下方面板或目标面板的块。在目标面板中,这些块必须被正确地放置,并形成一个单词。源面板代表被打乱的项目列表,目标面板代表将要组合游戏解决方案的列表。块可以自由地在面板之间移动,当然,需要遵循链表使用的现有规则。按钮组用于移动块。请注意,按钮的标题代表了程序中使用的通用列表的方法。这样,操作游戏的人很容易直观地了解每种方法是如何工作的,因为每种方法在按下按钮时都会被可视化地表示出来。另一个有趣的点是,因为程序在操作列表,所以必须遵守一些规则才能使程序正确运行。例如,如果单击“Add After”(稍后添加)按钮,并且目标面板中没有块,用户将收到一条错误消息,说明程序无法在另一个块之后添加一个块,因为目标面板中还没有块。这听起来很明显?是的,但人们往往会忽略这些小细节。

关于程序

尽管此项目仅用于演示`Namespace Generic`的某些功能,但我决定利用Visual Studio .Net 2005中的一些新功能,例如重构(Refactoring)来加速和标准化代码生成,分部类(partial)来分割大型类,以及FxCop来分析代码并找出需要修正的地方。目的是要遵循编程的最佳实践。图2所示的图表由Visual Studio生成,展示了程序使用的架构。

正如您在图表中看到的,程序使用了 `GameForm, GameItems, Item, LinkedItems<T>,` 和 `LinkedList<T>` 类。

GameForm 类

游戏界面由 `GameForm` 类生成,该类封装了所有允许用户与游戏交互的功能。在这种架构下,该类抽象了管理包含项目的列表的方式。这样,当按钮被点击时,控制权会传递到逻辑层的 `GameItems` 类,该类负责适当地操作列表。当需要移动一个块时,`GameForm` 类需要知道要移动的块当前在哪个面板中。为此,该类使用了名为 `sourcePanelSelected` 的布尔变量。当鼠标进入面板区域时,会触发 `MouseEnter` 事件,该变量会根据此事件进行更改,如下面的代码所示。

代码块应使用 <pre> 标签包装,如下所示

 
private void sourcePn_MouseEnter(object sender, EventArgs e)
{
         sourcePanelSelected = true;
}
 
private void targetPn_MouseEnter(object sender, EventArgs e)
{
         sourcePanelSelected = false;
}
 

Item 类

此类存储每个项目的相关功能,例如,块在面板中的初始位置、是否被选中等。稍后我们将详细讨论项目选择。

LinkedItem<T> 类

此类继承自 `LinkedList<T>`,如下面的代码所示。

 
public class LinkedItems<T> : LinkedList<T>
{
         // some code
}
 

这样,我们就创建了一个通用的双向链表,可以用来存储不同类型的对象,而无需修改其代码。这就是为什么我们称之为通用列表。通过使用 `Generic Namespace`,我们可以构建一个强类型安全列表,提高性能,并获得更清晰、易于维护的代码。尽管 `LinkedItems` 类继承了基类的方法,但我还是决定添加一些功能来方便游戏中的块操作。还要注意的是,尽管一些方法接受参数重载,但这些方法使用的是节点列表,并且由于 `GameItems` 类仅操作列表中的块而不是其节点,因此我不得不实现接受仅列表项的方法,并在内部调用使用节点列表作为参数的基类方法。例如,看下面的 `AddAfterItem` 和 `AddBeforeItem` 方法。

 
public LinkedListNode<T> AddAfterItem(T newItem, T item)
{
        LinkedListNode<T> node = Find(newItem);
        return base.AddAfter(node, item);
}
 
public LinkedListNode<T> AddBeforeItem(T newItem, T item)
{
        LinkedListNode<T> node = Find(newItem);
        return base.AddBefore(node, item);
}
 

基类方法 `AddAfter` 和 `AddBefore` 仅接受 `LinkedListNode` 类型的第一个参数。因此,首先需要找到与项相关的节点,然后才能调用基类方法。`LinkedItems` 类还添加了另一个方法 `GetSiblingItem`,该方法返回与最后一个项相关的兄弟项的引用。该方法还有一个第二个参数,一个布尔值,指示要查找的项是参考项的前一个兄弟还是后一个兄弟。您可以在下面的代码中看到。

 
public T GetSiblingItem(T item, bool forward)
{
        // because the list is doubly linked, we can
        // search the entire list forward and backward, easily
        LinkedListNode<T> node = null;
        
        node = Find(item);
        if (node == null)
           return default(T);
        else
        {
            // look forward
            if (forward)
            {
                node = node.Next;
            }
            // look backward
            else
            {
                node = node.Previous;
            }
        }
        // if not found, return null 
        if (node == null)
            return default(T);
        else
            return node.Value;
}
 

正如您所见,双向链表的一个巨大优点是它可以轻松地向前和向后遍历。在这种情况下,当在低级语言中,程序员必须担心正确更新所有内部指针时,这成为一个巨大的优势,因为它们需要同时指向向前和向后。如果这些指针使用不当,您的程序就会出错。在C#语言中,它生成托管代码,您不必担心这些细节。这个方法的另一个有趣之处是使用了 `Nullable<T>` 类。

 
return default(T);  
 

您可以看到,如果作为参考的项在列表中找不到,该方法必须返回 null。现在的问题是:如何为一个通用对象 T 返回 null?如果我在代码中强制返回 null,编译器会显示一条错误消息,说无法将 null 类型转换为要返回的类型 T。解决方案是使用类型的默认值,即 `default (T)`。默认值也称为可空类型的空值。这里会发生一个隐式转换,将 null 字面量转换为任何类型,然后生成所需类型的 null 值。

GemeItems 类

为了使此类代码更清晰,我使用了一些VS.Net 2005的新功能,如 `partial` 关键字,它允许将一个大型类、结构或接口分解成多个文件。此类对程序的运行方式具有根本性的重要性。原因是它将利用链表方法。实例化时,`GameItems` 类会创建两个 `LinketItems<T>` 列表,它们将接收 `Item` 类作为参数,如代码所示。

 
public GameItems(GameForm frm)
{
            //savedItemsLocation = new List<Item>();
            // creates a target linked list
            targetListItems = new LinkedItems<Item>();
            // creates a source linked list
            sourceListItems = new LinkedItems<Item>();
 
            // save the initial appearance of the game
            SaveSourceLocationAndItems(frm);
}
 

您可以看到,创建的列表 `targetListItems` 和 `sourceListItems` 是强类型的,因为参数是预定义类型。这使得它们的使用更加容易,因为在从列表中获取元素时不需要使用“cast”,因为列表只接受一种类型的项目。`GameItems` 类还在其构造函数中接收表单的引用,该引用被传递给 `SaveSourceLocationAndItems` 方法,从而可以保存源面板中所有现有块的初始位置。另一个有趣的点是,`GameItems` 类通过在列表之间交换块并更新其位置来操作游戏块,而 `GameForm` 类则操作面板中的块。

添加和删除项目

在列表的开头或结尾添加项目的过程很简单,代码在这方面也很直观。您可以看到,要从列表中添加或删除一个元素,需要更新列表指针,这由.Net Framework在内部完成。在C++、C或汇编等低级语言中,这必须由程序员手动完成。让我们看看如何在一个项目之前添加一个项目,如下面的代码所示。

 
public PictureBox AddPieceBefore()
{
            Item selectedSrcItem = null;
            Item selectedTgtItem = null;
 
            // get the selected item in source list
            selectedSrcItem = GetSelectedItem(sourceListItems);
            // get the selected item in target list
            selectedTgtItem = GetSelectedItem(targetListItems);
 
            // if null, needs to select one item
            if (selectedSrcItem != null && selectedTgtItem != null)
            {
                // add item to end list
                targetListItems.AddBeforeItem(selectedTgtItem, selectedSrcItem);
                // remove item from source list
                sourceListItems.Remove(selectedSrcItem);
                // unselect the item
                selectedSrcItem.IsSelected = false;
                selectedTgtItem.IsSelected = false;
 
                // try to shift all items to acomodate the added item 
                Point nextPosition = solutionPosition;
                // update the list
                UpdatePositionForEntireList(targetListItems, nextPosition);
            }
            else
                return null;
            return selectedSrcItem.PicItem;
}
 

在另一个项目之前添加项目时,必须满足一些条件,如下面的代码所示。

 
if (selectedSrcItem != null && selectedTgtItem != null)
 

代码会验证源列表中是否存在要移除的选中项目,以及目标列表中是否存在要用作参考的选中项目,如 `AddBeforeItem` 方法(该方法接受两个参数)所示。如果插入操作成功,则该项目将从源列表中移除,并且块将被设为未选中状态。插入后,仍需要更新项目的位置,这相当于更新其指针,如上所述。`UpdatePositionForEntireList` 方法会进行此更新,将每个项目安排到其正确的位置。`AddAfterItem` 方法的行为类似,只是更改了插入的位置,即稍后添加。请注意,对于移除方法,不存在 `RemoveBefore` 或 `RemoveAfter`。一旦用户选择了一个块进行移动,在该程序中,拥有一个从参考块之前或之后移除块的方法就没有意义了。当然,如果需要,这可以很容易地实现。

选择项目

如前所述,根据要在游戏中执行的操作,需要选择一个或两个块。此选择顺序在 `theGameForm` 类中通过 `PieceItem_Click` 方法完成,该方法又将命令转发到 `GemeItems` 类中的 `SelectPicItem` 方法。选中项目状态的更改由 `Item` 类中的 `IsSelected` 属性完成,如下面的代码所示。

 
public bool IsSelected
{
            get{ return selected;}
 
            set
            {
                selected = value;
                // if the item is selected, change its appearence
                // to notify the user
                if (selected)
                    item.BorderStyle = BorderStyle.FixedSingle;
                else
                    item.BorderStyle = BorderStyle.None;
            }
}
 

关注点

该项目通过代码以直观的方式展示了使用 `Generic` 命名空间处理双向链表是多么简单。使用强类型列表可以更好地编程系统。由于主要目标是以直观的方式展示这些概念,因此如果目标是创建一个真正的游戏,则显然可以进行一些改进。请注意,按钮可以简单地移除,并且块的移动可以完全通过鼠标完成。当然,这个示例可以作为真正拼图游戏的起点。尽情享受吧!

© . All rights reserved.