1,client与server的消息体的设计
既然client和server之间的通信是不可避免的。那需要先定义通信消息的数据格式。第一个被定义的是登陆消息格式。如下:
[DataContract]
public class LogonFormat
{
[DataMember]
public string LogonPerson { get; set; }
[DataMember]
public string IPPortServiceString { get; set; }
}
这里只有用户的用户名和其ip地址。 public class LogonFormat
{
[DataMember]
public string LogonPerson { get; set; }
[DataMember]
public string IPPortServiceString { get; set; }
}
第二个被定义的是交谈消息的格式。如下:
[DataContract]
public class ChatFormat
{
//ID
[DataMember]
public string Sender { get; set; }
[DataMember]
public string MsgBody { get; set; }
//TODO:When chat one to one
//"" if this message is not to a given person
[DataMember]
public string Receipter { get; set; }
//Time flag, indicate daytime or night
[DataMember]
public TimeFlag TimeFlag { get; set; }
}
最后一个被定义消息体是,voteforamt,如下:
[DataContract]
public class VoteFormat
{
[DataMember]
public string Voter { get; set; }
[DataMember]
public string Condidate { get; set; }
//Time flag, indicate daytime or night
[DataMember]
public TimeFlag TimeFlag { get; set; }
}
public class VoteFormat
{
[DataMember]
public string Voter { get; set; }
[DataMember]
public string Condidate { get; set; }
//Time flag, indicate daytime or night
[DataMember]
public TimeFlag TimeFlag { get; set; }
}
无论是用户杀人还是投票,都可以需要向服务器端发送消息。我们把杀人和投票这两个行为需要的消息体进行了整合。用timeflog来区别他们。当是白天时,那就是投票,而黑夜时,就是杀人。
2,游戏逻辑从实体对象的转移
有了这些数据格式的定义,该做的就是通信方法的定义了。定义如下接口:
通信接口的定义
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace ServerLib
{
// NOTE: If you change the interface name "IMessageDistribute" here, you must also update the reference to "IMessageDistribute" in Web.config.
[ServiceContract(Namespace = "KillPersonServerApp", Name = "MessageDistribution")]
public interface IMessageDistribute
{
//nTime:Daytime and night, in Daytime, all person can send message to anyone
// At night, only bad guys can send messages to those persons who are also bad persons
//nSenderID: The IdentityID of the sender.
//According to the time and the role of sender, the server host decides which persons this messages will be send to.
//-------------------------------------------
//| Time | Sender Role | Reciever |
//| DayTime | Civilian | All |
//| DayTime | Bad person | All |
//| DayTime | Dead person | All |
//| Night | Civilian | None |
//| Night | Bad person | Bad person|
//| Night | Dead person | None |
//-------------------------------------------
[OperationContract]
bool DistribChatMsg(ChatFormat chat);
//-------------------------------------------
//| Time | Sender Role | Valid? |
//| DayTime | Civilian | Yes |
//| DayTime | Bad person | Yes |
//| DayTime | Dead person | No |
//| Night | Civilian | No |
//| Night | Bad person | Yes |
//| Night | Dead person | No |
//-------------------------------------------
[OperationContract]
bool DistribVoteMsg(VoteFormat vote);
//For server to broadcast news to client.
[OperationContract]
List<string> Logon(LogonFormat logon);
}
}
这个接口有三个方法。登陆,发送消息,和投票。在这里,游戏的逻辑继续被简化。首先,没有登陆游戏,也没有创建游戏,更没有开始游戏和退出游戏。那这些逻辑都在那里呢?这些逻辑在用户登陆server时,server自己判断。一旦人数超过了设定的人数,server自动创建一个游戏,并启动这个游戏。这里的简化和前面的设计有关,即server只维系一个game.这里依然将来需要做大的彻底更改的地方。 using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace ServerLib
{
// NOTE: If you change the interface name "IMessageDistribute" here, you must also update the reference to "IMessageDistribute" in Web.config.
[ServiceContract(Namespace = "KillPersonServerApp", Name = "MessageDistribution")]
public interface IMessageDistribute
{
//nTime:Daytime and night, in Daytime, all person can send message to anyone
// At night, only bad guys can send messages to those persons who are also bad persons
//nSenderID: The IdentityID of the sender.
//According to the time and the role of sender, the server host decides which persons this messages will be send to.
//-------------------------------------------
//| Time | Sender Role | Reciever |
//| DayTime | Civilian | All |
//| DayTime | Bad person | All |
//| DayTime | Dead person | All |
//| Night | Civilian | None |
//| Night | Bad person | Bad person|
//| Night | Dead person | None |
//-------------------------------------------
[OperationContract]
bool DistribChatMsg(ChatFormat chat);
//-------------------------------------------
//| Time | Sender Role | Valid? |
//| DayTime | Civilian | Yes |
//| DayTime | Bad person | Yes |
//| DayTime | Dead person | No |
//| Night | Civilian | No |
//| Night | Bad person | Yes |
//| Night | Dead person | No |
//-------------------------------------------
[OperationContract]
bool DistribVoteMsg(VoteFormat vote);
//For server to broadcast news to client.
[OperationContract]
List<string> Logon(LogonFormat logon);
}
}
在 杀人游戏系列 之二 中,有游戏的逻辑图。在投票和杀人整合后,用户登陆以及启动游戏的地方再一次做了简化。刚才提到的投票和杀人的逻辑,事实是在 DistribVoteMsg发放中完成的。而 DistribChatMsg只负责单纯的交谈,并没有太多的游戏逻辑。写到这里,整个游戏设计思路就该明了了。游戏中出现的实体类,只是被驱动的对象,而不是游戏逻辑的载体。游戏逻辑被封装到了wcf层面的通信上了。 而与数据库的交互,被单独做成了静态的方法,独自行成一个类库。这些方法承载了部分的游戏逻辑。但又相互独立。在这个类库里,实体类依然只是传递数据,而不封装游戏逻辑。或许这与传统的oo方法有出入,但走linq To Sql这条路,这样的设计才能发挥其优势。
3,UI的设计
既然游戏的逻辑被简化,ui上也就不会有太多的麻烦。一个登陆框,加一个游戏的主界面。在这个游戏的主界面中,左边显示用户。中间是对话框,下面是消息输入框和杀人及投票的按钮。如图
如果,不简化游戏逻辑,还要增加游戏大厅里的对话框,以及创建game,加入game,开始game和退出game等。
4,一些新鲜设计或技术点
4.1 我们知道wcf只是一个类库,它实际上需要一个宿主来提供它的服务。我们在这里建立了一个console 的application来提供wcf的服务。但是,消息是单向的,只能是client从server请求,怎么样才能把消息从server端发到client端呢?这里,client也不得不启动一个wcf的服务,来接收从server的消息。这时,server是做为普通client端。
4.2 wcf的宿主还起到了另一个功能,初始化数据库。通常的数据库的项目。布置和配制数据库是相当的麻烦。这里,程序在启动服务时,先检查数据是否存在,如果数据库不存在,它讲初始化数据库。当然,这里也是人写好的。如下:
初始化数据库
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DAL;
using Utility;
using System.Reflection;
namespace ServerHostApp
{
internal static class Database
{
internal static void InitialDatabase()
{
using (DataMappingDataContext db = new DataMappingDataContext(true))
{
if (!db.DatabaseExists())
{
// create new database
db.CreateDatabase();
///我们不打算支持用户注册,初始化用户#region///我们不打算支持用户注册,初始化用户
User u1 = new User();
u1.UserID = "tom";
u1.Name = "tom";
u1.Password = "tom";
User u2 = new User();
u2.UserID = "boler";
u2.Name = "boler";
u2.Password = "boler";
User u3 = new User();
u3.UserID = "peter";
u3.Name = "peter";
u3.Password = "peter";
User u4 = new User();
u4.UserID = "sean";
u4.Name = "sean";
u4.Password = "sean";
User u5 = new User();
u5.UserID = "cloudy";
u5.Name = "cloudy";
u5.Password = "cloudy";
db.Users.InsertOnSubmit(u1);
db.Users.InsertOnSubmit(u2);
db.Users.InsertOnSubmit(u3);
db.Users.InsertOnSubmit(u4);
db.Users.InsertOnSubmit(u5);
#endregion
/**//// 初始化Role
foreach (var item in Enum.GetValues(typeof(RoleFlag)))
{
Role role = new Role();
role.RoleID = (int)item;
role.Name = item.ToString();
db.Roles.InsertOnSubmit(role);
}
/**////初始化gamestatus
foreach (var item in Enum.GetValues(typeof(EnumGameStatus)))
{
GameStatus gamestatus = new GameStatus();
gamestatus.StatusID = (int)item;
gamestatus.StatusName = item.ToString();
db.GameStatus.InsertOnSubmit(gamestatus);
}
/**//// 初始化ActivityType
foreach (var item in Enum.GetValues(typeof(ActivityTypeFlag)))
{
ActivityType ac = new ActivityType();
ac.ActivityTypeID = (int)item;
ac.ActivityTypeName = item.ToString();
ac.TimeCollapse = 1;
if (item.ToString() == "Vote")
{
ac.TimeCollapse = 4;
}
db.ActivityTypes.InsertOnSubmit(ac);
}
db.SubmitChanges();
Console.WriteLine("Create database done!!");
}
}
}
}
}
4.3 wpf中的回调 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DAL;
using Utility;
using System.Reflection;
namespace ServerHostApp
{
internal static class Database
{
internal static void InitialDatabase()
{
using (DataMappingDataContext db = new DataMappingDataContext(true))
{
if (!db.DatabaseExists())
{
// create new database
db.CreateDatabase();
///我们不打算支持用户注册,初始化用户#region///我们不打算支持用户注册,初始化用户
User u1 = new User();
u1.UserID = "tom";
u1.Name = "tom";
u1.Password = "tom";
User u2 = new User();
u2.UserID = "boler";
u2.Name = "boler";
u2.Password = "boler";
User u3 = new User();
u3.UserID = "peter";
u3.Name = "peter";
u3.Password = "peter";
User u4 = new User();
u4.UserID = "sean";
u4.Name = "sean";
u4.Password = "sean";
User u5 = new User();
u5.UserID = "cloudy";
u5.Name = "cloudy";
u5.Password = "cloudy";
db.Users.InsertOnSubmit(u1);
db.Users.InsertOnSubmit(u2);
db.Users.InsertOnSubmit(u3);
db.Users.InsertOnSubmit(u4);
db.Users.InsertOnSubmit(u5);
#endregion
/**//// 初始化Role
foreach (var item in Enum.GetValues(typeof(RoleFlag)))
{
Role role = new Role();
role.RoleID = (int)item;
role.Name = item.ToString();
db.Roles.InsertOnSubmit(role);
}
/**////初始化gamestatus
foreach (var item in Enum.GetValues(typeof(EnumGameStatus)))
{
GameStatus gamestatus = new GameStatus();
gamestatus.StatusID = (int)item;
gamestatus.StatusName = item.ToString();
db.GameStatus.InsertOnSubmit(gamestatus);
}
/**//// 初始化ActivityType
foreach (var item in Enum.GetValues(typeof(ActivityTypeFlag)))
{
ActivityType ac = new ActivityType();
ac.ActivityTypeID = (int)item;
ac.ActivityTypeName = item.ToString();
ac.TimeCollapse = 1;
if (item.ToString() == "Vote")
{
ac.TimeCollapse = 4;
}
db.ActivityTypes.InsertOnSubmit(ac);
}
db.SubmitChanges();
Console.WriteLine("Create database done!!");
}
}
}
}
}
在登陆界面后,需要显示主游戏界面。而此时,牵扯到wpf中的回调。如下:
wpf中的回调
private void ShowMainDailog()
{
var lstGame = App.gameMainWindow;
if (!lstGame.CheckAccess())
{
lstGame.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new System.Threading.ThreadStart(this.ShowMainDailog));
}
else
{
App.gameMainWindow.Show();
App.gameMainWindow.labelClientName.Content = App.currentClientName;
App.gameMainWindow.imageTimeFlagMoon.Visibility = Visibility.Hidden;
App.gameMainWindow.imageTimeFlagSun.Visibility = Visibility.Hidden;
}
}
在这里, ShowMainDailog函数体内,需要再一次调用 ShowMainDailog函数。主要是因为 !lstGame.CheckAccess()条件成立,即form是不可以访问的。当第二次调用时,就走else字句了。(说实话,这是wpf里的技术,我也晕着呢。) private void ShowMainDailog()
{
var lstGame = App.gameMainWindow;
if (!lstGame.CheckAccess())
{
lstGame.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new System.Threading.ThreadStart(this.ShowMainDailog));
}
else
{
App.gameMainWindow.Show();
App.gameMainWindow.labelClientName.Content = App.currentClientName;
App.gameMainWindow.imageTimeFlagMoon.Visibility = Visibility.Hidden;
App.gameMainWindow.imageTimeFlagSun.Visibility = Visibility.Hidden;
}
}
5 结语
可能会让好多人失望,没有谈wpf和wcf技术。但是,谈游戏才是本篇的主题。技术是为这个主题服务的。在这里,游戏逻辑因为时间上的因素被简化依然让笔者心痛。这是个比赛,需要在第三天的时候,整个程序就能跑起来,剩下的2天调试排错。这无疑让我们不得忍痛割爱。在以后的日子里,如果时间允许的话,笔者还是会加上被砍掉的游戏逻辑。 放出所有的代码,供参考。(大家需要注意的是,我和peter最近再做修改,希望能添加上被砍掉的部分,所以,代码和blog里有些出入。)
上一篇
杀人游戏系列 之二
杀人游戏系列 之一
其他:
Linq To Sql进阶系列 -目录导航
C# 3.0入门系列-目录导航