找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 2620|回复: 0
收起左侧

基于c#泡泡堂代码源码及讲解

[复制链接]
ID:682283 发表于 2020-1-6 16:37 | 显示全部楼层 |阅读模式
绘制游戏主角

   绘制游戏的角色,并且让角色能动起来是我们本节的重点。泡泡堂游戏的角色主要分为两大类:玩家与敌人。没有阵营划分的游戏永远是没有趣味性的,我们目前开发的游戏是单机版本,没有涉及多人对战的形式。所以玩家就只有你自己,你的敌人就是电脑(NPC)了。当然我们都知道,要熟悉一款游戏,首先要和电脑对战熟悉游戏的操作模式,这样到时候我们再为你的朋友开发一个对战模式,游戏就更加充满乐趣了。 首先我们来看看游戏的对战双方的素材组成:

玩家的素材

敌人的素材

我们发现,游戏的角色素材都是由4*4的图片效果来描述角色的移动方位。每个方位还有角色的移动动作。在开始制作游戏前,我们先来看看游戏的组成元素。如图所示:

图 游戏的对象关系

从上面的图中,我们可以看出游戏中角色对象的继承关系。首先我们看看BaseEntity类的代码:

  1. /// <summary>
  2. /// 游戏中所有元素的基类
  3. /// </summary>
  4. public abstract class BaseEntity
  5. {
  6.     /// <summary>
  7.     /// 表示元素的X坐标(以像素为单位)
  8.     /// </summary>
  9.     public int X { get; set; }
  10.     /// <summary>
  11.     /// 表示元素的Y坐标(以像素为单位)
  12.     /// </summary>
  13.     public int Y { get; set; }
  14.     /// <summary>
  15.     /// 该元素的绘制方法
  16.     /// </summary>
  17.     /// <param name="g">封装一个 GDI+ 绘图图面不能被继承的类</param>
  18.     public abstract void Draw(Graphics g);
  19. }</font></font></font></i>
复制代码

我们的游戏不仅仅有可以移动的角色,还包括一些静态的物体。这些物体将组成游戏不可缺少的场景,我们将在第二节学习到绘制游戏场景动画。无论是角色还是物体它们在游戏中都是有坐标点的(X横向坐标,Y纵向坐标)。它们都有不同的绘图方式,所以我们把这些共同的特征定义为所有游戏对象的基类。并且提供一个Draw的抽象方法来实现对不同对象的绘制。这就是我们平时学习面向对象中用到的多态。接下来,我们看看GameRole类的实现代码:
  1. /// <summary>
  2.     /// 游戏角色基类
  3.     /// </summary>
  4.     public class GameRole : BaseEntity
  5.     {
  6.         /// <summary>
  7.         /// 构造函数:初始化游戏角色
  8.         /// </summary>
  9.         public GameRole(string role)
  10.         {
  11.             if (!string.IsNullOrEmpty(role))
  12.             {
  13.                 //角色名称
  14.                 this.Role = role;
  15.                 //创建游戏角色
  16.                 CreateGameRole(role);
  17.             }
  18.         }

  19.         /// <summary>
  20.         /// 方向的枚举
  21.         /// </summary>
  22.         public Direction direction;
  23.         /// <summary>
  24.         /// 角色名称(玩家和敌人)
  25.         /// </summary>
  26.         public string Role;
  27.         /// <summary>
  28.         /// 生命值
  29.         /// </summary>
  30.         public int Life;
  31.         /// <summary>
  32.         /// 速度
  33.         /// </summary>
  34.         public int Speed=1;
  35.         /// <summary>
  36.         /// 记录桢(根据角色移动方位改变)
  37.         /// </summary>
  38.         private int Frame = 0;

  39.         //创建游戏角色位图二维数组,保存角色行走方向图
  40.         protected Bitmap[][] bitmaps = new Bitmap[UtilityResource.RoleDirection][]
  41.         {
  42.             new Bitmap[UtilityResource.RoleStep],//下
  43.             new Bitmap[UtilityResource.RoleStep],//左
  44.             new Bitmap[UtilityResource.RoleStep],//右
  45.             new Bitmap[UtilityResource.RoleStep] //上
  46.         };

  47.         /// <summary>
  48.         /// 初始化游戏角色对象
  49.         /// </summary>
  50.         /// <param name="role"></param>
  51.         public void CreateGameRole(string role)
  52.         {
  53.             //初始化游戏角色二维数组
  54.             bitmaps = InitResource.CreateInstance().InitGameRole(role);
  55.         }

  56.         /// <summary>
  57.         /// 角色移动
  58.         /// </summary>
  59.         public virtual void Move()
  60.         {
  61.             switch (direction)
  62.             {
  63.                 case Direction.Left:
  64.                     {
  65.                         this.X -= this.Speed;
  66.                         break;
  67.                     }
  68.                 case Direction.Right:
  69.                     {
  70.                         this.X += this.Speed;
  71.                         break;
  72.                     }
  73.                 case Direction.Down:
  74.                     {
  75.                         this.Y += this.Speed;
  76.                         break;
  77.                     }
  78.                 case Direction.Up:
  79.                     {
  80.                         this.Y -= this.Speed;
  81.                         break;
  82.                     }
  83.                 case Direction.Static:
  84.                     break;
  85.             }
  86.         }

  87.         /// <summary>
  88.         /// 记录桢
  89.         /// </summary>
  90.         public void RecordFrame()
  91.         {
  92.             switch (direction)
  93.             {
  94.                 case Direction.Down:
  95.                     {
  96.                         this.Frame = 0;
  97.                         break;
  98.                     }
  99.                 case Direction.Left:
  100.                     {
  101.                         this.Frame = 1;
  102.                         break;
  103.                     }
  104.                 case Direction.Right:
  105.                     {
  106.                         this.Frame = 2;
  107.                         break;
  108.                     }
  109.                 case Direction.Up:
  110.                     {
  111.                         this.Frame = 3;
  112.                         break;
  113.                     }
  114.                 case Direction.Static:
  115.                     break;
  116.             }
  117.         }

  118.         /// <summary>
  119.         /// 绘制游戏中人物对象
  120.         /// </summary>
  121.         /// <param name="g"></param>
  122.         private int i = 0;//列索引
  123.         public override void Draw(System.Drawing.Graphics g)
  124.         {
  125.             switch (direction)
  126.             {
  127.                 case Direction.Down:
  128.                 case Direction.Up:
  129.                 case Direction.Left:
  130.                 case Direction.Right:
  131.                         i = i+1  < UtilityResource.RoleStep ? i + 1 : 0;
  132.                         break;
  133.                 case Direction.Static:
  134.                         i = 0;
  135.                         break;
  136.             }
  137.             //绘制图形
  138.             g.DrawImage(bitmaps[Frame][i], this.X, this.Y, UtilityResource.GridSize, UtilityResource.GridSize);
  139.         }
  140.     }</font></font></font></i>
复制代码

代码比较多,我们从上往下一步一步的分析。首先我们需要明确GameRole类是所有游戏角色的基类,它继承了BaseEntity类。在GameRole中我们定义了游戏角色移动的方向枚举Direction,代码如下:
  1. /// <summary>
  2.     /// 定义方向的枚举
  3.     /// </summary>
  4.     public enum Direction
  5.     {
  6.         Static, //原地不动
  7.         Left,   //向左  
  8.         Up,    //向上  
  9.         Right,  //向右  
  10.         Down //向下
  11.     }</font></font></font></i>
复制代码

我们还定义了角色的公共属性:角色类型,移动方向,生命力,移动速度等,代码如下:
  1. /// <summary>
  2.         /// 方向的枚举
  3.         /// </summary>
  4.         public Direction direction;
  5.         /// <summary>
  6.         /// 角色名称(玩家和敌人)
  7.         /// </summary>
  8.         public string Role;
  9.         /// <summary>
  10.         /// 生命值
  11.         /// </summary>
  12.         public int Life;
  13.         /// <summary>
  14.         /// 速度
  15.         /// </summary>
  16.         public int Speed=1;
  17.         /// <summary>
  18.         /// 记录桢(根据角色移动方位改变)
  19.         /// </summary>
  20.         private int Frame = 0;</font></font></font></i>
复制代码

