不久前寫的一個小游戲,最近拿出來稍微修改完善了一下,因為自己現在“不得已”改行學Java了,這個小游戲就當是自己與C#的告別吧,不過以后如果自己有什么想寫的小程序,C#仍然是首先考慮的語言了,因為Java做GUI太蛋疼了。
首先聲明本人菜鳥一個,快畢業的學生黨,這篇文章完全是記錄自己的一些點滴吧。
游戲的規則很簡單,大概是:10X10的方格,游戲開始時隨機出5個球,顏色也是隨機的,用戶點擊球讓其移動,5個(或更多)相同顏色的球在一起就可以消掉,如果沒有可以消的,就又隨機出3個球,直到棋盤滿為止。
游戲界面如下:
具體思路如下:
左邊的是一個panel面板,用來當做棋盤,啟動時把方格線畫好,這些球都是一些事先弄好的圖片文件(考慮過用圖形學的方法代碼生成,但是感覺效率太低,最重要的是不好看,所以還是放棄了),通過g.DrawImage()的方法畫在面板上,清空的話就是用背景色填充,點擊某個球會動態的變化大小,點擊空白處會將之前點過的球動態的移動到那里,球每次移動時需要查找能夠到達指定位置的最短路徑,所以會用到《人工智能》課上用過的查找算法查找最短路徑,出子就是用Random隨機函數隨機的在某個位置畫某種顏色的球,每次移動球后都要判斷在橫、豎、左斜、右斜四個方向上是否有可以消的球,消完球后隨機出3個球,出球的同時要判斷棋盤是否滿。
簡單的實現了保存成績的功能(以及對成績進行加密),功能做的很簡陋,其實還可以添加一些聲音的,限於時間就沒弄了,有興趣的可以嘗試一下。
好了,也不多寫什么了,因為我覺得代碼里面的注釋已經夠詳細了,更多的問題還是看代碼里面的注釋吧。
下面把幾個比較重要的地方單獨寫出來。
首先最重要的是畫圖要怎么畫,也就是采用什么函數來畫。
C#畫圖最常見的一般有3種方式:
一種是,這種方法優點是窗體最小化或者被其它窗體遮擋后畫的圖不會消失,缺點是每次畫完圖都要刷新整個區域,所以有可能閃屏很嚴重:
Bitmap bit = new Bitmap(panel游戲區.Width,panel游戲區.Height);//實例化一個Bitmap Graphics g = Graphics.FromImage(bit); g.DrawImage(Image.FromFile("picturePath"),left,top,width,height);//畫圖 panel游戲區.BackgroundImage = bit;
還有一種是用控件的CreateGraphics()方法創建一個Graphic對象,優點很明顯,就是非常方便,不用每次都要刷新,所以一般不會出現閃屏現象,但是當窗體最小化還原或者被其它窗體遮擋后就一片空白了,到網上查過一些資料,好像是因為這種畫圖方式數據都是保存在緩存中的,窗體只要最小化就會觸發paint事件重繪,系統自帶的控件重繪的代碼都寫好了,但是我們自己的這個畫圖區域因為沒有寫重繪事件,所以還原后一片空白,解決辦法就是在paint事件里手動對空白區域進行重繪,本游戲采用的就是這種方法:
Graphics g = panel游戲區.CreateGraphics(); g.DrawImage(Image.FromFile("picturePath"), left, top, width, height);
還有一種方法是寫在控件的Paint事件里,缺點很明顯,都是不方便,很多代碼我們沒辦法寫在這里,比如鼠標事件發生的一些畫圖代碼。
Graphics g = e.Graphics;
g.DrawImage(Image.FromFile("picturePath"), left, top, width, height);
首先是畫方格線,比較簡單:
private void drawLine()//畫方格線的函數 { Graphics g = panel游戲區.CreateGraphics(); for (int i = 0; i < m; i++) { g.DrawLine(new Pen(Color.Black), 0, panel游戲區.Height / m * i, panel游戲區.Width, panel游戲區.Height / m * i); g.DrawLine(new Pen(Color.Black), panel游戲區.Width / m * i, 0, panel游戲區.Width / m * i, panel游戲區.Height); } }
其次是在指定行和列處畫指定顏色的球,因為后面有些地方有需要,重載了3次,這里只列出其中一個,其它類似。這里順便講一下關於資源文件的使用,因為把所以圖片放在文件夾里不方便,所以我還是想把所以圖片放到資源文件里面去,但是C#里根據資源文件名來查找資源文件還確實不是那么容易,找了很久的資料才解決,具體的就是ResourceManager里面的一個參數不好寫,很容易寫錯,具體事項看下面代碼和注釋吧:
:
/// <summary> /// 畫球的函數 /// </summary> /// <param name="i">行數</param> /// <param name="j">列數</param> /// <param name="color">要畫的球的顏色</param> /// <param name="dx">指定球比方格要小的像素個數</param> private void drawBall(int i, int j, Color color,int dx) { //關於圖片:如果直接將這些不同顏色的球的圖片放在程序根目錄文件夾來操作比較簡單, //但是缺點就是老是要跟一個文件夾,不方便,所以將所有圖片放入程序的資源文件 //至於怎樣調用程序的資源文件,查找了很多資料后終於得到解決,具體方法看下面的代碼。 Graphics g = panel游戲區.CreateGraphics(); g.InterpolationMode = InterpolationMode.HighQualityBicubic;//高質量顯示圖片 int x = panel游戲區.Width / m, y = panel游戲區.Height / m;//x,y分別為單個方塊寬和高 string temp = color.ToString().Substring(7).Replace("]", "");//color的顏色值轉換為字符串后形如:color[Red],本句代碼執行后的結果為Red //string picturePath = Application.StartupPath + @"\球\" + temp + ".png";//到程序目錄中查找指定顏色球的路徑,如:C:\Documents and Settings\Administrator\桌面\彩色連珠\bin\Debug\球\Red.png System.Resources.ResourceManager manager = new System.Resources.ResourceManager("彩色連珠.Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly()); //上面一句話是實例化一個ResourceManager,這里一定要注意baseName的寫法,為:命名空間+文件夾名+資源文件名(不帶后綴名),不知道怎么寫的可以到“Resources.Designer.cs”這個文件里去找 //用這個寫法的目的是為了方便根據資源文件名來查找,如果不需要查找的畫則比較簡單,如下: //首先添加以下引用:using 彩色連珠.Properties;然后直接寫:Resources.Red就可以獲取資源文件了。 g.DrawImage((Bitmap)manager.GetObject(temp), x * j + dx, y * i + dx, x - dx - dx, y - dx - dx);//將圖片畫在游戲區 ball[i, j] = color;//同時更新ball數組 //g.FillEllipse(new SolidBrush(color),x*j+5,y*i+5,x-10,y-10);//如果是直接用系統函數畫圓的畫就用這句話 }
清空某個方格的代碼:
/// <summary> /// 用背景色填充指定方格,以達到清除的目的 /// </summary> /// <param name="i">行</param> /// <param name="j">列</param> private void clearBall(int i,int j) { Graphics g = panel游戲區.CreateGraphics(); int x = panel游戲區.Width / m, y = panel游戲區.Height / m; g.FillRectangle(new SolidBrush(panel游戲區.BackColor), x * j + 2, y * i + 2, x - 4, y - 4); ball[i, j]= panel游戲區.BackColor; }
隨機出球的函數:
/// <summary> /// 游戲開始時隨機出球,位置隨機,顏色也隨機(用於沒有下一組提示的時候) /// </summary> private void drawBallRandom() { if (!checkOver()) { Random random = new Random(); bool flag = true; while (flag) { int i = random.Next(0, 10); int j = random.Next(0, 10); if (ball[i, j] == panel游戲區.BackColor) { flag = false; int c = random.Next(0, colorNum); //MessageBox.Show(i + "," + j + ":" + color[c].ToString()); drawBall(i, j, color[c]); checkSuccess(i, j);//出子后要判斷是否有可以消的球 } } } }
產生下一組隨機球:
/// <summary> /// 產生下一組隨機球 /// </summary> private void makeNextColor() { Graphics g = pictureBox下一組.CreateGraphics(); g.Clear(pictureBox下一組.BackColor); Random random = new Random(); for (int i = 0; i < 3; i++) { nextColor[i] = random.Next(0,colorNum); drawBall(i,nextColor[i]); } }
panel的paint事件,作用在下面的注釋已經寫明了,有些函數是在后面定義的,這里暫時還沒寫出來:
//游戲區的重繪事件,這個事件的作用主要有2個:一個是讓游戲第一次運行時畫方格線 //以及隨機出5個子(這些代碼不能放在Form_Loaded事件里,因為窗體第一次生成會觸發 //Paint事件進而覆蓋原圖),第二個作用是解決當窗體最小化或改變大小時繪圖區一片空 //白的問題,解決的思路就是方格線和球全部重繪。 //用“Bitmap bit= new Bitmap(x,y);Graphics g=Graphics.FromImage(bit);”的方法 //不會出現最小化變空白的現象,但每次畫完圖后都必須刷新,因此閃屏現象嚴重。 private void panel游戲區_Paint(object sender, PaintEventArgs e) { drawLine();//畫方格線 for (int i = 0; i < m; i++) for (int j = 0; j < m; j++) if (ball[i, j] != panel游戲區.BackColor) { drawBall(i,j,ball[i,j]); //如果該位置的顏色不是背景色(即沒有球)則按照指定顏色畫球 } makeNextColor();//防止窗口最小化后還原一片空白 if (isFirstRun)//如果是第一次運行 { for (int i = 0; i < 5; i++) drawBallRandom();//隨機出5個球 makeNextColor(); isFirstRun = false; } }
鼠標的單擊事件,游戲的主要驅動都來自這個事件:
private void panel游戲區_MouseClick(object sender, MouseEventArgs e)//游戲區的鼠標單擊事件 { timer縮放.Enabled = false;//結束球的縮放 int x = panel游戲區.Width / m, y = panel游戲區.Height / m; if (ball[e.Y / y, e.X / x] != panel游戲區.BackColor)//如果單擊的是球 { dx = 5;//讓dx恢復到默認值 if(move_i>=0) drawBall(move_i, move_j, ball[move_i, move_j], dx); //在新的球縮放時將上次點的球(如果有)重置為默認大小(因為球動態變換大 //小,所以停止縮放時可能不是默認大小,這句話是重新畫一個默認大小的球) move_i = e.Y / y; move_j = e.X / x; timer縮放.Enabled = true;//讓單擊過的球開始動態變換大小 } else if (move_i >= 0 && move_j >= 0)//如果單擊的是空白處,且有一個即將移動的球 { bool[,] isHaveBall = new bool[m, m];//保存棋盤上每個位置是否有球的信息 for (int i = 0; i < m; i++) for (int j = 0; j < m; j++) { if (ball[i, j] == panel游戲區.BackColor) isHaveBall[i, j] = false; else isHaveBall[i, j] = true; } int end_i = e.Y / y, end_j = e.X / x;//目標行與列 Search s = new Search(isHaveBall, m, move_i, move_j, end_i, end_j);//實例化一個查找類 path = s.start();//開始查找 if (path[0][0] != 0)//如果查找成功 { path_idx = 2;//path數組第一組數據是長度,第二組數據是起點,所以要從第三組數據開始 timer移動.Enabled = true; //string t = ""; //下面注釋的代碼用來查看文本形式的路徑信息,僅供程序員調試看,玩家不需要管 //for (int i = 1; i <= path[0][0]; i++) //{ // t += path[i][0] + "," + path[i][1] + " "; //} // MessageBox.Show(t); } } }
檢查是否有5個(或更多)顏色相同的球在一起,並計算分數,5個子10分,6個子20分,以此類推:
/// <summary> /// 檢查是否有5個顏色相同的球連在一起 /// </summary> /// <param name="h">起始查找位置的行</param> /// <param name="lie">起始查找位置的列,為什么不用字母“l”呢?因為和數字“1”長得太像了!(汗)</param> /// <returns>檢查的結果</returns> private bool checkSuccess(int h,int lie) { bool f = false; int[][] res = new int[4][];//4行N列的二維數組,每一行用來存放一個方向檢查的結果 int i = 1; int j = 1; int sum=1; for (i = 1; lie-i>=0&&ball[h, lie - i] == ball[h, lie]; i++)//往左 sum++; for (j = 1; lie+j<m&&ball[h, lie + j] == ball[h, lie]; j++)//往右 sum++; if (sum >= 5) res[0] = new int[] { sum, i - 1, j - 1 };//從左往右,第一個數存放sum的值,后面2個數分別存放左和右的索引 else res[0] = new int[] { 0 }; sum = 1; for (i = 1; h-i>=0&&ball[h - i, lie] == ball[h, lie]; i++)//往上 sum++; for (j = 1; h+j<m&&ball[h + j, lie] == ball[h, lie]; j++)//往下 sum++; if (sum >= 5) res[1] = new int[] { sum, i - 1, j - 1 };//從上往下 else res[1] = new int[] { 0}; sum = 1; for (i = 1; h-i>=0&&lie-i>=0&&ball[h-i, lie - i] == ball[h, lie]; i++)//往左上 sum++; for (j = 1; h+j<m&&lie+j<m&&ball[h+j, lie + j] == ball[h, lie]; j++)//往右下 sum++; if (sum >= 5) res[2] = new int[] { sum, i - 1, j - 1 };//從左上往右下 else res[2] = new int[] { 0 }; sum = 1; for (i = 1; h+i<m&&lie-i>=0&&ball[h+i, lie - i] == ball[h, lie]; i++)//往左下 sum++; for (j = 1; h-j>=0&&lie+j<m&&ball[h-j, lie + j] == ball[h, lie]; j++)//往右上 sum++; if (sum >= 5) res[3] = new int[] { sum, i - 1, j - 1 };//從左下往右上 else res[3] = new int[] { 0 }; for (int p = 0; p < 4;p++ ) { if (res[p][0] !=0) { for (int q = -res[p][1]; q <= res[p][2]; q++) { switch (p) { case 0: clearBall(h, lie+q); break; case 1: clearBall(h+q, lie); break; case 2: clearBall(h + q, lie + q); break; case 3: clearBall(h - q, lie + q); break; } } score += 10*(res[p][0]-4);//5個棋子加10分,6個棋子加20分,依次類推 label分數.Text = "分數:"+score;//分數加上去 f = true; } } return f; }
很重要的查找算法,具體看注釋,太晚了不願打字了,有不明白的可以留言一起討論,我把整個查找算法單獨放在一個名為Search.cs的類里面:
using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; namespace 彩色連珠 { class Search//查找函數 { bool[,] ball;//一個二維數組,記錄每行每列是否有球 int m, start_i, start_j, end_i, end_j;//分別表示行數(或列數),需要移動的球的行及列,目標位置的行及列 /// <summary> /// 查找方法的構造函數 /// </summary> /// <param name="ball">存放每個游戲方格上是否有球的bool二維數組</param> /// <param name="m">方格的行數(或列數)</param> /// <param name="start_i">需要移動的球的行</param> /// <param name="start_j">需要移動的球的列</param> /// <param name="end_i">目標位置的行</param> /// <param name="end_j">目標位置的列</param> public Search(bool[,] ball, int m, int start_i, int start_j, int end_i, int end_j) { this.ball = ball; this.m = m; this.start_i = start_i; this.start_j = start_j; this.end_i = end_i; this.end_j = end_j; } class Open //Open表 { public Open(int child_i, int child_j, int parent_i, int parent_j) { this.child_i = child_i; this.child_j = child_j; this.parent_i = parent_i; this.parent_j = parent_j; } public int child_i; public int child_j; public int parent_i; public int parent_j; } class Closed:Open //Closed表,繼承自Open表,只多了一個id號 { public Closed(int id, Open o) :base(o.child_i,o.child_j,o.parent_i,o.parent_j) { this.id = id; } public int id; } class Queue//隊列 { const int maxsize = 1000; Open[] queue = new Open[maxsize]; int front; int rear; public void iniQueue()//初始化 { this.front = this.rear = 0; } public void add(Open x)//進棧 { if ((this.rear + 1) % maxsize != this.front) { this.rear = (this.rear + 1) % maxsize; this.queue[this.rear] = x; } } public Open delete()//出棧 { Open o = this.queue[(this.front + 1) % maxsize]; if (this.rear != this.front) this.front = (this.front + 1) % maxsize; return o; } public bool isEmpty()//判斷是否為空 { if (this.front == this.rear) return true; else return false; } } public int[][] start()//開始查找 { int[][] result=new int[1000][];//記錄結果的二維數組,如果查找失敗,第一組數據放入(0,0),否則放入(1,1),從第二組數據開始存放數據 int n = 0;//記錄Closed表中的個數 Queue q = new Queue();//實例化一個Open表的隊列Queue q.iniQueue(); Closed[] c = new Closed[100000]; q.add(new Open(start_i, start_j, -1, -1));//因第一個點不存在父節點,故用-1來表示NULL bool flag = false;//判斷是否退出while循環的標志 while (!flag) { if (q.isEmpty())//如果堆棧為空,退出循環 flag = true; else { Open temp = q.delete();//從Open表取出隊頭元素 //MessageBox.Show(temp.child_i + "," + temp.child_j); c[n] = new Closed(n, temp); if (c[n].child_i == end_i && c[n].child_j == end_j) flag = true; else//按照左上右下的順序查找 { if (c[n].child_j - 1 >= 0 && !ball[c[n].child_i, c[n].child_j - 1])//左 { q.add(new Open(c[n].child_i, c[n].child_j - 1, c[n].child_i, c[n].child_j)); ball[c[n].child_i, c[n].child_j - 1]=true; } if (c[n].child_i - 1 >= 0 && !ball[c[n].child_i - 1, c[n].child_j])//上 { q.add(new Open(c[n].child_i - 1, c[n].child_j, c[n].child_i, c[n].child_j));ball[c[n].child_i - 1, c[n].child_j]=true; } if (c[n].child_j + 1 < m && !ball[c[n].child_i, c[n].child_j + 1])//右 { q.add(new Open(c[n].child_i, c[n].child_j + 1, c[n].child_i, c[n].child_j));ball[c[n].child_i, c[n].child_j + 1]=true; } if (c[n].child_i + 1 < m && !ball[c[n].child_i + 1, c[n].child_j])//下 { q.add(new Open(c[n].child_i + 1, c[n].child_j, c[n].child_i, c[n].child_j));ball[c[n].child_i + 1, c[n].child_j]=true; } } n++; } } if (c[n - 1].child_i == end_i && c[n - 1].child_j == end_j)//表示如果查找成功 { int sum = 0; string res = end_i + "," + end_j; result[sum]=new int[]{end_i,end_j}; int b_i = end_i, b_j = end_j; for (int i = n - 1; i > 0; i--) { if (c[i].child_i == b_i && c[i].child_j == b_j) { sum++; result[sum] = new int[] { c[i].parent_i,c[i].parent_j}; res = c[i].parent_i + "," + c[i].parent_j + " " + res; b_i = c[i].parent_i; b_j = c[i].parent_j; } } sum++; result[sum] = new int[] { sum,sum};//記錄需移動的次數 for (int i = 0; i < (sum + 1) / 2; i++)//數組倒序 { int[] temp = result[i]; result[i] = result[sum - i]; result[sum - i] = temp; } // MessageBox.Show("查找結果為:\n" + res, "查找成功", MessageBoxButtons.OK, MessageBoxIcon.Information); return result; } else { result[0] = new int[] { 0,0}; return result; } } } }
下面的這個名為SaveScore.cs的類是用來保存成績和讀取成績的,因為要避免用戶手動查看或修改成績文件,所以采用了簡單的加密算法,具體代碼如下:
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Windows.Forms; using System.Security.Cryptography; namespace 彩色連珠 { class SaveScore { public SaveScore() { } public const string scoreFile = "score.dat"; /// <summary> /// 保存成績 /// </summary> /// <param name="name">姓名</param> /// <param name="score">成績</param> public static void saveScore(string name, int score) { string file = Application.StartupPath + "\\" + scoreFile; if (!File.Exists(file)) File.CreateText(file); StreamReader sr = new StreamReader(file); List<string> names = new List<string> { }; List<int> scores = new List<int> { }; string temp = ""; try { while ((temp = sr.ReadLine()) != null) { temp = SimplyDecrypt(temp); names.Add(temp.Split(':')[0]); scores.Add(Convert.ToInt32(temp.Split(':')[1])); } } catch { } sr.Close(); int max = score;//假設傳過來的score是最高分 int i = 0; for (; i < scores.Count; i++) if (scores[i] < max)//一旦發現有比max小的數,跳出循環 break; scores.Insert(i, score);//在指定位置插入成績和姓名 names.Insert(i, name); StreamWriter sw = new StreamWriter(file); for (int j = 0; j < names.Count; j++) sw.WriteLine(SimplyEncrypt(names[j] + ":" + scores[j]));//寫入文件 sw.Close(); } public static int redMax()//讀取最高分 { int max = 0; try { string file = Application.StartupPath + "\\" + scoreFile; if (File.Exists(file)) { StreamReader sr = new StreamReader(file); max =Convert.ToInt32(SimplyDecrypt(sr.ReadLine()).Split(':')[1]); sr.Close(); } } catch { } return max; } public static string readScore()//讀取所有成績 { string result = "成績排行榜如下:\n"; try { string file = Application.StartupPath + "\\" + scoreFile; if (File.Exists(file)) { string temp = ""; StreamReader sr = new StreamReader(file); int i = 1; while ((temp = sr.ReadLine()) != null) { temp = SimplyDecrypt(temp); result += "第" + i + "名:" + temp.Split(':')[0] + ",成績:" + temp.Split(':')[1] + "\n"; i++; } sr.Close(); } } catch{ } return result; } private const string Secret = "timizhuo"; // Secret的長度必須為8位!! private const string IV = "xiaomingtongxue"; // IV的長度必須在8位以上! private static string SimplyEncrypt(string rawContent)//加密 { try { var des = new DESCryptoServiceProvider(); var encryptor = des.CreateEncryptor(Encoding.ASCII.GetBytes(Secret), Encoding.ASCII.GetBytes(IV)); var dataToEnc = Encoding.UTF8.GetBytes(rawContent); var resultStr = encryptor.TransformFinalBlock(dataToEnc, 0, dataToEnc.Length); return Convert.ToBase64String(resultStr); } catch { return ""; } } private static string SimplyDecrypt(string encryptedContent)//解密 { try { var result = string.Empty; var des = new DESCryptoServiceProvider(); var decryptor = des.CreateDecryptor(Encoding.ASCII.GetBytes(Secret), Encoding.ASCII.GetBytes(IV)); var dataToDec = Convert.FromBase64String(encryptedContent); var resultBytes = decryptor.TransformFinalBlock(dataToDec, 0, dataToDec.Length); result = Encoding.UTF8.GetString(resultBytes); return result; } catch { return ""; } } } }
整個程序主要就三個類,除了上面寫明的2個外,最后一個就是Form主窗口.cs,程序大部分代碼都在這里,所以這里全部一起貼一下,個人覺得綠化率這么高,應該非常容易看懂吧,呵呵。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Collections; using 彩色連珠.Properties; namespace 彩色連珠 { public partial class Form主窗口 : Form { public Form主窗口() { InitializeComponent(); } public int m = 10;//方格的行數與列數 public Color[,] ball= new Color[100, 100];//一個二維數組,用來存放整個棋盤每個位置的顏色信息 public Color[] color = { Color.Red,Color.Green,Color.Blue,Color.Brown,Color.Yellow,Color.LightBlue,Color.Violet,Color.White,Color.Black};//所有球可能的顏色值 //public string[] color = { "紅色","綠色","藍色","褐色","黃色","淺藍色","紫色","白色","黑色"}; public int colorNum = 9; public int move_i=-1, move_j=-1;//用來存放即將移動的球的行和列 public int[][] path;//用來存放每次查找成功后的路徑信息 public int path_idx=2;//path路徑信息的索引,之所以要從2開始,是因為第0個是長度,第1個存放的是起點坐標 public int score = 0;//分數 int dx = 5;//僅用在對球進行縮放時記錄縮放的程度,默認設為5 public bool isFirstRun = true;//判斷游戲是否為第一次運行的標志,當第一次運行時,在Paint事件中隨機出5個球 public int[] nextColor = new int[3];//下一組顏色信息 private void Form主窗口_Load(object sender, EventArgs e) { startGame(); readMaxScore(); } //游戲區的重繪事件,這個事件的作用主要有2個:一個是讓游戲第一次運行時畫方格線 //以及隨機出5個子(這些代碼不能放在Form_Loaded事件里,因為窗體第一次生成會觸發 //Paint事件進而覆蓋原圖),第二個作用是解決當窗體最小化或改變大小時繪圖區一片空 //白的問題,解決的思路就是方格線和球全部重繪。 //用“Bitmap bit= new Bitmap(x,y);Graphics g=Graphics.FromImage(bit);”的方法 //不會出現最小化變空白的現象,但每次畫完圖后都必須刷新,因此閃屏現象嚴重。 private void panel游戲區_Paint(object sender, PaintEventArgs e) { drawLine();//畫方格線 for (int i = 0; i < m; i++) for (int j = 0; j < m; j++) if (ball[i, j] != panel游戲區.BackColor) { drawBall(i,j,ball[i,j]); //如果該位置的顏色不是背景色(即沒有球)則按照指定顏色畫球 } makeNextColor();//防止窗口最小化后還原一片空白 if (isFirstRun)//如果是第一次運行 { for (int i = 0; i < 5; i++) drawBallRandom();//隨機出5個球 makeNextColor(); isFirstRun = false; } } /// <summary> /// 開始游戲的准備工作 /// </summary> private void startGame() { panel游戲區.BackColor = Color.LightGray;//首次運行,給游戲區填充灰色背景色 Graphics g = panel游戲區.CreateGraphics(); g.Clear(panel游戲區.BackColor);//用背景色清空 drawLine();//畫方格線 for (int i = 0; i < m; i++) for (int j = 0; j < m; j++) ball[i, j] = panel游戲區.BackColor;//將ball數組全部用背景色來標記 score = 0; label分數.Text = "分數:0";//分數置0 } private void drawLine()//畫方格線的函數 { Graphics g = panel游戲區.CreateGraphics(); for (int i = 0; i < m; i++) { g.DrawLine(new Pen(Color.Black), 0, panel游戲區.Height / m * i, panel游戲區.Width, panel游戲區.Height / m * i); g.DrawLine(new Pen(Color.Black), panel游戲區.Width / m * i, 0, panel游戲區.Width / m * i, panel游戲區.Height); } } /// <summary> /// 畫球的函數 /// </summary> /// <param name="i">行數</param> /// <param name="j">列數</param> /// <param name="color">要畫的球的顏色</param> /// <param name="dx">指定球比方格要小的像素個數</param> private void drawBall(int i, int j, Color color,int dx) { //關於圖片:如果直接將這些不同顏色的球的圖片放在程序根目錄文件夾來操作比較簡單, //但是缺點就是老是要跟一個文件夾,不方便,所以將所有圖片放入程序的資源文件 //至於怎樣調用程序的資源文件,查找了很多資料后終於得到解決,具體方法看下面的代碼。 Graphics g = panel游戲區.CreateGraphics(); g.InterpolationMode = InterpolationMode.HighQualityBicubic;//高質量顯示圖片 int x = panel游戲區.Width / m, y = panel游戲區.Height / m;//x,y分別為單個方塊寬和高 string temp = color.ToString().Substring(7).Replace("]", "");//color的顏色值轉換為字符串后形如:color[Red],本句代碼執行后的結果為Red //string picturePath = Application.StartupPath + @"\球\" + temp + ".png";//到程序目錄中查找指定顏色球的路徑,如:C:\Documents and Settings\Administrator\桌面\彩色連珠\bin\Debug\球\Red.png System.Resources.ResourceManager manager = new System.Resources.ResourceManager("彩色連珠.Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly()); //上面一句話是實例化一個ResourceManager,這里一定要注意baseName的寫法,為:命名空間+文件夾名+資源文件名(不帶后綴名),不知道怎么寫的可以到“Resources.Designer.cs”這個文件里去找 //用這個寫法的目的是為了方便根據資源文件名來查找,如果不需要查找的畫則比較簡單,如下: //首先添加以下引用:using 彩色連珠.Properties;然后直接寫:Resources.Red就可以獲取資源文件了。 g.DrawImage((Bitmap)manager.GetObject(temp), x * j + dx, y * i + dx, x - dx - dx, y - dx - dx);//將圖片畫在游戲區 ball[i, j] = color;//同時更新ball數組 //g.FillEllipse(new SolidBrush(color),x*j+5,y*i+5,x-10,y-10);//如果是直接用系統函數畫圓的畫就用這句話 } private void drawBall(int i, int j, Color color)//重載上面的函數,將dx默認為5 { drawBall(i,j,color,5); } /// <summary> /// 在下一組提示區畫指定顏色的球 /// </summary> /// <param name="i">位置索引,因為每次出3個球,所以i只可能是0、1、2三種情況</param> /// <param name="colorIdx">顏色索引,實際上就是要畫的球的顏色信息</param> private void drawBall(int i, int colorIdx) { Graphics g = pictureBox下一組.CreateGraphics(); g.InterpolationMode = InterpolationMode.HighQualityBicubic;//高質量顯示圖片 string temp = color[colorIdx].ToString().Substring(7).Replace("]", "");//color的顏色值轉換為字符串后形如:color[Red],本句代碼執行后的結果為Red System.Resources.ResourceManager manager = new System.Resources.ResourceManager("彩色連珠.Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly()); g.DrawImage((Bitmap)manager.GetObject(temp), i * 55 + 10, 3, 50, 50);//將圖片畫在下一組的地方 } /// <summary> /// 用背景色填充指定方格,以達到清除的目的 /// </summary> /// <param name="i">行</param> /// <param name="j">列</param> private void clearBall(int i,int j) { Graphics g = panel游戲區.CreateGraphics(); int x = panel游戲區.Width / m, y = panel游戲區.Height / m; g.FillRectangle(new SolidBrush(panel游戲區.BackColor), x * j + 2, y * i + 2, x - 4, y - 4); ball[i, j]= panel游戲區.BackColor; } /// <summary> /// 游戲開始時隨機出球,位置隨機,顏色也隨機(用於沒有下一組提示的時候) /// </summary> private void drawBallRandom() { if (!checkOver()) { Random random = new Random(); bool flag = true; while (flag) { int i = random.Next(0, 10); int j = random.Next(0, 10); if (ball[i, j] == panel游戲區.BackColor) { flag = false; int c = random.Next(0, colorNum); //MessageBox.Show(i + "," + j + ":" + color[c].ToString()); drawBall(i, j, color[c]); checkSuccess(i, j);//出子后要判斷是否有可以消的球 } } } } /// <summary> /// 隨機在某個位置畫指定顏色的球,位置隨機,顏色不隨機 /// </summary> /// <param name="colorIdx">顏色索引</param> private void drawBallRandom(int colorIdx) { if (!checkOver()) { Random random = new Random(); bool flag = true; while (flag) { int i = random.Next(0, 10); int j = random.Next(0, 10); if (ball[i, j] == panel游戲區.BackColor) { flag = false; drawBall(i, j, color[colorIdx]); checkSuccess(i, j); } } } } /// <summary> /// 產生下一組隨機球 /// </summary> private void makeNextColor() { Graphics g = pictureBox下一組.CreateGraphics(); g.Clear(pictureBox下一組.BackColor); Random random = new Random(); for (int i = 0; i < 3; i++) { nextColor[i] = random.Next(0,colorNum); drawBall(i,nextColor[i]); } } /// <summary> /// 檢查游戲是否結束 /// </summary> /// <returns>返回真假</returns> private bool checkOver() { bool isFull = true; for (int i = 0; i < m; i++) for (int j = 0; j < m; j++) { if (ball[i, j] == panel游戲區.BackColor) isFull = false; } return isFull; } private void panel游戲區_MouseClick(object sender, MouseEventArgs e)//游戲區的鼠標單擊事件 { timer縮放.Enabled = false;//結束球的縮放 int x = panel游戲區.Width / m, y = panel游戲區.Height / m; if (ball[e.Y / y, e.X / x] != panel游戲區.BackColor)//如果單擊的是球 { dx = 5;//讓dx恢復到默認值 if(move_i>=0) drawBall(move_i, move_j, ball[move_i, move_j], dx); //在新的球縮放時將上次點的球(如果有)重置為默認大小(因為球動態變換大 //小,所以停止縮放時可能不是默認大小,這句話是重新畫一個默認大小的球) move_i = e.Y / y; move_j = e.X / x; timer縮放.Enabled = true;//讓單擊過的球開始動態變換大小 } else if (move_i >= 0 && move_j >= 0)//如果單擊的是空白處,且有一個即將移動的球 { bool[,] isHaveBall = new bool[m, m];//保存棋盤上每個位置是否有球的信息 for (int i = 0; i < m; i++) for (int j = 0; j < m; j++) { if (ball[i, j] == panel游戲區.BackColor) isHaveBall[i, j] = false; else isHaveBall[i, j] = true; } int end_i = e.Y / y, end_j = e.X / x;//目標行與列 Search s = new Search(isHaveBall, m, move_i, move_j, end_i, end_j);//實例化一個查找類 path = s.start();//開始查找 if (path[0][0] != 0)//如果查找成功 { path_idx = 2;//path數組第一組數據是長度,第二組數據是起點,所以要從第三組數據開始 timer移動.Enabled = true; //string t = ""; //下面注釋的代碼用來查看文本形式的路徑信息,僅供程序員調試看,玩家不需要管 //for (int i = 1; i <= path[0][0]; i++) //{ // t += path[i][0] + "," + path[i][1] + " "; //} // MessageBox.Show(t); } } } /// <summary> /// 檢查是否有5個顏色相同的球連在一起 /// </summary> /// <param name="h">起始查找位置的行</param> /// <param name="lie">起始查找位置的列,為什么不用字母“l”呢?因為和數字“1”長得太像了!(汗)</param> /// <returns>檢查的結果</returns> private bool checkSuccess(int h,int lie) { bool f = false; int[][] res = new int[4][];//4行N列的二維數組,每一行用來存放一個方向檢查的結果 int i = 1; int j = 1; int sum=1; for (i = 1; lie-i>=0&&ball[h, lie - i] == ball[h, lie]; i++)//往左 sum++; for (j = 1; lie+j<m&&ball[h, lie + j] == ball[h, lie]; j++)//往右 sum++; if (sum >= 5) res[0] = new int[] { sum, i - 1, j - 1 };//從左往右,第一個數存放sum的值,后面2個數分別存放左和右的索引 else res[0] = new int[] { 0 }; sum = 1; for (i = 1; h-i>=0&&ball[h - i, lie] == ball[h, lie]; i++)//往上 sum++; for (j = 1; h+j<m&&ball[h + j, lie] == ball[h, lie]; j++)//往下 sum++; if (sum >= 5) res[1] = new int[] { sum, i - 1, j - 1 };//從上往下 else res[1] = new int[] { 0}; sum = 1; for (i = 1; h-i>=0&&lie-i>=0&&ball[h-i, lie - i] == ball[h, lie]; i++)//往左上 sum++; for (j = 1; h+j<m&&lie+j<m&&ball[h+j, lie + j] == ball[h, lie]; j++)//往右下 sum++; if (sum >= 5) res[2] = new int[] { sum, i - 1, j - 1 };//從左上往右下 else res[2] = new int[] { 0 }; sum = 1; for (i = 1; h+i<m&&lie-i>=0&&ball[h+i, lie - i] == ball[h, lie]; i++)//往左下 sum++; for (j = 1; h-j>=0&&lie+j<m&&ball[h-j, lie + j] == ball[h, lie]; j++)//往右上 sum++; if (sum >= 5) res[3] = new int[] { sum, i - 1, j - 1 };//從左下往右上 else res[3] = new int[] { 0 }; for (int p = 0; p < 4;p++ ) { if (res[p][0] !=0) { for (int q = -res[p][1]; q <= res[p][2]; q++) { switch (p) { case 0: clearBall(h, lie+q); break; case 1: clearBall(h+q, lie); break; case 2: clearBall(h + q, lie + q); break; case 3: clearBall(h - q, lie + q); break; } } score += 10*(res[p][0]-4);//5個棋子加10分,6個棋子加20分,依次類推 label分數.Text = "分數:"+score;//分數加上去 f = true; } } return f; } private void timer移動_Tick(object sender, EventArgs e)//這部分代碼是為了實現球的動態移動位置效果 { //注意:這里一定要注意要用上一個點的顏色來填充,而不是起始點的顏色, //因為每畫完一個點之后,它的顏色已經被填充為背景色了 drawBall(path[path_idx][0], path[path_idx][1],ball[path[path_idx-1][0], path[path_idx-1][1]]);//在指定位置畫置頂顏色的球 clearBall(path[path_idx-1][0], path[path_idx-1][1]);//畫過的方格要清空 path_idx++; if (path_idx > path[0][0]) { timer移動.Enabled = false; move_i = move_j = -1; if (!checkSuccess(path[path_idx - 1][0], path[path_idx - 1][1]))//如果新移動一個球后沒有五子連線的,再隨機出3個球 { for (int i = 0; i < 3&&!checkOver(); i++)//當游戲沒有結束時隨機出3個球 drawBallRandom(nextColor[i]); if (checkOver()) { Form保存成績 f = new Form保存成績(score,this); f.ShowDialog(); } makeNextColor(); } } } bool isGrow = false;//真表示球變大,假表示球變小 private void timer縮放_Tick(object sender, EventArgs e)//這個計時器僅用來實現點擊某一個球后動態變換大小的效果 { //注意:dx是表示球的起始坐標離方格的距離,所以dx越大表示球越小 if (dx >= 12) isGrow = true;//球變到最小后,讓它開始變大 if (dx <= 5) isGrow = false;//球變到最大后,讓它開始變小 if (isGrow) dx-=2;//讓球變大一點 else dx+=2;//讓球變小一點 Color temp=ball[move_i,move_j]; clearBall(move_i, move_j);//先清空 ball[move_i, move_j] = temp; drawBall(move_i,move_j,temp,dx);//然后重新畫球 } private void button開始_Click(object sender, EventArgs e) { startGame(); for (int i = 0; i < 5; i++) drawBallRandom();//隨機出5個球 makeNextColor(); } public void readMaxScore()//讀取最高分 { int max = SaveScore.redMax(); label最高分.Text = "最高分:"+max; } private void 重新開始ToolStripMenuItem_Click_1(object sender, EventArgs e) { button開始_Click(null, null); } private void 退出ToolStripMenuItem_Click_1(object sender, EventArgs e) { Application.Exit(); } private void 查看成績ToolStripMenuItem_Click_1(object sender, EventArgs e) { MessageBox.Show(SaveScore.readScore(), "成績排行榜", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void 幫助ToolStripMenuItem1_Click_1(object sender, EventArgs e) { MessageBox.Show("如有問題,請至新浪微博@小茗同學(http://weibo.com/liuxianan)", "幫助", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void 關於ToolStripMenuItem_Click_1(object sender, EventArgs e) { MessageBox.Show("游戲名稱:彩色連珠 v1.0\n作者:小茗同學\n微博:http://weibo.com/liuxianan\n2012年7月18日", "彩色連珠", MessageBoxButtons.OK, MessageBoxIcon.Information); } } }
好了,整個小游戲到此為止,最后留一下個人的聯系方式,有需要交流討論的可以到我新浪微博來:http://weibo.com/liuxianan
源代碼下載:
游戲exe可執行程序下載:
http://vdisk.weibo.com/s/8X6Vm/1342628629(已失效)