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