详细解析用C#写的小游戏《彩色连珠》(附源代码)

不久前写的一个小游戏,最近拿出来稍微修改完善了一下,因为自己现在“不得已”改行学Java了,这个小游戏就当是自己与C#的告别吧,不过以后如果自己有什么想写的小程序,C#仍然是首先考虑的语言了,因为Java做GUI太蛋疼了。

首先声明本人菜鸟一个,快毕业的学生党,这篇文章完全是记录自己的一些点滴吧。

游戏的规则很简单,大概是:10X10的方格,游戏开始时随机出5个球,颜色也是随机的,用户点击球让其移动,5个(或更多)相同颜色的球在一起就可以消掉,如果没有可以消的,就又随机出3个球,直到棋盘满为止。

游戏界面如下:

具体思路如下:

左边的是一个panel面板,用来当做棋盘,启动时把方格线画好,这些球都是一些事先弄好的图片文件(考虑过用图形学的方法代码生成,但是感觉效率太低,最重要的是不好看,所以还是放弃了),通过g.DrawImage()的方法画在面板上,清空的话就是用背景色填充,点击某个球会动态的变化大小,点击空白处会将之前点过的球动态的移动到那里,球每次移动时需要查找能够到达指定位置的最短路径,所以会用到《人工智能》课上用过的查找算法查找最短路径,出子就是用Random随机函数随机的在某个位置画某种颜色的球,每次移动球后都要判断在横、竖、左斜、右斜四个方向上是否有可以消的球,消完球后随机出3个球,出球的同时要判断棋盘是否满。

简单的实现了保存成绩的功能(以及对成绩进行加密),功能做的很简陋,其实还可以添加一些声音的,限于时间就没弄了,有兴趣的可以尝试一下。

好了,也不多写什么了,因为我觉得代码里面的注释已经够详细了,更多的问题还是看代码里面的注释吧。

下面把几个比较重要的地方单独写出来。

首先最重要的是画图要怎么画,也就是采用什么函数来画。

C#画图最常见的一般有3种方式:

一种是,这种方法优点是窗体最小化或者被其它窗体遮挡后画的图不会消失,缺点是每次画完图都要刷新整个区域,所以有可能闪屏很严重:

Bitmap bit = new Bitmap(panel游戏区.Width,panel游戏区.Height);//实例化一个Bitmap
Graphics g = Graphics.FromImage(bit);
g.DrawImage(Image.FromFile("picturePath"),left,top,width,height);//画图
panel游戏区.BackgroundImage = bit;

还有一种是用控件的CreateGraphics()方法创建一个Graphic对象,优点很明显,就是非常方便,不用每次都要刷新,所以一般不会出现闪屏现象,但是当窗体最小化还原或者被其它窗体遮挡后就一片空白了,到网上查过一些资料,好像是因为这种画图方式数据都是保存在缓存中的,窗体只要最小化就会触发paint事件重绘,系统自带的控件重绘的代码都写好了,但是我们自己的这个画图区域因为没有写重绘事件,所以还原后一片空白,解决办法就是在paint事件里手动对空白区域进行重绘,本游戏采用的就是这种方法:

Graphics g = panel游戏区.CreateGraphics();
g.DrawImage(Image.FromFile("picturePath"), left, top, width, height);

还有一种方法是写在控件的Paint事件里,缺点很明显,都是不方便,很多代码我们没办法写在这里,比如鼠标事件发生的一些画图代码。

Graphics g = e.Graphics;
g.DrawImage(Image.FromFile("picturePath"), left, top, width, height);

首先是画方格线,比较简单:

private void drawLine()//画方格线的函数
        {
            Graphics g = panel游戏区.CreateGraphics();
            for (int i = 0; i < m; i++)
            {
                g.DrawLine(new Pen(Color.Black), 0, panel游戏区.Height / m * i, panel游戏区.Width, panel游戏区.Height / m * i);
                g.DrawLine(new Pen(Color.Black), panel游戏区.Width / m * i, 0, panel游戏区.Width / m * i, panel游戏区.Height);
            }
        }

