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

为 Windows Mobile 设备创建托管代码的休闲益智游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (22投票s)

2008年11月29日

CPOL

17分钟阅读

viewsIcon

63317

downloadIcon

1420

本文介绍了如何在托管代码中为 Windows Mobile 设备创建休闲(轻松、简单且有趣)的益智游戏。

引言

本文介绍了如何在托管代码中为 Windows Mobile 设备创建休闲(轻松、简单且有趣)的益智游戏。此示例使用 Visual Studio 2008 和 C# 进行开发。生成的可执行文件以安装了 .NET Compact Framework 3.5 的 Windows Mobile 5 或更高版本设备为目标。稍作修改,它应该向后兼容早期 Windows Mobile 设备。由于 SoundPlayer 类是 .NET Compact Framework 3.5 的新增功能,声音例程需要使用其他方法重写。

本文可以学到什么

  1. 益智算法
  2. 整数数组到屏幕坐标到控件坐标的映射
  3. 使用 PointStack<>
  4. 少量图形和声音编程

我读了一篇关于游戏 Trism 及其开发者 Steve Demeter 的 CNN 文章,他发布这款基于 iPhone 的游戏后几个月内就赚了二十五万美元。哇。Steve 干得漂亮!考虑到他的游戏当时获得的媒体报道,他可能又赚了二十五万美元。

正如你所见,开发者通过 Trism、Jawbreaker 和 Bejeweled 等软件赚取了数十万美元。无论游戏是滑动三角形、彩色球、闪亮宝石还是其他图形对象,当所有花哨的东西都被剥离后,这些游戏实际上就是一些通用的算法作用于一个整数矩阵!

读完 Trism 的文章后不久,我收到了一封微软移动邮件,提到了 CodeProject 的微软移动开发者竞赛。我想我应该尝试开发一款休闲益智游戏。以下游戏 Fruit Drop,就是我辛勤劳动的成果(双关语)。

Fruit Drop

Fruit Drop Screen Shot

Fruit Drop 是一款类似 Jawbreaker、Bubble Breaker 和/或 Bubblet 的游戏。

游戏玩法

随机生成的游戏棋盘由各种水果图像矩阵组成。有五种不同的水果图像:樱桃、香蕉、葡萄、橙子和草莓。玩家点击两个或更多相同类型的水果,它们在垂直或水平方向相邻。一次移除的水果图像越多,获得的积分越多。

当没有相邻的同类水果图像时,游戏结束。

评分

获得的积分用公式表示:积分 = (移除的图像数)2。因此,一次移除的同类水果图像越多,得分就越高。例如

Scoring System

代码

生成游戏棋盘

有许多方法可以生成伪随机游戏棋盘。这里有三种简单的方法。

第一种基本方法是逐个遍历游戏棋盘的每个位置,生成一个介于一到包含水果图像总数之间的伪随机数。这种方法相对较快,但可能会生成一种水果图像比另一种多得多的游戏棋盘。事实上,有可能(但不很可能)生成一个全是一种水果图像的游戏棋盘。

public void InitializeGrid()
{
   for(int x=0; x < MAX_X; ++x)
   {
      for(int y=0; y < MAX_Y; ++y)
      {
         m_PuzzleGrid[x, y] = m_Random.Next(1, 6);
      }
   }
}

第二种基本方法是计算矩阵中的位置数,然后将该数字除以水果图像的数量。然后,遍历每种水果图像,将该水果图像的正确数量放置在游戏棋盘的伪随机、未占用位置上。这种方法会将每种水果图像相等数量(或接近相等,取决于单元格数除以水果图像数是否为整数)放置在游戏棋盘上,但可能会花费较长时间来生成游戏棋盘,试图为每种水果图像找到随机、未占用的位置。

我的同事 Sam Domenico 提出了第三种随机生成游戏结构的方法。虽然此游戏未使用此方法,但它也提供了平衡的游戏数据结构,并且在最佳情况下,与上述方法二相比,它执行得快得多。它需要更多的设置,所以在最坏的情况下,执行速度会比方法二稍慢。

当将方法三与方法二进行比较时,方法三的最佳情况是方法二在为任何剩余值查找未占用位置时陷入了几乎无限的循环。方法三的最坏情况是方法二在第一次尝试时就为每个值找到了一个未占用位置。