其中需要注意记录桢Frame属性,它是根据角色移动方位改变为改变的。现在大家可能还不能理解它的作用,我们先看看初始化游戏角色对象的方法实现后,大家就明白了。在GameRole类的构造函数中,我们调用了CreateGameRole这个方法。我们是如何实现游戏角色的创建的呢,我们一起来分析一下实现代码:
  1. /// <summary>
  2.         /// 初始化游戏角色对象
  3.         /// </summary>
  4.         /// <param name="role"></param>
  5.         public void CreateGameRole(string role)
  6.         {
  7.             //初始化游戏角色二维数组
  8.             bitmaps = InitResource.CreateInstance().InitGameRole(role);
  9.         }</font></font></font></i>
复制代码

上面代码中并没有很直观的告诉我们如何创建游戏角色。我们还需要继续寻根问底。我们发现其中用到了一个名叫InitResource的类,这个类是用来为某个游戏对象(如:一个人物,障碍物等元素)加载其所用到的资源文件的。我们看看代码的具体实现:
  1. /// <summary>
  2.     /// 初始化资源文件类
  3.     /// </summary>
  4.     public class InitResource
  5.     {
  6.         private static InitResource instance;
  7.         /// <summary>
  8.         /// 实例化资源文件类
  9.         /// </summary>
  10.         /// <returns></returns>
  11.         public static InitResource CreateInstance()
  12.         {
  13.             if (instance == null)
  14.             {
  15.                 instance = new InitResource();
  16.             }
  17.             return instance;
  18.         }

  19.         /// <summary>
  20.         /// 初始化游戏角色人物(4*4素材)
  21.         /// </summary>
  22.         /// <param name="value">资源文件值</param>
  23.         /// <returns>角色4个方向走动图</returns>
  24.         public Bitmap[][] InitGameRole(string value)
  25.         {
  26.             //创建游戏角色位图二维数组
  27.             Bitmap[][] bitmaps= new Bitmap[UtilityResource.RoleDirection][]
  28.             {
  29.                 //创建角色移动每个方位都有四个动作
  30.                 new Bitmap[UtilityResource.RoleStep],
  31.                 new Bitmap[UtilityResource.RoleStep],
  32.                 new Bitmap[UtilityResource.RoleStep],
  33.                 new Bitmap[UtilityResource.RoleStep]
  34.             };
  35.             //创建单个对象的位图
  36.             Bitmap bitmap = new Bitmap(UtilityResource.BitmapWidth, UtilityResource.BitmapHeight);
  37.             //访问资源文件初始化游戏素材(游戏素材是一张包含角色不同方位不同动作的图片)
  38.             bitmap = (Bitmap)Properties.Resources.ResourceManager.GetObject(value);
  39.             //通过循环,初始化游戏角色单帧素材(i:行数,j:列数,x:图片人物X坐标 y:图片人物Y坐标)
  40.             for (int y = 0, i = 0; y < UtilityResource.ResourceHeight; y += UtilityResource.BitmapHeight, i++)
  41.             {
  42.                 for (int x = 0, j = 0; x < UtilityResource.ResourceWidth; x += UtilityResource.BitmapWidth, j++)
  43.                 {
  44.                     //通过指定坐标切割游戏素材,获得游戏角色单桢素材
  45.                     bitmaps[i][j] = bitmap.Clone(new Rectangle(x, y, UtilityResource.BitmapWidth, UtilityResource.BitmapHeight), System.Drawing.Imaging.PixelFormat.DontCare);
  46.                 }
  47.             }
  48.             return bitmaps;
  49.         }
  50. }</font></font></font></i>
复制代码

代码中定义了两个方法,CreateInstance方法的主要作用就是帮助我们创建资源文件类的实例。此处采用了单例模式,目的是为了防止一个类有多个实例。我们还定义了InitGameRole()方法,该方法返回一个Bitmap类型的二维数组。为什么需要返回二维数组了?这点我们需要详细说明一下。我们的游戏角色素材都是采用的4*4的图形。图形中包含了游戏角色4个方位移动的不同的4组动作。如图描述:


图 4*4角色素材

我们只需要在加载素材的时候,对素材进行行列拆分就可以获得角色的每个方位的每个动作。这样做的好处就是我们无需制作16张图片来显示角色移动的动作改变。二维数组的代码如下:

  1. Bitmap[][] bitmaps= new Bitmap[UtilityResource.RoleDirection][]
  2.             {
  3.                 //创建角色移动每个方位都有四个动作
  4.                 new Bitmap[UtilityResource.RoleStep],
  5.                 new Bitmap[UtilityResource.RoleStep],
  6.                 new Bitmap[UtilityResource.RoleStep],
  7.                 new Bitmap[UtilityResource.RoleStep]
  8.             };</font></font></font></i>
复制代码

其中我们发现又出现了一个新的类: UtilityResource,这个类是用来初始化游戏中所用到的所有素材的尺寸以及资源访问值的。参考代码如下:
  1. /// <summary>
  2.     ///资源文件定义类
  3.     /// </summary>
  4.     public class UtilityResource
  5.     {
  6.         #region 资源文件尺寸
  7.         //资源大小(玩家和敌人的原始图片大小)
  8.         public static int ResourceWidth = 256;
  9.         public static int ResourceHeight = 256;
  10.         //图像大小(玩家和敌人的实际游戏图片大小)
  11.         public static int BitmapWidth = 64;
  12.         public static int BitmapHeight = 64;
  13.         //爆竹大小
  14.         public static int BombWidth = 144;
  15.         public static int BombHeight = 48;
  16.         //火焰大小
  17.         public static int FireWidth = 240
  18.         public static int FireHeight = 48
  19.         //地板大小
  20.         public static int FloorWidth = 192;
  21.         public static int FloorHeight = 48;
  22.         //土墙大小(可以被爆竹炸掉)
  23.         public static int SoilWallWidth = 48;
  24.         public static int SoilWallHeight = 48;
  25.         //石墙大小(坚固不可摧毁)
  26.         public static int StoneWallWidth = 240;
  27.         public static int StoneWallHeight = 48;
  28.         #endregion

  29.         #region 资源文件动画帧数
  30.         //火焰帧数(图片组成元素个数)
  31.         public static int FireFrames = 5;
  32.         //爆竹帧数
  33.         public static int BombFrames = 3;
  34.         //地板帧数
  35.         public static int FloorFrames = 4;
  36.         //土墙帧数
  37.         public static int SoilWallFrames = 1;
  38.         //石墙帧数
  39.         public static int StoneWallFrames = 5;
  40.         #endregion

  41.         #region 资源文件访问值
  42.         /// <summary>
  43.         /// 爆竹资源值
  44.         /// </summary>
  45.         public static string BombValue = "Bomb";
  46.         /// <summary>
  47.         /// 火焰资源值
  48.         /// </summary>
  49.         public static string FireValue = "Fire";
  50.         /// <summary>
  51.         /// 玩家资源值
  52.         /// </summary>
  53.         public static string HeroValue = "Hero";
  54.         /// <summary>
  55.         /// 敌人资源值
  56.         /// </summary>
  57.         public static string EnemyValue = "Enemy";
  58.         /// <summary>
  59.         /// 土墙资源值
  60.         /// </summary>
  61.         public static string SoilWallValue = "SoilWall";
  62.         /// <summary>
  63.         /// 石墙资源值
  64.         /// </summary>
  65.         public static string StoneWallValue = "StoneWall";
  66.         /// <summary>
  67.         /// 地板资源值
  68.         /// </summary>
  69.         public static string FloorValue = "Floor";
  70.         #endregion

  71.         #region 游戏地图尺寸
  72.         //网格大小(宽度和高度均为48像素)
  73.         public static int GridSize = 48;
  74.         //游戏地图尺寸
  75.         public static int MapRows = 15;
  76.         public static int MapCols = 15;
  77.         #endregion

  78.         #region 角色移动动作
  79.         //角色移动方位数(上,下,左,右)
  80.         public const int RoleDirection = 4;
  81.         //角色完成单方向一套动作需要的步数
  82.         public const int RoleStep = 4;
  83.         #endregion
  84.     }</font></font></font></i>
复制代码

