程式設計 之 C#實現《拼圖游戲》 (下) 原理篇

来源:http://www.cnblogs.com/labixiaohei/archive/2017/04/15/6713761.html
-Advertisement-
Play Games

在程式設計 之 C#實現《拼圖游戲》 (上)中,上傳了各模塊代碼,在本文中將詳細剖析原理,使讀者更容易理解並學習,程式有諸多問題,歡迎指出,共同學習成長! ...


前言:在 http://www.cnblogs.com/labixiaohei/p/6698887.html 程式設計 之 C#實現《拼圖游戲》(上),上傳了各模塊代碼,而在本文中將詳細剖析原理,使讀者更容易理解並學習,程式有諸多問題,歡迎指出,共同學習成長!

正文:

拼圖是一個非常經典的游戲,基本每個人都知道他的玩法,他的開始,運行,結束。那麼,當我們想要做拼圖的時候如何入手呢?答案是:從現實出發,去描述需求(儘量描述為文檔),當我們擁有了全面的需求,就能夠提供可靠的策略,從而在代碼中實現,最終成為作品!

(一)需求:(這個需求書寫較為潦草,為廣大小白定製,按照最最最普通人的思維來,按照參與游戲的流程來)

   1.圖片:我們玩拼圖 最起碼有個圖

   2.切割:拼圖不是一個圖,我們需要把一個整圖它切割成N*N的小圖

   3.打亂:把這N*N的小圖打亂順序,但是要保證通過游戲規則行走能還原回來

   4.判斷:判拼圖成功

   5.交互:我們使用哪一種交互方式,這裡我選擇滑鼠點擊

     6.展示原圖片完整的縮略圖

    以上為基本功能,以下為擴展功能

     7.記錄步數:記錄完成需要多少步

     8.更換圖片:一個圖片玩久了我們是不是可以換一換啊 哈哈

     9.選擇難度:太簡單?不要!3*3搞定了有5*5,5*5搞定了有9*9,舍友挑戰最高難度 3000多步,心疼我的滑鼠TAT

(二)分析:

   有了需求,我們就可以分析如何去實現它(把現實需求映射在電腦中),其中包括:

           1.開發平臺:這裡選擇C#語言

    1.存儲:其中包括我們要存什麼?我們用什麼結構存?我們反觀需求,會發現,有一些需要存儲的資源

      圖片:使用 Image 對象存儲

      單元(原圖片切割後的子圖像集合):自定義結構體 struct Node ,其中包括Image對象用來存儲單元小圖片,和用整形存儲的編號(切割以後,每個小單元都弄個編號,利於檢驗游戲是否完成)。

      各單元(原圖片切割後的子圖像集合):使用二維數組(像拼圖,五子棋,消消樂,連連看,俄羅斯方塊等平面點陣游戲都可以用他來存儲,為什麼?因為長得像嘛!)來存儲

      難度:使用自定義的枚舉類型(簡單and普通and困難)存儲

      步數:整形變數 int Num存儲 

  有了存儲,我們就可以去思考模塊的劃分(正確的邏輯劃分已於擴展,也可以使通信變得更加清晰)並搭建,並實現各模塊涉及到的具體演算法

      首先程式的模塊分為四個:

  邏輯型:

    1.拼圖類:用於描述拼圖

    2.配置類:存儲配置變數

  交互型:

    3.游戲菜單視窗:進行菜單選項

    4.游戲運行視窗:游戲的主要界面

    

 

  1.通過游戲菜單可以操縱配置,如難度或圖片。

  2.運行視窗可以訪問並獲得游戲配置,並利用其對應構造拼圖對象。

  3.用戶通過運行視窗進行交互,間接使拼圖對象調用移動方法,獲得圖案方法

  看代碼的同學,我覺得最有問題的地方,不合理的地方就是把 難度的枚舉類型寫在了拼圖類中,應該寫在配置類中,或單獨成類,讀者們自行更改

 public enum Diff         //游戲難度
        {
            simple,//簡單
            ordinary,//普通
            difficulty//困難
        }

 

我們可以認為,配置類就像數據存儲,而拼圖類呢作為邏輯處理,菜單和運行視窗作為表現用於交互,我承認這種設計不是很合理,但是在問題規模不夠大的時候,過分的考慮設計,會不會使程式變得臃腫?我想一定是有一個度,具體是多少,我不得而知,但我感覺,針對這個程式,實現就好,沉迷設計(套路型),有時得不償失。(個人不成熟的小觀點)

