连连看--详解及实现

来源:互联网 发布:golang 2.0 编辑:程序博客网 时间:2024/05/20 09:09

看似简单的游戏,实现起来也并不是那么轻松。在消除的算法上面卡壳了整整一天(脑袋笨),然后就是游戏的各种状态控制也十分繁琐。因此想通过此来给大家提供我对于解决这些问题的思路。
虽然使用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();    }};

——

//图类Graphpublic 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//对于用绝对路径、相对路径来设置图片的,可将此函数的返回值换为Imagepublic 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#实现

0 0
原创粉丝点击