使用XNA為Windows phone 7開發簡單拼圖遊戲

pamxy發表於2013-11-10

轉自:http://blog.csdn.net/fsafs168/article/details/7747744

使用XNA為Windows phone 7開發簡單拼圖遊戲

引言

這裡是天幕的部落格,今天我們要簡單的學習一下使用XNA4.0平臺開發Windows Phone 7 的拼圖遊戲。這個遊戲把一張圖片分成15個小圖片(每個小圖片擁有一樣的大小),玩家一個一個的移動小圖片,最終還原成原圖。下面是開始遊戲時的畫面:


如果玩家點選螢幕,遊戲就會開始,圖片會被分成15個部分。


在螢幕右下角你能看到一個缺口,這個缺口就是代表玩家可以移動缺口旁邊的圖片到缺口,一個一個的移動,直到解決這個遊戲!

Game Components

如果你開啟我們的解決方案,你會發現有一個GameManager (就在GameManager.cs程式碼檔案裡)你在這個檔案裡不會看到除了XNA自定義程式碼以外太多的程式碼,裡面的方法都是XNA自定義的!那麼為什麼這麼做?我們遊戲的程式碼在哪裡?你再開啟解決方案管理器會發現裡面有一個PuzzleGame 如果你開啟這個類,你會看到PuzzleGame這個類繼承了DrawableGameComponent. (需要自行繪製時通知遊戲的元件),那麼什麼是(Drawable)GameComponent?

為了使你遊戲的程式碼具有更好的獨立性,XNA框架為你提供了GameComponent 類。你能通過這個類重寫覆蓋其父類的方法,來獲得最初的Game類的方法:Initialize Update LoadContent 等等。它並不包括Draw方法,然而你可以通過簡單的繼承DrawableGameComponent (GameComponent的子類)來實現它。

game類加入一個組建只需要簡單的呼叫 Game.AddComponent 例項方法。然後,在遊戲初始化和進行遊戲迴圈的時候,也就是在進入遊戲主函式之前,XNA框架會呼叫GameComponent 類相關的方法,比如說我們的遊戲,它首先呼叫在PuzzleGame類中的Update方法,然後在呼叫GameManager類的Update方法。更重要的是,從GameComponent繼承的類可以實現它的屬性,它可以使用或者不使用GameComponent,也能相應的修改它,使它參與或者不參與遊戲的迴圈!!

現在讓我們詳細的看一下組成我們遊戲的類。

Tile

Tile類是負責維護每一個小圖片資訊的,它的程式碼如下:

   1:     class Tile

   2:     {

   3:         public Vector2 TextureLocationInSpriteSheet;

   4:         /// <summary>

   5:         /// 這裡包含一個數字,數字從1-15它代表了小圖片所在

   6:         /// 的位置。如果它排列有序了的話,就代表玩家勝出了

   7:         /// </summary>

   8:         public int OriginalLocationInPuzzle;

   9:      }

第二個變數OriginalLocationInPuzzle會維護小圖片的位置 ( 比如說小圖片在第一行就是數字1,2,3, 第二行就是4,5,6啊等等),就是為了維護小圖片在遊戲中的位置,最終決定玩家是不是勝出了。第一個變數, TextureLocationInSpriteSheet,會使用一個包含用於顯示本地小圖片的 spritesheet(將一個大圖片分割顯示)你並不知道 spritesheet是什麼?那就接著往下看.

Spritesheet

我們從磁碟載入一個檔案需要一定時間,載入更多的檔案的話需要更多的時間。在我們的遊戲裡,我們需要顯示一個完整的圖片(拼圖遊戲的原圖),然後又要將它分割成15個小圖片進行顯示(遊戲進行時)。所以我們就要在我們的遊戲裡顯示16(!)個圖片,然而他需要一定的時間。於是我們選擇實現一個分割圖片用的spritesheetspritesheet是一個非常有用的技術,當我們需要一個大圖片的許多小圖片時(子圖形),它能減少載入時間!(這樣我們也不用去學什麼PS來把圖片一點一點的分開… …)這個技術在一些遊戲裡非常的有用!在我們的遊戲裡,我們將用Texture2D物件來引用這個圖片。那麼我們應該用什麼去畫這個圖片?我們只需要覆蓋SpriteBatch.Draw方法。更多的內容在後面,現在我們只需要記住 TextureLocationInSpriteSheet能夠分割一個圖片就可以了。

PuzzleGame

