连连看--详解及实现

看似简单的游戏,实现起来也并不是那么轻松。在消除的算法上面卡壳了整整一天(脑袋笨),然后就是游戏的各种状态控制也十分繁琐。因此想通过此来给大家提供我对于解决这些问题的思路。
虽然使用C#写的,但是其设计思路及核心的消除算法可借鉴并由其他语言轻松实现。解释也尽量详细,希望能帮到大家。
代码中解释也十分详细,嫌文章太长可直接看代码。

资源在此:连连看–C#实现
注意!由于设计缺陷(图标太大,窗体尺寸太大),此程序(资源)只能运行在1080P屏幕上,且放缩比例为100%(比例太高看不到RESET和START按钮)。
对于屏幕为1366x768的朋友,可用VS2015打开,通过设计窗口进行查看。
对此文章或资源有任何的问题,请提出,我会尽量做出改进。

游戏主界面介绍

游戏主界面

① scoreLabel ② recordLabel ③ resetBtn ④ startBtn

界面中共有100个PictureBox,即pictureBox0~99,其中外圈的pictureBox不用于放图片,而是为了连线(画线)时方便。即连线效果是通过对应位置的pictureBox显示不同图片来实现的。例如点击左侧第二列两个间隔开的相同图片(如两个齿轮)时,需要通过左侧第一列对应位置图片改变来实现(通过显示左右线 ,上下线 | ,以及四种拐弯线 来模拟连线)。

点类Point和图类Graph

  • 点类,每个pictureBox对应于一个点。如第一个不可见的pictureBox对应(0,0),第一张图片对应(1,1)。
  • 图类,有成员变量graph,是一个二维10*10的数组,每个元素对应于图中的点。通过这些元素的值,来操纵对应位置的pictureBox,实现图片加载、消除、画线等。
//点类
public class Point {
    int x;
    int y;
    bool valid;  //有效点
    /*
     * 通过valid来判定此点是否属于图类。假如点为(-1,-1),则通过此点调用某些函数会导致访问越界
     * 因此需要用valid来断定,让函数在应对无效点时不会得出错误结果
     */
    //构造函数,通过坐标点(x,y)
    public Point(int x, int y) {
        valid = true;  //先默认点为有效点
        if(x < 0 || x > 9 || y < 0 || y > 9) valid = false;  //点无效
        else {
            this.x = x;
            this.y = y;
        }
    }
    //复制构造函数
    public Point(Point p) {
        this.x = p.x;
        this.y = p.y;
        //不必根据p的valid来设置此valid,每次获取valid都需要通过判断
    }
    //x属性的get和set
    public int X {
        get { return x; }
        set { x = value; }
    }
    //y属性的get和set
    public int Y {
        get { return y; }
        set { y = value; }
    }
    //valid属性的get和set
    public bool Valid {
        get {
            if(x >= 0 && x <= 9 && y >= 0 && y <= 9)
                //根据x和y的值,设置valid。放在此处更新以避免通过直接设置valid而导致错误
                valid = true;
            else valid = false;
            return valid;
        }
        //无set属性,避免误用。因为get中会自动判断valid的值,因此valid的值一定为正确的
    }
    //通过索引设置点
    public void setPoint(int index) {
        x = index / 10;
        y = index % 10;
    }
    //获取点的信息----Test
    public string getPointInfo() {
        return "X: " + x.ToString() + "  Y: " + y.ToString() + "  Valid: " + valid.ToString();
    }
};

——

//图类Graph
public class Graph {
    int[,] graph;
    //用于描述10*10pictureBox中图片的类型
    //0:无图    1:图片1    2:图片2    3:图片3    ...
    public Graph() {
        graph = new int[10, 10];  //10*10
        //重置图数组
        for(int i = 0; i < 10; i++)
            for(int j = 0; j < 10; j++)
                graph[i, j] = 0;
    }
    //获取图的信息----Test
    public string getInfo() {
        string s = "";
        for(int i = 0; i < 10; i++) {
            for(int j = 0; j < 10; j++) {
                s = s + graph[i, j].ToString() + " ";
            }
            s += "\n";  //按行输出
        }
        return s;
    }
    //重载1:设置图中某位置的值,通过坐标点(i,j)
    public void setValue(int i,int j,int value) {
        //坐标点错误,Exception
        if(i<0 || i >= 10 || j<0 || j >= 10) throw new Exception("坐标无效");
        graph[i, j] = value;
    }
    //重载2:设置图中某位置的值,通过索引index
    public void setValue(int index,int value) {
        //索引错误,Exception
        if(index < 0 || index > 99) throw new Exception("索引无效");
        graph[index/10, index%10] = value;  //通过计算索引对应的点来设置
    }
    //重载1:获取点在图中位置的值
    public int getValue(Point p) {
        if(p.Valid) return graph[p.X, p.Y];  //点有效,就返回对应位置的值
        else throw new Exception("点无效");
    }
    //重载2:获取索引在图中位置的值
    public int getValue(int index) {
        if(index >= 0 && index <= 99) return graph[index / 10, index % 10];
        else throw new Exception("索引无效");
    }
    //重载1:判断点对应的图中是否标记有图片(非0)
    public bool hasPicture(Point p) {
        if(p.Valid) {
            if(graph[p.X, p.Y] != 0) return true;  //有图片,true
            else return false;
        }
        else throw new Exception("点无效");
    }
    //重载2:判断索引对应的图中是否标记有图片(非0)
    public bool hasPicture(int index) {
        if(index >= 0 && index <= 99) {
            Point p = new Point(index / 10, index % 10);
            return hasPicture(p);
        }
        else throw new Exception("索引无效");
    }
};

