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

WPF Alien Sokoban

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (40投票s)

2007年11月1日

BSD

9分钟阅读

viewsIcon

128862

downloadIcon

3467

一个有趣的推箱子游戏实现,旨在展示 WPF、C# 3.0、Expression Design 和 Visual Studio 2008 的一些特性。

Screenshot - WpfSokoban.jpg

目录

引言

可下载的 WPF 推箱子项目旨在作为创建 WPF 应用程序的有趣而富有指导性的介绍,并介绍 .NET 3.5 的一些新特性。它受到 Sacha Barber 的优秀文章 WPF:经典贪吃蛇的 WPF 化 的启发。本文概述了 WPF 数据绑定:控件的样式和模板。在后续文章中,我打算将此应用程序移植到 Silverlight,以探索这两种技术之间的一些差异。

背景

如果你必须玩,请在开始时决定三件事:游戏规则、赌注和退出时间。

- 中国谚语

推箱子,像许多其他烧脑和耗时的谜题一样,玩起来可能很有趣,但很难戒掉。它规则很少,有时可能具有挑战性。该游戏由今林宏行于 1980 年发明。它无处不在,可以在游戏机、手机上找到,根据 维基百科条目,甚至在佳能 PowerShot 数码相机上也有。

如何玩游戏

将蓝色能量方块(宝藏)推到黄色能量站上。当所有方块都放置在能量站中时,我们的外星朋友将被传送到下一个级别。一次只能推动一个能量方块,并且不能拉动它们。
一旦达到新关卡,请记下关卡代码。这可用于稍后返回该关卡。

控件

可以使用箭头键或鼠标来控制角色。如果使用箭头键,则角色可以向上、向下、向左或向右移动。如果使用鼠标,则角色将尝试遍历关卡网格到用户单击的点。鼠标可以通过将角色放置在能量方块旁边,然后单击它来移动能量方块。

撤销和重做

使用 Ctrl+Z 和 Ctrl+Y 撤消和重做移动和跳跃。

关卡物品

  • 能量方块需要移动到目标单元格(能量站)
  • 角色由用户控制
  • 墙单元格角色无法进入,也不能在其中放置内容
  • 地面单元格是角色可以移动或放置能量方块的地方
  • 空间单元格通常位于关卡网格的围墙之外。它们不能包含内容

移动

角色可以一步移动到相邻的地面单元格,也可以跳跃到关卡网格上任何可到达的地面单元格。在跳跃的情况下,应用程序将计算最佳路径,即由最少步数组成的路径。

扩展可玩性

通过创建或修改位于 Levels 目录中的地图文件,可以扩展外星人推箱子的可玩性。

地图文件格式

地图文件中使用以下字符来表示关卡结构

  • "#" 墙
  • " " 空地面单元格
  • "$" 能量站中的能量单元格(或目标)
  • "." 能量站(或目标)
  • "@" 地面单元格中的角色
  • "!" 空间单元格

XAML 和数据绑定

此应用程序使用填充了按钮的 System.Windows.Controls.Grid 显示游戏关卡。也就是说,网格上的每个单元格都包含一个按钮。我们为每个按钮使用一个 Style,并结合 Style 触发器来显示单元格和任何单元格内容。
以下来自 MainWindow.xaml 的摘录展示了如何实现这一点

<Style x:Key="Cell" TargetType="{x:Type Button}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Grid>
                    <!-- The cell, -->
                    <Rectangle Width="40" Height="40" 
                        Style="{DynamicResource CellStyle}" />
                    <!-- and its content. -->
                    <Rectangle Style="{DynamicResource CellContentStyle}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

当绑定到按钮的 CellCellContents 发生变化时,Style 触发器会改变单元格的显示方式。以下摘录展示了当 CellContents.Name 属性变为 Actor 时,如何设置 Actor 进行显示。

<MultiDataTrigger>
    <MultiDataTrigger.Conditions>
        <!-- If the cell contains the Actor. -->
        <Condition Binding="{Binding Path=CellContents.Name}" Value="Actor" />
    </MultiDataTrigger.Conditions>
    <Setter Property="Fill" Value="{StaticResource PlayerCellContentBrush}"/>
</MultiDataTrigger>

CellContent 类的 PropertyChanged 事件触发时,会评估触发器。当关卡在 MainWindow 代码隐藏的 InitialiseLevel 方法中首次初始化时,按钮的 DataContext 被设置为 Orpius.Sokoban.Cell。在这种情况下,数据绑定是单向的。也就是说,UI 不会改变 Cell 实例。DataContext (在本例中是按钮)只是一个实现了 INotifyPropertyChanged 接口的对象实例。当 DataContext 中的属性发生变化时,它会反映在 UI 中。有关 WPF 数据绑定的更详细解释,请参阅 MSDN 上的数据绑定。请注意,实现 INotifyPropertyChanged 接口对于数据绑定不是必需的,但这允许 DataContext 的更改反映在 UI 中。在应用程序的整个生命周期中,使用单个 Orpius.Sokoban.Game 实例。它在 XAML 中指定,如下摘录所示

<Sokoban:Game x:Key="sokobanGame"/>

当 MainWindow 初始化时,会创建一个 Game 类的实例并作为窗口资源提供。然后,我们能够任意将数据绑定到该实例,如下摘录所示

<Label Name="label_Moves" Style="{StaticResource CenterLabels}" 
    Content="{Binding Level.Actor.MoveCount}"/>

这里,Level 是前面提到的 Game Window.Resource 的一个属性。Game 实例也用于代码隐藏中,并作为属性私有公开,如下所示

Game Game
{
    get
    {
        return (Game)TryFindResource("sokobanGame");
    }
}

推箱子项目

