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月份就会更加的忙,希望我能忙中偷闲,写下一篇新的博文
浅测Try&Catch的性能到底有多差(3)
上2节咱们使用30000行杨辉三角来测试try&catch的性能,结果差强人意.原因是什么我不是很确定,但是有一点我觉得有点问题,而且结果很有可能就差在这上面:我们无法确定try块被执行了多少次,catch块被执行了多少次.当然算是能算,就是太麻烦.而这次我把算法设定的很简单.代码如下:
using System;
using System.Diagnostics;
public class MyClass
{
public static void Main()
{
string[] strs=
{
"I'm",
"Snake"
};
int rounds=1000000;
Stopwatch sw=new Stopwatch();
for(int r=0;r<5;r++)
{
sw.Reset();
sw.Start();
for(int i=0;i<rounds;i++)
{
string s=strs[i%2];
if(s.Length>5)
s.Substring(3,1);
else
s.Substring(0,1);
/*
try
{
s.Substring(3,1);
}
catch
{
s.Substring(0,1);
}
*/
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
Console.ReadLine();
}
}
这个算法优点是不占内存,另外使用了引用类型的简单操作.,并且我们可以知道算法中各个块被执行了多少次.
这次我毫不留情地把基数(即:rounds)设定得很大,达到一百万.那么我们的if&else还有块在这个算法中执行了多少次呢?
循环次数:1,000,000
if块: 1,000,000次
else块 500,000次
try块 1,000,000次
catch块 500,000次
好的,让我们来看看运行结果吧!
If&Else
1. 40ms
2. 39ms
3. 37ms
4. 37ms
5. 40ms
6. 38ms
7. 37ms
8. 37ms
9. 37ms
10. 39ms
平均值:38.1 ms
Try&Catch
1. 39947ms
2. 39744ms
3. 39054ms
4. 39971ms
5. 40006ms
然后我实在等不下去了…于是中止了10次循环.
好吧,性能差了整整1000倍.
这次再改改代码,事先说一下,这次改代码是出于个人兴趣,比较一下引用类型和值类型的性能,跟本主题无关.
更改代码如下:
int[] ints={0,1};
int rounds=1000000;
for(int i=0;i<rounds;i++)
{
int n=ints[i%2];
if(n>0)
n = n/2;
else
n = n+1/2;
}
运行结果是清一色的1ms.感叹一下值类型的操作果然比引用类型快,那么题外话就赶快结束吧.
回到我们之前操作string的代码中,我们这次的代码修改如下:
public class MyClass
{
public static void Main()
{
string str="Hi";
int rounds=1000000;
Stopwatch sw=new Stopwatch();
for(int r=0;r<10;r++)
{
sw.Reset();
sw.Start();
for(int i=0;i<rounds;i++)
{
if(str.Length>5)
str.Substring(3,1);
else
str.Substring(0,1);
/*
try
{
s.Substring(3,1);
}
catch
{
s.Substring(0,1);
}
*/
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
Console.ReadLine();
}
}
这次的执行次数如下:
循环次数:1,000,000
if块: 1,000,000次
else块 1,000,000次
try块 1,000,000次
catch块 1,000,000次
运行结果如下:
if&else运行10次结果如下:
1. 37 ms
2. 36 ms
3. 37 ms
4. 36 ms
5. 36 ms
6. 36 ms
7. 36 ms
8. 40 ms
9. 36 ms
10. 36 ms
见鬼,比之前的还快= =
那么轮到测试try&catch块了:
1. 85498ms
2. 84335ms
我不想再测试了.这次性能更可怕了.原来try块跟catch块都有性能消耗.那finally块呢?我们来验证一下吧!
加上finally以后,耗时还是85ms左右.那就不让catch块执行看看执行时间有什么变化呢.
结果竟然和if&else一样的37ms!
本节总结:
根据上面的分析,特别是文章末尾的分析非常明显的表明了如果在try块中执行无误的代码的性能损失几乎为0,但是一旦try块中的代码出现错误,那么CLR(应该是这玩意)就会生成一个异常,并且进入catch块中.catch块中搞了什么鬼我不知道,也暂时没能力知道,但是最终倒是让我明白了原来罪魁祸首是出了错的try块中的代码导致运行catch才出现的性能损耗.
最后我要说个前两节都忘记说的事:我是用Snippet Compiler编写并执行所有的代码的,而据说Snippet Compiler是以debug的形式执行的代码.所以所有之前的代码性能可能会被debug模式影响到.但具体影响的大不大就请高手告知我一声,而完成了这篇代码之后我还有其他正式得干呢,呵呵.
浅测Try&Catch的性能到底有多差(2)
昨晚因为快要熄灯断电的缘故,发表上一篇文章有些仓促.在昨晚熄灯之后我考虑到算法可以简化,于是今早放学之后就马上回来改了改算法.代码如下:
public class MyClass
{
public static void Main()
{
int maxLine=30000;
int[][] ns=new int[maxLine][];
Stopwatch sw=new Stopwatch();
sw.Reset();
sw.Start();
for(int i=0;i<maxLine;i++)
{
ns[i]=new int[i+1];
ns[i][0]=1;
for(int j=1;j<=i;j++)
{
if(j==i)
{
ns[i][j]=1;
continue;
}
else
{
ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
}
/*
try
{
ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
}
catch(Exception ex)
{
ns[i][j]=1;
}
}
*/
}
sw.Stop();
Console.WriteLine(sw.Elapsed.ToString());
Console.ReadLine();
}
}
最后执行10000行的时间缩短了0.1~0.15秒.但这显然不是我现在要阐述的论题.
在上一篇文章我并未提出我的平台和操作系统,这显然是对测试的一种不尊重,所以赶紧放上我的测试环境详情:
操作系统:windows 7 Ultimate
主板芯片组: A780G(超级缩水版)
CPU:AMD X2 2700MHZ(5000+黑盒)
内存:4G(金士顿 ddr2 800 2G*2)
还好我没有高估我的内存.刚才想提高测试的函数,于是在10000后面加了个零,差点没把我电脑给搞歇菜了.于是只再加了20000行.那么现在在改进了算法和行数之后他们的效率又会是如何呢?
ps:这算法明显给if和else减少压力从原来的四段变为两段,而try&catch负担依旧很重.另外我就不截图了,截图太麻烦,而且我之准备让程序在运行期间只执行2次,并不是原先的5次,可能是程序内存占用的原因吧,每次运行时间都相继减少,而且内存也吃不消,30000行1趟占用2G左右的内存.
下面是if&else方式执行30000行的记录:
1.1 30000lines 2.956s
1.2 30000lines 3.751s
2.1 30000lines 2.826s
2.2 30000lines 3.619s
3.1 30000lines 3.068s
3.2 30000lines 3.306s
现在是try&catch方式执行30000行的记录:
1.1 30000lines 7.611s
1.2 30000lines 7.813s
2.1 30000lines 6.804s
2.2 30000lines 7.064s
3.1 30000lines 6.945s
3.2 30000lines 7.036s
这次虽然算法偏向了if&else,但是最终的结果并没有跟try&catch拉开更大的距离,反而缩小了一倍.
在执行30000行测试的时候发现内存占用特别厉害,将近100%,可能是在这方面遇上瓶颈,于是我缩减10000行代码再试试看.
1.1 20000lines 1.430s
1.2 20000lines 1.755s
2.1 20000lines 1.404s
2.2 20000lines 1.851s
3.1 20000lines 1.410s
3.2 20000lines 1.764s
现在是try&catch方式执行30000行的记录:
1.1 20000lines 3.536s
1.2 20000lines 4.092s
2.1 20000lines 3.613s
2.2 20000lines 4.103s
3.1 20000lines 3.558s
3.2 20000lines 4.080s
我郁闷了.不过也没关系,事实已经证明了try&catch是性能低下的东西.
本节总结:
本节使用杨辉三角的优化(较前一节)算法,并且提升了运算量,但结果if&else的优势反而变小了,先不管原因是什么,以后您还会在代码中大量的"踹"一下再"咔吃"一下吗?事实上很多异常都可以提前判断,特别是"引用类型是否为空"和指定格式"是否为预期类型"的判断基本上不需要使用try&catch块,特别在大量的循环中.
try&catch的确给我们带来方便,但它的性能也的确难以恭维,另外也降低了我们原有的编写代码的谨慎程度.所以在我个人编写的代码中一般是不会出现try&catch的,除非必要.
好了,本节也正式完成,但是因为杨辉三角的复杂度导致程序运算中try&catch发生的概率难以确定(确定是肯定可以,但是我实在懒得去验证,很久没有拿笔在草纸上计算数学问题了).那么下一节我将以一个简单算法来验证if&else和try&catch之间的性能,我们至少要确定一次循环内至少触发了多少次try和catch,或执行了多少次判断才能更精确地确定这两者的性能.那么如果您还有兴趣看下去的话,咱们就在下一篇"不见不散" : )
浅测Try&Catch的性能到底有多差(1)
今天中午本来是java课老师叫我们用它来实现杨辉三角.当初已经临近下课,由于用不惯Eclipse,一直卡在一个下标越界的错误上,直到下课也没把程序正常运行出来.晚上回宿舍就特别不爽,于是用C#将之重写了一遍,首次测试运行,结果成功.心理稍稍有点欣喜.
由于未来的就业压力,我脑子突然一热,再想未来面试途中如果面试官刚好叫我输出杨辉三角我应该怎么写它比较快,切不容易错呢?在我的办法呈现之前,先把我的算法代码贴上来(由于本人数学的那根筋比较细,算法不一定是最佳算法)
public class MyClass
{
public static void Main()
{
int maxLine=8;
int[][] ns=new int[maxLine][];
for(int i=0;i<maxLine;i++)
{
ns[i]=new int[i+1];
for(int j=0;j<i+1;j++)
{
if(i-1<0)
{
ns[i][j]=1;
continue;
}
else if(j - 1 < 0)
{
ns[i][j]=1;
continue;
}
else if(ns[i-1].Length<j+1)
{
ns[i][j]=1;
continue;
}
else
{
ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
}
}
}
for(int i=0;i<MaxLine;i++)
{
for(int j=0;j<i+1;j++)
{
int t=ns[i][j];
if(t<10)
Console.Write(" ");
Console.Write(t.ToString()+" ");
}
Console.WriteLine("");
}
Console.ReadLine();
}
}
于是结果如下:

是不是相当标准 : )
好了,由于前面的代码有一长串if和else语句,个人认为这个代码丑了点,于是决定直接不判断,直接try和catch一下.于是上面的代码发生的变化:
for(int i=0;i<maxLine;i++)
{
try
{
ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
}
catch
{
ns[i][j]=1;
}
}
现在代码清爽了点.但是由于以前听说过try&catch的性能相当的差,于是谨慎得测试一下性能会比较好,而且到头来还能多长点见识.于是改写代码如下(这时就不输出结果了,只是纯粹后台操作):
public class MyClass
{
public static void Main()
{
int maxLine=10000;
int[][] ns=new int[maxLine][];
Stopwatch sw=new Stopwatch();
for(int t=0;t<5;t++)
{
sw.Reset();
sw.Start();
for(int i=0;i<maxLine;i++)
{
ns[i]=new int[i+1];
for(int j=0;j<i+1;j++)
{
if(i-1<0)
{
ns[i][j]=1;
continue;
}
else if(j - 1 < 0)
{
ns[i][j]=1;
continue;
}
else if(ns[i-1].Length<j+1)
{
ns[i][j]=1;
continue;
}
else
{
ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
}
// try
// {
// ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
// }
// catch(Exception ex)
// {
// ns[i][j]=1;
// }
}
}
sw.Stop();
Console.WriteLine(sw.Elapsed.ToString());
}
Console.ReadLine();
}
}
我先是测试了一下多次if和else的10000行操作:
结果我已经截图下来了:总共10次.


10000万行的总共用时大概在0.5秒左右.而这次我要运行try&catch块的代码:


这个结果果然验证了try&catch性能差的命题了!相差3~4倍的时间!
所以如果您下次要在超级循环中用到try&catch的时候,该谨慎点了.
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就是描绘这些块的画布啦!
好啦,讲到这里游戏大部分初始化工作就完成了.那么下一节我们就要开始写游戏的规则算法.
Godaddy虚拟主机用后感.
Snake于2月2日购买了Godaddy的Economy Hosting-Windows-Monthly虚拟主机服务(不要问我为什么不用Linux,肯定是有原因的)但是使用了近一个月之后发现在20:30~24:00(美国的12:00左右吧)这一段时间的访问速度超级慢,甚至有时候可以到当机的地步,于是我就向Godaddy的Support team伸手援助.当然就不要在语法方面刁难英语水平很烂的我了,还好对方大致会看得懂.
-----------------------------my first email to godaddy support team-------------------------------------
I don't know you guys did it,
my host almost can't visit at the daytime!
It's too slow too show any page,and it's too slow to visit as if my web host is down!
Please solve this problem as soon as posible.Maybe some website bear away all the bandwidth,or the server which my host in it was overload?
-----------------------------my first email to godaddy support team-------------------------------------
大致内容就是说这个虚拟主机在美国白天的访问速度很慢,是不是本台服务器承载了太多虚拟主机或者某个虚拟主机抢走了几乎所有的流量?到了对方上班的时间,他回复到:
-----------------------------the first response from godaddy support team-------------------------------------
Thank you for contacting online support.
During our review of your account, we did not see any issue with the operational status of your hosting account and your site appears to be setup and accessible without issue. I have tested with multiple browsers, and multiple connections and was not able to duplicate the latency that you have described.
Please let us know if you are still experiencing an issue along with a detailed account of the error message(s) or other symptoms and a list of steps taken to replicate the issue and we will be glad to further research this for you.
Please let us know if we can help you in any other way.
-----------------------------the first response from godaddy support team-------------------------------------
大致意思就是说他经过测试,没有发现我描述的状况(还使用多种浏览器测试= =)可是当天晚上服务器访问速度还是明显的减慢.于是我回复到:
-----------------------------my 2nd email to godaddy support team-------------------------------------
No,no,My host will be very slow from 20:00 to 24:00 in China(10:00
a.m.~2:00p.m. at the server location).I know the server will very busy at
this time but my host will be very very slow that sometimes I can't open a
page in 1minite! so you'd better test the server bandwidth at the "very busy
time",'cause my host is very fast when you're sleeping.
But thank you for your response.
-----------------------------my 2nd email to godaddy support team-------------------------------------
当我第二天早上醒来的时候发现邮箱已经收到了Godaddy的回复,这次内容很长,我先放上内容的主要部分:
-----------------------------the 2nd response from godaddy support team-------------------------------------
Unfortunately I am unable to duplicate any slowness on your website during the time frame you have mentioned. However, I did notice that you are currently utilizing Legacy hosting. If you find that load times for this type of hosting are unacceptable at times, you may wish to consider switching to Web Hosting.
-----------------------------the 2nd response from godaddy support team-------------------------------------
他说到:我还是不能检测出您的虚拟主机有什么毛病,如果你对我们的虚拟主机速度不满意,您可以尝试一下我们的别的Web Hosting..
余下的内容就是在推销我使用Grid Hosting,Grid Hosting vs Shared Hosting,我简直就不想看,也不想想我是个学生,也不想想人名币和美元的汇率,Grid Hosting根本offer不起= =
看来无解了,godaddy support team 否认我描述的情况,并且摆出一副"即使有你说的这种情况,也是你一分钱一分货买到东西的质量问题"的架子让我根本不知道要怎么回复他们了.算了.最近再测试一下这个虚拟主机,看看还会不会发生类似的事情.
老实说,国外的东西就是没有在国内的放心.
还不明白什么是接口(Interface)?看我能不能帮到你!
ASP.NET 2.0以上版本中(没接触过1.1)有一个非常有用的东西叫做接口(Interface).当初理解接口时花了不少时间.也在了解的过程中看到过误人子弟的文章和说法.由于Snake并非特别注重概念.所以我将以比喻方法向大家解释什么是接口,当然,遇到一些不严谨的地方我尽可能提醒您.
以下是接口在MSDN中的定义:
接口描述的是可属于任何类或结构的一组相关功能。接口可由方法、属性、事件、索引器或这四种成员类型的任意组合构成。接口不能包含字段。接口成员一定是公共的。
当类或结构继承接口时,意味着该类或结构为该接口定义的所有成员提供实现。接口本身不提供类或结构能够以继承基类功能的方式继承的任何功能。但是,如果基 类实现接口,派生类将继承该实现。
类和结构可以按照类继承基类或结构的类似方式继承接口,但有两个例外:
- 类或结构可继承多个接口。
- 类或结构继承接口时,仅继承方法名称和签名,因为接口本身不包含实现。
接口具有下列属性:
- 接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
- 不能直接实例化接口。
- 接口可以包含事件、索引器、方法和属性。
- 接口不包含方法的实现。
- 类和结构可从多个接口继承。
- 接口自身可从多个接口继承。
首先接口在好多人的第一印象中就是:”像USB接口那样”.很好,这就是一个入口点,此时我们虚拟一下我们的思路:
接口->USB接口?
我下一步想的是—USB接口可以连接MP3,可以连接U盘,可以连接USB台灯等等..
请注意,上面这个思路是错误的!我们要把注意力放在USB接口上,并且微观化!那么现在的思路是:
接口->USB接口?->USB接口的结构是什么?
据我所知,USB接口里有4个”针脚”,所能提供的仅仅只有5V直流电和数据交换通路,那么我们回归编程语言,这个USB接口用C#到底怎么写.
public interface IUsbHub
{
void Power();
void DataTransfer(object data, bool isPut);
}
(看到这里如果你要问”为什么不给接口里面的这两个方法写具体实现代码”的话,请您先搞清楚接口是如何定义的,再继续看这篇文章.)
好了.接口定义完了.这就是接口的指责,非常的简单.那么电脑就可以不管这个USB接口上插的是什么东东,只提供5V直流电和数据交换的通道.而到底传递的是什么数据?这就不是接口要管的东西,具体的数据应该交给别的硬件,比如主板.
看到这里您是不是感觉”好像明白了接口是什么,但是不清楚接口是干嘛的”?那么我现在再来举个例子告诉您接口是干什么用的.继续研究USB接口.现在再把思路展现出来就是这样的:
接口->USB接口?->USB接口的结构是什么?->接口是干什么用的?
上面已经定义过接口,现在就不重复定义了.我们现在要定义几个使用USB接口的产品.
MP3:
public class MP3 : IUsbHub
{
private bool hasPower = false;
public void Power()
{
hasPower = true;
}
public void DataTransfer(object data, bool isPut)
{
if (hasPower)
{
if (isPut)
{
Console.WriteLine("发送数据包予电脑中.");
Console.WriteLine("已发送" + data.GetType() + "类型的数据.");
}
else
{
Console.WriteLine("接收来自电脑传来的数据中..");
Console.WriteLine("已接收" + data.GetType() + "类型的数据.");
}
}
else
{
Console.WriteLine("无法连接电脑,请检查电源是否打开.");
}
}
public void PlayMusic(int musicIndex)
{
Console.WriteLine("正在播放第{0}首歌", musicIndex);
}
}
这个MP3在通入电源的时候(也就是执行Power函数)的时候会使其私有变量hasPower成为true,这样就代表MP3已经通电了.可以工作了.而执行DataTransfer方法的时候,先会判断就是已经连接电源了,然后再进行进一步与电脑进行数据交互.
USB台灯:
public class UsbLight : IUsbHub
{
private void Light()
{
Console.WriteLine("电灯被点亮了");
}
public void Power()
{
Light();
}
public void DataTransfer(object data, bool isPut)
{
}
}
这个USB台灯显然就简单的多,虽然执行了DataTransfer方法,但并没有任何代码实现.说明台灯是不需要跟电脑交互的,只需要电源供电就好.
那么现在我们写一个”电脑”这个类,负责调用这两个USB产品.先给代码,再在代码里用注释说明.
public class Computer
{
//首先,电脑在不经过数据交换是不可能知道插在USB接口上的东东是啥
//所以我们要把这个USB产品定义为IUsbHub类型
//当然既然成为了IUsbHub类型就只能执行IUsbHub已有的方法--那就是Power()和DataTransfer(object data,bool isPut)了.
protected void Main()
{
IUsbHub mp3 = new MP3();
mp3.DataTransfer(1, true);
mp3.Power();
mp3.DataTransfer(1, true);
mp3.DataTransfer("1", false);
//我们此时无法执行MP3的PlayMusic这个方法,因为这个方法和USB接口没啥关系.
IUsbHub light = new UsbLight();
light.Power();
//至于USB台灯就没啥好讲了.
}
}
好了,到此我们知道接口应该怎么用或者干啥用的了,那么什么时候用到接口才比较好呢?
本来想讲些概念再说例子的,但是肚子里的墨水不够多,有些观念不知道正确错误,所以不好说出口,但是下面的例子一定是最好的说明:
我们知道GridView是一个强大的数据源控件,那么GridView在我们定义GridView.DataSource的时候按F12进入GridView代码中,点开DataSource的注释说明可以看到下面这句话.
// 返回结果:System.Collections.IEnumerable 或 System.ComponentModel.IListSource 对象,包含用于为此控件提供数据的值的集合。默认值为null。
也就是说GridView的数据源必须实现了IEnumerable或者IListSource接口,而这两个接口必须实现的方法就是可以一一列举出该类型的每一条数据.比如List,数组,SqlDataSource.GridView在显示数据时无非就是使用了一个foreach或for循环一条一条将数据源里面的数据枚举出来再加以显示.
上面的例子说明IEnumerable或者IListSource接口给与了实现这2个接口的类型以可枚举的能力.而这个枚举能力恰好就是GridView.DataSource所迫切需要的.
所以,接口就像一个通用的规范,规范其实现者必须拥有这个规范的具体内容.
举个例子,每个人都需要有身份证,那么只要你有了身份证我们就可以称你为持有中华人名共和国居民身份证的公民,只要你是合法公民,就是进行银行贷款的前提,而不管你是男人,女人,老人,小孩--这就扩大了限制范围,也更通用了.
扩大了限制范围的好处就是提高代码的利用率.就拿前面的GridView.DataSource来说吧.
我们知道 List,数组,SqlDataSource都可以作为它的数据源,如果脱离了接口来写这个DataSource的属性就得这样:
public List<object> SqlDataSource{}
public object[] SqlDataSource{}
public SqlDataSource SqlDataSource{}
public XmlDataSource SqlDataSource{}
不但麻烦,而且缺乏扩展性.这个GridView的DataSource不就是需要一个可枚举的类型将之一个一个提取出来并且绑定显示就可以了嘛(管他来的人是中国的还是美国的,是黑人还是白人,只要是人,那么就具有其他动物不可比拟的智商水平一样的道理).
接口就在这时候起到了非常强大的作用,我们称其为—可扩展性强.
这时再会想下USB接口,我只管提供电源和数据通路,那么实现IUsbHub这个接口的东西也一定需要电源支持和与电脑进行数据交互.那么电脑就可以通过USB接口与连接在USB接口上的任何一个东东进行电源输送和数据交互.而不管差在USB接口上的东东到底能不能播放MP3—这明显不是电脑要担心的事情.
再说人就要懵了.希望能帮到你
如果有什么不对的地方欢迎指出,我会立即更正!
Chrome出现0xc00000a5的错误的解决办法
前几天明明能打得开的chrome网页浏览器这两天竟然出现了下面问题:—————————
Windows – 应用程序错误
—————————
应用程序无法正常启动(0xc00000a5)。请单击“确定”关闭应用程序。
—————————
确定
—————————
解决办法是有,就是在chrome的快捷方式后面添加"--no-sandbox"(no的前面有两个减号).可是不能用chrome的沙箱总是感觉怪怪的,明明前两天还能用的啊!
思考了一会儿,有结论了.我们知道 chrome是基于webkit内核的浏览器,基于webkit的浏览器还有著名safari和国内的双核浏览器Sogou Explorer 2.x,今天还貌似出了个世界之窗chrome内核版本.这都是跟chrome本身是有冲突的.而我前两天图新鲜,装了个Sogou Explorer 2.0就与chrome内核发生冲突了.
于是我果断的删除了Sogou Explorer 2.0,而结果恰恰在我预料之中.OK,问题解决完了,可是chrome的"体质"可就令人郁闷了.话说堂堂世界互联网巨头的浏览器也能随随便便被一个同内核的浏览器给搞的半残废,这是google员工应该解决的一个小问题.
而在国外的网站里我也搜索到了有用户因为safari而无法启动chrome的原因.
好了,问题解决了.但是,我可不保证如果你的chrome浏览器出了这个问题,就一定是这个原因,不好说的:)
希望能帮到您!
TyreStoreBase说明文档
本文是TyreStore(轮子仓库)的基础函数库说明文档.若您还不清楚TyreStore是干啥的,请移步至前面的链接.
TyreStoreBase是一个特殊层,夹在jQuery和TyreStore具体插件中间.为什么要产生这个看似有点麻烦的层呢?主要原因为提高代码的可复用性,缩减单个插件的代码量.
就按照drag这个非常基础的方法来说,jQuery并没有提供具体的方法给我们调用,而很多函数都需要用到drag方法,因此我把这一个基础的函数写在独立的代码里面,方便所有的插件调用.
那么目前为止,在我写的5个小插件当中,我总共分离出了3个基础函数,他们分别是:
- drag(拖动)
- globalOverlay(全局遮罩层)
- mousewheel(鼠标滚轮事件处理)//此代码非本人所写,代码来由Brandon Aaron所写,链接在这里.
首先TyreStore的任何代码均基于jQuery框架自主编写完成,并享有著作权,且遵循MIT与GPL v2协议.
其次.TyreStoreBase的函数都以"_$"开头.
最后.我个人觉得目前这个基础函数写的还是不够基础,修改和整合的余地非常大.
选项.
我就摘抄一下Source里面的部分内容,其实Source里注释的很清楚了.
var defaults = {
xMove: true, //横向拖动--boolean
yMove: true, //纵向拖动--boolean
onDrag: function() { }, //当鼠标按下刚开始拖动的回调函数--function
drag: function() { }, //当拖动时的回调函数--function
stopDrag: function() { }, //当鼠标松开刚停止拖动的回调函数--function
region: { //限制区域(以含有position:relative的父级容器为准)--object
top: -1, //上 右 下 左--numeric
right: -1,
bottom: -1,
left: -1
},
tick: 0, //按照网格拖动,该数值为网格的大小--numeric
realDrag: true //真实的拖动?(我在怀疑是否改将这属性去掉,但是有些人说不定就会用到拖动时鼠标的坐标,而并不想让物体移动.比如我XD)--boolean
};
事件.
其实在开始拖动,拖动进行时,完成拖动的3个回调函数中,都带有一个参数它的类型是object,其中包含7个对象:
- drawable(是否可以被拖动)
- curx(鼠标开始拖动时的X坐标)
- cury(鼠标开始拖动时的Y坐标)
- absx(鼠标当前对于window的绝对X坐标)
- absy(不多做解释)
- x(当前元素对于window或含有position:relative的父级元素的X坐标)
- y(不多做解释)
怎么用,就靠您的想象啦!
使用方法.
//超超超超简单.只要这样
$("#elem")._$drag();
//当然,也可以这样
$("#elem_extend")._$drag({
region:{top:0,right:200,bottom:200,left:0}
//写上上面任何一个参数.
});
选项.
- speed(显示速度)--numeric(基本单位毫秒)
- handleEvent(绑定处理事件)--object.其中有2个对象,一个是绑定的事件名称:event,另一个是绑定的事件处理函数:callback
- opacity--numeric(0~1)
用法.
//也是很简单.
$._$globalOverlay();
//全面的用法是:
$._$globalOverlay(
300,
{
event:"click",
callback:function(){
//alert("You clicked me!");
},
0.6
);
//最后还有一个隐藏遮罩层的办法:
$._$globalOverlay.hide();
本文修改于2010-2-14