(三)代碼實現:

  說明:本塊重點描述 Puzzle(拼圖)類與游戲運行類的具體實現及實體通訊:

拼圖的構造方法:

    1.賦值 :

public Puzzle(Image Img,int Width, Diff GameDif)// 拼圖的圖片,寬度(解釋:正方形的邊長,單位是像素,名字有歧義,抱歉),游戲的難度

 

 游戲的難度決定你分割的程度,分割的程度,決定你存儲的數組的大小,如簡單對應3行3列,普通對應5行5列,困難對應9行9列

 

 switch(this._gameDif)
            {
                case Diff.simple:    //簡單則單元格數組保存為3*3的二維數組
                    this.N = 3;
                    node=new Node[3,3];
                    break;
                case Diff.ordinary:    //一般則為5*5
                    this.N = 5;
                    node = new Node[5, 5];
                    break;
                case Diff.difficulty:  //困難則為9*9
                    this.N = 9;
                    node = new Node[9, 9];
                    break;
            }

2.分割圖片

            //分割圖片形成各單元保存在數組中
            int Count = 0;
            for (int x = 0; x < this.N; x++)
            {
                for (int y = 0; y < this.N; y++)
                {

                    node[x, y].Img = CaptureImage(this._img, this.Width / this.N, this.Width / this.N, x * (this.Width / this.N), y * (this.Width / this.N));
                    node[x, y].Num = Count;
                    Count++;
                }
            }

 其實對單元數組進行賦值的過程,使用雙層for迴圈對二維數組進行遍歷操作,然後按序賦值編號node[x,y].Num;

 

然後對node[x,y].Img,也就是單元的小圖片賦值,賦值的方法是,C#的圖像的類庫,寫一個截圖方法,使用這個方法,將大圖中對應對位置的對應大小的小圖截取下來,並保存在node[x,y].Img中;

width/N是什麼?是邊長除以行數,也就是間隔嘛,間隔也就是每個單元的邊長嘛!然後起始坐標(X,Y)起始就是在說,隔了幾個單元後,我的位置,

即 :(x,y)=(單元邊長*距離起始X軸相距單元數,單元邊長*距離起始點Y軸相距單元數);

關於此類問題,希望讀者能夠多畫畫圖,然後自然就明白了;

 public  Image CaptureImage(Image fromImage, int width, int height, int spaceX, int spaceY)

主要邏輯:利用DrawImage方法:

            //創建新圖點陣圖   
            Bitmap bitmap = new Bitmap(width, height);
            //創建作圖區域   
            Graphics graphic = Graphics.FromImage(bitmap);
            //截取原圖相應區域寫入作圖區   
            graphic.DrawImage(fromImage, 0, 0, new Rectangle(x, y, width, height), GraphicsUnit.Pixel);
            //從作圖區生成新圖   
            Image saveImage = Image.FromHbitmap(bitmap.GetHbitmap());

分割了以後,我們要做一個特殊處理,因為我們知道,總有那麼一個位置是白的吧?我們預設為最後一個位置,即node[N-1,N-1];

就是寫改成了個白色的圖片,然後四周的邊線都給畫成紅色,已於被人發現,顯著一些,之前的其他單元我也畫了邊線,但是是白色,也是為了在拼圖的觀賞性上得到區分。該代碼不做介紹。

 

3.打亂圖片:

其實就是將二維數組打亂,我們可以採取一些排序打亂方法但是請註意!不是每一種打亂都能夠複原的!

那麼如何做到可行呢?方法理解起來很簡單,就是讓我們的電腦在開局之前,將完整的有序的單元按照規則中提供的行走方式進行無規則,大次數的行走!也就是說這種方法一定能走回去!

先理解,具體打亂方法,在後面講解。

 

移動方法(Move):

拼圖游戲中方格的移動,其實就是兩個相鄰單元的交換,而這兩個單元中,必定存在一個白色單元(即上面提到的node[N-1,N-1]單元,他的編號為N*N-1,建議自己動筆算一算)

所以我們的判斷條件是,如果移動一個方塊,他的上下左右四個方向中,一旦有一個相鄰的是白色單元,即N*N-1號單元,則與其交換。這是基本邏輯,但不包括約束條件,當我們的數組達到邊界的時候,我們就不能對越界數據進行訪問,如當單元為node[0,0]時,你就不能對他上面和右面的數據進行訪問,因為Node[-1,0] Node[0,-1]都會越界,發生異常

移動成功,返回TRUE 