此方法需要将二维游戏数据结构展开为一维对象集合,其中每个对象包含 x、y 和 value 成员。初始化此集合,以便每个对象具有对应于游戏数据结构位置的 x 和 y 值。然后,遍历集合,从列表中剩余的对象中选择一个随机索引,更新该位置上的对象,然后将该位置上的对象移到列表末尾,并递减可用对象的计数。每个对象只能被随机选择一次。循环完成后,将集合映射回游戏数据结构。

考虑以下控制台代码和输出

class Program
{
    public class CCell
    {
        public int X;
        public int Y;
        public int Value;
    }

    private static readonly int MAX_X = 8;
    private static readonly int MAX_Y = 8;
    private static Random m_random = new Random();

    static void Main(string[] args)
    {
        // Populate a list with an equal number of each value desired.
        // In this case the numbers 1,2,3,4 repeat until array is filled,
        // giving an equal number of each value (fruits, cards, colors, etc.).
        int[] aValueArray = new int[MAX_X * MAX_Y];
        for (int x = 0; x < aValueArray.Length; ++x)
        {
            aValueArray[x] = (x % 4) + 1;
        }

        // Initialize list for random placement.
        List<CCell> aPlacementList = new List<CCell>();
        for (int x = 0; x < MAX_X; ++x)
        {
            for (int y = 0; y < MAX_Y; ++y)
            {
                CCell aCell = new CCell();
                aCell.X = x;
                aCell.Y = y;
                aCell.Value = 0;
                aPlacementList.Add(aCell);
            }
        }

        // Get count from random placement list.
        int aPlacementListCount = aPlacementList.Count;

        // Keep track of the ValueArray index.
        int aValueArrayIndex = 0;

        while (aPlacementListCount > 0)
        {
            // Select random index to update.
            int aSelectedIndex = m_random.Next(0, aPlacementListCount);

            // Copy the cell that will be updated.
            CCell aCell = aPlacementList[aSelectedIndex];

            // Remove it from the list.
            aPlacementList.RemoveAt(aSelectedIndex);

            // Update the cell.
            aCell.Value = aValueArray[aValueArrayIndex];

            // Then add it back to the end of the list.
            aPlacementList.Add(aCell);

            // Increment the ValueArray index to move to next value.
            aValueArrayIndex++;

            // Decrement the PlacementList count to 'shrink' size of list so
            // that the previously updated cell(s) will not be looked at again.
            --aPlacementListCount;
        }

        // Map the psuedo-random placement list to game data structure.
        int[,] aGameStructure = new int[MAX_X, MAX_Y];
        for (int x = 0; x < aPlacementList.Count; ++x)
        {
            CCell aCell = aPlacementList[x];
            aGameStructure[aCell.X, aCell.Y] = aCell.Value;
        }

        // Display results.
        for (int y = 0; y < MAX_Y; ++y)
        {
            for (int x = 0; x < MAX_X; ++x)
            {
                Console.Write(aGameStructure[x, y].ToString() + " ");
            }
            Console.WriteLine();
        }
    }
}

为了简单和速度,Fruit Drop 使用了上面讨论的第一种方法,尽管第二种或第三种方法可能是更平衡的游戏的更好选择。

渲染屏幕

通过遍历拼图矩阵中的每个单元格,通过检查单元格值来确定要显示的正确图像来渲染游戏棋盘。然后,将每个图像映射到其正确位置并绘制在屏幕上。

// Clear the picture box.
pbGameBox.Image = m_Bitmap;

Image aGameBoxImage = this.pbGameBox.Image;