所有游戏逻辑都包含在 Orpius.Sokoban 项目中。选择这种方法是为了提供游戏逻辑和表现逻辑的清晰分离,以便我们可以轻松地将游戏逻辑重用于 WPF 以外的技术。

游戏逻辑概述

Sokoban project class diagram
图:类图,概述了主要的推箱子项目类之间的关系

格子

如上图所示,一个 Game 在任何时候最多只有一个 Level。每个 Level 都有一个 Cell 集合,每个 Cell 有零个或一个 CellContents 实例。

Cell 是游戏中使用的所有单元格的基本实现。

Cell Class Diagram
图:Cell 类和具体实现

单元格内容

一个 Cell 可以容纳一个 CellContents 类的单个实例。也就是说,在任何时候,一个单元格中都可能存在 ActorTreasure。两者不能同时存在。

Cell Contents Class Diagram
图:CellContents 基类及其两个具体实现:TreasureActor

命令管理器

表现层项目中 Game 实例的每次修改都是通过传递给 CommandManagerCommandBase 实例完成的,它是 命令模式 的实现。这允许我们撤消和重做角色执行的移动。Jump 被视为一个单一命令,因此可以一步撤消。CommandManager 维护一个可撤消和可重做命令的 Stack

Command Manager Class Diagram
图:CommandManager 及相关类的类图

移动

移动被传递给 Actor 实例。角色知道如何执行移动,无论是移动到相邻单元格,还是跳跃到关卡上某个非相邻单元格。

Actor 实例累积移动次数。跳跃被视为一组移动,因此跳跃会使移动次数增加一或更多。撤消移动或跳跃会相应地减少移动次数。

Move Class Diagram
图:MoveBase 类及其相关的具体类

跳跃和路径搜索

JumpActor 实例在 PathSearch 实例的帮助下执行。PathSearch 尝试递归地找到到 Jump.Destination 位置的最短路径。递归路径搜索方法如下所示

bool Step(Location location, int steps)
{
    bool found = false;
    steps++;

    if (location.Equals(destination))
    {
        if (minSteps == 0 || steps < minSteps)
        {
            route = new Move[steps];
            minSteps = steps;
            foundPath = true;
            arrayIndex = minSteps - 1;
        }
        else
        {
            return false;
        }
    }
    else
    {
        for (int i = 0; i < 4; i++)
        {
            Direction direction = GetDirection(i);
            Location neighbour = location.GetAdjacentLocation(direction);

            int traversedLength = traversed.Count;

            if (level[neighbour].CanEnter
                && IsNewLocation(neighbour)
                && Step(neighbour, steps)) /* Recursive call. */
            {
                route[arrayIndex--] = new Move(direction);
                found = true;
            }
            traversed.RemoveRange(traversedLength, 
                traversed.Count - traversedLength);
        }
        return found;
    }
    return foundPath;
}

这种方法的效率应该提高。对于有大片开放区域的关卡,它太慢了。它已被列为 未来的增强功能

Expression Design 和资源字典

Expression Design 用于创建游戏中所有可见的图像。

Expression Design Cell
图:Expression Design

我没有在 Photoshop 中模拟整个游戏板,而是选择在 Expression Design 中进行设计实验并随之导出。这种方式的效率肯定较低,因为浪费了时间反复导出和查看。尽管如此,过程还是有所简化:通过导出为资源字典,可以为文档中的每个图层创建一个画刷。

Expression Design Export
图:使用 Expression Design 导出

App.xaml 文件中,为每个 Expression Design 导出的文档创建了一个 ResourceDictionary 元素。

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="ResourceDictionaries/Cell.xaml" />
            <ResourceDictionary Source="ResourceDictionaries/Banner.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

然后可以在触发器中为单元格设置样式,如下所示

<Setter Property="Fill" Value="{StaticResource WallCellBrush}"/>

关注点

异步属性更改

应用程序中发生了两个异步操作。第一个是关卡加载。为了弥补地图文件数据加载缓慢(例如在 Web 应用程序中)的问题,关卡数据加载是异步完成的。第二个是为了模拟角色在关卡中行走,当发生 Jump 时,执行线程需要休眠特定时间。这同样是异步完成的。与 Windows Forms 编程一样,WPF 使用具有线程亲和性的单个执行线程。CellCellContents 的基类是 LevelContentBase。此类提供异步引发 PropertyChanged 事件的功能;不使用主 UI 线程。这是通过在实例化期间使用 SynchronizationContext.Current 属性初始化 SynchronizationContext 实例,并将调用发送到主 UI 线程来完成的,如 LevelContentBase.cs 中的以下摘录所示。

context.Send(delegate
{
    OnPropertyChanged(new PropertyChangedEventArgs(property));
}, null);

关卡代码

关卡代码用于在成功完成关卡后跳到该关卡。LevelCode 类有 500 个预生成的关卡代码可用。尽管只有 51 个关卡,但如果添加更多关卡文件(Levels 目录中的 *.skbn 文件),关卡代码将打开。LevelCode 类中存在一些 static 方法,可以在必要时重新生成关卡代码。

未来的增强

  • 提高搜索路径算法的效率。它在大片开放区域中表现不佳
  • 为角色/方块等添加一些动画
  • 创建关卡编辑器

结论

这个项目的目标是探索 WPF,并运用 C# 3.5 的一些新语言特性,包括扩展方法、自动属性和对象初始化器。WPF 使快速创建复杂 GUI 界面变得异常容易,而无需编写大量样板代码。在我的下一篇文章中,我打算将“外星人推箱子”移植到 Silverlight。

希望您觉得这个项目有用。如果是,您可以对它进行评分和/或在下方留下反馈。

致谢

参考文献

历史

  • 2007 年 11 月:首次发布
  • 2008 年 6 月 15 日:改进了路径搜索算法和图形。
© . All rights reserved.