控制变量设计

下面的变量设计中,包含了游戏的设计思路及各种权衡,因此请仔细查看。

  • 图片需要随机产生,因此有Random random
  • 游戏音效需要SoundPlayer soundPlayer,要using System.Media;才能使用。
  • 点击两张图片才进行消除判断,因此需要两个点Point point1Point piont2
  • 判断是否属于游戏状态(玩家可操控装态)bool inGame。此变量可根据设计的不同而更改。我的设计是消除时让消除线显示一会儿,之后再重置游戏中的各种状态(point1point2、点击计数、游戏图片更新等)。如果在显示连线时玩家点击了图片,后果很难预料。
  • 游戏时间int time。即游戏用时,用于玩家分数计算。
  • 游戏的点击计数int clickCount,在pictureBox的点击事件中更新。当点击次数为1时,只更新点击位置的pictureBox图片背景颜色(标记为选中),以及point1等。当次数为2时,更新point2等属性并开始判断两点是否能消除,并做出相应操作。并最后通过重置函数(后面会讲)进行重置。
  • 连线路径点数组Point[] pointRout。此数组用于在连线时,将连线上的点放入,以便连线完后,将对应图片重置为空,实现连线消失,否则连线将一直存在。
  • 二线连通的拐点Point tp2。判断两点是否能消除,是通过判断两点能否通过一条直线连通,或者通过两条直线连通,或者三条直线连通。这种方法的好处在于,写好一线连通时,二线连通可调用一线连通来实现,而三线连通又可通过调用二线连通一线连通来实现。而二线连通时,产生一个拐点,三线连通时,产生两个拐点。因此用tp2和tp3来记录,便于后用。
  • 三线连通的拐点Point tp3
  • 判断是否产生消除int eliminate。此变量不用bool类型是因为,判断消除时,有4种情况:无法消除、一线连通、二线连通、三线连通。每种情况都对应不同的画线函数。因此需要用int型。
  • 定义图片种类及每种图片可用张数int[] picCount,其中数组长度为图片可用种类数。我用了10*10的布局,因此需要放图片64张(外围无图片),故选用8种图片,数组长度为8,每种图片可用8张,即每个元素的值为8。
  • 判断游戏是否结束bool gameOver。游戏结束时,调用结算框。
  • 统计图中剩余的图片数量int leftPic。为避免游戏出现死循环,即无法消除时,需要重置剩余图片的位置。网上的解法是循环判断图中两两能否消除,如果不能,则进入死结,需要重置。我也试过此方法,但是当游戏网格太大时,就会导致严重的性能问题,如10*10的网格,i从0~98,j从i+1到99。每次都判断两点能否一线连通、二线连通、三线连通,则第一次就会导致接近一万次判断三种消除,游戏就卡死了。因此我放了一个RESET按钮在窗体上,默认不可点击。当图片张数小于等于8张(可根据具体情况设置)时,变为可点击。然后为此按钮编写Click事件,来实现图片重置。这样虽然当游戏没有出现死结时也可点击,但是避免了性能问题。
  • 游戏的纪录int record,用于在标签上显示。
  • 最后是位图对象Bitmap bmx。将图片导入资源文件,然后构造位图对象。也可不用位图对象,直接用将图片放入image文件夹,并放在工程的bin>debug中,使用时写相对路径即可。

下面是变量代码

Graph graph;  //图对象
Random random = new Random();  //随机数对象
SoundPlayer soundPlayer = new SoundPlayer();  //音效文件对象
Point point1;  //点击的第一个点
Point point2;  //点击的第二个点
bool inGame;  //游戏中(用于控制鼠标点击是否有用,如未按开始按钮时)
int time;  //游戏用时
int clickCount;  //点击计数,值为2时开始进行消除判断,并重置
Point[] pointRout;  //连线路径点
int pointCount;  //连线路径长度
Point tp2;  //二线连接时的转点
Point tp3;  //三线连接时的转点
int eliminate;  //消除信息,0,1,2,3对应无消除、一线消除、二线消除、三线消除
int[] picCount;  //数组长度用于规定图片种类数,元素值为对应图片种类可产生的数目
bool gameOver;  //游戏是否结束
int leftPic;  //图中剩余的图片数量
int record;  //游戏记录