// Render the screen using preloaded images.
using (Graphics g = Graphics.FromImage(aGameBoxImage))
{

    for (int aRow = 0; aRow < BOARD_MAX_HEIGHT; ++aRow)
    {
        for (int aColumn = 0; aColumn < BOARD_MAX_WIDTH; ++aColumn)
        {
            switch (m_PuzzleGrid[aColumn, aRow])
            {
                case 0:
                    g.DrawImage(pbZero.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
                case 1:
                    g.DrawImage(pbOne.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
                case 2:
                    g.DrawImage(pbTwo.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
                case 3:
                    g.DrawImage(pbThree.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
                case 4:
                    g.DrawImage(pbFour.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
                case 5:
                    g.DrawImage(pbFive.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
                case 9:
                    g.DrawImage(pbPop.Image, 
                        new Rectangle(aColumn * IMAGE_MAX_WIDTH, aRow * 
            IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                        new Rectangle(0, 0, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT), 
                            GraphicsUnit.Pixel);
                    break;
            }
        }
    }
}

声音

由于 .NET Compact Framework 3.5 支持 SoundPlayer 类,因此使用此方法生成游戏声音。我遇到的一个问题是,第一次播放声音时,屏幕渲染会出现明显的延迟。为了减轻这种烦恼,我使用 GoldWave 创建了一个带有两秒钟静音的 WAV 文件,该文件在窗体加载事件期间加载并播放。这为将来的声音“预热了水壶”。

游戏声音嵌入在 Resources.resx 文件中。然后,这些资源被转换为 MemoryStream,并在需要时播放。

if (m_IsSoundOn)
{
    try
    {
        m_SoundPlayer.Stop();
        m_SoundPlayer.Stream = new MemoryStream
        (com.langesite.fruitdrop.Properties.Resources.Quiet);
        m_SoundPlayer.Load();
        m_SoundPlayer.Play();
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }
}

邻近搜索算法

这款游戏中,最有趣的算法是“邻近搜索算法”。该算法查看玩家选择的水果图像及其所有必需的相邻水果图像,以确定应该移除哪些水果图像。

“邻近搜索算法”遵循以下模式

Search Route

使用一个 Point 对象堆栈 Stack<> 来实现“邻近搜索算法”。一旦玩家单击一个图像,该图像的单元格位置就会被推入堆栈,方法是使用 Point 对象来保存单元格的坐标。一旦第一个点被推入堆栈,就会执行一个循环,该循环会测试并更新堆栈中的点,直到堆栈为空。

Fruit Search Route

伪代码看起来大致如下

  • 玩家单击单元格 (3,3)
  • 单元格 (3,3) 被推入堆栈
  • 单元格 (3,3) **上方**的单元格 (3,2) 被测试极端边界并成功
  • 单元格 (3,3) **上方**的单元格 (3,2) 被测试图像类型并成功
  • 单元格 (3,3) **上方**的单元格 (3,2) 被测试未访问并成功
  • 单元格 (3,3) 上一次检查的方向被保存,并且单元格 (3,2) 被标记为已访问并推入堆栈
  • 单元格 (3,2) **上方**的单元格 (3,1) 被测试极端边界并成功
  • 单元格 (3,2) **上方**的单元格 (3,1) 被测试图像类型并失败
  • 单元格 (3,2) 的检查方向更改为 **东方**
  • 单元格 (3,2) **右侧**的单元格 (4,2) 被测试极端边界并成功
  • 单元格 (3,2) **右侧**的单元格 (4,2) 被测试图像类型并成功
  • 单元格 (3,2) **右侧**的单元格 (4,2) 被测试未访问并成功
  • 单元格 (3,2) 上一次检查的方向被保存,并且单元格 (4,2) 被标记为已访问并推入堆栈
  • 单元格 (4,2) **上方**的单元格 (5,1) 被测试极端边界并成功
  • 单元格 (4,2) **上方**的单元格 (5,1) 被测试图像类型并成功
  • 单元格 (4,2) **上方**的单元格 (5,1) 被测试未访问并成功
  • 单元格 (4,2) 上一次检查的方向被保存,并且单元格 (5,1) 被标记为已访问并推入堆栈
  • 单元格 (4,1) **上方**的单元格 (undefined) 被测试极端边界并失败
  • 单元格 (4,1) 的检查方向更改为 **东方**
  • 单元格 (4,1) **右侧**的单元格 (5,1) 被测试极端边界并成功
  • 单元格 (4,1) **右侧**的单元格 (5,1) 被测试图像类型并失败
  • 单元格 (4,1) 的检查方向更改为 **南方**
  • 单元格 (4,1) **下方**的单元格 (4,2) 被测试极端边界并成功
  • 单元格 (4,1) **下方**的单元格 (4,2) 被测试图像类型并成功
  • 单元格 (4,1) **下方**的单元格 (4,2) 被测试未访问并失败
  • 单元格 (4,1) 的检查方向更改为 **西方**
  • 单元格 (4,1) **左侧**的单元格 (3,1) 被测试极端边界并成功
  • 单元格 (4,1) **左侧**的单元格 (3,1) 被测试图像类型并失败
  • 单元格 (4,1) 的所有方向都已测试完毕,因此将其从堆栈中弹出,并继续测试堆栈中的下一个单元格
  • 单元格 (4,2) 现在位于堆栈顶部,其上一次检查的方向是 **北方**
  • 单元格 (4,2) 的检查方向更改为 **东方**
  • 单元格 (4,2) **右侧**的单元格 (5,2) 被测试极端边界并成功
  • 单元格 (4,2) **右侧**的单元格 (5,2) 被测试图像类型并成功
  • 单元格 (4,2) **右侧**的单元格 (5,2) 被测试未访问并成功
  • 单元格 (4,2) 上一次检查的方向被保存,并且单元格 (5,2) 被标记为已访问并推入堆栈
  • 单元格 (5,2) **上方**的单元格 (5,1) 被测试极端边界并成功
  • 单元格 (5,2) **上方**的单元格 (5,1) 被测试图像类型并失败
  • 单元格 (5,2) 的检查方向更改为 **东方**
  • 单元格 (5,2) **右侧**的单元格 (undefined) 被测试极端边界并失败
  • 单元格 (5,2) 的检查方向更改为 **南方**
  • 单元格 (5,2) **下方**的单元格 (5,3) 被测试极端边界并成功
  • 单元格 (5,2) **下方**的单元格 (5,3) 被测试图像类型并失败
  • 单元格 (5,2) 的检查方向更改为 **西方**
  • 单元格 (5,2) **左侧**的单元格 (4,2) 被测试极端边界并成功
  • 单元格 (5,2) **左侧**的单元格 (4,2) 被测试图像类型并成功
  • 单元格 (5,2) **左侧**的单元格 (4,2) 被测试未访问并失败
  • 单元格 (5,2) 的所有方向都已测试完毕,因此将其从堆栈中弹出,并继续测试堆栈中的下一个单元格
  • 单元格 (4,2) 现在再次位于堆栈顶部,其上一次检查的方向是 **东方**
  • 单元格 (4,2) 的检查方向更改为 **南方**
  • 单元格 (4,2) **下方**的单元格 (4,3) 被测试极端边界并成功
  • 单元格 (4,2) **下方**的单元格 (4,3) 被测试图像类型并失败
  • 单元格 (4,2) 的检查方向更改为 **西方**
  • 单元格 (4,2) **左侧**的单元格 (3,2) 被测试极端边界并成功
  • 单元格 (4,2) **左侧**的单元格 (3,2) 被测试图像类型并成功
  • 单元格 (4,2) **左侧**的单元格 (3,2) 被测试未访问并失败
  • 单元格 (4,2) 的所有方向都已测试完毕,因此将其从堆栈中弹出,并继续测试堆栈中的下一个单元格
  • 单元格 (3,2) 现在再次位于堆栈顶部,其上一次检查的方向是 **东方**
  • 单元格 (3,2) 的检查方向更改为 **南方**
  • 单元格 (3,2) **下方**的单元格 (3,3) 被测试极端边界并成功
  • 单元格 (3,2) **下方**的单元格 (3,3) 被测试图像类型并成功
  • 单元格 (3,2) **下方**的单元格 (3,3) 被测试未访问并失败
  • 单元格 (3,2) 的检查方向更改为 **西方**
  • 单元格 (3,2) **左侧**的单元格 (2,2) 被测试极端边界并成功
  • 单元格 (3,2) **左侧**的单元格 (2,2) 被测试图像类型并成功
  • 单元格 (3,2) **左侧**的单元格 (2,2) 被测试未访问并成功
  • 单元格 (3,2) 上一次检查的方向被保存,并且单元格 (2,2) 被标记为已访问并推入堆栈
  • 单元格 (2,2) **上方**的单元格 (2,1) 被测试极端边界并成功
  • 单元格 (2,2) **上方**的单元格 (2,1) 被测试图像类型并失败
  • 单元格 (2,2) 的检查方向更改为 **东方**
  • 单元格 (2,2) **右侧**的单元格 (3,2) 被测试极端边界并成功
  • 单元格 (2,2) **右侧**的单元格 (3,2) 被测试图像类型并成功
  • 单元格 (2,2) **右侧**的单元格 (3,2) 被测试未访问并失败
  • 单元格 (2,2) 的检查方向更改为 **南方**
  • 单元格 (2,2) **下方**的单元格 (2,3) 被测试极端边界并成功
  • 单元格 (2,2) **下方**的单元格 (2,3) 被测试图像类型并失败
  • 单元格 (2,2) 的检查方向更改为 **西方**
  • 单元格 (2,2) **左侧**的单元格 (1,2) 被测试极端边界并成功
  • 单元格 (2,2) **左侧**的单元格 (1,2) 被测试图像类型并失败
  • 单元格 (2,2) 的所有方向都已测试完毕,因此将其从堆栈中弹出,并继续测试堆栈中的下一个单元格
  • 单元格 (3,2) 的所有方向都已测试完毕,因此将其从堆栈中弹出,并继续测试堆栈中的下一个单元格
  • 单元格 (3,3) 现在再次位于堆栈顶部,其上一次检查的方向是 **北方**
  • 单元格 (3,3) 的检查方向更改为 **东方**
  • 单元格 (3,3) **右侧**的单元格 (4,3) 被测试极端边界并成功
  • 单元格 (3,3) **右侧**的单元格 (4,3) 被测试图像类型并失败
  • 单元格 (3,3) 的检查方向更改为 **南方**
  • 单元格 (3,3) **下方**的单元格 (3,4) 被测试极端边界并成功
  • 单元格 (3,3) **下方**的单元格 (3,4) 被测试图像类型并失败
  • 单元格 (3,3) 的检查方向更改为 **西方**
  • 单元格 (3,3) **左侧**的单元格 (2,3) 被测试极端边界并成功
  • 单元格 (3,3) **左侧**的单元格 (2,3) 被测试图像类型并失败
  • 单元格 (3,3) 的所有方向都已测试完毕,因此将其从堆栈中弹出
  • 堆栈现在为空

更新游戏棋盘

一旦“邻近搜索算法”完成,如果初始单元格以外的任何单元格被标记为“已访问”,则初始单元格也同样被标记为“已访问”。所有被标记为“已访问”的单元格都将被移除(如果存在)。

水果下落

Fruit Drop

任何现在下方有空单元格的水果图像都会根据 PerformDrop 算法“掉落”穿过这些空单元格

for (int aRow = BOARD_MAX_HEIGHT - 1; aRow > 0; --aRow)
{
    for (int aColumn = 0; aColumn < BOARD_MAX_WIDTH; ++aColumn)
    {
        while (m_PuzzleGrid[aColumn, aRow] == 9)
        {
            for (int aMoveRow = aRow; aMoveRow >= 0; --aMoveRow)
            {
                if (aMoveRow > 0)
                {
                    m_PuzzleGrid[aColumn, aMoveRow] = 
                m_PuzzleGrid[aColumn, aMoveRow - 1];
                }
                else
                {
                    m_PuzzleGrid[aColumn, aMoveRow] = 0;
                }
            }
        }
    }
}

for (int aColumn = 0; aColumn < BOARD_MAX_WIDTH; ++aColumn)
{
    if (m_PuzzleGrid[aColumn, 0] == 9)
    {
        m_PuzzleGrid[aColumn, 0] = 0;
    }
}

移除空列

Column Removal

如果一整列单元格被清空,则该空列左侧的所有列将向右移动一列(或更多),从而在游戏棋盘的最左侧创建一列(或更多)空列。

while (aCurrentColumn > aZeroColumnInsertedCounter)
{
    for (int aCurrentRow = 0; aCurrentRow < BOARD_MAX_HEIGHT; aCurrentRow++)
    {
        if (tGrid[aCurrentColumn, aCurrentRow] > 0)
        {
            aNonzeroCounter++;
        }
    }

    if (aNonzeroCounter > 0)
    {
        aCurrentColumn--;
        aNonzeroCounter = 0;
    }
    else
    {
        for (int aMoveColumn = aCurrentColumn; aMoveColumn > 0; aMoveColumn--)
        {
            for (int aMoveRow = 0; aMoveRow < BOARD_MAX_HEIGHT; aMoveRow++)
            {
                tGrid[aMoveColumn, aMoveRow] = tGrid[aMoveColumn - 1, aMoveRow];
            }
        }

        for (int aZeroRow = 0; aZeroRow < BOARD_MAX_HEIGHT; aZeroRow++)
        {
            tGrid[0, aZeroRow] = 0;
        }
        aZeroColumnInsertedCounter++;
    }
}

检查游戏结束状态

每次移动后,代码必须检查游戏是否结束,即检查是否还有相邻的同类水果图像。这可以通过几个循环来实现,非常简单。

首先,通过循环遍历矩阵的每一行来检查水平匹配,检查该行是否包含两个水平相邻的同类水果图像。如果任何两个同类水果图像在水平方向上相邻,则算法完成,无需检查垂直方向。

for (int aImageType = 1; aImageType <= 6; ++aImageType)
{
    for (int aRow = 0; aRow < BOARD_MAX_HEIGHT; ++aRow)
    {
        int aMatchCount = 0;
        for (int aColumn = 0; aColumn < BOARD_MAX_WIDTH; ++aColumn)
        {
            if (m_PuzzleGrid[aColumn, aRow] == aImageType)
            {
                ++aMatchCount;
            }
            else
            {
                aMatchCount = 0;
            }

            if (aMatchCount > 1)
            {
                aReturnVal = true;
            }
        }
    }
}

其次,如果需要,通过循环遍历矩阵的每一列来检查垂直匹配,检查该列是否包含两个垂直相邻的同类水果图像。垂直检查算法与水平检查算法非常相似。

一旦任一条件为 **true**,算法就会短路,游戏继续。如果两个条件都为 **false**,则没有更多可用移动,游戏结束。这些算法只需要检查两个相邻的同类水果图像,而不是“两个或更多”,因为两个相邻的同类水果图像满足了移动的定义。

游戏资源

这款游戏中的图形是从 molotov.nu 下载的,特别是 Angband 图形集的城镇图块。这个图形集的作者似乎未知,并且根据该网站的说法,它们属于公共领域。下载的图形然后使用 Paint.Net、Microsoft Paint 和/或 Microsoft Visio 进行处理。

游戏声音是用 GoldWave 创建的,并从 freesound.org 下载。

其他说明

Fruit Drop 是在 Visual Studio 2008 中使用 C# 开发的。尽管它最初是为 Windows Mobile 设计的,但它开始时是一个完整的 .NET Framework 应用程序,因为在我看来,由于屏幕空间大以及在测试期间无需部署到设备或模拟器,开发和测试 Windows 应用程序要容易得多,也快得多。一旦我对主要的游戏算法满意,我就简单地向解决方案添加了一个 Windows Mobile 项目并将代码复制过去。

将代码复制到 Windows Mobile 项目并调整为可以在 .NET Compact Framework 中编译后,我惊讶于它的运行速度有多慢。点击相邻同类水果图像时,现在需要两到三秒钟才能移除。ugh!这使得游戏无法玩。

为了加快游戏速度,我首先尝试缩小游戏区域的大小,从 12 列 12 行的动态生成的彩色球改为 8 列 8 行的动态生成的彩色球,认为这将提高所有游戏算法的速度,而这些算法大多是具有 N2 的大 O 循环。这个改变并没有产生太大影响,游戏仍然无法玩。

我的下一个改变是从使用动态生成的彩色球改为使用五种预加载的(图片框)水果图像。这实现了我所期望的游戏速度。

结论

这款游戏显然是基于 Jawbreaker。Fruit Drop 只是一个演示托管代码中一些益智游戏算法的载体。

通过原创想法,或通过回收现有想法并添加一些有趣的新功能,富有创意的开发者可以在 Windows、Windows Mobile、手机或其他平台上,使用托管代码制作一款具有可接受可玩性的有趣休闲益智游戏。

参考文献

修订历史

  • 12-02-2008
    • 添加了第三种游戏棋盘生成方法和代码片段。
  • 11-29-2008
    • 原始文章。
© . All rights reserved.