在前面的GameRole类中,我们还定义了一个Move方法使游戏角色能够移动,移动的原理跟电影的原理是一样的,都是通过一张张图像帧快速切换形成的效果。只是这里的移动是四组不同方向的4个动作组成的。于是我们要在GameRole类中添加以下代码:
  1. //创建游戏角色位图二维数组,保存角色行走方向图
  2.         protected Bitmap[][] bitmaps = new Bitmap[UtilityResource.RoleDirection][]
  3.         {
  4.             new Bitmap[UtilityResource.RoleStep],//下
  5.             new Bitmap[UtilityResource.RoleStep],//左
  6.             new Bitmap[UtilityResource.RoleStep],//右
  7.             new Bitmap[UtilityResource.RoleStep] //上
  8.         };

  9.         /// <summary>
  10.         /// 角色移动
  11.         /// </summary>
  12.         public virtual void Move()
  13.         {
  14.             switch (direction)
  15.             {
  16.                 case Direction.Left:
  17.                     {
  18.                         this.X -= this.Speed;
  19.                         break;
  20.                     }
  21.                 case Direction.Right:
  22.                     {
  23.                         this.X += this.Speed;
  24.                         break;
  25.                     }
  26.                 case Direction.Down:
  27.                     {
  28.                         this.Y += this.Speed;
  29.                         break;
  30.                     }
  31.                 case Direction.Up:
  32.                     {
  33.                         this.Y -= this.Speed;
  34.                         break;
  35.                     }
  36.                 case Direction.Static:
  37.                     break;
  38.             }
  39.         }

  40.         /// <summary>
  41.         /// 记录桢
  42.         /// </summary>
  43.         public void RecordFrame()
  44.         {
  45.             switch (direction)
  46.             {
  47.                 case Direction.Down:
  48.                     {
  49.                         this.Frame = 0;
  50.                         break;
  51.                     }
  52.                 case Direction.Left:
  53.                     {
  54.                         this.Frame = 1;
  55.                         break;
  56.                     }
  57.                 case Direction.Right:
  58.                     {
  59.                         this.Frame = 2;
  60.                         break;
  61.                     }
  62.                 case Direction.Up:
  63.                     {
  64.                         this.Frame = 3;
  65.                         break;
  66.                     }
  67.                 case Direction.Static:
  68.                     break;
  69.             }
  70.         }

  71.         /// <summary>
  72.         /// 绘制游戏中人物对象
  73.         /// </summary>
  74.         /// <param name="g"></param>
  75.         private int i = 0;//列索引
  76.         public override void Draw(System.Drawing.Graphics g)
  77.         {
  78.             switch (direction)
  79.             {
  80.                 case Direction.Down:
  81.                 case Direction.Up:
  82.                 case Direction.Left:
  83.                 case Direction.Right:
  84.                         i = i+1  < UtilityResource.RoleStep ? i + 1 : 0;
  85.                         break;
  86.                 case Direction.Static:
  87.                         i = 0;
  88.                         break;
  89.             }
  90.             //绘制图形
  91.             g.DrawImage(bitmaps[Frame][i], this.X, this.Y, UtilityResource.GridSize, UtilityResource.GridSize);
  92.         }</font></font></font></i>
复制代码

这里我们为GameRole类建立了一个数组来保存四个方向的每个动作。然后通过绘制不同的图片帧来形成动作的效果。真正的移动还是通过Move方法来改变GameRole的坐标来实现的。动作的切换和坐标的改变是必不可少的。然后游戏中每个元素都需要绘制到窗体上,所以我们要重写Draw方法。
  1. /// <summary>
  2.         /// 绘制游戏中人物对象
  3.         /// </summary>
  4.         /// <param name="g"></param>
  5.         private int i = 0;//列索引
  6.         public override void Draw(System.Drawing.Graphics g)
  7.         {
  8.             switch (direction)
  9.             {
  10.                 case Direction.Down:
  11.                 case Direction.Up:
  12.                 case Direction.Left:
  13.                 case Direction.Right:
  14.                         i = i+1  < UtilityResource.RoleStep ? i + 1 : 0;
  15.                         break;
  16.                 case Direction.Static:
  17.                         i = 0;
  18.                         break;
  19.             }
  20.             //绘制图形
  21.             g.DrawImage(bitmaps[Frame][i], this.X, this.Y, UtilityResource.GridSize, UtilityResource.GridSize);
  22.         }</font></font></font></i>
复制代码

在这个方法中,我们根据方向

游戏所需要的资源已经准备好,现在我们来添加一个Hero类,即玩家控制的角色类,代码如下:

/// <summary>
/// 游戏主角
/// </summary>
public class Hero : GameRole
{
//静态属性和方法不能被继承
//构造函数不能被继承
//所以使用base关键字调用父类的构造函数或方法。
    public Hero():base(UtilityResource.HeroValue)
    {

    }

    public Hero(string role)
        : base(role)
    {

    }

    /// <summary>
    /// 主角移动
    /// </summary>
    public void Move(bool isMove)
    {
        if (isMove)
        {
            base.Move();
            base.RecordFrame();
        }
        else
        {
            base.RecordFrame();
        }
    }
}

这个类继承了GameRole类,这样Hero类就继承了GameRole类中的属性和方法。

到此为止,一个角色的属性及需要的资源都有了,我们还需要添加一个游戏逻辑类来管理这些资源。游戏逻辑类代码如下:

/// <summary>
/// 游戏逻辑处理类
/// </summary>
public class GameManager
{
    //游戏主角
    private Hero hero;
    public GameManager()
    {
        //初始化游戏
        InitGame();
    }
    /// <summary>
    /// 初始化游戏
    /// </summary>
    public void InitGame()
    {
        //实例化游戏主角
        hero = new Hero();
        //主角移动速度
        hero.Speed = 4;
        //主角的生命值
        hero.Life = 5;
        //初始化游戏地图网格
        int cols = UtilityResource.MapCols;
        int rows = UtilityResource.MapRows;
        //创建了地图(石墙和地面)
        map = new GameMap(cols, rows);
        //cols * rows:获得地图网格的面积
    }
    /// <summary>
    /// 绘制游戏
    /// </summary>
    /// <param name="g"></param>
    public void Draw(Graphics g)
    {
        //绘制玩家
        hero.Draw(g);
}
}
有了这个类,我们就可以将角色绘制到游戏窗体中去。

新建一个窗体,修改窗体的标题、名字以及窗体大小等基本属性。

public partial class GameMain : Form
{
    public GameMain()
    {
        InitializeComponent();
        //绘制游戏角色移动的网格宽度和高度
        this.Width = UtilityResource.MapCols * 48;
        this.Height = UtilityResource.MapRows * 48+24;
    }
}

在这里我们通过后台代码的方式修改了窗体的大小。游戏窗体的属性设置好后怎么才能把刚才我们做的角色放置到游戏窗体中去呢?这时我们需要使用窗体的Paint事件。


图 窗体属性及事件

为该事件添加以下代码:

//游戏主要逻辑实现类
private GameManager manager = new GameManager();

private void GameMain_Paint(object sender, PaintEventArgs e)
{
    //绘制游戏
    manager.Draw(e.Graphics);
}

这里我们调用了主要逻辑类中的Draw方法,这个时候我们就可以把角色绘制到窗体中了。

运行窗体,会发现人物已经在窗体中,但是还不能移动。角色移动当然需要键盘上的方向键。这时我们还需要给窗体添加两个事件,一个是键盘按键按下时触发的事件,这个时候人物会移动;另一个是键盘按键弹起的事件,这个时候人物会停止移动。



图 窗体属性及事件

添加这两个事件后,角色移动该怎么移动,停止该怎么停止呢?这时我们还需要写一个KeyBoard类来管理键盘按键事件。

/// <summary>
/// 键盘事件类
/// </summary>
public class Keyboard
{
    private static Keyboard instance;

    //键盘按下事件的集合
    private List<Keys> keys = new List<Keys>();

    public Keyboard()
    {
        keys.Clear();
    }

    /// <summary>
    /// 创建键盘类实例
    /// </summary>
    /// <returns></returns>
    public static Keyboard CreateInstance()
    {
        if (instance == null)
        {
            instance = new Keyboard();
        }
        return instance;
    }

    /// <summary>
    /// 判断键是否被按下
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public bool IsKeyDown(Keys key)
    {
        return keys.Contains(key);
    }

    /// <summary>
    /// 键盘按下的键的集合
    /// </summary>
    /// <param name="e"></param>
    public void KeyDown(Keys key)
    {
        if (!keys.Contains(key))
        {
            keys.Add(key);
        }
    }

    /// <summary>
    /// 移除键盘按起的键(防止按键后角色一直移动)
    /// </summary>
    /// <param name="key"></param>
    public void KeyUp(Keys key)
    {
        if (keys.Contains(key))
        {
            keys.Remove(key);
        }
    }
}

这里我们通过一个集合来判断玩家按下了哪些键。然后通过KeyDown和KeyUp两个方法来控制集合中的键。另外还有一个IsKeyDown方法来判断某个键是否被按下,这对后面的角色移动很有用。然后再在窗体后台代码中添加以下代码:

private void GameMain_KeyDown(object sender, KeyEventArgs e)
{
    //键盘按下
    Keyboard.CreateInstance().KeyDown(e.KeyCode);
}

private void GameMain_KeyUp(object sender, KeyEventArgs e)
{
    //键盘按上
    Keyboard.CreateInstance().KeyUp(e.KeyCode);
}

这里我们把键盘的按键状态存储到程序中,程序可以通过获得这些按键的状态来根据GameManager游戏逻辑类中的逻辑去修改游戏中各种元素的状态。这时我们需要在GameManager类中添加以下代码:

    /// <summary>
    /// 键盘事件
    /// </summary>
    public void KeyBoardEvent()
    {
        if (Keyboard.CreateInstance().IsKeyDown(Keys.Up))
        {
            hero.direction = Direction.Up;
            hero.Move(IsMove(hero));
        }
        else if (Keyboard.CreateInstance().IsKeyDown(Keys.Left))
        {
            hero.direction = Direction.Left;
            hero.Move(IsMove(hero));
        }
        else if (Keyboard.CreateInstance().IsKeyDown(Keys.Right))
        {
            hero.direction = Direction.Right;
            hero.Move(IsMove(hero));
        }
        else if (Keyboard.CreateInstance().IsKeyDown(Keys.Down))
        {
            hero.direction = Direction.Down;
            hero.Move(IsMove(hero));
        }
        else
        {
            hero.direction = Direction.Static;
        }
    }
    /// <summary>
    /// 更新游戏桢,实现动画
    /// </summary>
    public void UpdateGameFrames()
    {
        #region 更新键盘事件
        KeyBoardEvent();
        #endregion
    }
    /// <summary>
    /// 判断前方是否通过
    /// </summary>
    /// <param name="role">游戏角色对象</param>
    /// <returns></returns>
    public bool IsMove(GameRole role)
    {
        int newX = role.X;
        int newY = role.Y;
        //前进一步,记录改变的坐标点
        switch (role.direction)
        {
            case Direction.Down:
                newY += role.Speed;
                break;
            case Direction.Left:
                newX -= role.Speed;
                break;
            case Direction.Right:
                newX += role.Speed;
                break;
            case Direction.Up:
                newY -= role.Speed;
                break;
        }
        //窗体临界点检测
        if (newX < 0 || newX > (map.Cols - 1) * UtilityResource.GridSize || newY < 0 || newY > (map.Rows - 1) * UtilityResource.GridSize)
        {
            return false;
        }
        return true;
    }
但是到目前位置,角色还是不能移动,因为窗体中的元素是通过绘制来表现出来的。所以我们需要给窗体添加一个Timer控件来不断重绘窗体。例如,开始一个角色在窗体的第一个格子,我们通过代码修改这个角色所在位置的属性过后,必须通过这个角色的位置属性来重绘窗体中角色所在位置。给窗体添加Timer控件并添加以下代码:

private void GameTimer_Tick(object sender, EventArgs e)
{
    //重绘窗体
    this.Invalidate(new Rectangle(0, 0, this.Width, Height), true);
    //更新游戏桢,实现动画
    manager.UpdateGameFrames();
}

但是这个时候,由于电脑的不同,窗体中的角色可能会闪动,我们需要在窗体构造方法中加入以下代码:

public GameMain()
{
    InitializeComponent();
    //采用双缓冲,解决角色移动屏幕闪烁
    this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer, true);
    //绘制游戏角色移动的网格宽度和高度
    this.Width = UtilityResource.MapCols * 48;
    this.Height = UtilityResource.MapRows * 48 + 24;
}

到目前为止,角色已经可以在窗体中移动了,绘制敌人的方法跟角色的方法差不多,只是Hero是通过键盘按键来控制移动方向,而敌人则是自动移动和改变方向的。由于敌人产生的位置是随机的,这需要我们把地图绘制出来之后再去绘制敌人。

第二天目标:绘制游戏场景

绘制游戏的场景主要包含游戏中涉及到的各种物品。游戏场景主要由地面背景,石墙背景,箱子背景组成。地图上的障碍物设置也为游戏增加了不少可玩性。地图由一系列坐标构成,不同的坐标设置不同的标识并加以绘制,便可以构造出一张张不同的地图出来。首先我们看看游戏场景素材的组成:

石墙图片

地板图片

土墙图片

首先我们需要考虑如何对组合的图片素材进行分割,我们可以对图片桢进行分解。实现图片分离的效果。我们在InitResource类中创建InitGameGood方法,实现对1*N素材的图片进行分离,单独构造每张图片素材。实现代码如下:

/// <summary>
        /// 初始化游戏物品(1*N素材)
        /// </summary>
        /// <param name="value">资源文件值</param>
        /// <param name="frames">图片帧数</param>
        /// <param name="size">图片尺寸</param>
        /// <returns>一次动画需要的桢</returns>
        public Bitmap[] InitGameGood(string value,int frames,Size size)
        {
            Bitmap[] bitmaps = new Bitmap[frames];
            Bitmap bitmap = new Bitmap(size.Width, size.Height);
            //访问资源文件初始化游戏素材
            bitmap = (Bitmap)Properties.Resources.ResourceManager.GetObject(value);
            //size.Width/frames:获得每张图片的像素宽度
            for (int i = 0, j = 0; i < size.Width; i += size.Width / frames, j++)
            {
                bitmaps[j] = bitmap.Clone(new Rectangle(i, 0, size.Width / frames, size.Height), System.Drawing.Imaging.PixelFormat.DontCare);
            }
            return bitmaps;
        }
我们的游戏地图是由多个48*48像素的网格组成的,在每个网格内我们放置不同的素材类型,因此我们需要对素材类型进行管理,我们通过GridType枚举来管理游戏素材类型,代码实现如下:

/// <summary>
    /// 地图格式
    /// </summary>
    public enum GridType
    {
        Empty,  //空地
        Stone,  //石墙
        Soil,   //土墙
        Floor, //地板
        Bomb //爆竹
    }

说明:空地类型没有对应的图片,在地图初始化的时候,所有的网格默认均为空地。

接着创建地图类(GameMap)初始化地图,先绘制空地,再随机绘制石墙(不可通过不可炸毁),再在为空地标记的坐标绘制箱子(可以被炸毁),便可以构造出一幅原始的地图。

GameMap类的实现代码如下:

   /// <summary>
    /// 游戏地图类
    /// </summary>
    public class GameMap : BaseEntity
    {
        //定义地图行
        public int Rows = UtilityResource.MapRows;
        //定义地图列
        public int Cols = UtilityResource.MapCols;
        //定义地图格式枚举数组
        public GridType[,] Grids;

        //石墙图片数组
        private Bitmap[] bmpsStone = new Bitmap[UtilityResource.StoneWallFrames];
        //地板图片数组
        private Bitmap[] bmpsFloor = new Bitmap[UtilityResource.FloorFrames];

        //定义随机数对象
        private Random random = new Random();
        //定义石墙和地板产生的随机变量
        private int stones = 0;
        private int floors = 0;

        /// <summary>
        /// 初始化地图
        /// </summary>
        /// <param name="rows">格子行数</param>
        /// <param name="cols">格子列数</param>
        public GameMap(int rows, int cols)
        {
            this.Rows = rows;
            this.Cols = cols;
            //初始化格式(Cols:X坐标,Rows:Y坐标)
            Grids = new GridType[Cols, Rows];

            //创建石墙图片
            bmpsStone = InitResource.CreateInstance().InitGameGood(UtilityResource.StoneWallValue, UtilityResource.StoneWallFrames, new Size(UtilityResource.StoneWallWidth, UtilityResource.StoneWallHeight));
            //创建地板图片
            bmpsFloor = InitResource.CreateInstance().InitGameGood(UtilityResource.FloorValue, UtilityResource.FloorFrames, new Size(UtilityResource.FloorWidth, UtilityResource.FloorHeight));

            //随机产生地面
            floors = random.Next(0, UtilityResource.FloorFrames);

            //建立原始地图
            for (int x = 0; x < Cols; x++)
            {
                for (int y = 0; y < Rows; y++)
                {
                    //默认为空地
                    Grids[x, y] = GridType.Empty;
                    //随机产生地图格式(随机数从1开始的目的就是防止第一个单元格被填充)
                    int i = random.Next(1, Cols - 1);
                    int j = random.Next(1, Rows - 1);
                    //随机产生石墙(先产生石墙占位,然后再产生其他的物体)
                    if (Grids[i, j] != GridType.Stone)
                    {
                        Grids[i, j] = GridType.Stone;
                    }
                }
            }
        }

        //定义记录随机产生石墙下标的数组
        private List<int> stoneType = new List<int>();
        public void RandStoneType()
        {
            for (int x = 0; x < Cols; x++)
            {
                for (int y = 0; y < Rows; y++)
                {
                    stones = random.Next(0, UtilityResource.StoneWallFrames);
                    stoneType.Add(stones);
                }
            }
        }
        /// <summary>
        /// 绘制游戏地图
        /// </summary>
        /// <param name="g"></param>
        private bool isCreate = true;
        public override void Draw(System.Drawing.Graphics g)
        {
            if (isCreate)
            {
                RandStoneType();
                isCreate = false;
            }

            for (int x = 0; x < Cols; x++)
            {
                for (int y = 0; y < Rows; y++)
                {
                    //首先绘制地图
                    g.DrawImage(bmpsFloor[floors], x * (UtilityResource.FloorWidth / UtilityResource.FloorFrames), y * (UtilityResource.FloorHeight), UtilityResource.FloorWidth / UtilityResource.FloorFrames, UtilityResource.FloorHeight);
                    //如果是石墙,就在地图上添加石墙
                    if (Grids[x, y] == GridType.Stone)
                    {
                        g.DrawImage(bmpsStone[stoneType[x * Rows + y]], x * (UtilityResource.StoneWallWidth / UtilityResource.StoneWallFrames), y * (UtilityResource.StoneWallHeight), UtilityResource.StoneWallWidth / UtilityResource.StoneWallFrames, UtilityResource.StoneWallHeight);
                    }
                }
            }
        }
    }


