上回说到Server少爷和Client小姐好不容易踏入婚姻的殿堂,洞房花烛之夜,Client小姐却要Server少爷签下婚后协议。Server一脸不快:“都一家人还签什么协议啊?”Client道:“你们男人啊,就是花心,不看紧点,不知道跑那野去了。为了以后我们能琴瑟相合,还是签了协议的好。”Server呵呵一笑:“好老婆,那就签吧,你开心的时候,我就陪你开心;你不开心的时候,我会哄你开心...”一番话哄得Client心花怒放,看着Server写完“奴隶宣言”,拿到手里仔细查看,却见Server写到:
{
哄老婆开心,
上交工资,
陪老婆逛街,
带老婆去兜风
......
}
Client看到这里,笑颜如花,连enum都用上了,真是个好夫君,这美满的婚姻也真是来之不易。
-----------------------------------------------分割线,以上Server和Client的爱情剧完美剧终---------------------------------
前五章,我们用讲故事的方式把Server端和Client的通讯机制讲解了下。此后我们要在此基础上开始我们的Web传奇的开发之路,让我们感谢Server和Client的精彩演出。
每一个玩过传奇的人都记得那古朴的登录画面,当我们输入账号和密码,点击登录,随着铿锵的击鼓声,一扇石门徐徐而开,一个游戏的世界在向我们召唤,让我们赶快开启我们的传奇之旅吧!!
游戏的开始,我们需要注册账号。这账号数据放在那里呢?放客户端,显然不可能,用户数据太不安全了。只好放服务端了,再说用户账号数据可是很重要,那些网游公司为此争得头破血流。经典的案例那是网易从九城夺取“魔兽世界”的代理权,期间坎坎坷坷,勾心斗角,俨然一部商业大片。还是“魔兽世界”的开发公司暴雪大神有远见,所有的用户数据都是属于暴雪的,这500W用户账号是属于暴雪公司的,数据库里的这些数据早就在停服前被暴雪的技术人员备份走了。
所以说我们做Web传奇也要有远见,这个账号信息呢,还是归我williams所有吧,我把它们都储存在Sql Server2005数据库里呢。
一张简单的账号表如下:
账号信息表 Users | |||
字段名称 |
数据类型 |
含义 |
约束 |
UserID |
int |
|
|
UEmail |
Nvarchar(32) |
|
|
Upwd |
Nvarchar(16) |
|
|
UaddTime |
DateTime |
|
|
Ubalance |
int |
用户账号金额,用来消费 |
|
UloginTime |
DateTime |
|
|
UloginIP |
Nvarchar(32) |
|
|
Ustate |
int |
账号状态 |
|
看到这里,你们会说这咋个和做网站的差不多呀?反正也不用忽悠大家,WebGame上的注册和网站注册有啥区别呢?本质上没什么区别,不就是把用户输入数据写到数据库里。
用asp.net做过网站的都知道,做一个注册页面很简单,双击按钮写事件,调用BLL层的方法,执行insert语句就OK。但是在silverlight里,客户端不能直接和数据库打交道,怎么办呢?
如果你看了前五章,你很容易想到客户端可以把用户数据放到byte[]数组里,封装到一个Message里发给服务端就可以了。服务端收到这个Message,判定后再调用BLL层的方法执行Insert语句也就可以了。和asp.net网站不同的是,在silverlight WebGame里数据是从Silverlight客户端发送到服务端,而在网站里数据是通过表单提交到Web服务端,asp.nett网站表单提交数据的过程作一个asp.net程序员可以不关心是怎么传递的,但是对于silverlight客户端来说,由于选用了原生态的Socket的通讯方式,我们得了解清楚些。
看到这里,你可能豁然开朗,前五章讲的那肉麻的爱情故事就是为了方便的传递数据啊。哎,谁让我们这些asp.net程序员比较少接触到Socket通讯方式呢,我也是花了一周才看个明白点。
罗嗦了这么多,让我们开始设计界面吧,我这个人很喜欢怀旧的,也没有美术功底,就把传奇里的界面搬来了,我在这里特此声明:本教程只供学习研究,所有素材来自网络,请勿跨省,勿发律师函。
注册界面:
我们先来完成注册界面的功能,玩一个游戏,如果不注册个账号,是没办法登陆进去的,玩过游戏的都知道。
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
{
Users user = new Users { UserName = this.txtUserName.Text, Upwd =GameCMDFormat.HashFormat(this.txtPwd.Password), Uemail = this.txtUserName.Text, Ustate = 1 };
MessagePool.AddSendMessage(GameCMDFormat.CreateNewUser(user));//把发送的消息放到消息队列里
}
①这里我们用一个User对象来保存了用户注册数据,这样才显得OO一些。
②这里我们写了一个GameCMDFormat类来产生Message对象
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
/// 新建账号命令
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
internal static SocketClient.Message CreateNewUser(Users user)
{
return new SocketClient.Message(Convert.ToByte(GameCmdEnums.CreateNewUser), 1, System.Text.UTF8Encoding.UTF8.GetBytes(SerializeUtil.Encoder<Users>(user)));
}
让我重点来介绍下这个GameCMDFormat类,以后我们的命令基本上都是这个类帮我们产生的。
在构造返回的Message对象时,第一个参数是class参数,我们用来存放命令的类型,这里我们用到的就是enum,这就是我们的server少爷和client小姐签婚后协议用到的。女人都喜欢男人专一点,enum值是唯一的,刚好满足女人的要求。当然这协议嘛,男方女方手里都有一份才算有效。所以server端有这样的enum,client端也会有这样的enum
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
/// 客户端的请求,或服务器发给客户端的命令
/// </summary>
public enum GameCmdEnums
{
/// <summary>
/// 注册新账号
/// </summary>
CreateNewUser,
}
/// <summary>
/// 服务器执行结果
/// </summary>
public enum GameCmdResult
{
CreateUserTrue,
CreateUserFalse,
}
这协议一式两份,童叟无欺,大家都看得明白,无可非议,客户端端告诉服务端: CreateNewUser,服务端拿来相同的enum一比对,哦,原来是要我去建一个新账号,那好我就把新用户数据写入到数据库。写完了,服务端发送一个Message给客户端,这个Message带的命令是CreateUserTrue,客户端拿来enum一看,哦,账号建立成功了。
③这里我们用了SerializeUtil.Encoder<Users>(user)方法,这里用的是json序列化,为什么要用json序列化呢?你想啊,一个User对象,我怎么好放到byte[]数组里,这个比较难,不太好描述。如果你想想我们在.net里的序列化,我们可以把一个对象变成xml数据或者二进制流。
但是很多平常在.net framework里使用的序列化和反序列化帮助类(如System.Runtime.Serialization命名空间下的许多类和方法)在SilverLight下都不可使用了。谁让silverlight是个精简框架呢。
查下资料发现
Silverlight可以用序列化,但是不能直接使用,必须通过服务来调用,比如WCF和Web Service。
Silverlight 内置支持WCF的 DataContractSerializer 和 DataContractJsonSerializer 两种序列化方式,也可以用System.Xml.Serialization 里的 XmlSerializer 。
一般的过程是,通过服务提供序列化的对象,然后Silverlight调用服务获得这些对象。
请参考:Silverlight中的序列化
不过我们这里用的是socket,以上的方式不适合了,还有XML序列化的结果比json的数据量要大不少,为了减少通讯量,在描述同样数据的情况下我选择了json序列化,我在网上我找到了一个.net下的 json序列化组件:
Json.NET.
使用了里面的两个方法:
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
/// 对象序列化和反序列化,Json序列化
/// </summary>
public class SerializeUtil
{
/// <summary>
/// 序列化
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
public static string Encoder<T>(T data)
{
return JsonConvert.SerializeObject(data);
}
/// <summary>
/// 反序列化
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
public static T Decoder<T>(string data)
{
return JsonConvert.DeserializeObject<T>(data);
}
}
这样我们很方便把一个对象放变成string,再把string用 System.Text.UTF8Encoding.UTF8.GetBytes()方法变成byte[]存放到一个Message对象就可以了。
④我们用到了自己写的消息队列类MessagePool类里的AddSendMessage方法。
至于为什么要用队列来储存发送的消息,这是由于MMorpg客户端和服务端交换数据很频繁,大量的数据如果拥塞在一起发送就不太好,你说我们中国人就是多,春节买火车票如果我们不排队,都挤到售票窗口去抢,那不乱了套么?
在MessagePool类里,我用了多线程发送,这里就涉及到了队列queue<T>的线程不安全问题,如何保证线程安全,就要用到了lock锁定,至于是否会影响效率,JeffreyZhao在一篇文章里说,也许lock的影响微乎其微,我没有做实验来判定,有兴趣的同学检验下。
可能对于一些.net初学者来说在多个线程中保证queue<T>的线程安全不太好理解,那就多查查资料吧,我也是查资料,花了些时间写好的。
当然,客户端发送数据的同时,也是要接受数据的,所以MessagePool类里有发送队列也有接受队列。
对于MessagePool类我这里就不多讲了,留着专门一节再讲消息队列。我们本节的重点是要完成注册的过程。
以上四点只是为了说明我们要把一个Message对象组装起来放到消息队列里中间的过程也不简单,涉及到队列,序列化,线程安全,做游戏不容易,反正我觉得比做网站要难一些。
好了,我们折腾了这么久,那么服务端接收到这个客户端发送的这个消息后会这么做呢?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// 执行客户端发送来的请求
/// </summary>
/// <param name="cmdstr"></param>
internal static Message ExcuteCmd(int type, int flag, string cmdstr,System.Net.Sockets.Socket clientSocket)
{
GameServerCmd cmd = new GameServerCmd();
GameCmdEnums t = (GameCmdEnums)Enum.Parse(typeof(GameCmdEnums), type.ToString());
byte[] content = UTF8Encoding.UTF8.GetBytes(".");
switch (t)
{
case GameCmdEnums.CreateNewUser:
{
int userID = 0;
if (cmd.CreateNewUser(cmdstr))//执行注册命令
{
return new Message((byte)GameCmdResult.CreateUserTrue, (byte)userID, content, clientSocket);
}
else
{
return new Message((byte)GameCmdResult.CreateUserFalse, (byte)userID, content, clientSocket);
}
}
}
GameServerCmd 这是个命令执行类,它调用了BLL层里的方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// 注册新账号
/// </summary>
/// <param name="cmdstr"></param>
/// <returns></returns>
internal bool CreateNewUser(string cmdstr)
{
Users user = SerializeUtil.Decoder<Users>(cmdstr);
if (UsersCtrlBase.Instance().CreateNewUser(user))
{
return true;
}
else
{
return false;
}
}
写入账号信息到数据库后,然后发送一个注册成功的Message对象,我们也把它装到服务端的消息队列里等待发送给客户端,在服务端同理我们也有接收队列和发送队列,服务端接收和发送的队列是客户端的N倍,你可以近似估计N为客户端的个数,如果一个服1000人,那么就至少是1000倍于客户端。不开多线程,服务端估计是不能快速处理完这些数据。写到这里我想到,现在服务端早就是多核了,是不是要在服务端要到多核编程呢?把服务端多核优势发挥出来。这就留给大家去实现吧,我也是要花些时间研究下多核编程的。
不小心写了这么多,我喜欢写游戏,去年我下班后吃完饭就写Web传奇,中间遇到不少的困难,一路摸索,是梦想支撑着我,前方困难重重,我们只有披荆斩棘,努力前行。
我说过这篇文章里,我要把Demo提供给大家,由于持续花了近3个月的业余时间,写的代码有点多,从创建人物到下载数据,保存数据,合成地图,人物,怪物,再到即时的PK,中途还写WPF的地图编辑器,一下子把这些东西都发出来,估计看得也是头痛。我就一步步把这些代码精简出来吧。
下载文件列表:
1.数据库设计文档 2.json.net组件
5.怪物数据包 (重要:请放在MyMirWebGame.Web项目的clientbin里的Data文件夹下)
有兄弟反应MyMirWebGameDemo1无法下载,我做了分包文件。
Demo框架机构图:
Demo运行提示:
1.附加MyMirGameDB数据库
2.修改MyMirGameServer里的app.config里的connectstring数据库连接字符串
3.启动MyMirGameServer项目里bin/debug里的MyMirGameServer.exe程序
4.在IIS里hostMyMirWebGame.web站点。
5.账号 w,密码1;账号xiangwei,密码:1,当然也可以注册账号
6.游戏只实现了男战士职业,只完成了物理PK过程
7.只做了人物之间的PK互动,人与怪物间的PK互动还没写完。
Demo的客户端已经升级到SL4,建议使用vs2010,并下载 Silverlight 4 Tools RC2 for Visual Studio 2010.
Demo测试环境:win7+ms sqlserver2005+silverlight4;
局域网测试通过,感谢以前在武汉创美的同事们。
互联网测试通过,感谢"深蓝WPF/Silverlight(群号:73068105)"群里的兄弟们。
申明:Web传奇客户端代码是在“深蓝色右手”silverlight 游戏引擎基础上开发做了30%左右的变动,特此申明。
备注:代码文件已经上传,附上PK画面: