連連看--詳解及實現
看似簡單的遊戲,實現起來也並不是那麼輕鬆。在消除的演算法上面卡殼了整整一天(腦袋笨),然後就是遊戲的各種狀態控制也十分繁瑣。因此想通過此來給大家提供我對於解決這些問題的思路。
雖然使用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事件: