使用XNA为Windows phone 7开发简单拼图游戏

来源:互联网 发布:mac 文件权限 去掉加号 编辑:程序博客网 时间:2024/04/30 04:47

使用XNAWindows 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

这是一个自由的技术博客!

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

翻译:石拓

原创粉丝点击