讓我們看一下PuzzleGame類中的私有變數

   1:     class PuzzleGame : DrawableGameComponent

   2:     {

   3:         GameState gameState;

   4:  

   5:         SpriteFont font;

   6:         Texture2D fullPicture;

   7:         SpriteBatch spriteBatch;

   8:  

   9:         const int Columns = 3;

  10:         const int Rows = 5;

  11:  

  12:         const int TileWidth = 160;

  13:         const int TileHeight = 160;

  14:  

  15:         Tile[,] tilesArray;

  16:         Tile[,] tilesArrayTemp;

  17:         Random random = new Random();

  18:         public PuzzleGame(Game game) : base(game) { }

gameState儲存了遊戲的狀態它來自於GameState列舉型別(StartScreen,Playing, Winner),開始、遊戲、勝利三個狀態其它的變數主要是為了描述遊戲中3x5個小圖片,它是一個2維陣列儲存的。

LoadTilesToTheirInitialLocation這個方法初始化了小圖片陣列。

   1:         private void LoadTilesToTheirInitialLocation()

   2:         {

   3:              tilesArray = new Tile[Columns,Rows];

   4:              for (int column = 0; column <Columns; column++)

   5:                  for (int row = 0; row <Rows; row++)

   6:                  {

   7:                      Tile t = new Tile();

   8:                     t.TextureLocationInSpriteSheet.X = TileWidth * column;

   9:                     t.TextureLocationInSpriteSheet.Y = TileHeight * row;

  10:                      t.OriginalLocationInPuzzle= row * Columns + column + 1;

  11:                      tilesArray[column, row] =t;

  12:                  }

  13:         }

首先我們例項化小圖片陣列tilesArray,然後經過一個二重for迴圈例項化每一個小圖片Tile,並給它的spritesheet賦值 (之前提到的那個),也就是 OriginalLocationInPuzzle 變數.

 

   1:         public override void Update(GameTime gameTime)

   2:         {

   3:              TouchLocation? tl = null;

   4:              TouchCollection tc =TouchPanel.GetState();

   5:              if (tc.Count > 0 &&tc[0].State == TouchLocationState.Released)

   6:              {

   7:                  tl = tc[0];

   8:              }

   9:  

  10:              switch (gameState)

  11:              {

  12:                  case GameState.StartScreen:

  13:                      if (tl != null)

  14:                      {

  15:                         LoadTilesIntoRandomLocations();

  16:                          gameState =GameState.Playing;

  17:                      }

  18:                      break;

  19:                  case GameState.Playing:

  20:                      if (tl != null)

  21:                      {

  22:                          int hitTileColumn =(int)(tl.Value.Position.X / TileWidth);

  23:                          int hitTileRow =(int)(tl.Value.Position.Y / TileHeight);

  24:                         CheckAndSwap(hitTileColumn, hitTileRow);

  25:                          if(CheckIfPlayerWins())

  26:                              gameState =GameState.Winner;

  27:                      }

  28:                      else if (tl == null)//user has not touched the screen, so don't bother drawing

  29:                          Game.SuppressDraw();

  30:                      break;

  31:                  case GameState.Winner:

  32:                      if (tl != null)

  33:                          gameState =GameState.StartScreen;

  34:                      break;

  35:              }

  36:  

  37:              base.Update(gameTime);

  38:         }

Update 方法裡我們首先檢查玩家是不是點選了螢幕。之後我們檢查遊戲的狀態也就是gameState 變數。

  • 如果我們在開始介面,我們會給每個小圖片隨即一個位置,然後開始遊戲。
  • 如果我們在遊戲介面,我們需要獲得玩家點選螢幕的點的座標,如果玩家點選的是空圖片旁邊的小圖片的話,就移動它。如果這個時候玩家解決了這個拼圖遊戲,我們就跳到勝利介面(進入勝利狀態)。
    • 其中比較有趣的是Game.SuppressDraw 方法。如果使用者沒有點選螢幕,我們會防止呼叫Draw方法,直到下一次呼叫Update方法。所以這個時候螢幕不會更新(直到下次Draw方法呼叫前,螢幕會一直以當前狀態顯示)而且這樣做會省電!(驚喜吧,哈哈)
  • 最後一點,如果在勝利介面使用者點選了螢幕的話,我們會讓遊戲重新開始(回到最初顯示完整圖片的介面)。

   1:         private void LoadTilesIntoRandomLocations()

   2:         {

   3:              tilesArrayTemp = newTile[Columns, Rows];

   4:              for (int column = 0; column <Columns; column++)

   5:                  for (int row = 0; row <Rows; row++)

   6:                  {

   7:                      if (column == Columns - 1&& row == Rows - 1) break;

   8:  

   9:                      int newColumn; // =random.Next(0, Columns);

  10:                      int newRow; // =random.Next(0, Rows);

  11:                      do

  12:                      {

  13:                          newColumn =random.Next(0, Columns);

  14:                          newRow =random.Next(0, Rows);

  15:                      } while(tilesArrayTemp[newColumn, newRow] != null || (newColumn == Columns - 1&& newRow == Rows - 1));

  16:                      tilesArrayTemp[newColumn,newRow] = tilesArray[column, row];

  17:                  }

  18:              tilesArray = tilesArrayTemp;

  19:         }

為了隨即載入我們的小圖片Tile,我們使用了一個臨時的二維陣列(tilesArrayTemp) 並隨機產生數字給所有的小圖片,賦值它們的位置(右下角除外)。此外,我們在右下角留下了一個‘空的’小圖片。這個演算法為每個小圖片賦值了一個新的位置(行、列)。如果這個位置已經被佔了(被剛初始化的小圖片),那麼這個演算法會再次嘗試分配一個位置(這是靠do..while實現的)。注意:這個演算法並不效率,實話說我寧願使用類似於“隨即分配一些數字給小圖片,然後依靠數字大小來確定小圖片的位置”一類的演算法。

   1:         private void CheckAndSwap(int column, int row)

   2:         {

   3:              if (row > 0 &&tilesArray[column, row - 1] == null)

   4:              {

   5:                  tilesArray[column, row - 1] =tilesArray[column, row];

   6:                  tilesArray[column, row] =null;

   7:              }

   8:             else if (column > 0&& tilesArray[column - 1, row] == null)

   9:              {

  10:                  tilesArray[column - 1, row] =tilesArray[column, row];

  11:                  tilesArray[column, row] =null;

  12:              }

  13:              else if (row < Rows - 1&& tilesArray[column, row + 1] == null)

  14:              {

  15:                  tilesArray[column, row + 1] =tilesArray[column, row];

  16:                  tilesArray[column, row] =null;

  17:              }

  18:              else if (column < Columns - 1&& tilesArray[column + 1, row] == null)

  19:              {

  20:                  tilesArray[column + 1, row] =tilesArray[column, row];

  21:                  tilesArray[column, row] =null;

  22:             }

  23:         }

CheckAndSwap 方法的引數column(列)和 row(行)是使用者現在點選的小圖片的位置。在裡面使用了if-else語句判斷了越界比如說點選的是“空”小圖片(空小圖片的值是null),然後確定點選的是不是空圖片的旁邊的圖片,如果是的話交換它們(這也是遊戲規則)。

   1:         private bool CheckIfPlayerWins()

   2:         {

   3:              bool playerWins = true;

   4:  

   5:              for (int column = 0; column <Columns; column++)

   6:                  for (int row = 0; row <Rows; row++)

   7:                  {

   8:                      if (tilesArray[column,row] == null) continue; //if we are at the empty tile, just continue

   9:                      if (tilesArray[column,row].OriginalLocationInPuzzle != row * Columns + column + 1)

  10:                      {

  11:                          playerWins = false;

  12:                          break;

  13:                      }

  14:                  }

  15:  

  16:              return playerWins;

  17:         }

CheckIfPlayerWins 方法是為了確定玩家是不是勝利了,它只是簡單的比較每一張小圖片的LocationInPuzzle屬性,來確定小圖片是不是在最初的位置,如果全部都是那麼返回true,玩家就勝利了。

   1:         private void DrawTiles()

   2:         {

   3:              for (int column = 0; column <Columns; column++)

   4:              {

   5:                  for (int row = 0; row <Rows; row++)

   6:                  {

   7:                      if (tilesArray[column,row] == null) continue;

   8:  

   9:                     spriteBatch.Draw(fullPicture,

  10:                          new Vector2(column *TileWidth, row * TileHeight),

  11:                          newRectangle((int)tilesArray[column, row].TextureLocationInSpriteSheet.X,

  12:                             (int)tilesArray[column, row].TextureLocationInSpriteSheet.Y,

  13:                              TileWidth,TileHeight),

  14:                          Color.White,

  15:                          0f, //rotation

  16:                          Vector2.Zero,//origin

  17:                          1,//scale

  18:                          SpriteEffects.None,

  19:                          0);

  20:                  }

  21:              }

  22:         }

你還記得我們說的spritesheet 嗎?上面這個方法就使用了它,SpriteBatch.Draw方法的第三個引數使用了 Rectangle 來表示我們想要畫出的小圖片的位置。很簡單吧?嘻嘻

因為它們都比較簡單,所以我就不再這裡列出其它的繪圖方法了。在我結束這篇文章之前,我們還需要討論一下其它的內容。

SpriteBatch.DrawLine

XNA框架裡並沒有方法SpriteBatch.DrawLine。那麼,你需要XNA畫線呢?回到文章剛開始的地方,你能看到第二張圖片裡有白色的線。怎麼畫出來的呢?我只是匯入了一個1x1的圖片在我的工程裡(pixel.png)。為了達到畫線的目的,我們調整SpriteBatch.Draw 方法的第二個引數Rectangle 並使用了vertical24根水平線,來畫出我們的子圖形(白色的這個,也就是白線的實現)。

   1:         private void DrawLines()

   2:         {

   3:              // 畫第一條垂直線

   4:              spriteBatch.DrawLine(newVector2(TileWidth - 1, 0), 2, Game.GraphicsDevice.Viewport.Height,

   5:                  Color.White, 100, 0,SpriteEffects.None, 0);

   6:              // 畫第二條垂直線

   7:              spriteBatch.DrawLine(newVector2(2 * TileWidth - 1, 0), 2, Game.GraphicsDevice.Viewport.Height,

   8:                  Color.White, 100, 0,SpriteEffects.None, 0);

   9:              // 畫第一條水平線

  10:              spriteBatch.DrawLine(newVector2(0,TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,

  11:                  Color.White, 100, 0,SpriteEffects.None, 0);

  12:              // 畫第二條水平線

  13:              spriteBatch.DrawLine(newVector2(0, 2* TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,

  14:                 Color.White, 100, 0,SpriteEffects.None, 0);

  15:              // 畫第三條水平線

  16:              spriteBatch.DrawLine(newVector2(0, 3 * TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,

  17:                 Color.White, 100, 0,SpriteEffects.None, 0);

  18:              // 畫第四條水平線

  19:              spriteBatch.DrawLine(newVector2(0, 4 * TileHeight - 1), Game.GraphicsDevice.Viewport.Width, 2,

  20:                 Color.White, 100, 0,SpriteEffects.None, 0);

  21:         }

但是我只是說在SpriBatch裡沒有DrawLine方法。在哪裡能找到它?答案是C#裡面一個神奇的特性繼承方法( extensionmethods)。我不會在這裡詳細的列出C#的特性,但會簡要的提一下這些非常好的特性。如果你想在現有類新增更多的功能,並沒有打破封裝哦(這點很重要)。讓我們看一下繼承實現DrawLine 方法的類:

   1:     static class Extensions

   2:     {

   3:  

   4:         public static Texture2D LineTexture { get; set; }

   5:  

   6:         public static void DrawLine(this SpriteBatch spriteBatch, Vector2position, int width, int height,

   7:              Color color, byte opacity, floatrotation, SpriteEffects effects, float layerDepth)

   8:         {

   9:              color.A = opacity;

  10:              spriteBatch.Draw(LineTexture,

  11:                  newRectangle((int)position.X,(int)position.Y,width,height),

  12:                  null,

  13:                  color,

  14:                  rotation,

  15:                 Vector2.Zero,

  16:                  effects,

  17:                  layerDepth);

  18:         }

  19:  

  20:                 

  21:     }

這裡建立的LineTexture 靜態引用請參考PuzzleGame類中的 LoadContent 方法。這個 DrawLine 方法只是簡單的呼叫了SpriBatch.Draw 方法第二個引數被用來壓縮我們要畫出的物件的空間。

更新: Simon Jackson 提出了一個觀點我們並不需要那個1x1pixel檔案,我們也許會建立一個新的Texture2D物件,用它來畫出線!樣例程式碼:

BlankTexture =new Texture2D(GraphicsDevice, 1, 1);

BlankTexture.SetData(newColor[] { Color.White });

Source code

這個程式碼依舊是開源的https://skydrive.live.com/?sc=documents#cid=10E568ADBB498DC8&id=10E568ADBB498DC8%211600&sc=documents

希望你能喜歡它!

 

博文來源

本博文轉載自http://studentguru.gr/b/dt008/archive/2010/09/14/simple-puzzle-game-for-windows-phone-7-using-xna.aspx

這是一個自由的技術部落格!

感謝作者Δημήτρης Γκανάτσιος 

翻譯:石拓


相關文章