连连看--详解及实现
来源:互联网 发布: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 point1
和Point piont2
。 - 判断是否属于游戏状态(玩家可操控装态)
bool inGame
。此变量可根据设计的不同而更改。我的设计是消除时让消除线显示一会儿,之后再重置游戏中的各种状态(point1
、point2
、点击计数、游戏图片更新等)。如果在显示连线时玩家点击了图片,后果很难预料。 - 游戏时间
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#实现
- 连连看--详解及实现
- JAVA实现连连看
- WPF实现连连看
- 连连看的实现
- 连连看算法及源代码
- 连连看算法及源代码
- 连连看算法及源代码
- 连连看 代码的实现
- QT+C++实现连连看
- 连连看算法(控制台实现)
- 梁朝伟版连连看[PyQt实现]
- 回溯法实现连连看
- 连连看游戏的实现
- [Flex] 连连看核心算法详解
- FLASH连连看算法分析及源代码
- 编程实现连连看机器人外挂
- j2me 实现连连看的算法
- 简单flash代码实现连连看
- Android Studio 之 Live Templates 高效利用
- DNS原理及其解析过程
- [2016/8/22][Unix网络编程]第一章:①时间获取程序
- JavaScript 入门学习
- 使用myeclipse插入数据到mysql 出现中文乱码|utf8乱码解决方案[适合tomcat部署的jsp应用]
- 连连看--详解及实现
- #22 Flatten List
- 枚举集合--二进制法
- javascript ECMAScript-闭包
- java 流的复用
- 获取到百度天气
- studio 如何更改,查看或者修改jdk和sdk的路径
- set的用法
- #20 Dices Sum