//可忽略,用图片时写绝对路径(相对路径)也可
Bitmap bm0;  //bm0到bm7为此连连看游戏的8种图片
Bitmap bm1;
Bitmap bm2;
Bitmap bm3;
Bitmap bm4;
Bitmap bm5;
Bitmap bm6;
Bitmap bm7;
Bitmap bm_updown;  //上下线   ┊
Bitmap bm_leftright;  //左右线  ┈
Bitmap bm_upleft;  //上转左线  ┘
Bitmap bm_upright;  //上转右线  └
Bitmap bm_downleft;  //下转左线  ┐
Bitmap bm_downright;  //下转右线  ┌

游戏主要函数

如果直接讲游戏思路,有点空中楼阁的意思,不容易理解。因此通过将游戏详细思路嵌入到函数的注释中,来让大家看到实际的效果。
如果嫌看函数代码过于麻烦,可在最后找到我的资源链接。

一线连通:public bool checkOneLine(Point p1,Point p2);
判断一线连通,即判断两点是否x方向共线,或y方向共线,且中间无图片。一旦确定x方向共线,但是中间有图,则false。y方向同理。

//一线连通
public bool checkOneLine(Point p1,Point p2) {
    if(p1.X == p2.X && p1.Y == p2.Y)  //两点为同一点,false
        return false;
    if(p1.X != p2.X && p1.Y != p2.Y)  //两点不在同一横向或竖向,即不在同一直线上
        return false;

    //确定两点横向或竖向共线后,只要在此方向上有图片(阻隔),则不连通(false)
    if(p1.X == p2.X) {  //两点横向共线
        //不进行p1和p2位置(左右)判断,下面的for函数会自动区分两点的位置
        //即通过类似i<p2.Y来区分。第一点在左则进入第一个for循环,否则进入第二个for循环
        //横向+扫描(p1在左)
        for(int i = p1.Y + 1; i < p2.Y; i++) {
            if(graph.hasPicture(new Point(p1.X, i))) return false;
        }
        //横向-扫描(p1在右)
        for(int i = p1.Y - 1; i > p2.Y; i--) {
            if(graph.hasPicture(new Point(p1.X, i))) return false;
        }
    }
    else {  //两点竖向共线
        //竖向+扫描(p1在上)
        for(int i = p1.X + 1; i < p2.X; i++) {
            if(graph.hasPicture(new Point(i, p1.Y))) return false;
        }
        //竖向-扫描(p1在下)
        for(int i = p1.X - 1; i > p2.X; i--) {
            if(graph.hasPicture(new Point(i, p1.Y))) return false;
        }
    }
    return true;  //在连点共线的方向上没有图片阻隔,true(一线连通)
}

二线连通:public bool checkTwoLine(Point p1,Point p2);
二线连通
两点可通过两条直线连接,则两点必定处于矩形的对角点上。因此只需要找出另外两个对角点A、B,若p1和A一线连通,且A和p2一线连通;或p1和B一线连通,且B和p2一线连通,则可二线连通。在获得二线连通时,需要设置转点tp2的值A或B。

//二线连通
public bool checkTwoLine(Point p1,Point p2) {
    //两线连通时,两点组成一个矩形。另外两个顶点A和B即二线连通情况的可能转点
    Point A = new Point(p1.X, p2.Y);
    Point B = new Point(p2.X, p1.Y);
    if(graph.hasPicture(A) && graph.hasPicture(B))  //两顶点都有图,即两顶点都不可用作转点
        return false;
    if(graph.getValue(A.X * 10 + A.Y) == 0) {  //A点无图情况
        //p1与A可一线连接,且A与p2可一线连接
        if(checkOneLine(p1, A) && checkOneLine(A, p2)) {
            tp2 = A;  //设置两线连接的转点为A
            return true;
        }
    }
    if(graph.getValue(B.X * 10 + B.Y) == 0) {  //B点无图情况
        //p1与B可一线连接,且B与p2可一线连接
        if(checkOneLine(p1, B) && checkOneLine(B, p2)) {
            tp2 = B;
            return true;
        }
    }
    //A、B点都无图,但是在p1通往A、B或A、B通往p2路径上有图片阻隔
    return false;
}

三线连通:public bool checkThreeLine(Point p1,Point p2);
通过p1向上下左右四个方向搜索,获取不同的可和p2二线连通的点A。但是此时的A不一定是最优的点。因此用点数组turnPoint来存储它们,最后分别判断p1通过每个点到达p2所需的路径长度,来获取最优的A,此A即tp3。
此时需要获取路径长度的函数,因此临时定义两个获取路径长度的函数:
public int distance1(Point p1,Point p2); //计算两点直线距离
public int distance2(Point p1,Point p2); //计算两点折线距离

//计算两点直线距离
public int distance1(Point p1,Point p2) {
    if(p1.X != p2.X && p1.Y != p2.Y) throw new Exception("两点非同一直线");
    int dis = 0;
    if(p1.X == p2.X) dis = Math.Abs(p1.Y - p2.Y);  //两点同横向
    else dis = Math.Abs(p1.X - p2.X);  //两点同竖向
    return dis;
}
//计算两点折线距离
public int distance2(Point p1,Point p2) {
    checkTwoLine(p1, p2);  //通过调用checkTwoLine来重置tp2,通过tp2来调用distance1
    return distance1(p1, tp2) + distance1(tp2, p2);  //通过tp2做链接,两次调用distance1
}

