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

Sokoban Pro

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (15投票s)

2004年12月24日

5分钟阅读

viewsIcon

114249

downloadIcon

3139

Sokoban Pro 是一款经典的 Sokoban 益智游戏的现代版本。

Sample Image - SokobanPro.jpg

引言

Sokoban Pro 是一款经典的 Sokoban 益智游戏的现代版本。游戏规则极其简单,但游戏极具挑战性和令人上瘾。游戏规则是将所有箱子推到正确的位置。你只能推箱子,不能拉。您可以通过按 U 来撤销最后一步。

游戏的新版本现已在 Sokoban Pro 网站 上发布。由于所有最重要的功能(如读取/写入 XML、移动和绘图)都在 CodeProject 的源代码中,因此我决定保留第一个原始版本。您可以从 Sokoban Pro 网站下载最新版本。

最新版本(v1.0b)相比 CodeProject 上的版本包含以下改进:

  • “官方”测试版发布
  • 添加了菜单
  • 我发现窗口大小随关卡变化很烦人,所以现在它已固定大小
  • 您可以跳转到不同的关卡,但只能跳转到您之前完成过的关卡
  • 1 步撤销功能
  • 启动屏幕和图标
  • 修复了许多错误
  • 可能还有其他一些东西

游戏开始时,您可以创建一个新玩家或选择一个现有玩家。由于 Sokoban Pro 会保存您的进度,因此您还可以选择是否继续之前的游戏。创建玩家后,您可以选择关卡集。关卡集包含任意数量的关卡。Sokoban Pro 附带了原始 BoxWorld 游戏的前 40 个关卡。关卡集存储在 XML 文件中,这意味着您可以从互联网上不同的 Sokoban 网站下载关卡集。您也可以创建自己的关卡。当您将关卡集放入 levels 目录时,Sokoban Pro 会自动识别它们。

您的存档游戏也是一个 XML 文件。它保存最后一个玩的关卡集和最后一个玩的关卡,以便您可以从上次玩游戏的地方继续。它还保存您已完成的关卡以及得分(移动次数和推动次数)。如果您重试某个关卡并且表现更好,您的得分将得到更新。

基本上,游戏包含以下类:

  • LevelSet - 包含关卡集的所有信息(作者信息、关卡数量等)。它还将关卡从关卡集 XML 加载到内存中。
  • Level - 代表关卡集中的一个关卡。这里发生的最重要的事情是它会跟踪您的所有移动。当玩家移动或推动箱子时,它会更新关卡中的项目。它会更新相应的图形。它实现了撤销功能,最后,它会在屏幕上绘制关卡。
  • PlayerData - 跟踪所有玩家信息。基本上,它反映了您的存档游戏。
  • Board (Form) - 主窗体处理所有玩家输入并初始化所有对象。
  • Players (Form) - 允许您创建新玩家或选择现有玩家。
  • Levels (Form) - 允许您选择要玩的关卡集。

该应用程序使用 XML 文件的读写。以这个方法为例 - SaveLevel() - 它会在玩家完成关卡后保存玩家数据。

public void SaveLevel(Level level)
{
    XmlDocument doc = new XmlDocument();
    doc.Load(filename);

    XmlNode lastFinishedLvl = doc.SelectSingleNode("//lastFinishedLevel");
            lastFinishedLvl.InnerText = level.LevelNr.ToString();

    XmlNode setName = doc.SelectSingleNode("/savegame/levelSets/" +
                "levelSet[@title = \"" + level.LevelSetName + "\"]");
    XmlNode nodeLevel = setName.SelectSingleNode("level[@levelNr = " +
                level.LevelNr + "]");

    if (nodeLevel == null)
    {            
        XmlElement nodeNewLevel = doc.CreateElement("level");
        XmlAttribute xa = doc.CreateAttribute("levelNr");
        xa.Value = level.LevelNr.ToString();
        nodeNewLevel.Attributes.Append(xa);
        XmlElement moves = doc.CreateElement("moves");
        moves.InnerText = level.Moves.ToString();
        XmlElement pushes = doc.CreateElement("pushes");
        pushes.InnerText = level.Pushes.ToString();

        nodeNewLevel.AppendChild(moves);
        nodeNewLevel.AppendChild(pushes);
        setName.AppendChild(nodeNewLevel);
    }
    else
    {
        XmlElement moves = nodeLevel["moves"];
        XmlElement pushes = nodeLevel["pushes"];
        int nrOfMoves = int.Parse(moves.InnerText);
        int nrOfPushes = int.Parse(pushes.InnerText);

        if (level.Pushes < nrOfPushes)
        {
            pushes.InnerText = level.Pushes.ToString();
            moves.InnerText = level.Moves.ToString();
        }
        else if (level.Pushes == nrOfPushes && level.Moves < nrOfMoves)
            moves.InnerText = level.Moves.ToString();
    }

        doc.Save(filename);
}

