C#連連看(新手入門)
先上圖片預覽
需要的所有屬性欄位:
//背景色集 Color[] cls = new Color[] {Color.White,Color.AliceBlue,Color.Aqua,Color.Azure ,Color.Bisque,Color.Gray,Color.Red,Color.Coral,Color.Brown,Color.Yellow}; String[] images = new string[] {"1.png","2.png", "3.png", "4.png" ,"5.png", "6.png", "7.png", "8.png" , "9.png" , "10.png" }; //隨機數 Random r; //地圖大小 public int mapSize = 10; //地圖按鈕集合 public Button[,] btns; //記錄上一個按鈕 public static Button last_b = null; //記錄按鈕對應座標 public Hashtable BtoI; //記錄按鈕是否已熄滅 public int[]map_index; //劃線點 public static Point p1; public static Point p2; //畫圖工具 Pen p; Graphics g;
//得分視窗 public int score; public TextBox text; //計時器 ProgressBar pb;
在窗體載入時開始窗體控制元件的設計(已在設計面板上建立panel容器panel1用於放置各元素,個人認為使用panel的好處之一是可更方便定位元素放置位置):
private void Form2_Load(object sender, EventArgs e) { Font f = new Font("楷體", 20); //label樣式設計 Label score_l = new Label(); score_l.Height =50; score_l.Width = 100; score_l.Font = f; score_l.Text = "得分:"; score_l.Location = new Point(panel1.Width + 200, 150); //textbox樣式設計 TextBox score_t = new TextBox(); text = score_t; text.Text = score.ToString(); score_t.Location = new Point(panel1.Width+300,120); score_t.Font = f;//字型 score_t.Height = 70;//高度 score_t.Width = 200; score_t.Multiline = true; this.Controls.Add(score_l); this.Controls.Add(score_t); this.Location = new Point(5, 5); this.Width = 1200; this.Height = 800; textBox1.Hide();textBox2.Hide();textBox3.Hide(); panel1.Width = 750; panel1.Height = 750; //進度條樣式設計 //pb = new ProgressBar(); //pb.Width = 500;pb.Height = 50; //pb.Location = new Point(100, 0);//放置位置 //panel1.Controls.Add(pb); //pb.Value = 100;//初始長度拉滿 initialize(); }
以上設定了所需的各類自定義控制元件(可根據需求自行修改);
接下來設定最重要的按鈕集及其各項屬性:
private void initialize() { btns = new Button[mapSize, mapSize]; //timer1.Enabled = true; //pb.Value = 100; ArrayList arr = new ArrayList(); BtoI = new Hashtable(); for (int i=0;i<50;i++) { int x = r.Next(0, 10); arr.Add(x); arr.Add(x); } //初始化mapindex int c = arr.Count; map_index = new int[c]; while(--c>=0) { int i = r.Next(0, c + 1); map_index[c] =(int) arr[i]; arr.RemoveAt(i); } //初始化按鈕集座標及大小 int w = panel1.Width; int h = panel1.Height; for (int i= 0;i<mapSize;i++) { for (int j = 0; j < mapSize; j++) { btns[i, j] = new Button(); btns[i, j].Width = btns[i, j].Height= w / 12;//設定大小 btns[i, j].Location = new Point(btns[i,j].Width+ j* (btns[i, j].Width+1) ,btns[i,j].Height+ i*(1+btns[i, j].Height));//設定座標 panel1.Controls.Add(btns[i, j]); //btns[i, j].Text = map_index[i * mapSize + j].ToString(); btns[i, j].BackgroundImage = Image.FromFile("Resources"+"\\"+images[map_index[i * mapSize + j]]); btns[i, j].BackgroundImageLayout = ImageLayout.Zoom; btns[i, j].BackColor = cls[map_index[i*mapSize+j]]; btns[i, j].Click += new EventHandler(this.Image_Click);//新增點選事件 Point p = new Point(i, j); BtoI.Add(btns[i, j], p); } } }
-由於每個按鈕需要隨機設定圖案,map_Index陣列用於存放該隨機值。一個難點是如何設計成對的隨機數字:
我這裡的做法是先產生所需隨機數的一半,另一半則通過第二次迴圈來填充。
-而我在這裡具體使用到的的生成隨機數的辦法是:
Arraylist陣列,其可根據需要插入和刪除,並且內部的Count方法可以返回當前陣列中元素個數。其類似一個可隨機訪問,但只可尾端插入的棧。
-原理是:每次生成一個隨機數,將其拷貝一份一同插入Arraylist陣列中,這樣在結束插入時,陣列中存放著兩份完全相同的隨機數序列。接下來每次生成一個隨機數作為Arraylist的索引(前文提到其具隨機訪問和刪除特性),將該索引處的資料作為按鈕集
buttons的隨機值,然後刪除該索引處的資料。注意:此時Arraylist陣列Count計數減一,相應的隨機索引也需要對Count-1取模。以上實現成對的隨機數。
-接下來對每個隨機數自定義操作,可以針對每個索引使用不同的圖片或背景色等等;可適當考慮需求。
對每個按鈕新增點選事件:
private void Image_Click(object sender, EventArgs e) { var cur_btn = (Button)sender; //初始 if (last_b == null) { last_b = cur_btn; return; } //Point p_1 = new Point();Point p_2 = new Point();Point p_3 = new Point();Point p_4 = new Point(); var p_1 = (Point)BtoI[last_b]; var p_2 = (Point)BtoI[cur_btn]; var p_3 = btns[p_1.X, p_1.Y].Location; var p_4 = btns[p_2.X, p_2.Y].Location; var p_start = new Point(p_3.X + last_b.Width / 2, p_3.Y + last_b.Height / 2); var p_end = new Point(p_4.X + last_b.Width / 2, p_4.Y + last_b.Height / 2); if (last_b.BackColor != cur_btn.BackColor || !isConnected(last_b, cur_btn)) { //MessageBox.Show("您有事嗎?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Information); last_b = cur_btn; } else { last_b.Hide(); cur_btn.Hide(); //以下劃線 g = panel1.CreateGraphics(); p = new Pen(Color.Red, 3); MyDrawLine(g, p, p_1, p_2, p_start, p_end); //以上劃線 map_index[((Point)BtoI[last_b]).X * mapSize + ((Point)BtoI[last_b]).Y] = -1; map_index[((Point)BtoI[cur_btn]).X * mapSize + ((Point)BtoI[cur_btn]).Y] = -1; score += 100; text.Text = score.ToString(); if (GameOver()) { MessageBox.Show("通關成功√", "提示", MessageBoxButtons.OKCancel); initialize(); } last_b = null; } }
先梳理流程:
-使用者開始遊戲,此時沒有任何點選動作。
-點選第一個方塊,此時需要判斷是否有已選中的方塊:如果沒有(如遊戲開始階段),記錄當前的圖案(其實也就是先前生成的隨機數,記住隨機的根源是前面產生的隨機數,圖案等為裝飾,但也可作為判斷標記);
而如果已有選中方塊:判斷兩者圖案是否相同:相同則消去,並且置空前一個按鈕圖案。(想象如果消除了一對圖案,那麼下一次點選是不會消除的)
不同則將當前圖案置為對比圖。
由於需要從點選的按鈕返回尋找按鈕對應的位置(將map_index置為訪問過的標記等等),此處我的做法是在初始化按鈕集時生成一個雜湊表儲存每個按鈕對應位置。(由於還未十分了解C#雜湊表部分內容,無法多作講解)
在消除時增加了一個劃線函式,即圖示的消除紅線。
private void MyDrawLine(Graphics g,Pen p, Point p_1, Point p_2, Point p_start,Point p_end) { if (IsStraightConnected(p_1.X, p_1.Y, p_2.X, p_2.Y)) { g.DrawLine(p, p_start, p_end); Delay_Show(); } else if (IsLConnected(p_1.X, p_1.Y, p_2.X, p_2.Y)) { var p_lx = new Point(btns[p1.X, p1.Y].Location.X + last_b.Width / 2, btns[p1.X, p1.Y].Location.Y + last_b.Height / 2); g.DrawLine(p, p_start, p_lx); g.DrawLine(p, p_lx, p_end); Delay_Show(); } else if (IsZConnected(p_1.X, p_1.Y, p_2.X, p_2.Y)) { var p_lx1 = new Point(btns[p1.X, p1.Y].Location.X + last_b.Width / 2, btns[p1.X, p1.Y].Location.Y + last_b.Height / 2); var p_lx2 = new Point(btns[p2.X, p2.Y].Location.X + last_b.Width / 2, btns[p2.X, p2.Y].Location.Y + last_b.Height / 2); g.DrawLine(p, p_start, p_lx1); g.DrawLine(p, p_lx1, p_lx2); g.DrawLine(p, p_lx2, p_end); Delay_Show(); } }
具體的畫法也就是通過雜湊尋找到按鈕座標,在其中心處向外劃線。(注意,我曾在劃線處遇到過問題,即在Form函式中使用drawline是無法看見畫出的線的,與系統的渲染順序有關,具體原因有興趣的讀者可以查閱資料)
-以及,Location方法獲得的是圖形左上角的座標。需將其做一定調整才能變為中心處劃線。
-由於設定劃線為一閃而過,大致定個100ms:劃線定時清除程式碼:
//按時刪除劃線 async public void Delay_Show() { await Task.Delay(100); textBox1.Text = ".."; g = panel1.CreateGraphics(); g.Clear(this.BackColor); }
用到async非同步方法,不過合理只是簡單呼叫了一個匿名task物件,完成簡單的時延任務。
最後是判斷按鈕連線的邏輯程式碼:
//按鈕判斷條件 private bool isConnected(Button b1,Button b2) { int r1 = ((Point)BtoI[b1]).X; int c1 = ((Point)BtoI[b1]).Y; int r2 = ((Point)BtoI[b2]).X; int c2 = ((Point)BtoI[b2]).Y; if (IsStraightConnected(r1, c1, r2, c2)) { textBox3.Text = "straight"; //g= btns[r1,c1]. CreateGraphics(); //p = new Pen(Color.Red); //g.DrawLine(p,btns[r1,c1].Location,btns[r2,c2].Location); //Delay_Show(); return true; } else if (IsLConnected(r1, c1, r2, c2)) { textBox3.Text = "L"; return true; } else if (IsZConnected(r1, c1, r2, c2)) { textBox3.Text = "Z"; return true; } return false; } //直接相連 private bool IsStraightConnected(int r1, int c1,int r2, int c2) { if (c1 == c2) { if (r2 < r1) { int t = r1; r1 = r2; r2 = t; } for (int i = r1 + 1; i != mapSize; i++) { if (i == r2) return true; else if (map_index[i * mapSize + c1] != -1) return false; } } else if (r1 == r2) { if (c2 < c1) { int t = c1; c1 = c2; c2 = t; } for (int j = c1 + 1; j != mapSize; j++) { if (j == c2) return true; else if (map_index[r1*mapSize+j] != -1) return false; } } return false; } //L型 private bool IsLConnected(int r1, int c1, int r2, int c2) { if (IsStraightConnected(r1, c2, r2, c2) && IsStraightConnected(r1, c2, r1, c1) && map_index[r1 * mapSize + c2] == -1) { p1 = new Point(r1, c2); return true; } if (IsStraightConnected(r2, c1, r2, c2) && IsStraightConnected(r2, c1, r1, c1) && map_index[r2 * mapSize + c1] == -1) { p1 = new Point(r2, c1); return true; } return false; } //Z型 private bool IsZConnected(int r1, int c1, int r2, int c2) { int direction = 1; for (int count=0;count<2;count++) { direction = -direction; //橫向先行 for (int i = c1 + direction; (i>=0&&i<mapSize); i += direction) { if (IsLConnected(r1, i, r2, c2) && IsLConnected(r2, i, r1, c1)) { p1 = new Point(r1, i); p2 = new Point(r2, i); return true; } } } for (int a=0;a<2;a++) { direction = -direction; //縱向先行 for (int i = r1 + direction; (i >= 0 && i < mapSize); i += direction) { if (IsLConnected(i, c1, r2, c2) && IsLConnected(i, c2, r1, c1)) { p1 = new Point( i,c1); p2 = new Point(i,c2); return true; } } } return false; }
有一點迭代的意思:即直線連線的邏輯也適用於L型或Z型連線;L型即有一次轉折,Z型為有兩次轉折。本處用到direction用於雙向迴圈,從而避免對兩個方向分別增加邏輯程式碼,造成冗餘。
最後是遊戲結束判斷邏輯:
private bool GameOver() { for (int i=0;i<mapSize*mapSize;i++) { if (map_index[i] != -1) return false; } return true; }
即map_index全部被置為-1時,遊戲結束。
本次設計也遇到了許多問題:1.我嘗試使用ProgressBar進度條來限定一局遊戲時間;並添加了Timer計時器,在time結束時呼叫initialize方法。但當計時結束但未消除完畢時,呼叫initialize方法後報錯:按鈕到索引的BtoI雜湊表未設定引用到物件的例項。目前還 不知道如何解決。
2.還未實現地圖外的連線,比如邊界上兩個圖示可以通過邊界外加的一行消除。思路大概是將map_index表向外擴充套件一圈,但具體實現還沒能進行。
3.由於本次設計是三分鐘熱度,因而沒有提前進行程式碼的設計,程式碼冗餘十分嚴重,而且結構並不清晰,還望各位讀者多多包涵。