其次是在指定行和列处画指定颜色的球,因为后面有些地方有需要,重载了3次,这里只列出其中一个,其它类似。这里顺便讲一下关于资源文件的使用,因为把所以图片放在文件夹里不方便,所以我还是想把所以图片放到资源文件里面去,但是C#里根据资源文件名来查找资源文件还确实不是那么容易,找了很久的资料才解决,具体的就是ResourceManager里面的一个参数不好写,很容易写错,具体事项看下面代码和注释吧:

        /// <summary>
        /// 画球的函数
        /// </summary>
        /// <param name="i">行数</param>
        /// <param name="j">列数</param>
        /// <param name="color">要画的球的颜色</param>
        /// <param name="dx">指定球比方格要小的像素个数</param>
        private void drawBall(int i, int j, Color color,int dx)
        {
            //关于图片:如果直接将这些不同颜色的球的图片放在程序根目录文件夹来操作比较简单,
            //但是缺点就是老是要跟一个文件夹,不方便,所以将所有图片放入程序的资源文件
            //至于怎样调用程序的资源文件,查找了很多资料后终于得到解决,具体方法看下面的代码。
            Graphics g = panel游戏区.CreateGraphics();
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;//高质量显示图片
            int x = panel游戏区.Width / m, y = panel游戏区.Height / m;//x,y分别为单个方块宽和高
            string temp = color.ToString().Substring(7).Replace("]", "");//color的颜色值转换为字符串后形如:color[Red],本句代码执行后的结果为Red
            //string picturePath = Application.StartupPath + @"\球\" + temp + ".png";//到程序目录中查找指定颜色球的路径,如:C:\Documents and Settings\Administrator\桌面\彩色连珠\bin\Debug\球\Red.png
            System.Resources.ResourceManager manager = new System.Resources.ResourceManager("彩色连珠.Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly());
            //上面一句话是实例化一个ResourceManager,这里一定要注意baseName的写法,为:命名空间+文件夹名+资源文件名(不带后缀名),不知道怎么写的可以到“Resources.Designer.cs”这个文件里去找
            //用这个写法的目的是为了方便根据资源文件名来查找,如果不需要查找的画则比较简单,如下:
            //首先添加以下引用:using 彩色连珠.Properties;然后直接写:Resources.Red就可以获取资源文件了。
            g.DrawImage((Bitmap)manager.GetObject(temp), x * j + dx, y * i + dx, x - dx - dx, y - dx - dx);//将图片画在游戏区
            ball[i, j] = color;//同时更新ball数组
            //g.FillEllipse(new SolidBrush(color),x*j+5,y*i+5,x-10,y-10);//如果是直接用系统函数画圆的画就用这句话 
        }

清空某个方格的代码:

        /// <summary>
        /// 用背景色填充指定方格,以达到清除的目的
        /// </summary>
        /// <param name="i"></param>
        /// <param name="j"></param>
        private void clearBall(int i,int j)
        {
            Graphics g = panel游戏区.CreateGraphics();
            int x = panel游戏区.Width / m, y = panel游戏区.Height / m;
            g.FillRectangle(new SolidBrush(panel游戏区.BackColor), x * j + 2, y * i + 2, x - 4, y - 4);
            ball[i, j]= panel游戏区.BackColor;
        }

随机出球的函数:

        /// <summary>
        /// 游戏开始时随机出球,位置随机,颜色也随机(用于没有下一组提示的时候)
        /// </summary>
        private void drawBallRandom()
        {
            if (!checkOver())
            {
                Random random = new Random();
                bool flag = true;
                while (flag)
                {

                    int i = random.Next(0, 10);
                    int j = random.Next(0, 10);
                    if (ball[i, j] == panel游戏区.BackColor)
                    {
                        flag = false;
                        int c = random.Next(0, colorNum);
                        //MessageBox.Show(i + "," + j + ":" + color[c].ToString());
                        drawBall(i, j, color[c]);
                        checkSuccess(i, j);//出子后要判断是否有可以消的球
                    }
                }
            }
        }

产生下一组随机球:

/// <summary>
        /// 产生下一组随机球
        /// </summary>
        private void makeNextColor()
        {
            Graphics g = pictureBox下一组.CreateGraphics();
            g.Clear(pictureBox下一组.BackColor);
            Random random = new Random();
            for (int i = 0; i < 3; i++)
            {
                nextColor[i] = random.Next(0,colorNum);
                drawBall(i,nextColor[i]);
            }
        }

panel的paint事件,作用在下面的注释已经写明了,有些函数是在后面定义的,这里暂时还没写出来:

        //游戏区的重绘事件,这个事件的作用主要有2个:一个是让游戏第一次运行时画方格线
        //以及随机出5个子(这些代码不能放在Form_Loaded事件里,因为窗体第一次生成会触发
        //Paint事件进而覆盖原图),第二个作用是解决当窗体最小化或改变大小时绘图区一片空
        //白的问题,解决的思路就是方格线和球全部重绘。
        //用“Bitmap bit= new Bitmap(x,y);Graphics g=Graphics.FromImage(bit);”的方法
        //不会出现最小化变空白的现象,但每次画完图后都必须刷新,因此闪屏现象严重。
        private void panel游戏区_Paint(object sender, PaintEventArgs e)
        {
            drawLine();//画方格线
            for (int i = 0; i < m; i++)
                for (int j = 0; j < m; j++)
                    if (ball[i, j] != panel游戏区.BackColor)
                    {
                        drawBall(i,j,ball[i,j]); //如果该位置的颜色不是背景色(即没有球)则按照指定颜色画球
                    }
            makeNextColor();//防止窗口最小化后还原一片空白
            if (isFirstRun)//如果是第一次运行
            {
                for (int i = 0; i < 5; i++)
                    drawBallRandom();//随机出5个球
                makeNextColor();
                isFirstRun = false;
            }
        }

鼠标的单击事件,游戏的主要驱动都来自这个事件:

        private void panel游戏区_MouseClick(object sender, MouseEventArgs e)//游戏区的鼠标单击事件
        {
            timer缩放.Enabled = false;//结束球的缩放
            int x = panel游戏区.Width / m, y = panel游戏区.Height / m;
            if (ball[e.Y / y, e.X / x] != panel游戏区.BackColor)//如果单击的是球
            {
                dx = 5;//让dx恢复到默认值
                if(move_i>=0)
                    drawBall(move_i, move_j, ball[move_i, move_j], dx);
                //在新的球缩放时将上次点的球(如果有)重置为默认大小(因为球动态变换大
                //小,所以停止缩放时可能不是默认大小,这句话是重新画一个默认大小的球)

                move_i = e.Y / y;
                move_j = e.X / x;
                timer缩放.Enabled = true;//让单击过的球开始动态变换大小
            }
            else if (move_i >= 0 && move_j >= 0)//如果单击的是空白处,且有一个即将移动的球
            {
                bool[,] isHaveBall = new bool[m, m];//保存棋盘上每个位置是否有球的信息
                for (int i = 0; i < m; i++)
                    for (int j = 0; j < m; j++)
                    {
                        if (ball[i, j] == panel游戏区.BackColor)
                            isHaveBall[i, j] = false;
                        else
                            isHaveBall[i, j] = true;
                    }
                int end_i = e.Y / y, end_j = e.X / x;//目标行与列
                Search s = new Search(isHaveBall, m, move_i, move_j, end_i, end_j);//实例化一个查找类
                path = s.start();//开始查找
                if (path[0][0] != 0)//如果查找成功
                {
                    path_idx = 2;//path数组第一组数据是长度,第二组数据是起点,所以要从第三组数据开始
                    timer移动.Enabled = true;
                    //string t = "";   //下面注释的代码用来查看文本形式的路径信息,仅供程序员调试看,玩家不需要管
                    //for (int i = 1; i <= path[0][0]; i++)
                    //{
                    //    t += path[i][0] + "," + path[i][1] + "  ";
                    //}
                    // MessageBox.Show(t);
                }
            }
        }

检查是否有5个(或更多)颜色相同的球在一起,并计算分数,5个子10分,6个子20分,以此类推:

        /// <summary>
        /// 检查是否有5个颜色相同的球连在一起
        /// </summary>
        /// <param name="h">起始查找位置的行</param>
        /// <param name="lie">起始查找位置的列,为什么不用字母“l”呢?因为和数字“1”长得太像了!(汗)</param>
        /// <returns>检查的结果</returns>
        private bool checkSuccess(int h,int lie)
        {
            bool f = false;
            int[][] res = new int[4][];//4行N列的二维数组,每一行用来存放一个方向检查的结果
            int i = 1;
            int j = 1;

            int sum=1;
            for (i = 1; lie-i>=0&&ball[h, lie - i] == ball[h, lie]; i++)//往左
                sum++;
            for (j = 1; lie+j<m&&ball[h, lie + j] == ball[h, lie]; j++)//往右
                sum++;
            if (sum >= 5)
                res[0] = new int[] { sum, i - 1, j - 1 };//从左往右,第一个数存放sum的值,后面2个数分别存放左和右的索引
            else
                res[0] = new int[] { 0 };

            sum = 1;
            for (i = 1; h-i>=0&&ball[h - i, lie] == ball[h, lie]; i++)//往上
                sum++;
            for (j = 1; h+j<m&&ball[h + j, lie] == ball[h, lie]; j++)//往下
                sum++;
            if (sum >= 5)
                res[1] = new int[] { sum, i - 1, j - 1 };//从上往下
            else
                res[1] = new int[] { 0};

            sum = 1;
            for (i = 1; h-i>=0&&lie-i>=0&&ball[h-i, lie - i] == ball[h, lie]; i++)//往左上
                sum++;
            for (j = 1; h+j<m&&lie+j<m&&ball[h+j, lie + j] == ball[h, lie]; j++)//往右下
                sum++;
            if (sum >= 5)
                res[2] = new int[] { sum, i - 1, j - 1 };//从左上往右下
            else
                res[2] = new int[] { 0 };

            sum = 1;
            for (i = 1; h+i<m&&lie-i>=0&&ball[h+i, lie - i] == ball[h, lie]; i++)//往左下
                sum++;
            for (j = 1; h-j>=0&&lie+j<m&&ball[h-j, lie + j] == ball[h, lie]; j++)//往右上
                sum++;
            if (sum >= 5)
                res[3] = new int[] { sum, i - 1, j - 1 };//从左下往右上
            else
                res[3] = new int[] { 0 };
            for (int p = 0; p < 4;p++ )
            {
                if (res[p][0] !=0)
                {
                    for (int q = -res[p][1]; q <=  res[p][2]; q++)
                    {
                        switch (p)
                        {
                            case 0: clearBall(h, lie+q); break;
                            case 1: clearBall(h+q, lie); break;
                            case 2: clearBall(h + q, lie + q); break;
                            case 3: clearBall(h - q, lie + q); break;
                        }
                    }
                    score += 10*(res[p][0]-4);//5个棋子加10分,6个棋子加20分,依次类推
                    label分数.Text = "分数:"+score;//分数加上去
                    f = true;
                }
            }
            return f;
        }

很重要的查找算法,具体看注释,太晚了不愿打字了,有不明白的可以留言一起讨论,我把整个查找算法单独放在一个名为Search.cs的类里面:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
namespace 彩色连珠
{
    class Search//查找函数
    {
        bool[,] ball;//一个二维数组,记录每行每列是否有球
        int m, start_i, start_j, end_i, end_j;//分别表示行数(或列数),需要移动的球的行及列,目标位置的行及列
        /// <summary>
        /// 查找方法的构造函数
        /// </summary>
        /// <param name="ball">存放每个游戏方格上是否有球的bool二维数组</param>
        /// <param name="m">方格的行数(或列数)</param>
        /// <param name="start_i">需要移动的球的行</param>
        /// <param name="start_j">需要移动的球的列</param>
        /// <param name="end_i">目标位置的行</param>
        /// <param name="end_j">目标位置的列</param>
        public Search(bool[,] ball, int m, int start_i, int start_j, int end_i, int end_j)
        {
            this.ball = ball;
            this.m = m;
            this.start_i = start_i;
            this.start_j = start_j;
            this.end_i = end_i;
            this.end_j = end_j;
        }
        class Open  //Open表
        {
            public Open(int child_i, int child_j, int parent_i, int parent_j)
            {
                this.child_i = child_i;
                this.child_j = child_j;
                this.parent_i = parent_i;
                this.parent_j = parent_j;
            }
            public int child_i;
            public int child_j;
            public int parent_i;
            public int parent_j;
        }
        class Closed:Open  //Closed表,继承自Open表,只多了一个id号
        {
            public Closed(int id, Open o)
                :base(o.child_i,o.child_j,o.parent_i,o.parent_j)
            {
                this.id = id;
            }
            public int id;
        }
        class Queue//队列
        {
            const int maxsize = 1000;
            Open[] queue = new Open[maxsize];
            int front;
            int rear;
            public void iniQueue()//初始化
            {
                this.front = this.rear = 0;
            }
            public void add(Open x)//进栈
            {
                if ((this.rear + 1) % maxsize != this.front)
                {
                    this.rear = (this.rear + 1) % maxsize;
                    this.queue[this.rear] = x;
                }
            }
            public Open delete()//出栈
            {
                Open o = this.queue[(this.front + 1) % maxsize];
                if (this.rear != this.front)
                    this.front = (this.front + 1) % maxsize;
                return o;
            }
            public bool isEmpty()//判断是否为空
            {
                if (this.front == this.rear)
                    return true;
                else
                    return false;
            }
        }
        public int[][] start()//开始查找
        {
            int[][] result=new int[1000][];//记录结果的二维数组,如果查找失败,第一组数据放入(0,0),否则放入(1,1),从第二组数据开始存放数据
            int n = 0;//记录Closed表中的个数
            Queue q = new Queue();//实例化一个Open表的队列Queue
            q.iniQueue();
            Closed[] c = new Closed[100000];
            q.add(new Open(start_i, start_j, -1, -1));//因第一个点不存在父节点,故用-1来表示NULL
            bool flag = false;//判断是否退出while循环的标志
            while (!flag)
            {
                if (q.isEmpty())//如果堆栈为空,退出循环
                    flag = true;
                else
                {
                    Open temp = q.delete();//从Open表取出队头元素
                    //MessageBox.Show(temp.child_i + "," + temp.child_j);
                    c[n] = new Closed(n, temp);
                    if (c[n].child_i == end_i && c[n].child_j == end_j)
                        flag = true;
                    else//按照左上右下的顺序查找
                    {
                        if (c[n].child_j - 1 >= 0 && !ball[c[n].child_i, c[n].child_j - 1])//
                        { q.add(new Open(c[n].child_i, c[n].child_j - 1, c[n].child_i, c[n].child_j)); ball[c[n].child_i, c[n].child_j - 1]=true; }
                        if (c[n].child_i - 1 >= 0 && !ball[c[n].child_i - 1, c[n].child_j])//
                        { q.add(new Open(c[n].child_i - 1, c[n].child_j, c[n].child_i, c[n].child_j));ball[c[n].child_i - 1, c[n].child_j]=true; }
                        if (c[n].child_j + 1 < m && !ball[c[n].child_i, c[n].child_j + 1])//
                        { q.add(new Open(c[n].child_i, c[n].child_j + 1, c[n].child_i, c[n].child_j));ball[c[n].child_i, c[n].child_j + 1]=true;  }
                        if (c[n].child_i + 1 < m && !ball[c[n].child_i + 1, c[n].child_j])//
                        { q.add(new Open(c[n].child_i + 1, c[n].child_j, c[n].child_i, c[n].child_j));ball[c[n].child_i + 1, c[n].child_j]=true; }
                    }
                    n++;
                }
            }
            if (c[n - 1].child_i == end_i && c[n - 1].child_j == end_j)//表示如果查找成功
            {
                int sum = 0;
                string res = end_i + "," + end_j;
                result[sum]=new int[]{end_i,end_j};
                int b_i = end_i, b_j = end_j;
                for (int i = n - 1; i > 0; i--)
                {
                    if (c[i].child_i == b_i && c[i].child_j == b_j)
                    {
                        sum++;
                        result[sum] = new int[] { c[i].parent_i,c[i].parent_j};
                        res = c[i].parent_i + "," + c[i].parent_j + "  " + res;
                        b_i = c[i].parent_i;
                        b_j = c[i].parent_j;
                    }
                }
                sum++;
                result[sum] = new int[] { sum,sum};//记录需移动的次数
                for (int i = 0; i < (sum + 1) / 2; i++)//数组倒序
                {
                    int[] temp = result[i];
                    result[i] = result[sum - i];
                    result[sum - i] = temp;
                }
                // MessageBox.Show("查找结果为:\n" + res, "查找成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return result;

            }
            else
            {
                result[0] = new int[] { 0,0};
                return result;
            }

        }
    }
}

下面的这个名为SaveScore.cs的类是用来保存成绩和读取成绩的,因为要避免用户手动查看或修改成绩文件,所以采用了简单的加密算法,具体代码如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Security.Cryptography;

namespace 彩色连珠
{
    class SaveScore
    {
        public SaveScore()
        { }
        public const string scoreFile = "score.dat";
        /// <summary>
        /// 保存成绩
        /// </summary>
        /// <param name="name">姓名</param>
        /// <param name="score">成绩</param>
        public static void saveScore(string name, int score)
        {
            string file = Application.StartupPath + "\\" + scoreFile;
            if (!File.Exists(file))
                File.CreateText(file);
            StreamReader sr = new StreamReader(file);
            List<string> names = new List<string> { };
            List<int> scores = new List<int> { };
            string temp = "";
            try
            {
                while ((temp = sr.ReadLine()) != null)
                {
                    temp = SimplyDecrypt(temp);
                    names.Add(temp.Split(':')[0]);
                    scores.Add(Convert.ToInt32(temp.Split(':')[1]));
                }
            }
            catch { }
            sr.Close();
            int max = score;//假设传过来的score是最高分
            int i = 0;
            for (; i < scores.Count; i++)
                if (scores[i] < max)//一旦发现有比max小的数,跳出循环
                    break;
            scores.Insert(i, score);//在指定位置插入成绩和姓名
            names.Insert(i, name);
            StreamWriter sw = new StreamWriter(file);
            for (int j = 0; j < names.Count; j++)
                sw.WriteLine(SimplyEncrypt(names[j] + ":" + scores[j]));//写入文件
            sw.Close();
        }
        public static int redMax()//读取最高分
        {
            int max = 0;
            try
            {
                string file = Application.StartupPath + "\\" + scoreFile;
                if (File.Exists(file))
                {
                    StreamReader sr = new StreamReader(file);
                    max =Convert.ToInt32(SimplyDecrypt(sr.ReadLine()).Split(':')[1]);
                    sr.Close();
                }
            }
            catch { }
            return max;
        }
        public static string readScore()//读取所有成绩
        {
            string result = "成绩排行榜如下:\n";
            try
            {
                string file = Application.StartupPath + "\\" + scoreFile;
                if (File.Exists(file))
                {
                    string temp = "";
                    StreamReader sr = new StreamReader(file);
                    int i = 1;
                    while ((temp = sr.ReadLine()) != null)
                    {
                        temp = SimplyDecrypt(temp);
                        result += "" + i + "名:" + temp.Split(':')[0] + ",成绩:" + temp.Split(':')[1] + "\n";
                        i++;
                    }
                    sr.Close();
                }
            }
            catch{ }
            return result;
        }
        private const string Secret = "timizhuo"; // Secret的长度必须为8位!!
        private const string IV = "xiaomingtongxue";  // IV的长度必须在8位以上!

        private static string SimplyEncrypt(string rawContent)//加密
        {
            try
            {
                var des = new DESCryptoServiceProvider();
                var encryptor = des.CreateEncryptor(Encoding.ASCII.GetBytes(Secret), Encoding.ASCII.GetBytes(IV));
                var dataToEnc = Encoding.UTF8.GetBytes(rawContent);
                var resultStr = encryptor.TransformFinalBlock(dataToEnc, 0, dataToEnc.Length);
                return Convert.ToBase64String(resultStr);
            }
            catch { return ""; }
        }

        private static string SimplyDecrypt(string encryptedContent)//解密
        {
            try
            {
                var result = string.Empty;
                var des = new DESCryptoServiceProvider();
                var decryptor = des.CreateDecryptor(Encoding.ASCII.GetBytes(Secret), Encoding.ASCII.GetBytes(IV));
                var dataToDec = Convert.FromBase64String(encryptedContent);
                var resultBytes = decryptor.TransformFinalBlock(dataToDec, 0, dataToDec.Length);
                result = Encoding.UTF8.GetString(resultBytes);
                return result;
            }
            catch { return ""; }
        }
    }
}

整个程序主要就三个类,除了上面写明的2个外,最后一个就是Form主窗口.cs,程序大部分代码都在这里,所以这里全部一起贴一下,个人觉得绿化率这么高,应该非常容易看懂吧,呵呵。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Collections;
using 彩色连珠.Properties;
namespace 彩色连珠
{
    public partial class Form主窗口 : Form
    {
        public Form主窗口()
        { 
            InitializeComponent();
        }
        public int m = 10;//方格的行数与列数
        public Color[,] ball= new Color[100, 100];//一个二维数组,用来存放整个棋盘每个位置的颜色信息
        public Color[] color = { Color.Red,Color.Green,Color.Blue,Color.Brown,Color.Yellow,Color.LightBlue,Color.Violet,Color.White,Color.Black};//所有球可能的颜色值
        //public string[] color = { "红色","绿色","蓝色","褐色","黄色","浅蓝色","紫色","白色","黑色"};
        public int colorNum = 9;
        public int move_i=-1, move_j=-1;//用来存放即将移动的球的行和列
        public int[][] path;//用来存放每次查找成功后的路径信息
        public int path_idx=2;//path路径信息的索引,之所以要从2开始,是因为第0个是长度,第1个存放的是起点坐标
        public int score = 0;//分数
        int dx = 5;//仅用在对球进行缩放时记录缩放的程度,默认设为5
        public bool isFirstRun = true;//判断游戏是否为第一次运行的标志,当第一次运行时,在Paint事件中随机出5个球
        public int[] nextColor = new int[3];//下一组颜色信息
        private void Form主窗口_Load(object sender, EventArgs e)
        {
            startGame();
            readMaxScore();
        }

        //游戏区的重绘事件,这个事件的作用主要有2个:一个是让游戏第一次运行时画方格线
        //以及随机出5个子(这些代码不能放在Form_Loaded事件里,因为窗体第一次生成会触发
        //Paint事件进而覆盖原图),第二个作用是解决当窗体最小化或改变大小时绘图区一片空
        //白的问题,解决的思路就是方格线和球全部重绘。
        //用“Bitmap bit= new Bitmap(x,y);Graphics g=Graphics.FromImage(bit);”的方法
        //不会出现最小化变空白的现象,但每次画完图后都必须刷新,因此闪屏现象严重。
        private void panel游戏区_Paint(object sender, PaintEventArgs e)
        {
            drawLine();//画方格线
            for (int i = 0; i < m; i++)
                for (int j = 0; j < m; j++)
                    if (ball[i, j] != panel游戏区.BackColor)
                    {
                        drawBall(i,j,ball[i,j]); //如果该位置的颜色不是背景色(即没有球)则按照指定颜色画球
                    }
            makeNextColor();//防止窗口最小化后还原一片空白
            if (isFirstRun)//如果是第一次运行
            {
                for (int i = 0; i < 5; i++)
                    drawBallRandom();//随机出5个球
                makeNextColor();
                isFirstRun = false;
            }
        }
        /// <summary>
        /// 开始游戏的准备工作
        /// </summary>
        private void startGame()
        {
            panel游戏区.BackColor = Color.LightGray;//首次运行,给游戏区填充灰色背景色
            Graphics g = panel游戏区.CreateGraphics();
            g.Clear(panel游戏区.BackColor);//用背景色清空
            drawLine();//画方格线
            for (int i = 0; i < m; i++)
                for (int j = 0; j < m; j++)
                    ball[i, j] = panel游戏区.BackColor;//将ball数组全部用背景色来标记
            score = 0;
            label分数.Text = "分数:0";//分数置0
        }
        private void drawLine()//画方格线的函数
        {
            Graphics g = panel游戏区.CreateGraphics();
            for (int i = 0; i < m; i++)
            {
                g.DrawLine(new Pen(Color.Black), 0, panel游戏区.Height / m * i, panel游戏区.Width, panel游戏区.Height / m * i);
                g.DrawLine(new Pen(Color.Black), panel游戏区.Width / m * i, 0, panel游戏区.Width / m * i, panel游戏区.Height);
            }
        }
        /// <summary>
        /// 画球的函数
        /// </summary>
        /// <param name="i">行数</param>
        /// <param name="j">列数</param>
        /// <param name="color">要画的球的颜色</param>
        /// <param name="dx">指定球比方格要小的像素个数</param>
        private void drawBall(int i, int j, Color color,int dx)
        {
            //关于图片:如果直接将这些不同颜色的球的图片放在程序根目录文件夹来操作比较简单,
            //但是缺点就是老是要跟一个文件夹,不方便,所以将所有图片放入程序的资源文件
            //至于怎样调用程序的资源文件,查找了很多资料后终于得到解决,具体方法看下面的代码。
            Graphics g = panel游戏区.CreateGraphics();
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;//高质量显示图片
            int x = panel游戏区.Width / m, y = panel游戏区.Height / m;//x,y分别为单个方块宽和高
            string temp = color.ToString().Substring(7).Replace("]", "");//color的颜色值转换为字符串后形如:color[Red],本句代码执行后的结果为Red
            //string picturePath = Application.StartupPath + @"\球\" + temp + ".png";//到程序目录中查找指定颜色球的路径,如:C:\Documents and Settings\Administrator\桌面\彩色连珠\bin\Debug\球\Red.png
            System.Resources.ResourceManager manager = new System.Resources.ResourceManager("彩色连珠.Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly());
            //上面一句话是实例化一个ResourceManager,这里一定要注意baseName的写法,为:命名空间+文件夹名+资源文件名(不带后缀名),不知道怎么写的可以到“Resources.Designer.cs”这个文件里去找
            //用这个写法的目的是为了方便根据资源文件名来查找,如果不需要查找的画则比较简单,如下:
            //首先添加以下引用:using 彩色连珠.Properties;然后直接写:Resources.Red就可以获取资源文件了。
            g.DrawImage((Bitmap)manager.GetObject(temp), x * j + dx, y * i + dx, x - dx - dx, y - dx - dx);//将图片画在游戏区
            ball[i, j] = color;//同时更新ball数组
            //g.FillEllipse(new SolidBrush(color),x*j+5,y*i+5,x-10,y-10);//如果是直接用系统函数画圆的画就用这句话 
        }
        private void drawBall(int i, int j, Color color)//重载上面的函数,将dx默认为5
        {
            drawBall(i,j,color,5);
        }
        /// <summary>
        /// 在下一组提示区画指定颜色的球
        /// </summary>
        /// <param name="i">位置索引,因为每次出3个球,所以i只可能是0、1、2三种情况</param>
        /// <param name="colorIdx">颜色索引,实际上就是要画的球的颜色信息</param>
        private void drawBall(int i,  int colorIdx)
        {
            Graphics g = pictureBox下一组.CreateGraphics();
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;//高质量显示图片
            string temp = color[colorIdx].ToString().Substring(7).Replace("]", "");//color的颜色值转换为字符串后形如:color[Red],本句代码执行后的结果为Red
            System.Resources.ResourceManager manager = new System.Resources.ResourceManager("彩色连珠.Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly());
            g.DrawImage((Bitmap)manager.GetObject(temp), i * 55 + 10, 3, 50, 50);//将图片画在下一组的地方
        }
        /// <summary>
        /// 用背景色填充指定方格,以达到清除的目的
        /// </summary>
        /// <param name="i"></param>
        /// <param name="j"></param>
        private void clearBall(int i,int j)
        {
            Graphics g = panel游戏区.CreateGraphics();
            int x = panel游戏区.Width / m, y = panel游戏区.Height / m;
            g.FillRectangle(new SolidBrush(panel游戏区.BackColor), x * j + 2, y * i + 2, x - 4, y - 4);
            ball[i, j]= panel游戏区.BackColor;
        }
        /// <summary>
        /// 游戏开始时随机出球,位置随机,颜色也随机(用于没有下一组提示的时候)
        /// </summary>
        private void drawBallRandom()
        {
            if (!checkOver())
            {
                Random random = new Random();
                bool flag = true;
                while (flag)
                {

                    int i = random.Next(0, 10);
                    int j = random.Next(0, 10);
                    if (ball[i, j] == panel游戏区.BackColor)
                    {
                        flag = false;
                        int c = random.Next(0, colorNum);
                        //MessageBox.Show(i + "," + j + ":" + color[c].ToString());
                        drawBall(i, j, color[c]);
                        checkSuccess(i, j);//出子后要判断是否有可以消的球
                    }
                }
            }
        }
        /// <summary>
        /// 随机在某个位置画指定颜色的球,位置随机,颜色不随机
        /// </summary>
        /// <param name="colorIdx">颜色索引</param>
        private void drawBallRandom(int colorIdx)
        {
            if (!checkOver())
            {
                Random random = new Random();
                bool flag = true;
                while (flag)
                {
                    int i = random.Next(0, 10);
                    int j = random.Next(0, 10);
                    if (ball[i, j] == panel游戏区.BackColor)
                    {
                        flag = false;
                        drawBall(i, j, color[colorIdx]);
                        checkSuccess(i, j);
                    }
                }
            }
        }
        /// <summary>
        /// 产生下一组随机球
        /// </summary>
        private void makeNextColor()
        {
            Graphics g = pictureBox下一组.CreateGraphics();
            g.Clear(pictureBox下一组.BackColor);
            Random random = new Random();
            for (int i = 0; i < 3; i++)
            {
                nextColor[i] = random.Next(0,colorNum);
                drawBall(i,nextColor[i]);
            }
        }
        /// <summary>
        /// 检查游戏是否结束
        /// </summary>
        /// <returns>返回真假</returns>
        private bool checkOver()
        {
            bool isFull = true;
            for (int i = 0; i < m; i++)
                for (int j = 0; j < m; j++)
                {
                    if (ball[i, j] == panel游戏区.BackColor)
                        isFull = false;
                }
            return isFull;
        }

        private void panel游戏区_MouseClick(object sender, MouseEventArgs e)//游戏区的鼠标单击事件
        {
            timer缩放.Enabled = false;//结束球的缩放
            int x = panel游戏区.Width / m, y = panel游戏区.Height / m;
            if (ball[e.Y / y, e.X / x] != panel游戏区.BackColor)//如果单击的是球
            {
                dx = 5;//让dx恢复到默认值
                if(move_i>=0)
                    drawBall(move_i, move_j, ball[move_i, move_j], dx);
                //在新的球缩放时将上次点的球(如果有)重置为默认大小(因为球动态变换大
                //小,所以停止缩放时可能不是默认大小,这句话是重新画一个默认大小的球)

                move_i = e.Y / y;
                move_j = e.X / x;
                timer缩放.Enabled = true;//让单击过的球开始动态变换大小
            }
            else if (move_i >= 0 && move_j >= 0)//如果单击的是空白处,且有一个即将移动的球
            {
                bool[,] isHaveBall = new bool[m, m];//保存棋盘上每个位置是否有球的信息
                for (int i = 0; i < m; i++)
                    for (int j = 0; j < m; j++)
                    {
                        if (ball[i, j] == panel游戏区.BackColor)
                            isHaveBall[i, j] = false;
                        else
                            isHaveBall[i, j] = true;
                    }
                int end_i = e.Y / y, end_j = e.X / x;//目标行与列
                Search s = new Search(isHaveBall, m, move_i, move_j, end_i, end_j);//实例化一个查找类
                path = s.start();//开始查找
                if (path[0][0] != 0)//如果查找成功
                {
                    path_idx = 2;//path数组第一组数据是长度,第二组数据是起点,所以要从第三组数据开始
                    timer移动.Enabled = true;
                    //string t = "";   //下面注释的代码用来查看文本形式的路径信息,仅供程序员调试看,玩家不需要管
                    //for (int i = 1; i <= path[0][0]; i++)
                    //{
                    //    t += path[i][0] + "," + path[i][1] + "  ";
                    //}
                    // MessageBox.Show(t);
                }
            }
        }
        /// <summary>
        /// 检查是否有5个颜色相同的球连在一起
        /// </summary>
        /// <param name="h">起始查找位置的行</param>
        /// <param name="lie">起始查找位置的列,为什么不用字母“l”呢?因为和数字“1”长得太像了!(汗)</param>
        /// <returns>检查的结果</returns>
        private bool checkSuccess(int h,int lie)
        {
            bool f = false;
            int[][] res = new int[4][];//4行N列的二维数组,每一行用来存放一个方向检查的结果
            int i = 1;
            int j = 1;

            int sum=1;
            for (i = 1; lie-i>=0&&ball[h, lie - i] == ball[h, lie]; i++)//往左
                sum++;
            for (j = 1; lie+j<m&&ball[h, lie + j] == ball[h, lie]; j++)//往右
                sum++;
            if (sum >= 5)
                res[0] = new int[] { sum, i - 1, j - 1 };//从左往右,第一个数存放sum的值,后面2个数分别存放左和右的索引
            else
                res[0] = new int[] { 0 };

            sum = 1;
            for (i = 1; h-i>=0&&ball[h - i, lie] == ball[h, lie]; i++)//往上
                sum++;
            for (j = 1; h+j<m&&ball[h + j, lie] == ball[h, lie]; j++)//往下
                sum++;
            if (sum >= 5)
                res[1] = new int[] { sum, i - 1, j - 1 };//从上往下
            else
                res[1] = new int[] { 0};

            sum = 1;
            for (i = 1; h-i>=0&&lie-i>=0&&ball[h-i, lie - i] == ball[h, lie]; i++)//往左上
                sum++;
            for (j = 1; h+j<m&&lie+j<m&&ball[h+j, lie + j] == ball[h, lie]; j++)//往右下
                sum++;
            if (sum >= 5)
                res[2] = new int[] { sum, i - 1, j - 1 };//从左上往右下
            else
                res[2] = new int[] { 0 };

            sum = 1;
            for (i = 1; h+i<m&&lie-i>=0&&ball[h+i, lie - i] == ball[h, lie]; i++)//往左下
                sum++;
            for (j = 1; h-j>=0&&lie+j<m&&ball[h-j, lie + j] == ball[h, lie]; j++)//往右上
                sum++;
            if (sum >= 5)
                res[3] = new int[] { sum, i - 1, j - 1 };//从左下往右上
            else
                res[3] = new int[] { 0 };
            for (int p = 0; p < 4;p++ )
            {
                if (res[p][0] !=0)
                {
                    for (int q = -res[p][1]; q <=  res[p][2]; q++)
                    {
                        switch (p)
                        {
                            case 0: clearBall(h, lie+q); break;
                            case 1: clearBall(h+q, lie); break;
                            case 2: clearBall(h + q, lie + q); break;
                            case 3: clearBall(h - q, lie + q); break;
                        }
                    }
                    score += 10*(res[p][0]-4);//5个棋子加10分,6个棋子加20分,依次类推
                    label分数.Text = "分数:"+score;//分数加上去
                    f = true;
                }
            }
            return f;
        }
        private void timer移动_Tick(object sender, EventArgs e)//这部分代码是为了实现球的动态移动位置效果
        {
            //注意:这里一定要注意要用上一个点的颜色来填充,而不是起始点的颜色,
            //因为每画完一个点之后,它的颜色已经被填充为背景色了
            drawBall(path[path_idx][0], path[path_idx][1],ball[path[path_idx-1][0], path[path_idx-1][1]]);//在指定位置画置顶颜色的球
            clearBall(path[path_idx-1][0], path[path_idx-1][1]);//画过的方格要清空
            path_idx++;
            if (path_idx > path[0][0])
            {
                timer移动.Enabled = false;
                move_i = move_j = -1;
                if (!checkSuccess(path[path_idx - 1][0], path[path_idx - 1][1]))//如果新移动一个球后没有五子连线的,再随机出3个球
                {
                    for (int i = 0; i < 3&&!checkOver(); i++)//当游戏没有结束时随机出3个球
                        drawBallRandom(nextColor[i]);
                    if (checkOver())
                    {
                        Form保存成绩 f = new Form保存成绩(score,this);
                        f.ShowDialog();
                    }
                    makeNextColor();
                }
            }
        }

        
        bool isGrow = false;//真表示球变大,假表示球变小
        private void timer缩放_Tick(object sender, EventArgs e)//这个计时器仅用来实现点击某一个球后动态变换大小的效果
        {
            //注意:dx是表示球的起始坐标离方格的距离,所以dx越大表示球越小
            if (dx >= 12)
                isGrow = true;//球变到最小后,让它开始变大
            if (dx <= 5)
                isGrow = false;//球变到最大后,让它开始变小
            if (isGrow)
                dx-=2;//让球变大一点
            else
                dx+=2;//让球变小一点
            Color temp=ball[move_i,move_j];
            clearBall(move_i, move_j);//先清空
            ball[move_i, move_j] = temp;
            drawBall(move_i,move_j,temp,dx);//然后重新画球
        }

        private void button开始_Click(object sender, EventArgs e)
        {
            startGame();
            for (int i = 0; i < 5; i++)
                drawBallRandom();//随机出5个球
            makeNextColor();
        }
        
        public void readMaxScore()//读取最高分
        {
            int max = SaveScore.redMax();
            label最高分.Text = "最高分:"+max;
        }

        private void 重新开始ToolStripMenuItem_Click_1(object sender, EventArgs e)
        {
            button开始_Click(null, null);
        }

        private void 退出ToolStripMenuItem_Click_1(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void 查看成绩ToolStripMenuItem_Click_1(object sender, EventArgs e)
        {
            MessageBox.Show(SaveScore.readScore(), "成绩排行榜", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void 帮助ToolStripMenuItem1_Click_1(object sender, EventArgs e)
        {
            MessageBox.Show("如有问题,请至新浪微博@小茗同学(http://weibo.com/liuxianan)", "帮助", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void 关于ToolStripMenuItem_Click_1(object sender, EventArgs e)
        {
            MessageBox.Show("游戏名称:彩色连珠 v1.0\n作者:小茗同学\n微博:http://weibo.com/liuxianan\n2012年7月18日", "彩色连珠", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    }
}

好了,整个小游戏到此为止,最后留一下个人的联系方式,有需要交流讨论的可以到我新浪微博来:http://weibo.com/liuxianan 

源代码下载:

http://pan.baidu.com/s/1FuLqZ

游戏exe可执行程序下载:

http://vdisk.weibo.com/s/8X6Vm/1342628629(已失效)

原文地址:https://www.cnblogs.com/liuxianan/p/2598446.html