以下是发生的情况:

我们使用 XPath 来选择 XML 文件中的节点。这样,我们就不必逐行读取 XML 直到找到要添加新元素或更新现有元素的正确元素。这行代码

XmlNode setName = doc.SelectSingleNode("/savegame/levelSets/" + 
    "levelSet[@title = \"" + level.LevelSetName + "\"]");

选择我们当前正在玩的关卡的关卡集节点。(请记住,Sokoban Pro 支持多个关卡集,因此 XML 中可能有一个以上的关卡集)。然后,这行代码

XmlNode nodeLevel = setName.SelectSingleNode("level[@levelNr = " 
               + level.LevelNr + "]");

选择当前关卡集中的关卡编号。

如果我们找不到当前的关卡节点,则意味着我们之前没有完成该关卡,我们会添加一个新的关卡节点。如果我们找到了关卡节点,则意味着我们之前玩过该关卡,我们会检查我们的当前得分是否更好,如果是,则更新得分。

游戏大量使用 XML 文件的读写,正如您所见,使用 XPath 是一个非常强大的工具。

我想展示的另一件事是如何加载关卡。正如我所说,我们将关卡存储在 XML 中。关卡由不同的项目组成;一堵墙、一个地板、一个箱子等。加载关卡时,我们读取 XML 中包含关卡数据的行。关卡中的每个项目都由一个 ASCII 字符表示。读取关卡数据时,我们将项目存储在二维数组中。

当我们想绘制关卡时,我们会读取数组,然后可以像这样绘制关卡:

// Draw the level
    for (int i = 0; i < width; i++)
    {
        for (int j = 0; j < height; j++)
        {
            Image image = GetLevelImage(levelMap[i, j], sokoDirection);

            g.DrawImage(image, ITEM_SIZE + i * ITEM_SIZE, ITEM_SIZE 
                        + j * ITEM_SIZE, ITEM_SIZE, ITEM_SIZE);

            // Set Sokoban's position
            if (levelMap[i, j] == ItemType.Sokoban 
                || levelMap[i, j] == ItemType.SokobanOnGoal)
            {
                sokoPosX = i;
                sokoPosY = j;
            }
        }
    }

我们逐行、逐字符地读取数组。根据我们遇到的项目,我们将获得 GetLevelImage 方法返回的图像,该方法会检查我们有什么项目并返回相应的图像。最后,我们绘制图像,给定项目的位置以及宽度和高度。尺寸存储在 ITEM_SIZE 变量中。如果我们想以更小的尺寸绘制关卡(可能适用于分辨率较低的显示器),我们可以减小 ITEM_SIZE 的值(默认值为 30)。

另一个有趣的事情是检查 Sokoban 是否可以移动。只有当他面前的项目是空地,或者是一个后面有空地的箱子时,他才能移动。当他移动时,我们将 Sokoban 的新位置以及箱子和 Sokoban 留下的空地存储在三个独立的 Item 对象中,我们使用这些对象来只重绘关卡的这三个项目,而不是重绘整个关卡。撤销最后一次推动也是如此。

尽情享用!

© . All rights reserved.