上述代码实现了一张包含石墙和地板背景的游戏场景,游戏的场景采用了随机产生的组合效果,如图所示:

游戏初始化地图场景(1)

图 游戏初始化地图场景(2)

在上述代码中,需要注意的是地图的重绘问题。我们在窗体的定期器中设置了创建的重绘方法来保证角色的移动。这样就可能出现每隔指定毫秒游戏的地图出现重绘的问题。解决办法是在GameMap类中添加记录随机产生石墙下标的数组的方法,代码如下所示:

private List<int> stoneType = new List<int>();
public void RandStoneType()
{
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
stones = random.Next(0, UtilityResource.StoneWallFrames);
stoneType.Add(stones);
}
}
}
定义List<int>集合保存随机产生的石墙下标。在绘制的方法中判断是否只需要产生一次。这样就可以防止每次重绘地图的时候改变已经产生的石墙。我们还需要注意创建初始化地图的顺序,我们一定要先创建地板,再创建石墙。其实原理很简单,因为石墙图片是不规则的图形,背景色是透明的,地板创建完成后,石墙就覆盖在地板上面。这样不规则图片的底色就是地板的颜色了。

我们可能发现游戏初始化界面中还差了一个土墙图片素材元素。接下来我们还需要继续在地图上创建土墙的地图。定义土墙类SoilWall,代码实现如下:

/// <summary>
    /// 土墙类
    /// </summary>
    public class SoilWall : BaseEntity
    {
        //定义土墙图片集合
        private Bitmap[] bmpSoil = new Bitmap[UtilityResource.SoilWallFrames];

        public SoilWall()
        {
            //创建土墙
           bmpSoil =  InitResource.CreateInstance().InitGameGood(UtilityResource.SoilWallValue,UtilityResource.SoilWallFrames,new Size(UtilityResource.SoilWallWidth,UtilityResource.SoilWallHeight));
        }
        public override void Draw(System.Drawing.Graphics g)
        {
            g.DrawImage(bmpSoil[0], X, Y, UtilityResource.SoilWallWidth, UtilityResource.SoilWallHeight);
        }
    }

土墙图片素材是单桢的(没有连续图片动画),我们初始化土墙是通过GameManager完成的,实现代码如下:
在游戏初始化方法(InitGame)中新增代码:

//土墙集合(全局变量中定义)
private List<SoilWall> soilwall_list = new List<SoilWall>();

//初始化游戏地图网格(初始化方法中定义)
int cols = UtilityResource.MapCols;
int rows = UtilityResource.MapRows;
map = new GameMap(cols, rows);
//初始化土墙
soilwall_list.Clear();
//cols * rows:获得地图网格的面积
for (int i = 0; i < cols * rows; i++)
{
     //定义随机数
     int x = random.Next(0, cols);
     int y = random.Next(0, rows );
     //防止角色被堵住或者角色在土墙上被创建出来
     if ((x == 0 && y == 0) || (x == 1 && y == 0) || (x == 0 && y == 1))
     {
            continue;
     }
//将空地部分设置为土墙
if (map.Grids[x, y] == GridType.Empty)
{
          //创建土墙
         SoilWall soilWall = new SoilWall();
          soilWall.X = x*UtilityResource.SoilWallWidth;
         soilWall.Y = y * UtilityResource.SoilWallHeight;
         map.Grids[x, y] = GridType.Soil;
          soilwall_list.Add(soilWall);
}
}

在绘制游戏方法(Draw)中,添加绘制土墙实现代码:

        /// <summary>
        /// 绘制游戏
        /// </summary>
        /// <param name="g"></param>
        public void Draw(Graphics g)
        {
            //1.绘制地图
            map.Draw(g);
            //2.绘制土墙
            for (int i = 0; i < soilwall_list.Count; i++)
            {
                soilwall_list.Draw(g);
            }
            //3.绘制玩家
            hero.Draw(g);
        }

注意上述代码中此句代码的作用:防止角色被堵住或者角色在土墙上被创建出来

if ((x == 0 && y == 0) || (x == 1 && y == 0) || (x == 0 && y == 1))
{
            continue;
}


如果我们没有判断土墙创建坐标可能就会出现以下情况:

人物进入了死胡同BUG


土墙与人物重叠BUG

  经过判断后的正常运行效果:

但是我们发现那还不是最终效果,如下图所示:

石墙四周是封闭的

大家注意到上面图片上标记的红色区域,这种情况下是不允许这样创建地图的,在以后加载怪物的时候可能会进入到封闭的石墙区域内,这样的话无论如何我们的主角也没能力消灭它了,游戏也将不会胜利。

解决方式: 剔除存在封闭石墙的石块,以留一个路口供怪物或者主角出入

在Map类中定义一个DeleteAroundStone方法,判断能否在当前坐标周围建立石墙,代码如下:

/// <summary>
        /// 判断是否是四周环绕的石墙
        /// </summary>
        /// <param name="curCol">当前X坐标</param>
        /// <param name="curRow">当前Y坐标</param>
        /// <returns></returns>
        public bool DeleteAroundStone(int curCol,int curRow)
        {
            int count = 0;
            for (int x = curCol - 1; x <= curCol + 1; x++)
            {
                for (int y = curRow - 1; y <= curRow + 1; y++)
                {
                    if (Grids[x, y] == GridType.Stone)
                    {
                        count++;
                    }
                }
            }
            if (count >= 4)
            {
                return false;
            }
            return true;
        }
首先遍历当前坐标四周,如果存在石墙则count++,如果至少存在上下左右都是石墙的话就不允许创建了。在Map类构造函数中,初始化地图时,进行判断,代码如下:

//剔除四周封闭的石墙
for (int x = 0; x < Cols; x++)
{
for (int y = 0; y < Rows; y++)
{
if (Grids[x, y] == GridType.Stone)
{
//判断是否被封闭
if (DeleteAroundStone(x, y))
{
continue;
}
else
{
Grids[x, y] = GridType.Empty;
}
}
}
}

最终效果如下:


图 地图引擎最终效果

  到目前为止,我们的游戏基本场景就设计完成了,我们可以让上一节课中绘制好的主角在游戏地图中”畅游”了。为什么说是”畅游”了,我们发现主角可以穿越任何的障碍物。也许这就是所谓游戏的外挂吧。为了保障游戏的可玩性,我们需要对游戏中角色移动进行临界点检测以及障碍物的碰撞检测。精彩内容,下回分解,尽请期待。。。

第三天目标:角色移动检测

  上一节我们介绍了游戏的地图绘制,本节将针对角色移动检测进行展开。首先我们发现游戏的角色在移动过程中会跑出游戏地图的边界,我们成为移动的临界点吧。我们先来把这个问题解决了。角色的移动范围控制我们通过获得地图的宽度和高度就可以解决,在GameManager类中新增IsMove方法,代码如下:

        /// <summary>
        /// 判断前方是否通过
        /// </summary>
        /// <param name="role">游戏角色对象</param>
        /// <returns></returns>
        public bool IsMove(GameRole role)
        {
            int newX = role.X;
            int newY = role.Y;

            //前进一步,记录改变的坐标点
            switch (role.direction)
            {
                case Direction.Down:
                    newY += role.Speed;
                    break;
                case Direction.Left:
                    newX -= role.Speed;
                    break;
                case Direction.Right:
                    newX += role.Speed;
                    break;
                case Direction.Up:
                    newY -= role.Speed;
                    break;
            }
            //窗体临界点检测
            if (newX < 0 || newX > (map.Cols - 1) * UtilityResource.GridSize || newY < 0 || newY > (map.Rows - 1) * UtilityResource.GridSize)
            {
                return false;
            }
            return true;
        }
