Let’s Rock! 第一季-用ASP.NET扫雷!(3)
上回讲述了Rule这个最重要类的一部分关于游戏初始化的方法.而之前给出的Click方法中可以看出在鼠标左键或右键双击时会执行2个方法,分别为dig(挖)和flag(标记).那么我们开来看看这两个方法里有什么乾坤.
Dig(Block b)
我们获取到我们鼠标单击时的坐标,我们可以判断出我们点击了哪一个块.经过Click方法的分析呢,将这个获取到的块传递到Dig方法中.这时我们就得开始考虑b的一切可能性.如果b.IsMine==true的话,意味着我们的游戏就结束了.所以我们在判断完b.IsMine是否为true之后将执行一个GameOver方法.如果b.IsFlag的属性为true的话,我们将不做任何事情,直接return.余下就是判断它的b.IsDig的属性是否为true,如果为true,执行一段最容易出错的代码,如果为false,将它的IsDig属性设置为true,就代表这个块被挖开了,并且没被雷给炸到.
那么上面所说的那段最容易出错的代码是什么呢?
我们接下来思考一下我们点开一个已经翻开的块时,游戏要怎么做才让我们满意?—显然是展开它周围8个块中尚未翻开的的那一部分.
通常懂得玩扫雷游戏的玩家都知道当我们确定这个已翻开的块的周围没有雷的情况下我们才会点击它,并且命令它展开,那么此时有2种情况:
- 当前块标记的是周围有X个雷,但玩家并未给它标记慢X个旗--此时游戏应该丢弃玩家这次点击的请求,以示警告.
- 玩家给当前块标记满了X个或X个以上的旗子—此时游戏将会听话地将其周围的块都展开,无论玩家是否标记错误.如果标记错误,不好意思,Game Over.至于多标记的块,我们就当作一个隐患埋在游戏当中,并不挖开(也不能挖开,Dig方法碰到已经被标记的块会跳出方法块,什么事也不干的)谁叫玩家自己不小心的.
好了.现在我们健壮的Block类终于有用武之地啦!具体就不再多说,相信您已经被我给绕晕了.
没关系,代码来啦.
private static void Dig(Block b)
{
if (b.IsFlag)
{
return;
}
if (b.IsDig)
{
if (b.MineSurround == 0)
{
foreach (var ob in b.Surrounds)
{
if (!ob.IsDig) Dig(ob);
}
}
else
{
if (b.FlagSurround >= b.MineSurround)
{
foreach (var ob in b.Surrounds)
{
if (!ob.IsDig) Dig(ob);
}
}
}
}
else if (!b.IsDig && b.IsMine)
{
Runtime.GameOver(false);
}
else
{
b.IsDig = true;
if (b.MineSurround == 0)
Dig(b, true);
}
CheckAfterDig();
}
大家可以看到当判断b.IsDig的时候,我们的代码先判断了b周围的雷的个数,如果周围雷的个数为0,游戏将自动帮我们展开它周围的所有未展开项.而展开的方法还是Dig,也就是说游戏将进入一个Dig方法的循环当中.所以我们应当避免Dig方法永远的挖下去,再挖就内存溢出了(显然控制这段代码不进入死循环有点困难,我尝试了好多次,最终以最完美的姿态展示给大家.)!如果b周围雷的个数超过0个,那么游戏将不展开所有的项,那么方法执行到这里也就停了,也就控制了它进入死循环.
Flag(Block b)
标记的语法就没什么好说的代码也非常简单,不具备什么逻辑性.
private static void Flag(Block b)
{
if (!b.IsDig && !b.IsFlag)
{
b.IsFlag = true;
Runtime.GameInfo.Flags++;
}
else if (!b.IsDig && b.IsFlag)
{
b.IsFlag = false;
Runtime.GameInfo.Flags--;
}
}
好了,最近的时间不多,所以每一章节放的内容也不多,4月份就会更加的忙,希望我能忙中偷闲,写下一篇新的博文
Let’s Rock! 第一季-用ASP.NET扫雷!(2)
上一部分的内容介绍了扫雷的运行时配置,那么这一节我们需要编写游戏规则算法来操作运行时的配置.
首先我们得从游戏的初始化开始.游戏刚启动时,运行时配置中的Blocks[]一定是个null值,也就是说它还未初始化.那么我们就要为游戏写一个初始化函数.那么我们就要将前面的Config这个运行时配置类写一个构造函数.让它在初始化的时候将一些必要的成员给赋上值.
public Config(int blocksPerRow, int rowCount, int mineCount, string levelTitle, Control container)
{
MineCount = mineCount;
BlocksPerRow = blocksPerRow;
RowCount = rowCount;
LevelTitle = levelTitle;
Flags = 0;
TimeStarted = DateTime.Now;
container.Controls.Clear();
Panel = new SuperPanel()
{
Width = this.Width,
Height = this.Height,
Name = "SuperPanel"
};
if (Panel.Width < container.Width)
{
Panel.Left = (container.Width - Panel.Width) / 2;
}
if (Panel.Height < container.Height)
{
Panel.Top = (container.Height - Panel.Height) / 2;
}
container.Controls.Add(Panel);
}
在这个构造函数的前半部分,我们将那些必须的成员都已经初始化了我们想要的值,而后半部分就是实例化一个新的SuperPanel,并且调整了这个SuperPanel的位置,使它居中.最后在container中最佳这个Panel.
该构造函数的前四个参数应该很好理解,最后一个类型为Control的container到底是什么呢?因为我们的游戏中一定会有一块地方用来描绘我们扫雷所用到的”块”,那么这个地方我们将来会用一个Panel来规范它的位置及其大小.
可能有人会问Control和Panel是不一样的类型啊,为什么你要用Control类型而用Panel类型呢?首先winform中的所有空间均继承自Control类,而C#语言认为子类转换为父类是类型安全的行为;其次,如果我们哪一天用了不想用Panel,而改为用SplitControl或者GroupBox这些控件,那么我们最终还要回来修改这个函数;最后,在构造函数中我们只需要用到Control类的成员,如Add和Clear方法,Height和Width属性等.这是所有控件都有的成员(因为它们都继承自Control嘛..),所以我们用Control这个宽松的入口用来适应我们游戏未来需求的变更.
另外这个SuperPanel是啥?其实是我自行写的一个继承自Panel的类.它的实现非常简单,首先创建一个类,使他继承自Panel,最后在构造函数中添加几行代码,最终结果是这样:
public class SuperPanel:Panel
{
public SuperPanel()
{
this.SetStyle(
ControlStyles.OptimizedDoubleBuffer|
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint, true
);
this.DoubleBuffered = true;
this.UpdateStyles();
}
}
这是开启控件的双缓冲,解决控件在描绘画布时候闪烁的问题.具体什么是双缓冲就有请大家”百度一下”了.
终于完成了Config类的编写,现在就要开始编写一下运行时这个类了.运行时类里除了包含运行时配置以外,还得包含其他东西,这些函数我们现在暂时无法预知,所以我们就不要在运行时中添加其他未来可能用不到的东西.
新建一个类:Runtime.在游戏中我们的”运行时”是必须唯一且不能创建多个版本的,否则在游戏运行的时候我们的规则访问这些数据的时候乱套,不知道该访问那个运行时配置.所以我们在Runtime中的所有成员都必须标记为”static”.
public static class Runtime
{
public static void Initilaize() { IsInitialized = true; }
public static bool IsInitialized { get; private set; }
public static Config GameInfo
{
get
{
if (_gameInfo == null)
throw new NullReferenceException("游戏尚未初始化!");
return _gameInfo;
}
}
private static Config _gameInfo;
}
完成”运行时”这个数据中心之后,我们就可以开始创建规则算法了.还是老样子,新建一个新的类,名为”Rules”,跟上面一样,配置只能有一个,而规则也只能有一个,所以在游戏中我们也没必要创建多个版本的Rules来对运行时配置进行操作.所以我们将Rules内的方法全部设定为静态方法.
Ps:上一节我们的Blocks是一维数组,当然使用二维数组更为方便,由于多种原因我在接近完成扫雷这一游戏的时候,因为想让计算方法更快,引用了”图”这一数据结构,把二维数组改为一维数组.但是最后放弃了这个方法,感觉上有点多此一举,而后来也懒得把一维数组改回二维的了,反正也没麻烦到哪里去.
给运行时配置中的Blocks初始化.没什么好说的..
public class Rules
{
public static void GenBlocks()
{
Runtime.GameInfo.Blocks = new Block[Runtime.GameInfo.RowCount * Runtime.GameInfo.BlocksPerRow];
int bCount = Runtime.GameInfo.Blocks.Length;
for (int i = 0; i < bCount; i++)
{
Runtime.GameInfo.Blocks[i] = new Block()
{
Index = i,
IsFlag = false,
IsDig = false,
IsMine = false,
X = i % Runtime.GameInfo.BlocksPerRow * Runtime.GameInfo.BlockSize,
Y = i / Runtime.GameInfo.BlocksPerRow * Runtime.GameInfo.BlockSize
};
}
GenMines();
}
}
结尾部分出现了GenMines这个方法,这是给Block[]中添加地雷,这一布我放到初始化结束之后来做就方便很多:
private static void GenMines()
{
Random r = new Random(DateTime.UtcNow.Millisecond);
int mineCount = 0;
while (mineCount < Runtime.GameInfo.MineCount)
{
int x = r.Next(0, Runtime.GameInfo.BlocksPerRow * Runtime.GameInfo.RowCount);
if (!Runtime.GameInfo.Blocks[x].IsMine)
{
Runtime.GameInfo.Blocks[x].IsMine = true;
mineCount++;
}
}
}
首先我们创建一个Random实例用来生成随机数,用while循环和运行时配置中某一块是否是雷判别来保证生成足够多并且不重复的雷.多亏我们前面给Block设定了多个Is某某某的属性,我们只要设定块的isMine属性为true就能让它从一个无辜的方块变为恐怖的炸弹.
初始化游戏之后我们要进行鼠标点击的操作.传统的扫雷左键单击为挖方块,右键为标记为雷或不确定是不是雷,左右键一起按则是打开这一块周围所有的块.
我们就来个简化,左键依然是打开方块,但如果当前块已经被打开则打开它周围所有的块,右键仅标记当前块为雷.左右键就扔掉它.因为它实现起来不方便,我目前想到的方法是定义timer在鼠标某一个键按下的xx毫秒之内继续按下另一个键则触发左右键模式.
鼠标单击事件处理函数:
public static void Click(MouseEventArgs e)
{
if (!Runtime.IsGameOver)
{
int x = e.X / Runtime.GameInfo.BlockSize;
int y = e.Y / Runtime.GameInfo.BlockSize * Runtime.GameInfo.BlocksPerRow;
var btn = e.Button;
Block b = Runtime.GameInfo.Blocks[y + x];
if (OnClick != null)
OnClick(b);
switch (btn)
{
case MouseButtons.Left:
Dig(b);
break;
case MouseButtons.Right:
Flag(b);
break;
default:
break;
}
Runtime.GameInfo.Panel.Invalidate();
if (Clicked != null)
Clicked(b);
}
}
上面的代码应该很清楚,因为这些块是画上去的,而不是windows的控件,所以我们要在运行是配置中的panel的鼠标单击事件中提取鼠标的X,Y值进行计算分析得到鼠标点击所对应的块.
然后触发OnClick事件,鉴于这是玩家与游戏交互的最重要的操作,未来一定有不少操作要在Click事件的执行的时候同时执行.于是定义了Onclick和Clicked这两个事件.这两个事件的定义代码如下:
public delegate void ClickHandler(Block b); public static event ClickHandler OnClick; public static event ClickHandler Clicked;
事件完美地解决了编程中的强依赖关系,这个方法在执行的时候不知道会有谁对它有兴趣,即便这个方法知道,那么他的代码里就要添加一条对方的响应函数.如果某一天一个新的类对它也有兴趣怎么办?那我们还要回来在这个方法里再添加一行这个新类的响应函数.一个新类还好,三天两头的出新的类,并且这些类都对于前面那个方法有兴趣,那我们这么改也不是办法.引进事件之后,我们就关闭了这个方法的修改,并开放了这个类的扩展.谁要对它感兴趣,就订阅它的事件,当事件一旦被执行,就会通知所有的订阅者执行响应函数.对事件还不大了解的朋友们可以到张子阳的博客上看看他关于委托和事件的详细讲解.
好了,这一回就讲到这里.下一节继续讲我们的规则算法和处理函数.咱们下回再见: )
Let’s Rock! 第一季-用ASP.NET扫雷!(1)
我决定做个专题,专门用ASP.NET搞点比较酷的东西,但前提是面向初学者就叫他”Let’s Rock”吧.但毕竟本人的修为并不可以做到一览众山小的境界.首先来个最最普通的游戏—扫雷.一说扫雷可能部分人已经失去兴趣了.其实一个游戏,除了给定的规则以外,如何实现它可以说方法无穷多的,那么我在未来的开发过程中会遵循一下的原则(但可适当打破):
1. 使用.NET Framework 3.5框架开发.为什么, 我喜欢.net3.0的lambda表达式来简化许多操作,另外不去管Linq是否真正”已死”,在”Let’s Rock”系列中我们完全可以利用Linq的简便且快速的特性来保证项目的效率.
2. 尽量做到符合良好的面对对象特性.但首先我再OOP上也是属于菜鸟一个,之所以这么做就是为了在造福大家的时候造福下自己.让自己练习一下面对对象的编程方式.
那么废话不多说,咱们现在就开始吧!
游戏成品截图:
1. 游戏规则
已经知道游戏规则的朋友可以跳过这一块.
扫雷的核心规则就是当你打开一个”块”的时候,如果这个块不是”地雷”,那么它会提示你离他最近且环绕在它周围的那些”块”中有几个是”地雷”.那么通过多个非”雷”的块我们就可以确定到底那些”块”是”地雷”.如果你运气不好踩到了”地雷”,游戏结束,但是你可以为这些自己认为是”雷”的”块”做上标记,防止自己忘记那些是一推测出的地雷.
正确的标记出所有的”地雷”或以挖开所有的非”地雷”块,则游戏结束.
2. 定义配置
了解了游戏的规则我们就可以把部分游戏运行时所必须的东西给确定下来.首先就是游戏的核心-“块”,我并不打算再写几个特殊的”块”(如”地雷块”和”标记块”)首先是没必要,其次是增添开发难度.
首先在创建一个解决方案,选择.net 3.5框架,并且选择windows分类下的windows应用程序.我们将来的扫雷毫无疑问将不会运行在WEB上.
重命名Form1.cs窗体,我们把它叫做Main吧.毕竟未来它是扫雷的主窗体.
建立一个类,名叫Block.cs,下面是定义Block的成员.
namespace Game_MineSweeper
{
public class Block : IComparable<Block>
{
public Block() { }
public int Index { get; set; }
public bool IsMine { get; set; }
public bool IsDig { get; set; }
public bool IsFlag { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int MineSurround
{
get
{
return Surrounds.Count(b => b.IsMine == true);
}
}
public int FlagSurround
{
get
{
return Surrounds.Count(b => b.IsFlag == true);
}
}
public int BlockSurround
{
get
{
return Surrounds.Count(b => b.IsFlag == false && b.IsDig == false);
}
}
public List<Block> Surrounds
{
get
{
return _surrounds;
}
}
public Image Pic
{
get
{
return null;
}
}
private List<Block> _surrounds;
#region IComparable<Block> 成员
public int CompareTo(Block other)
{
return this.Index.CompareTo(other.Index);
}
#endregion
}
}
这就是大体的定义,并且也给出了部分成员的实现.
Index就是Block的ID,它未来可能会用到在对比两个Block的时候确定他们是否是同一个Block,于是我引用了IComparable这个接口来实现对比的功能.
IsMine,IsDig,IsFlag分别是这个块的状态,为什么不把这状态合成成一个枚举类型的状态呢,首先是Block可以具有多种状态(如它可以是雷,与此同时它也同时被标记),如果合成成一个枚举类型就会出现MineNFlag的状态,并且在判断雷的状态时也比较麻烦.
X,Y当然是块显示时的坐标.
MineSurround,FlagSurround,BlockSurround分别是当前块四周的块中雷,标记,未开挖块的数量.这里就不多说了,未来我们会用到.
Surrounds并未完成它的实现.但是我们现在要明确它的功能:装载离当前块最近距离的所有块.因为它周围块的数目是游戏开始以后就决定的,所以我们使用一个私有的_surrounds变量来装放这些块的引用.而上面那些以Surround结尾的属性则在游戏当中是不固定的,所以我们就不需要用容器来盛放结果.
同样的Pic属性在游戏中也是不固定的.它是用来传递当前状态应有的图像给描绘画布的对象.
Compare方法就不用说了.
那么我们在运行时如何来组织并且存放这些Block呢?我们将会用到单件模式.
我们会创造一个类,类中放有的都是游戏运行时不变,但游戏运行之前不确定的一些内容.包括块的数量,每行几个块,每列几个块等等.
public class Config
{
public Config(int blocksPerRow, int rowCount, int mineCount, string levelTitle, Control container)
{
if (rowCount > 50)
rowCount = 50;
else if (rowCount < 5)
rowCount = 5;
if (blocksPerRow > 50)
blocksPerRow = 50;
else if (blocksPerRow < 5)
blocksPerRow = 5;
MineCount = mineCount < 10 ? 10 : mineCount;
BlocksPerRow = blocksPerRow;
RowCount = rowCount;
LevelTitle = levelTitle;
Flags = 0;
TimeStarted = DateTime.Now;
container.Controls.Clear();
Panel = new SuperPanel()
{
Width = this.Width,
Height = this.Height,
Name = "SuperPanel"
};
container.Controls.Add(Panel);
}
private Config() { }
public readonly int BlockSize = 32;
public int MineCount { get; set; }
public int BlocksPerRow { get; set; }
public int RowCount { get; set; }
public int Width { get { return BlocksPerRow * BlockSize; } }
public int Height { get { return RowCount * BlockSize; } }
public int BlockCount { get { return BlocksPerRow * RowCount; } }
public int Flags { get; set; }
public string LevelTitle { get; set; }
public Block[] Blocks { get; set; }
public DateTime TimeStarted { get; set; }
public System.Windows.Forms.Control Panel { get; private set; }
}
BlockSize是不变的一个值,表明这些Block未来呈现的图像大小都是32*32像素.于是我现在已经在怀疑将它放在这里是不是合适.结论出来了,显然不合适.那么这就交给聪明的你来决定将它放在哪里比较好了.
MineCount显然是雷的数量,BlocksPerRow则是每行块的个数,RowCount是当前游戏有几行”块”. BlockCount是当前游戏Block的数量.Flags是当前游戏中已经被标记的块的个数.LevelTitle则是当前游戏的难度即便的名称.TimeStarted是游戏开始时的时间,用来计算玩扫雷总共花费的时间.
Width,Height是留给Panel决定它的高度和宽度,而Panel就是描绘这些块的画布啦!
好啦,讲到这里游戏大部分初始化工作就完成了.那么下一节我们就要开始写游戏的规则算法.