移動失敗,返回FALSE

/// <summary>
        /// 移動坐標(x,y)拼圖單元
        /// </summary>
        /// <param name="x">拼圖單元x坐標</param>
        /// <param name="y">拼圖單元y坐標</param>
        public bool Move(int x,int y)
        {
            //MessageBox.Show(" " + node[2, 2].Num);
            if (x + 1 != N && node[x + 1, y].Num ==  N * N - 1)
            {
                Swap(new Point(x + 1, y), new Point(x, y));
                return true;
            }
            if (y + 1 != N && node[x, y + 1].Num ==  N * N - 1)
            {
                Swap(new Point(x, y + 1), new Point(x, y));
                return true;
            }                
            if (x - 1 != -1 && node[x - 1, y].Num == N * N - 1)
            {
                Swap(new Point(x - 1, y), new Point(x, y));
                return true;
            }   
            if (y - 1 != -1 && node[x, y - 1].Num == N * N - 1)
            {
                Swap(new Point(x, y - 1), new Point(x, y));
                return true;
            }
            return false;
                
        }

  交換方法(Swap):交換數組中兩個元素的位置,該方法不應該被類外訪問,顧設置為private私有許可權

        //交換兩個單元格
        private  void Swap(Point a, Point b)
        {
            Node temp = new Node();
            temp = this.node[a.X, a.Y];
            this.node[a.X, a.Y] = this.node[b.X, b.Y];
            this.node[b.X, b.Y] = temp;
        }

 

打亂方法:

 前面提到,其實就是讓電腦幫著亂走一通,說白了就是大量的調用Move(int X,int y)方法,也就是對空白位置的上下左右四個相鄰的方塊中隨機抽取一個,並把它的坐標傳遞給Move使其進行移動,同樣要進行越界考慮,這樣的操作大量重覆!代碼自己看吧 ,利用隨機數。

   /// <summary>
        /// 打亂拼圖
        /// </summary>
        public void Upset()
        {
            int sum = 100000;
            if (this._gameDif == Diff.simple) sum = 10000;
            //if (this._gameDif == Diff.ordinary) sum = 100000;
            Random ran = new Random();
            for (int i = 0, x = N - 1, y = N - 1; i < sum; i++)
            {
                long tick = DateTime.Now.Ticks;
                ran = new Random((int)(tick & 0xffffffffL) | (int)(tick >> 32)|ran.Next());
                switch (ran.Next(0, 4))
                {
                    case 0:
                        if (x + 1 != N)
                        {
                            Move(x + 1, y);
                            x = x + 1;
                        }
                            
                        break;
                    case 1:
                        if (y + 1 != N)
                        {
                            Move(x, y + 1);
                            y = y + 1;
                        } 
                        break;
                    case 2:
                        if (x - 1 != -1)
                        {
                            Move(x - 1, y);
                            x = x - 1;
                        }      
                        break;
                    case 3:
                        if (y - 1 != -1)
                        {
                            Move(x, y - 1);
                            y = y - 1;
                        }
                        break;
                }

            }
        }

返回圖片的方法:

當時怎麼起了個這樣的鬼名字。。。DisPlay。。。

這個方法與分割方法剛好相背,這個方法其實就是遍曆數組,並將其進行組合,組合的方法很簡單,就是將他們一個一個的按位置畫在一張與原圖相等大小的空白圖紙上!最後提交圖紙,也就是return一個Image;

        public Image Display()
        {
            Bitmap bitmap = new Bitmap(this.Width, this.Width);
            //創建作圖區域   
            Graphics newGra = Graphics.FromImage(bitmap);
            for (int x = 0; x < this.N; x++)
                for (int y = 0; y < this.N; y++)
                    newGra.DrawImage(node[x, y].Img, new Point(x * this.Width / this.N, y * this.Width / this.N));
            return bitmap;
        }

同樣利用的是DrawImage方法,知道如何分割,這個應該很容易理解,自己算一算,在紙上比劃比劃就明白了;

 

判斷方法:

該方法很容易理解,就是按序按序!遍歷所有單元,如果他們的結果中有一個單元的編號

node[x, y].Num 不等於遍歷的序號,那麼說明,該單元不在原有位置上,即整個圖片還沒有完成,我們就可以直接返回假值false
如果所有遍歷結果都正確,我們可認為,圖片已複原,此時返回真值true

public bool Judge() { int count=0; for (int x = 0; x < this.N; x++) { for (int y = 0; y < this.N; y++) { if (this.node[x, y].Num != count) return false; count++; } } return true; }

 

 