记录角色前进后坐标点的改变,然后与窗体的最大值进行比较,没有得到窗体的最大值就可以继续移动,否则停止移动。当然上述代码只是判断角色是否可以继续移动,我们还要修改控制角色移动的方法,找到Hero类,修改Move方法:

        /// <summary>
        /// 主角移动
        /// </summary>
        public void Move(bool isMove)
        {            
            if (isMove)
            {
                base.Move();
                base.RecordFrame();
            }
            else
            {
                base.RecordFrame();
            }
        }

解决了角色移动超出临界点的问题,接下来就是重点了。如何防止角色移动过程中可以穿越障碍物。也就是游戏中必须考虑的障碍物碰撞检测。首先我们创建碰撞检测类HitCheck,实现代码如下:

    /// <summary>
    /// 碰撞检测类
    /// </summary>
    public class HitCheck
    {
        /// <summary>
        /// 判断物体是否相交
        /// </summary>
        /// <param name="x1">对象1 X坐标</param>
        /// <param name="y1">对象1 Y坐标</param>
        /// <param name="x2">对象2 X坐标</param>
        /// <param name="y2">对象2 Y坐标</param>
        /// <returns></returns>
        public static bool IsIntersect(int x1, int y1, int x2, int y2)
        {
            Rectangle r1 = new Rectangle(x1, y1, UtilityResource.GridSize, UtilityResource.GridSize);
            Rectangle r2 = new Rectangle(x2,y2,UtilityResource.GridSize-4,UtilityResource.GridSize-4);
            //判断对象是否相交
            if (r1.IntersectsWith(r2))
            {
                return true;
            }
            return false;
        }
    }

上面的代码主要是通过判断两个矩形是否相交,如果矩形相交就说明有碰撞产生。接下来我们需要修改GameManager类中的IsMove方法,增加角色的碰撞检测代码:

        /// <summary>
        /// 判断前方是否通过
        /// </summary>
        /// <param name="role">游戏角色对象</param>
        /// <returns></returns>
        public bool IsMove(GameRole role)
        {
            int newX = role.X;
            int newY = role.Y;
            //前进一步,记录改变的坐标点
            switch (role.direction)
            {
                case Direction.Down:
                    newY += role.Speed;
                    break;
                case Direction.Left:
                    newX -= role.Speed;
                    break;
                case Direction.Right:
                    newX += role.Speed;
                    break;
                case Direction.Up:
                    newY -= role.Speed;
                    break;
            }
            //窗体临界点检测
            if (newX < 0 || newX > (map.Cols - 1) * UtilityResource.GridSize || newY < 0 || newY > (map.Rows - 1) * UtilityResource.GridSize)
            {
                return false;
            }
            //碰撞检查
            for (int x = 0; x < map.Cols; x++)
            {
                for (int y = 0; y < map.Rows; y++)
                {
                    //如果前方网格是爆竹,土墙,石墙
                    if (map.Grids[x, y] == GridType.Bomb || map.Grids[x, y] == GridType.Soil || map.Grids[x, y] == GridType.Stone)
                    {
                        //记录当前障碍物坐标点
                        int posX = x * UtilityResource.GridSize;
                        int posY = y * UtilityResource.GridSize;
                        //判断角色与障碍物的焦点
                        if(HitCheck.IsIntersect(posX,posY,newX,newY))
                        {
                            return false;
                        }
                    }
                }
            }
            return true;
        }

到此为止角色就不能实现穿越的效果了。接下来我们开始创建敌人类了,敌人类(Enemy)同样继承GameRole类,实现代码与Hero类的代码类似,具体如下所示:

/// <summary>
    /// 敌人类
    /// </summary>
    public class Enemy : GameRole
    {
        public Enemy()
            : base(UtilityResource.EnemyValue)
        {
        }
        public Enemy(string role)
            : base(role)
        {
        }
        /// <summary>
        /// 敌人移动
        /// </summary>
        public override void Move()
        {
            base.Move();
            base.RecordFrame();
        }
    }

我们在GameManager游戏主要逻辑类中创建敌人,代码实现如下:

           //敌人集合
         private List<Enemy> enemy_list = new List<Enemy>();
//初始化敌人
            enemy_list.Clear();
            //默认设置5个敌人
            for (int i = 0; i < 5; i++)
            {
                //怪物出现的位置的随机坐标
                int rCol = random.Next(2, cols);
                int rRow = random.Next(2, rows);
                //在空地位置创建敌人
                if (map.Grids[rCol, rRow] == GridType.Empty)
                {
                    //创建敌人
                    Enemy enemy = new Enemy();
                    //设置敌人的初始化移动方向,默认是Static
                    enemy.direction = Direction.Left;
                    //设置敌人的移动速度
                    enemy.Speed = 4;
                    //设置敌人初始化的坐标
                    enemy.X = rCol * UtilityResource.GridSize;
                    enemy.Y = rRow * UtilityResource.GridSize;
                    //添加敌人到集合中
                    enemy_list.Add(enemy);
                }
                else
                {
                    //如果当前网格不是空地,就继续找空地,重新生成敌人
                    i = i > 0 ? i -= 1 : i;
                }
            }

我们默认创建5个敌人,随着后期对游戏设置了关卡,我们可以采用动态改变敌人数量的方式来添加敌人数量。需要注意的敌人的方位属性一定要设置为非Static的,否则角色永远也无法移动起来。敌人的移动速度默认设置为4,不能让敌人比玩家跑得慢吧。还需要注意敌人是在空地上创建出来的,如果当前网格不是空地,就继续找空地,直到找到空地创建敌人。

接下来,我们在GameManager类的Draw()中绘制敌人,代码如下:

/// <summary>
        /// 绘制游戏
        /// </summary>
        /// <param name="g"></param>
        public void Draw(Graphics g)
        {
            //1.绘制地图
            map.Draw(g);
            //2.绘制土墙
            for (int i = 0; i < soilwall_list.Count; i++)
            {
                soilwall_list.Draw(g);
            }
            //3.绘制玩家
            hero.Draw(g);
            //4.绘制敌人
            for (int i = 0; i < enemy_list.Count; i++)
            {
                enemy_list.Draw(g);
            }
        }

现在玩家和敌人都绘制在游戏地图上了,如图所示:

图 玩家与敌人效果

不过现在的敌人还不能移动,我们需要让它们充满活力。我们就需要为它们添加移动的方法。我们修改UpdateGameFrames()更新游戏桢的方法,添加敌人移动的行为,代码如下:

#region  更新敌人移动
            for (int i = 0; i < enemy_list.Count; i++)
            {
                //判断敌人是否允许移动
                if (IsMove(enemy_list))
                {
                    enemy_list.Move();
                }
                else
                {
                    //不能移动,就重新调整方位
                    AutoChangeDirection(enemy_list);
                }
                //如果敌人与主角相交,就进行碰撞检测
                if (HitCheck.IsIntersect(hero.X, hero.Y, enemy_list.X, enemy_list.Y))
                {
                    //主角生命值降低1次
                    hero.Life -= 1;
                }
            }
            #endregion

敌人移动同样需要进行临界检查与碰撞检测。我们修改IsMove方法,添加敌人的判断代码:

//如果是敌人
if (role is Enemy)
{
if (HitCheck.IsIntersect(posX, posY, newX, newY))
{
return false;
}
}
else
{
if (HitCheck.IsIntersect(posX, posY, newX, newY))
{
return false;
}
}

如果敌人不能移动,就重新调整方位,新增AutoChangeDirection方法代码如下:

/// <summary>
        /// 自动调整方位
        /// </summary>
        /// <param name="role">游戏角色</param>
        public void AutoChangeDirection(GameRole role)
        {
            //随机调用方位
            int rDirection = random.Next(0, 4);
            switch (rDirection)
            {
                case 0:
                    {
                        role.direction = Direction.Down;
                        break;
                    }
                case 1:
                    {
                        role.direction = Direction.Left;
                        break;
                    }
                case 2:
                    {
                        role.direction = Direction.Right;
                        break;
                    }
                case 3:
                    {
                        role.direction = Direction.Up;
                        break;
                    }
            }
        }