//三线连通
public bool checkThreeLine(Point p1,Point p2) {
    /*
     * 有可能找到的三线连通点不是最优,因此用一个Point[] turnPoint来
     * 存储所有找到的 能和p2二线连接的转点,最后通过判断通过各个点的
     * 路径长度,来选择最优转点作为tp3
     */
    Point[] turnPoint = new Point[100];
    int count = 0;  //找到的转点计数
    //横向+搜索
    for(int i = p1.Y + 1; i < 10; i++) {
        Point A = new Point(p1.X, i);
        if(graph.hasPicture(A)) break;  //有图,取消接下来的 横向+ 搜索
        else {
            if(checkTwoLine(A, p2))  //A点可与p2二线连通,则A点是转点,放入转点数组
                turnPoint[count++] = new Point(A);
        }
    }
    //横向-搜索
    for(int i = p1.Y - 1; i >= 0; i--) {
        Point A = new Point(p1.X, i);
        if(graph.hasPicture(A)) break;
        else {
            if(checkTwoLine(A, p2))
                turnPoint[count++] = new Point(A);
        }
    }
    //纵向+搜索
    for(int i = p1.X + 1; i < 10; i++) {
        Point A = new Point(i, p1.Y);
        if(graph.hasPicture(A)) break;
        else {
            if(checkTwoLine(A, p2))
                turnPoint[count++] = new Point(A);
        }
    }
    //纵向-搜索
    for(int i = p1.X - 1; i >= 0; i--) {
        Point A = new Point(i, p1.Y);
        if(graph.hasPicture(A)) break;
        else {
            if(checkTwoLine(A, p2))
                turnPoint[count++] = new Point(A);
        }
    }

    //找最优点tp3
    if(count != 0) {  //找到了转点
        Point p = turnPoint[0];
        //通过p1和转点p的两点直线距离 和p与p2的两点折线距离来获得
        //p1和p2通过转点p的三点折线距离
        int dis = distance1(p1, p) + distance2(p, p2);  //dis用于获取三点最短距离
        for(int i = 1; i < count; i++) {
            //内部_dis,分别获取p1和p2通过不同转点的三点折线距离
            int _dis = distance1(p1, turnPoint[i]) + distance2(turnPoint[i], p2);
            if(_dis < dis) {  //找到一个所需距离更短的转点turnPoint[i]
                dis = _dis;
                p = turnPoint[i];  //p设置为最优转点
            }
        }
        tp3 = p;  //设置tp3为三线连接的转点
        /*
         * 每次checkTwoLine都会重置tp2,
         * 而distance2中调用了此函数,且checkThreeLine函数最后调用的
         * checkTwoLine函数产生的tp2也不一定为正确的tp2。因此需要通过
         * 再次用最优点与p2找二线连通,来设置正确的tp2
        */
        checkTwoLine(tp3, p2);  //checkTwoLine会自动设置tp2
        return true;
    }
    return false;  //没有找到任何转点,故无三线连通
}

画直线函数:public void drawOneLine(Point p1, Point p2);

//画两点直线
public void drawOneLine(Point p1, Point p2) {
    //rout用于存储两点(直线连接)间的点
    Point[] rout = new Point[10];  //画直线最多10个点。将此函数拷贝到他处时,注意数组长度
    int routCount = 0;  //点计数
    if(p1.X == p2.X && p1.Y == p2.Y) return;  //两点为同一直线,不画线
    if(p1.X != p2.X && p1.Y != p2.Y) throw new Exception("两点不共线");
    if(p1.X == p2.X) {  //两点横向连通
        //p1在左
        for(int i = p1.Y + 1; i < p2.Y; i++) {
            rout[routCount++] = new Point(p1.X, i);  //将路径点放入局部路径点数组rout中
            getPictureBox(new Point(p1.X, i)).Image = bm_leftright;  //设置图片为左右直线
        }
        //p2在左
        for(int i = p2.Y + 1; i < p1.Y; i++) {
            rout[routCount++] = new Point(p1.X, i);
            getPictureBox(new Point(p1.X, i)).Image = bm_leftright;
        }
    }
    else {  //两点竖向连通
        //p1在上
        for(int i = p1.X + 1; i < p2.X; i++) {
            rout[routCount++] = new Point(i, p1.Y);
            getPictureBox(new Point(i, p1.Y)).Image = bm_updown;  //上下直线
        }
        //p2在上
        for(int i = p2.X + 1; i < p1.X; i++) {
            rout[routCount++] = new Point(i, p1.Y);
            getPictureBox(new Point(i, p1.Y)).Image = bm_updown;
        }
    }
    /*
     * 划线后,将放入局部路径点数组的路径放入最终的外部路径点数组pointRout中
     * 因为画直线的数组可能会被画折线(drawTwoLine)调用,因此不可直接覆盖
     * pointRout数组,只能将画直线(drawOneLine)的点添加到其中
    */
    for(int i = 0; i < routCount; i++) {  //将路径点添加到最终的路径点数组pointRout中
        pointRout[pointCount++] = new Point(rout[i]);
    }
}