游戲運行視窗:即游戲玩耍時用於交互的視窗

這裡只講一個方法:即當接受用戶滑鼠點擊事件時我們應該怎麼處理並作出什麼樣反應

其實說白了就這句難懂:

puzzle.Move(e.X / (puzzle.Width / puzzle.N), e.Y / (puzzle.Width / puzzle.N))

調用了移動方法,移動方塊

橫坐標為:e.X / (puzzle.Width / puzzle.N)
縱坐標為:e.Y / (puzzle.Width / puzzle.N)

我們編程中的整數除法和數學里的除法是不一樣的!比如10/4數學上等於2餘2或者2.5,電腦里直接就是等於2了,只取整數部分


行數=行坐標 / 方塊邊長

列數=列坐標 / 方塊邊長

我們看P1,P2這兩點

P1:40/30*30=1
P2:50/30*30=1

我們會發現同在一個單元格中,無論點擊哪個位置,通過這個演算法都能轉化為
同一個坐標。

(e.x,e.y)為滑鼠點擊事件點擊坐標

 private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
        {
            if (puzzle.Move(e.X / (puzzle.Width / puzzle.N), e.Y / (puzzle.Width / puzzle.N)))
            {
                Num++;
                pictureBox1.Image = puzzle.Display();
                if (puzzle.Judge())
                { 
                    if (MessageBox.Show("恭喜過關", "是否重新玩一把", MessageBoxButtons.OKCancel) == DialogResult.OK)
                    {
                        Num = 0;
                        puzzle.Upset();
                        pictureBox1.Image = puzzle.Display();
                        
                    }
                    else
                    {
                        Num = 0;
                        closefather();
                        this.Close();
                    }

                } 

            }
            NumLabel.Text = Num.ToString();
        }

 

 好,那麼大體的邏輯,程式中最需要思考的演算法已經講完了,還有不太懂的地方,歡迎交流~麽麽噠~

加了點小功能 音樂歷史成績 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 從開始接觸.Net Core到現在已經有將近一年的時間了,今天來做一下相關的學習總結,順便也回憶一下自己這段時間以來的成長。 有一點不得不承認的是,在接觸.Net Core之前,我對於linux系統一點也不瞭解,也未曾有過主動去學習的念頭,在接觸了.Net Core之後才開始慢慢學習linux相關知 ...
  • 轉自:http://blog.163.com/m13864039250_1/blog/static/2138652482015283397609/ 用Fluent API 配置/映射屬性和類型 簡介 通常通過重寫派生DbContext 上的OnModelCreating 方法來訪問Code Firs ...
  • 在一些數據的即時查詢場景中,我們可能需要對輸入信息進行模糊查詢併進行選擇,例如在一些文本輸入場景,如輸入某個站點編碼或者設備編碼,然後獲取符合的列表供用戶選擇的場景,本篇隨筆介紹在DevExpress程式中使用PopupContainerEdit和PopupContainer實現數據展示。 ...
  • Rookey.Frame v1.0經過一年時間的修改及沉澱,穩定版終於問世了,此版本經過上線系統驗證,各個功能點都經過終端用戶驗證並持續優化,主要優化以下幾個方面: 1.性能較原來提升3倍之多 2.修複BUG數達1000+上 3.模塊緩存即時生效無需手動刷新緩存 4.增加可配置的任務調度功能 5.表 ...
  • 一.IIS部署基本問題 將項目部署部署到IIS時,啟動網站常會遇到頁面報錯not found 403 可能原因: 1.應用程式池.Net Framework版本不對,解決方法打開控制面板-->管理工具-->Internet信息服務(IIS)管理器,打開應用程式池選擇項目的應用程式,配置為相應版本; ...
  • net core cli 是快速創建模板項目 1. 安裝CLI 參考: https://www.hanselman.com/blog/dotnetNewAngularAndDotnetNewReact.aspx 視頻參考: https://www.youtube.com/channel/UC_R2R ...
  • row.Table.Columns.Contains( "fieldname ") ...
  • 寫在開頭:看了一些視頻教程,感覺OD為什麼別人學個破解那麼容易,我就那麼難了呢,可能是沒有那麼多時間吧。 解釋:個人見解:所謂記憶體補丁,即:通過修改運行程式的內容,來達到某種目的的操作。修改使用OpenProcess打開,WriteProcessMemory寫入,CloseHandle關閉。部分需要 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...