主要敌人也可以自由地调整方位移动了,如图所示:

敌人移动

我们现在看到了地图上自由移动的敌人,作为玩家的我们,现在是不是迫不及待地想去解决这些敌人了。可是很无奈,我们没有武器,又无法穿越障碍物。所以我们现在需要制作游戏的武器爆竹。爆竹的素材如图所示:

爆竹素材

火焰素材

接下来,我们开始创建爆竹类Bomb,实现代码如下:

/// <summary>
    /// 爆竹类
    /// </summary>
    public class Bomb : BaseEntity
    {
        //设置爆竹延迟破时间为50毫秒
        public int DelayTime = 50;
        Bitmap[] bmps = new Bitmap[UtilityResource.BombFrames];

        public Bomb()
        {
            //初始化爆竹
            bmps = InitResource.CreateInstance().InitGameGood(UtilityResource.BombValue,UtilityResource.BombFrames,new Size(UtilityResource.BombWidth,UtilityResource.BombHeight));
        }
        private int i = 0;
        public override void Draw(System.Drawing.Graphics g)
        {
            //实现爆竹动画效果
            i = i + 1 < UtilityResource.BombFrames ? i + 1 : 0;
            g.DrawImage(bmps, X, Y, UtilityResource.GridSize, UtilityResource.GridSize);
        }
    }

我们需要在游戏逻辑类GameManager中绘制爆竹类,代码如下:

//爆竹集合(全局变量中定义)
        private List<Bomb> bomb_list = new List<Bomb>();

/// <summary>
        /// 绘制游戏
        /// </summary>
        /// <param name="g"></param>
        public void Draw(Graphics g)
        {
            //1.绘制地图
            map.Draw(g);
            //2.绘制土墙
            for (int i = 0; i < soilwall_list.Count; i++)
            {
                soilwall_list.Draw(g);
            }
            //3.绘制玩家
            hero.Draw(g);
            //4.绘制敌人
            for (int i = 0; i < enemy_list.Count; i++)
            {
                enemy_list.Draw(g);
            }
            //5.绘制爆竹
            for (int i = 0; i < bomb_list.Count; i++)
            {
                bomb_list.Draw(g);
            }
        }

爆竹可能有多个,默认只有一个,后面我们将介绍通过吃道具的方式,增加爆竹的数量。

我们如何让玩家放出爆竹呢?我们需要修改键盘事件,设置玩家按下空格键后,就在地图的空白位置放置一个爆竹,代码如下:

  else if (Keyboard.CreateInstance().IsKeyDown(Keys.Space))
  {
                //放置爆竹
                int col = 0;
                int row = 0;
                //放置爆竹时需要调整方位
                AdjustDirection(hero, ref col, ref row);
                if (BombNumber > 0 && map.Grids[col, row] != GridType.Bomb)
                {
                    //创建爆竹对象
                    Bomb bomb = new Bomb();
                    //设置爆竹的坐标点
                    bomb.X = col * UtilityResource.GridSize;
                    bomb.Y = row * UtilityResource.GridSize;
                    bomb_list.Add(bomb);
                    //爆竹已经添加
                    map.Grids[col, row] = GridType.Bomb;
                    BombNumber--;
                }
   }

我们需要注意放置爆竹是需要调整爆竹的位置,AdjustDirection()方式实现了对爆竹位置的调整,代码如下:

/// <summary>
        /// 调整爆竹的位置
        /// </summary>
        /// <param name="role"></param>
        /// <param name="col"></param>
        /// <param name="row"></param>
        public void AdjustDirection(GameRole role,ref int col,ref int row)
        {
            //获得当前角色所在的网格位置
            int cur_col = role.X / UtilityResource.GridSize;
            int cur_row = role.Y / UtilityResource.GridSize;
            //获得当前角色的偏移量坐标
            int posX = cur_col * UtilityResource.GridSize;
            int posY = cur_row * UtilityResource.GridSize;
            //角色X坐标位移量如果超过网格的一半,就在下一个网格放置爆竹
            if (Math.Abs(posX - role.X) > UtilityResource.GridSize / 2)
            {
                col = cur_col + 1;
            }
            else
            {
                col = cur_col;
            }
            if (Math.Abs(posY - role.Y) > UtilityResource.GridSize / 2)
            {
                row = cur_row + 1;
            }
            else
            {
                row = cur_row;
            }
        }

当我们按下空格键后的效果:


图 玩家与爆竹重合

我们发现玩家与爆竹重合了,玩家置于爆竹的下方。出现这样的原因是因为我们先绘制出玩家,再同一个网格位置又绘制爆竹,所以爆竹覆盖了玩家。解决办法就是最后绘制玩家。另外我们还注意到玩家这个时候无法移动了。因为我们在前面做了障碍物碰撞检测。我们需要对障碍物碰撞检测的代码进行修改,代码如下:

//碰撞检查
            for (int x = 0; x < map.Cols; x++)
            {
                for (int y = 0; y < map.Rows; y++)
                {
                    //如果前方网格是爆竹,土墙,石墙
                    if (map.Grids[x, y] == GridType.Bomb || map.Grids[x, y] == GridType.Soil || map.Grids[x, y] == GridType.Stone)
                    {
                        //记录当前障碍物坐标点
                        int posX = x * UtilityResource.GridSize;
                        int posY = y * UtilityResource.GridSize;
                        //如果是敌人
                        if (role is Enemy)
                        {
                            if (HitCheck.IsIntersect(posX, posY, newX, newY))
                            {
                                return false;
                            }
                        }
                        else
                        {
                            //判断玩家放置爆竹时可以通过,直到玩家离开爆竹就成了障碍物
                            if (HitCheck.IsIntersect(posX, posY, role.X, role.Y))
                            {
                                continue;
                            }
                            //判断角色与障碍物的焦点
                            if (HitCheck.IsIntersect(posX, posY, newX, newY))
                            {
                                return false;
                            }

                        }
                    }
                }
            }

我们首先判断玩家放置爆竹时可以通过,直到玩家离开爆竹后就成了障碍物。

图 爆竹变成了障碍物

我们实现了爆竹的创建,接下来就可以创建爆竹破时候的火焰效果了。我们首先创建火焰类Fire,具体代码如下:

    /// <summary>
    /// 火焰类
    /// </summary>
    public class Fire : BaseEntity
    {
        //设置火花延迟时间
        public int DelayTime = 5;
        //设置火花对象
        Bitmap[] bmpFire = new Bitmap[UtilityResource.FireFrames];
        public Fire()
        {
            bmpFire = InitResource.CreateInstance().InitGameGood(UtilityResource.FireValue, UtilityResource.FireFrames, new Size(UtilityResource.FireWidth, UtilityResource.FireHeight));
        }
        private int i = 0;
        public override void Draw(System.Drawing.Graphics g)
        {
            i = i + 1 < UtilityResource.FireFrames ? i + 1 : 0;
            g.DrawImage(bmpFire, X, Y, UtilityResource.GridSize, UtilityResource.GridSize);
        }
    }

接下来,我们在GameManager游戏业务逻辑类中,实现破的效果。在破效果实现前,我们首先需要制造火焰,创建CreateFire方法,实现代码如下:

/// <summary>
        /// 制造火焰
        /// </summary>
        /// <param name="col"></param>
        /// <param name="row"></param>
        public void CreateFire(int col,int row)
        {
            Fire fire = new Fire();
            fire.X = col * UtilityResource.GridSize;
            fire.Y = row* UtilityResource.GridSize;
            fire_list.Add(fire);
        }