画一折线:public void drawTwoLine(Point p1, Point p2);

//画两点折线
public void drawTwoLine(Point p1, Point p2) {
    Point[] rout = new Point[20];  //折线在此程序中最多20个点(实际18个,p1和p2不会入rout)
    int routCount = 0;
    //tp2与p1同横向
    if(p1.X == tp2.X) {
        //p1在tp2左
        if(p1.Y < tp2.Y) {
            drawOneLine(p1, tp2);  //p1到tp2画直线
            //tp2在p2上方
            if(tp2.X < p2.X) {
                getPictureBox(tp2).Image = bm_downleft;  //tp2显示下左转线
                rout[routCount++] = new Point(tp2);  //将tp2添加入rout中
                drawOneLine(tp2, p2);  //tp2到p2画直线
            }
            else {  //tp2在p2下方
                getPictureBox(tp2).Image = bm_upleft;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
        else {  //tp2在p1左
            drawOneLine(p1, tp2);
            //tp2在p2上方
            if(tp2.X < p2.X) {
                getPictureBox(tp2).Image = bm_downright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
            else {  //tp2在p2上方
                getPictureBox(tp2).Image = bm_upright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
    }
    else {  //tp2与p1同竖向
        //p1在tp2上
        if(p1.X < tp2.X) {
            drawOneLine(p1, tp2);
            if(tp2.Y < p2.Y) {  //tp2在p2左
                getPictureBox(tp2).Image = bm_upright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
            else {  //tp2在p2右
                getPictureBox(tp2).Image = bm_upleft;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
        else {  //tp2在p1上
            drawOneLine(p1, tp2);
            //tp2在p2左
            if(tp2.Y < p2.Y) {
                getPictureBox(tp2).Image = bm_downright;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
            else {  //tp2在p2右
                getPictureBox(tp2).Image = bm_downleft;
                rout[routCount++] = new Point(tp2);
                drawOneLine(tp2, p2);
            }
        }
    }
    //将画两点折线的点加入路径点数组中
    for(int i = 0; i < routCount; i++)
        pointRout[pointCount++] = new Point(rout[i]);
}

画二折线(三路连通):public void drawThreeLine(Point p1,Point p2);

//画三点折线
public void drawThreeLine(Point p1,Point p2) {
    Point[] rout = new Point[30];  //三线连通路径点少于30个,具体多少懒得算
    int routCount = 0;  //路径点个数计数
    //p1与tp3同横向
    if(p1.X == tp3.X) {
        if(p1.Y < tp3.Y) {  //p1在tp3左
            drawOneLine(p1, tp3);  //p1到tp3画直线
            //tp3在tp2上方
            if(tp3.X < tp2.X) {
                getPictureBox(tp3).Image = bm_downleft;  //tp3画下左折线
                rout[routCount++] = new Point(tp3);  //将tp3加入路径点
                drawTwoLine(tp3, p2);
            }
            else {  //tp3在tp2下方
                getPictureBox(tp3).Image = bm_upleft;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
        else {  //tp3在tp1左
            drawOneLine(p1, tp3);
            //tp3在tp2上方
            if(tp3.X < tp2.X) {
                getPictureBox(tp3).Image = bm_downright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
            else {  //tp3在tp2下方
                getPictureBox(tp3).Image = bm_upright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
    }
    else {  //tp3与p1同竖向
        //p1在tp3上
        if(p1.X < tp3.X) {
            drawOneLine(p1, tp3);
            if(tp3.Y < tp2.Y) {  //tp3在tp2左
                getPictureBox(tp3).Image = bm_upright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
            else {  //tp3在tp2右
                getPictureBox(tp3).Image = bm_upleft;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
        else {  //tp3在p1上
            drawOneLine(p1, tp3);
            if(tp3.Y < tp2.Y) {  //tp3在tp2左
                getPictureBox(tp3).Image = bm_downright;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
            else {
                //tp3在tp2右
                getPictureBox(tp3).Image = bm_downleft;
                rout[routCount++] = new Point(tp3);
                drawTwoLine(tp3, p2);
            }
        }
    }
    //将路径点放入最终路径点数组pointRout中
    for(int i = 0; i < routCount; i++) {
        pointRout[pointCount++] = new Point(rout[i]);
    }
}

判断两图片是否相同:public bool samePicture(Point p1, Point p2);

/*
 * 判断两点对应的图片是否相同
 * 不能通过判断pictureBox的Image属性,因为它是引用,会判断两者是否为同一对象
 * 网上还有说法是线程池的原因,不懂
 * 因此通过判断两点对应的图graph中的值是否相同来实现
 */
public bool samePicture(Point p1, Point p2) {
    if(graph.getValue(p1) == graph.getValue(p2)) return true;
    else return false;
}

播放声音:public void soundPlay(string s);

//播放声音,根据传入的字符串来确定音效文件位置
public void soundPlay(string s) {
    soundPlayer.SoundLocation = s;
    soundPlayer.Load();
    soundPlayer.Play();
}

根据索引或点获取对应的PictureBox对象:

//重载1:获取索引对应的pictureBox对象
public PictureBox getPictureBox(int index) {
    switch(index) {
        case 0: return pictureBox0;
        case 1: return pictureBox1;
        case 2: return pictureBox2;
        ...//中间省略n行
        case 99: return pictureBox99;
        default:
            throw new Exception("索引无效");
    }
}

//重载2:获取点对应的PictureBox对象
public PictureBox getPictureBox(Point p) {
    if(!p.Valid) throw new Exception("点无效");
    int index = p.X * 10 + p.Y;
    switch(index) {
        //这里当然不是手打的,通过for循环输出到Console,然后copy的。我当然没有那么蠢!
        case 0: return pictureBox0;
        case 1: return pictureBox1;
        case 2: return pictureBox2;
        case 3: return pictureBox3;
        ...//省略n行
        case 99: return pictureBox99;
        default: throw new Exception("获取pictureBox越界");
    }
}

重置游戏剩余图片:public void resetLeftPic();

//游戏进入死循环时,重置剩余图片
public void resetLeftPic() {
    int[] index = new int[100];  //存放需要重置图片的索引数组
    for(int i = 0; i < 100; i++) index[i] = 0;  //保险起见,重置元素为0
    int count = 0;  //剩余图片计数
    //获取右图片位置
    for(int i = 0; i < 100; i++) {
        if(graph.getValue(i) != 0) {  //此处有图
            index[count++] = i;  //将此点索引加入索引数组
            picCount[graph.getValue(i) - 1]++;  //将此图片添加到可用图片数组中
        }
    }
    //从剩余图片中随机一张放到各个位置
    for(int i = 0; i < count; i++) {
        int pic = random.Next(8);  //随机图片索引
        //设置对应pictureBox的图片,getPic会自动将pictureBox对应的点的值设置为对应图片索引
        //即只要设置了pictureBox的图片,就会更新其对应点在graph上的值
        getPictureBox(index[i]).Image = getPic(pic, index[i]);
    }
    //避免点击一次图片后再点RESET按钮时,对应pictureBox还是被标记为选中状态
    //因此手动取消point1对应pictureBox的选中状态
    if(point1.Valid) setPicBC(point1);
    clickCount = 0;  //点击次数清零
}

重置外围图片:public void resetPeripheralPic();

public void resetPeripheralPic() {
    pictureBox0.Image = null;
    ...//根据哪些是外围图片,来进行设置
}

获取图片:public Bitmap getPic(int x,int index);

//获取图片,通过第一个参数选择图片种类, 第二个参数选择pictureBox
//对于用绝对路径、相对路径来设置图片的,可将此函数的返回值换为Image
public Bitmap getPic(int x, int index) {
    int count = 0;  //获取剩余图片种类,主要用于判断是否为只剩下一种图片可用
    int onlyPic = 0;  //假如只有一种图片可用时,标记那种图片
    for(int i = 0; i < 8; i++) {
        if(picCount[i] != 0) {
            count++;
            onlyPic = i;
        }
    }
    //只剩一种图片,让x直接改变为这种图片的标号
    if(count == 1) x = onlyPic;
    //当可用图片已用完,则抛出异常
    if(count == 0) throw new Exception("图片可用数目已用完");
    //假如index对应的图片种类的剩余可生成数量为0,则重新随机
    //此时可用图片种类肯定不是1或0,上面的两个if已经判断并剔除
    if(picCount[x] == 0) {
        do {
            x = random.Next(8);
        } while(picCount[x] == 0);
    }
    switch(x) {
        case 0: {
                graph.setValue(index, 1);  //设置graph对应位置的值,值1代表图片0
                picCount[0]--;
                return bm0;
            }
        case 1: {
                graph.setValue(index, 2);  //设置graph对应位置的值,值2代表图片1
                picCount[1]--;
                return bm1;
            }
        case 2: {
                graph.setValue(index, 3);
                picCount[2]--;
                return bm2;
            }
        case 3: {
                graph.setValue(index, 4);
                picCount[3]--;
                return bm3;
            }
        case 4: {
                graph.setValue(index, 5);
                picCount[4]--;
                return bm4;
            }
        case 5: {
                graph.setValue(index, 6);
                picCount[5]--;
                return bm5;
            }
        case 6: {
                graph.setValue(index, 7);
                picCount[6]--;
                return bm6;
            }
        case 7: {
                graph.setValue(index, 8);
                picCount[7]--;
                return bm7;
            }
        default:
            throw new Exception("图片种类标记无效");
    }
}

各个PictureBox点击时统一调用:

//辅助函数,不同pictureBox_Click事件可通过统一调用此函数,简洁地完成其功能
public void pictureClicked(PictureBox pb,int index) {
    if(!inGame) return;  //非玩家可操控状态,如在显示连线且需要让连线显示一段时间的时候
    soundPlay("music\\click2.wav");
    clickCount++;  //点击次数+1;
    Point p = new Point(-1, -1);  //创建一个新的无效点
    p.setPoint(index);  //设置此点为 点击的点
    if(clickCount == 1) {  //第一次点击
        point1.setPoint(index);  //设置point1
        pb.BackColor = Color.LightGray;  //设置调用此函数的pictureBox的背景颜色,表示选中
    }
    if(clickCount == 2) {  //第二次点击
        inGame = false;  //进入非玩家可操控状态,进行消除判断和画线等操作
        point2.setPoint(index);  //设置point2
        pb.BackColor = Color.LightGray;  //图片选中
        //判断是否能消除
        if(samePicture(point1, point2)) {  //两点图片相同才进行消除判断
            if(checkOneLine(point1, point2)) eliminate = 1;  //一线消除
            else if(checkTwoLine(point1, point2)) eliminate = 2;  //二线消除
            else if(checkThreeLine(point1, point2)) eliminate = 3;  //三线消除
            //eliminate默认是0,即无消除状态
        }
        //开始画线
        if(eliminate == 1) drawOneLine(point1, point2);
        if(eliminate == 2) drawTwoLine(point1, point2);
        if(eliminate == 3) drawThreeLine(point1, point2);
        if(eliminate == 0) {  //没有产生消除,不进入延迟,直接重置所有信息
            soundPlay("music\\notElim.wav");  //消除失败音效
            reset();
            return;
        }
        //有消除,进入延迟,让消除线显示一会儿,时间由delayTimer的Interval属性来定
        delayTimer.Start();
    }
}

//PictureBox的点击事件调用此函数的格式:
private void pictureBox0_Click(object sender, EventArgs e) {
    if(inGame && (graph.getValue(0) > 0))  //假如在游戏状态,且此处有图片
        pictureClicked(pictureBox0, 0);  //传入本身,及其编号
}

delayTimer的Tick事件:

//delayTimer的Tick事件
private void delayTimer_Tick(object sender, EventArgs e) {
    /*
     * 要想保证此次事件产生后,不再有delayTimer_Tick,需要将
     * delayTimer.Stop();放在第一个位置(或其他更好的位置?)
     * 否则会导致getPictureBox在通过point1来找pictureBox的
     * 时候,point1已被reset()重置,从而出错
     */
    delayTimer.Stop();
    soundPlay("music\\elim.wav");  //消除成功音效
    //不把这四句放到reset()中是因为,只有产生消除,才重置
    //point1和point2的图片及重置图对应位置的值,不产生消除不会重置
    getPictureBox(point1).Image = null;  //重置point1的图片
    getPictureBox(point2).Image = null;  //重置point2的图片
    graph.setValue(point1.X, point1.Y, 0);  //重置point1对应的图位置的值为0
    graph.setValue(point2.X, point2.Y, 0);  //重置point2对应的图位置的值为0
    leftPic -= 2;  //减少2剩余图片数量
    reset();  //重置状态
}

START按钮点击事件:

//点击START按钮
private void startBtn_Click(object sender, EventArgs e) {
    soundPlay("music\\click1.wav");
    delayTimer.Stop();
    resetBtn.Enabled = false;  //每次开始游戏,都将重置按钮设为不可用,当满足条件才可用
    recordLabel.Text = File.ReadAllText("other\\record.txt");  //每次点击开始按钮,获取游戏记录,可保证游戏记录最新
    //假如是点击一次图片后,按START按钮,则重置此图片的背景色,否则刷新地图后,对应图片背景色还是选中色(灰色)
    if(point1.Valid) setPicBC(point1);

    startGame();
    timeTimer.Start();
}

RESET按钮点击事件:

private void resetBtn_Click(object sender, EventArgs e) {
    soundPlay("music\\click1.wav");
    resetLeftPic();
}

重置游戏部分内容:public void reset();

//重置游戏部分信息,两次点击后调用
public void reset() {
    //以下属性的重置位置对应声明位置
    //graph不用更新
    //random
    //soundPlayer
    setPicBC(point1);  //重置point1对应图片的背景颜色
    setPicBC(point2);
    point1.X = -1;point1.Y = -1;
    point2.X = -1;point2.Y = -1;
    //inGame属性在下方重置,为避免游戏出现无解的情况
    //time属性不更新
    clickCount = 0;  //重置点击计数
    //重置路线图片
    for(int i = 0; i < pointCount; i++)
        getPictureBox(pointRout[i]).Image = null;
    pointCount = 0;  //重置路径点数目,即重置路径点数组
    tp2.X = -1; tp2.Y = -1;
    tp3.X = -1; tp3.Y = -1;
    eliminate = 0;  //重置消除状态
    resetPeripheralPic();  //重置外围图片
    //判断游戏是否结束
    gameOver = true;  //暂时标记为游戏结束
    for(int i = 0; i < 100; i++) {
        if(graph.getValue(i) != 0) {
            gameOver = false;  //假如还有图片,则游戏未结束
            break;
        }
    }
    if(gameOver) {  //假如游戏结束,进入GameOverForm
        inGame = false;  //非玩家可操控状态
        timeTimer.Stop();  //停止游戏计时
        GameOverForm gof = new GameOverForm(time);
        gof.ShowDialog();
        time = 0;  //重置游戏时间
        return;
    }
    else {  //游戏未结束
        //只有当图片数量不大于8张时,才进行消除判定,否则会导致性能低下(到不能玩的程度)
        if(leftPic <= 8) resetBtn.Enabled = true;  //8张图片及以下,可重置剩余图片,避免游戏死结
    }
    //图片能够产生消除,但是可能是通过重置游戏剩余图片而来的,而重置游戏图片中调用
    //了canEliminate()函数,它又调用了checkOneLine(), checkTwoLine(), cheThreeLine()
    //因此可能会向已重置的tp2、tp3和pointCount写入数据,因此需要再次重置这些数据
    pointCount = 0;
    tp2.X = -1; tp2.Y = -1;
    tp3.X = -1; tp3.Y = -1;
    inGame = true;  //开放游戏装态为玩家可操控状态,即玩家可进行点击等操作
}

开始游戏:public void startGame();
通过这个函数来实现所有内容重置。在reset()函数中无法实现所有内容。

//开始游戏
public void startGame() {
    inGame = false;  //先设置为非玩家可操控状态
    eliminate = 0;  //重置消除状态
    time = 0;  //重置游戏时间
    clickCount = 0;  //重置点击次数
    graph = new Graph();  //重置图对象
    point1 = new Point(-1, -1);  //重置第一次点击的点
    point2 = new Point(-1, -1);  //重置第二次点击的点
    tp2 = new Point(-1, -1);  //重置转点2
    tp3 = new Point(-1, -1);  //重置转点3
    pointRout = new Point[20];  //重置连线路径点数组
    picCount = new int[8];  //重置图片种类(8种)
    for(int i = 0; i < 8; i++)
        picCount[i] = 8;  //重置每种图片可显示张数
    leftPic = 64;  //重置剩余图片数量

    scoreLabel.Text = "0";  //分数标签重置为0
    //最外围pictureBox用于显示连线,因此不会产生图片
    //第一行无图片
    //第二行首尾无图片
    pictureBox11.Image = getPic(random.Next(8), 11);
    pictureBox12.Image = getPic(random.Next(8), 12);
    pictureBox13.Image = getPic(random.Next(8), 13);
    pictureBox14.Image = getPic(random.Next(8), 14);
    pictureBox15.Image = getPic(random.Next(8), 15);
    pictureBox16.Image = getPic(random.Next(8), 16);
    pictureBox17.Image = getPic(random.Next(8), 17);
    pictureBox18.Image = getPic(random.Next(8), 18);
    //第三行首尾无图片
    //第四行首尾无图片
    //第五行首尾无图片
    //第六行首尾无图片
    //第七行首尾无图片
    //第八行首尾无图片
    //第九行首尾无图片
    //第十行无图片
    inGame = true;  //设置为玩家可操控状态
}

GameForm构造函数:

//GameForm构造函数
public GameForm() {
    InitializeComponent();
    inGame = false;
    soundPlayer = new SoundPlayer();

    //START按钮在点击时,必须判断point1和point2是否导致了图片变色,如果变色
    //需要重置两者的背景色,否则刷新游戏地图后,对应图片依旧是被选择色(灰色)
    //因此必须加入point1和point2的有效与否判定。然而第一次点击时必须point1和point2
    //已被实例化,因此放到窗口构造函数中来
    point1 = new Point(-1, -1);
    point2 = new Point(-1, -1);
    //为位图赋值。假如通过绝对路径或相对路径,无视此
    bm0 = new Bitmap(LLK.Properties.Resources.pic0);
    bm1 = new Bitmap(LLK.Properties.Resources.pic1);
    bm2 = new Bitmap(LLK.Properties.Resources.pic2);
    bm3 = new Bitmap(LLK.Properties.Resources.pic3);
    bm4 = new Bitmap(LLK.Properties.Resources.pic4);
    bm5 = new Bitmap(LLK.Properties.Resources.pic5);
    bm6 = new Bitmap(LLK.Properties.Resources.pic6);
    bm7 = new Bitmap(LLK.Properties.Resources.pic7);
    bm_leftright = new Bitmap(LLK.Properties.Resources.left_right);
    bm_updown = new Bitmap(LLK.Properties.Resources.up_down);
    bm_upleft = new Bitmap(LLK.Properties.Resources.up_left);
    bm_upright = new Bitmap(LLK.Properties.Resources.up_right);
    bm_downleft = new Bitmap(LLK.Properties.Resources.down_left);
    bm_downright = new Bitmap(LLK.Properties.Resources.down_right);
}

注意!由于设计缺陷(图标太大,窗体尺寸太大),此程序只能运行在1080P屏幕上,且放缩比例为100%(比例太高看不到RESET和START按钮)。
资源链接:连连看–C#实现

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值