当爆竹破时,火焰是四处延伸的。火焰根据当前位置(爆竹位置)不同方向递增或者递减蔓延。当遇到土墙,土墙被烧毁并结束该方向火焰的蔓延;当遇到石墙,直接结束该方向火焰的蔓延。具体实现的代码为:

        /// <summary>
        /// 爆竹破
        /// </summary>
        /// <param name="bomb"></param>
        public void CreateBomb(Bomb bomb)
        {
            //获得放置爆竹的网格
            int col = bomb.X / UtilityResource.GridSize;
            int row = bomb.Y / UtilityResource.GridSize;
            //清空火焰
            fire_list.Clear();
            //创建中心点火焰,制造火焰
            CreateFire(col, row);
            //创造上火焰效果(上火焰随Y坐标递减,递减的次数根据火焰的强度来决定)
            int cur_row = row-1;
            for(int i=0;i<FirePower;i++,cur_row--)
            {
                Fire fire = new Fire();
                //火焰结束
                if (cur_row < 0)
                {
                    break;
                }
                //如果遇到石墙
                if (map.Grids[col, cur_row] == GridType.Stone)
                {
                    break;
                }
                //如果遇到土墙
                else if (map.Grids[col, cur_row] == GridType.Soil)
                {
                    //产生火焰
                    CreateFire(col, cur_row);
                    break;
                }
                else
                {
                    CreateFire(col, cur_row);
                }
            }
            //创造下火焰效果(下火焰随Y坐标递增,递增的次数根据火焰的强度来决定)
            cur_row = row + 1;
            for (int i = 0; i < FirePower; i++, cur_row++)
            {
                Fire fire = new Fire();
                //火焰结束
                if (cur_row > map.Rows - 1)
                {
                    break;
                }
                //如果遇到石墙
                if (map.Grids[col, cur_row] == GridType.Stone)
                {
                    break;
                }
                //如果遇到土墙
                else if (map.Grids[col, cur_row] == GridType.Soil)
                {
                    //产生火焰
                    CreateFire(col, cur_row);
                    break;
                }
                else
                {
                    CreateFire(col, cur_row);
                }
            }
            //创造左火焰效果(左火焰随X坐标递减,递减的次数根据火焰的强度来决定)
            int cur_col = col - 1;
            for (int i = 0; i < FirePower; i++, cur_col--)
            {
                Fire fire = new Fire();
                //火焰结束
                if (cur_col < 0)
                {
                    break;
                }
                //如果遇到石墙
                if (map.Grids[cur_col, row] == GridType.Stone)
                {
                    break;
                }
                //如果遇到土墙
                else if (map.Grids[cur_col, row] == GridType.Soil)
                {
                    //产生火焰
                    CreateFire(cur_col, row);
                    break;
                }
                else
                {
                    CreateFire(cur_col, row);
                }
            }
            //创造右火焰效果(右火焰随X坐标递增,递增的次数根据火焰的强度来决定)
            cur_col = col + 1;
            for (int i = 0; i < FirePower; i++, cur_col++)
            {
                Fire fire = new Fire();
                //火焰结束
                if (cur_col > map.Cols-1)
                {
                    break;
                }
                //如果遇到石墙
                if (map.Grids[cur_col, row] == GridType.Stone)
                {
                    break;
                }
                //如果遇到土墙
                else if (map.Grids[cur_col, row] == GridType.Soil)
                {
                    //产生火焰
                    CreateFire(cur_col, row);
                    break;
                }
                else
                {
                    CreateFire(cur_col, row);
                }
            }
        }

在UpdateGameFrames方法中定义更新爆竹破的方法,新增代码如下:

#region 更新爆竹破
            for (int i = 0; i < bomb_list.Count; i++)
            {
                //更新破时间
                bomb_list.DelayTime--;
                if (bomb_list.DelayTime == 0)
                {
                    //调用破的方法
                    CreateBomb(bomb_list);
                    //获得当前爆竹的网格位置
                    int cur_col = bomb_list.X / UtilityResource.GridSize;
                    int cur_row = bomb_list.Y / UtilityResource.GridSize;
                    //破完成将当前坐标点设置为空地
                    map.Grids[cur_col, cur_row] = GridType.Empty;
                    //爆竹数量自加
                    BombNumber++;
                    //放置一个爆竹就移除一个爆竹
                    bomb_list.Remove(bomb_list);
                }
            }
            #endregion

当主角放置爆竹后,到达一定的时间爆竹便会自爆,前面爆竹类中有一个DelayTime的变量,用来存放爆竹延迟破时间,用List<Bomb>存放已放置爆竹,遍历每一个爆竹,并更新它们破时间。不过爆竹的破后的火焰效果还没有实现。我们还需要接着在Draw方法中绘制火焰。在GameManager类的Draw方法中,添加如下代码:

/// <summary>
        /// 绘制游戏
        /// </summary>
        /// <param name="g"></param>
        public void Draw(Graphics g)
        {
            //1.绘制地图
            map.Draw(g);
            //2.绘制土墙
            for (int i = 0; i < soilwall_list.Count; i++)
            {
                soilwall_list.Draw(g);
            }
            //4.绘制敌人
            for (int i = 0; i < enemy_list.Count; i++)
            {
                enemy_list.Draw(g);
            }
            //5.绘制爆竹
            for (int i = 0; i < bomb_list.Count; i++)
            {
                bomb_list.Draw(g);
            }
            //6.绘制火焰
            for (int i = 0; i < fire_list.Count; i++)
            {
                fire_list.Draw(g);
            }
            //3.绘制玩家
            hero.Draw(g);
}

图 绘制火焰效果

我们发现火焰一直停留在游戏界面中不能消失,并且火焰破后,对玩家以及箱子都没有产生任何摧毁的效果。这时,我们需要更新火焰的效果,当火焰与玩家,敌人,或者物品相交时都应该产生被炸毁的效果。我们需要在GameManager类中新增更新火焰效果的方法,具体实现代码如下:

            #region 更新火焰效果
            for (int i = 0; i < fire_list.Count; i++)
            {
                //火焰时间递减
                fire_list.DelayTime--;
                if (fire_list.DelayTime == 0)
                {
                    //获得火焰的网格位置
                    int col = fire_list.X / UtilityResource.GridSize;
                    int row = fire_list.Y / UtilityResource.GridSize;
                    //烧毁土墙
                    for (int j = 0; j < soilwall_list.Count; j++)
                    {
                        //如果火焰的坐标与土墙的坐标相同
                        if (soilwall_list[j].X == fire_list.X && soilwall_list[j].Y == fire_list.Y)
                        {
                            soilwall_list.Remove(soilwall_list[j]);
                        }
                    }
                    //烧毁敌人
                    for (int j = 0; j < enemy_list.Count; j++)
                    {
                        //敌人与火焰相交
                 if(HitCheck.IsIntersectDeep(enemy_list[j].X,enemy_list[j].Y,fire_list.X,fire_list.Y))
                        {
                            enemy_list.Remove(enemy_list[j]);
                        }
                    }
                    //烧毁玩家
                    if (HitCheck.IsIntersectDeep(hero.X, hero.Y, fire_list.X, fire_list.Y))
                    {
                        hero.X = 0;
                        hero.Y = 0;              
                    }
                    map.Grids[col, row] = GridType.Empty;
                    //移除火焰,否则火焰就会一直出现在地图上
                    fire_list.Remove(fire_list);
                }
            }
            #endregion

我们通过判断火焰产生的坐标点与物体的坐标点是否相等,来判断是否炸毁物体。玩家和敌人我们采用了IsIntersectDeep来判断两个物体是否相交来进行判断。注意此处的IsIntersectDeep方法,我们称为深度相交。目的是为了减少两个矩形相互点的范围,只有敌人或玩家近距离进入火焰范围的时候才被炸毁。

到目前为止,游戏的玩家便可以通过爆竹清理障碍物以及消灭敌人了。但还有一个问题需要在此解决,就是如果在爆竹破范围内还有其他爆竹,那么这个爆竹是不是也应该破?这个效果怎么实现呢?

图 两个爆竹

在这个图里,我们先放置爆竹1,隔1秒后再放置爆竹2。当爆竹1破后,爆竹2并不会马上破,而是再隔1秒后才会破,也就是说,爆竹间没能相互影响。实际上应该是爆竹1破的时候,只要爆竹2在爆竹1破的火焰范围内,那么爆竹2就应该立即破。这个问题看似很复杂,实际上很简单,我们只要在爆竹1破的时候将其火焰范围内的所有爆竹的破时间设置为立即破即可,代码如下:

//炸掉范围内的其他爆竹
for (int j = 0; j < bomb_list.Count; j++)
{
    //如果火焰的坐标与土墙的坐标相同
    if (bomb_list[j].X == fire_list.X && bomb_list[j].Y == fire_list.Y)
    {
        bomb_list[j].DelayTime = 0;
    }
}

这里把时间设置为0是不管这个爆竹本来应该还有多久才破都让该爆竹立即破。

到今天为止,我们的泡泡堂能够实现人和怪物的移动,爆竹的破以及连炸。明天我们将实现道具的功能。

第四天目标:道具的制作

道具跟障碍物有一定关系,经过分析关系如下表:


完整的Word格式文档51黑下载地址:

泡泡堂项目详细文档.doc (1.42 MB, 下载次数: 13)

评分

参与人数 1黑币 +50 收起 理由
admin + 50 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表