来咯!因为章节较多,先做小部分回顾,熟悉的朋友就直接跳过吧。
-----------回顾分割线-----------
此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的SOLID原则,提高对设计模式、重构的理解。
系列索引点这里,游戏整体流程介绍与技术选型点这里,之前做了一半的版本点这里(图文)
在设计业务对象与对象职责划分(1)中对之前做的版本进行了截图说明部分views的细节,相信大家已对游戏流程更熟悉了(我也回顾了一遍,包括游戏技巧很有意思~),本第(2)篇主讲MVC的M和C,深入剖析游戏业务在代码中的体现。如索引篇所说,大家的技术性吐槽与质疑是对其他读者和我最大的帮助;如上一篇所言,这个做了一半的版本坏味道很多,尤其从设计模式的角度看就更明显了(这个系列做完,我计划再写一个俄罗斯方块系列,俄罗斯方块是已经做好的C#+WinForm,但同样是坏味道严重,拓展不易,也才更需要update),所以才有了重写一遍捉鬼游戏,并想把整个项目建设过程同步更新到博客园,以给和我一样的初学者一个共同思考和进步的途径。我始终相信,知其然还要知其所以然,甚至和项目开发者同步思考过、犯过错、再一起探讨与改正过,会获得更深刻的经验。也欢迎朋友们勇敢的在最后把代码直接拿去说这是我和一位博客园朋友一期开发的小项目![自豪] 笔者对“开源”一词的理解还不太深刻,但我相信无偿的提供代码、写代码的心得,甚至写代码的全程思考会让大家比直接拿商用代码更有价值。
-----------回顾结束分割线-----------
-----------本篇开始分割线-----------
1. Models
业务类目录
从类目录可看出一共7个类,先从最辅助性的英雄(啊不是英雄,是类)Setting开始(已在上篇贴过代码)
(1)Setting(设置类)负责从Web.config获取游戏人数的设定(标配9人,开发测试3人),Setting作为全局访问点采用了singleton单例模式。
publicclassSetting
{PRivateint_civilianCount;privateint_GhostCount;privateint_idioCount;publicintIdioCount
{get{return_idioCount; }
}publicintGhostCount
{get{return_ghostCount; }
}publicintCivilianCount
{get{return_civilianCount; }
}publicintGetTotalCount()
{returnIdioCount + GhostCount +CivilianCount;
}//singletonprivatestaticSetting _instance;privateSetting()
{this._civilianCount = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["CivilianCount"]);this._ghostCount = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["GhostCount"]);this._idioCount = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["IdioCount"]);
}publicstaticSetting CreateInstance()
{if(_instance ==null)
{
_instance=newSetting();
}return_instance;
}
}
Setting
(2)Subject(题目类)负责从题库出题(题库系统未做,要做也是普通的增删改查即可,先暂写死在程序里),也是单例模式。
publicclassSubject
{//singletonprivatestaticSubject _instance;privateSubject()
{//Todo: get subject from dictionarythis._civilianWord="周星驰";this._idiotWord ="孙悟空";
}publicstaticSubject CreateInstance()
{if(_instance ==null)
{
_instance=newSubject();
}return_instance;
}privatestring_civilianWord;privatestring_idiotWord;publicstringIdioWord
{get{return_idiotWord; }
}publicstringCivilianWord
{get{return_civilianWord; }
}
}
Subject
(3)Table(游戏桌类)负责数桌上的人数以及开始或重开游戏【坏味道:职责过多】
代码过多,可先从贴图看起
Table游戏桌类
先看属性:可见也是单例模式(_instance),且存在与Game类的关联关系(_game),以及维护有一个audiences列表(_audiences,List<Audience>类型)
再看方法:先看第二个CheckTotalNum()——处理每次有人点击报名后,都核查一遍报名总人数是否与Setting设置类中的游戏人数相等,若想等则调用GameStart()方法。其他方法都是Add或Get开头的,想必看方法名就能理解了:都是获取所维护的_audiences列表中各个类型的角色的集合,举个带Without的GetAudiencesWithoutCivilians来说,就是获取除了Civilian子类中的其他Audiences,也就是旁听的有几人。
坏味道很明显:职责过多。(注意不是方法过多,而是类负责的职责过多。区别是:可能存在只有一个职责,但需要多个private私有方法来配合完成,这也是允许的)
代码还是贴一下吧,助于具体方法的理解。
publicclassTable
{//singletonprivatestaticTable _instance;privateTable() { }publicstaticTable CreateInstance()
{if(_instance ==null)
{
_instance=newTable();
}return_instance;
}//fieldprivateList<Audience> _audiences =newList<Audience>();privateGame _game;publicGame GetGame()
{returnthis._game;
}publicvoidAddAudience(Audience audience)
{if(!this.GetAudiences().Contains(audience))
{if(audienceisCivilian)
{this.GetAudiences().Add(audienceasCivilian);
CheckTotalNum();return;
}this.GetAudiences().Add(audience);
}
}publicvoidRemoveAudience(Audience audience)
{this._audiences.Remove(audience);
}privatevoidCheckTotalNum()
{if(this.GetCivilians().Count >=Setting.CreateInstance().GetTotalCount())
{
GameStart();
}
}//获取在线者publicList<Audience>GetAudiences()
{returnthis._audiences;
}//获取听众publicList<Audience>GetAudiencesWithoutCivilians()
{
List<Audience> result =newList<Audience>();foreach(Audience ainthis._audiences)
{if(!(aisCivilian))
{
result.Add(a);
}
}returnresult;
}//获取参与者publicList<Civilian>GetCivilians()
{
List<Civilian> result =newList<Civilian>();foreach(Audience ainthis._audiences)
{if(aisCivilian)
{
result.Add(aasCivilian);
}
}returnresult;
}//获取好人publicList<Civilian>GetCiviliansWithoutGhosts()
{
List<Civilian> result =newList<Civilian>();foreach(Civilian ainthis.GetCivilians())
{if(!(aisGhost))
{
result.Add(a);
}
}returnresult;
}//获取鬼publicList<Ghost>GetGhosts()
{
List<Ghost> result =newList<Ghost>();foreach(Audience ainthis._audiences)
{if(aisGhost)
{
result.Add(aasGhost);
}
}returnresult;
}//game startprivatevoidGameStart()
{this._game =newGame();this.GetGame().Start();
}//restartpublicvoidRestart()
{
_instance=null;
}
}
Table
(4)Audience(听众类)、Civilian(好人类)、Ghost(鬼类)存在继承关系
三个参与者身份类之间的继承
publicclassAudience
{privatestring_nickname;publicstringNickname
{get{return_nickname; }
}publicAudience(stringnickname)
{this._nickname =nickname;
}privateTable table;publicTable Table
{get{returntable; }set{ table =value; }
}publicstaticAudience CreateFromCivilian(Civilian civilian)
{returnnewAudience(civilian.Nickname) { Table =civilian.Table };
}
}
Audience
publicclassCivilian : Audience
{privatestring_word;publicstringWord
{get{return_word; }set{ _word =value; }
}privatebool_isAlive =true;publicboolIsAlive
{get{return_isAlive; }
}publicvoidSetDeath()
{this._isAlive =false;
}publicCivilian(stringnickname) :base(nickname) { }publicstaticCivilian CreateFromAudience(Audience audience)
{returnnewCivilian(audience.Nickname) { Table =audience.Table };
}publicstringSpeak(stringspeak)
{returnTable.CreateInstance().GetGame().RecordSpeak(speak.Replace("\r\n",""),this);
}
}
Civilian
publicclassGhost : Civilian
{publicGhost(stringnickname) :base(nickname) { }publicstaticGhost CreateFromCivilian(Civilian civilian)
{returnnewGhost(civilian.Nickname);
}
}
Ghost
Audience听众类:任何输入昵称(Audience听众类的nickname属性)的用户都会是Audience身份,Audience不能发言、看词、投票,只能看到发言内容的听众。细心的朋友一定发现了此类与Table游戏桌类的关联关系(所以关联关系是:Audience--Table--Game),类中还有一个CreateFromCivilian(从好人转为听众身份)的方法【坏味道:转换混乱】——负责处理已报名参加但在游戏开始前又想退出报名的用户。
Civilian好人类:继承了听众类的昵称属性,外加听众类没有的“词语”、“是否活着”属性,能做的动作有:被投死(SetDeath方法)、说话(Speak方法)、从听众转为好人的方法(CreateFromAudience)【坏味道:转换混乱】——在点击报名按钮后调用。
Ghost鬼类:唯一与好人类不同的是能从好人这个父类中转为鬼类(CreateFromCivilian)【坏味道:转换混乱】——在分配角色时,把随机抽到要当鬼的好人身份转化为鬼身份。
是不是很多坏味道了?YES,多的受不了了吧,首先Audience、Civilian、Ghost三个类之间的转化方法就受不了,转来转去、毫无秩序、混论一通;其次怎么没有Idiot白痴类(上面拼错了Idio,英文丢人又现),却在Setting设置类中有Idiot的人数记录、在Subject题目类中有Idiot的词语;再者Ghost和Civilian感觉就是一个标识的区别,甚至连标识属性都没有,只是换了一个类名;最后,好人类中出现的被投死方法不应在这里,因为自己是不能决定自己被投死的,应该由投票结果决定,而且这么做也显得好人的职责过多了。
没错,在上一篇看似还能玩的通的游戏背后,有那么多发臭的代码令人厌恶,不怕,坚持下去,就像整理混乱的书桌一样,要坚信最终的结果一定会帅自己一脸!
(5)Game(游戏类)
游戏类截图长的我就不想看,肯定【坏味道:职责过多】
不信你看~
Game游戏类
长的连图我都截不了...醉了醉了。
完整代码建议就更不要看了,想copy的拿走。
1publicclassGame2{3privateTable table =Table.CreateInstance();45//start6publicvoidStart()7{8SetGhostWithRandom();9SetWordWithRandom();10AppendLineToInter("鬼正在指定开始发言的顺序...");11AppendLineToGhostInter("已开启鬼内讨论,此时的发言只有鬼能看见...");12AppendLineToGhostInter("请指定首发言者...");13}1415privateStringBuilder _inter =newStringBuilder();16privateStringBuilder _ghostInter =newStringBuilder();17privatebool_isGhostSpeaking =true;18privatebool_isVoting =false;19privateCivilian _speaker;20privateCivilian _loopStarter;21privatebool_isFirstLoop =true;2223publicvoidSetStartSpeaker(Ghost ghost, Civilian speaker)24{25this._speaker =speaker;26this._loopStarter =speaker;27SetGhostSpeaked();28AppendLineToGhostInter(string.Format("{0} 选择了 {1} 作为首发言人", ghost.Nickname, speaker.Nickname));29}30privatevoidSetGhostSpeaked()31{32if(isGhostSpeaking())33{34this._isGhostSpeaking =false;35StartLooping();36}37}3839privatevoidStartLooping()40{41ClearInter();42AppendLineToInter("从"+this.GetCurrentSpeaker().Nickname +"开始发言...");43}4445publicvoidSetNextSpeaker()46{47List<Civilian> list =this.GetAliveCivilian();48for(inti =0; i < list.Count; i++)49{50Civilian current =list[i];51if(current ==this.GetCurrentSpeaker())52{53if(i == list.Count -1)54{55this._speaker = list[0];56CheckEndLoop();57return;58}59this._speaker = list[i +1];60CheckEndLoop();61return;62}63}64}6566privatevoidCheckEndLoop()67{68if(GetCurrentSpeaker() ==this._loopStarter)69EndLoop();70}7172privatevoidEndLoop()73{74Thread.Sleep(30000);75if(this._isFirstLoop)76{77this._isFirstLoop =false;78AppendLineToInter("第二轮开始");79return;80}81AppendLineToInter("30秒后开始投票");82ClearInter();83SetVoting();84}8586publicCivilian GetCurrentSpeaker()87{88returnthis._speaker;89}9091publicboolisGhostSpeaking()92{93returnthis._isGhostSpeaking;94}9596publicboolisVoting()97{98returnthis._isVoting;99}100101privatevoidSetVoting()102{103this._isVoting =true;104AppendLineToInter("开始投票");105}106107privatevoidSetVoted()108{109this._isVoting =false;110}111112publicstringRecordSpeak(stringspeak, Civilian civilian)113{114if(isGhostSpeaking())115{116if(civilianisGhost)117{118AppendLineToGhostInter(FormatSpeak(civilian.Nickname, speak));119return"ok";120}121return"天黑请闭眼!";122}123AppendLineToInter(FormatSpeak(civilian.Nickname, speak));124SetNextSpeaker();125return"ok";126}127128privatestringFormatSpeak(stringname,stringspeak)129{130returnstring.Format("【{0}】:{1}", name, speak);131}132133publicList<Civilian>GetAliveCivilian()134{135returnTable.CreateInstance().GetCivilians().Where(c =>c.IsAlive).ToList();136}137138publicstringGetGhostInter()139{140returnthis._ghostInter.ToString();141}142143publicstringGetInter()144{145returnthis._inter.ToString();146}147148privatevoidAppendLineToInter(stringline)149{150this._inter.AppendLine(line);151}152privatevoidAppendLineToGhostInter(stringline)153{154this._ghostInter.AppendLine(line);155}156157privatevoidClearInter()158{159this._inter.Clear();160this._ghostInter.Clear();161}162163privatevoidSetGhostWithRandom()164{165Random rd =newRandom();166while(true)167{168for(inti =0; i < Setting.CreateInstance().GetTotalCount(); i++)169{170Civilian c =table.GetCivilians()[i];171if(cisGhost)continue;172introle = rd.Next(1,4);//1/3几率173if(role ==1&& table.GetGhosts().Count <Setting.CreateInstance().GhostCount)174{175Ghost ghost =Ghost.CreateFromCivilian(c);176table.RemoveAudience(c);177table.GetAudiences().Insert(i, ghost);178}179}180181if(table.GetGhosts().Count >=Setting.CreateInstance().GhostCount)182
break;183}184}185//set word186privatevoidSetWordWithRandom()187{188Random rd =newRandom();189foreach(Civilian cintable.GetCivilians())190{191if(cisGhost)192{193SetGhostWord(casGhost);194continue;195}196197introle = rd.Next(4,10);198if(role <=7)199{200//4,5,6,7201SetCivilianWord(c);202}203else204{205//8,9206SetIdioWord(c);207}208}209}210privatevoidSetGhostWord(Ghost g)211{212if(HasWord(g))return;213g.Word =string.Format("鬼({0}字)", Subject.CreateInstance().CivilianWord.Length);214}215privatevoidSetCivilianWord(Civilian civilian)216{217if(HasWord(civilian))return;218stringword =Subject.CreateInstance().CivilianWord;219List<Civilian> list =table.GetCiviliansWithoutGhosts();220if(list.Where(c => c.Word == word).Count() <Setting.CreateInstance().CivilianCount)221{222civilian.Word =word;223return;224}225SetIdioWord(civilian);226}227privatevoidSetIdioWord(Civilian civilian)228{229if(HasWord(civilian))return;230List<Civilian> list =table.GetCivilians();231stringword =Subject.CreateInstance().IdioWord;232if(list.Where(c => c.Word == word).Count() <Setting.CreateInstance().IdioCount)233{234civilian.Word =word;235return;236}237SetCivilianWord(civilian);238}239privateboolHasWord(Civilian c)240{241return!string.IsNullOrEmpty(c.Word);242}243}
Game
按游戏顺序理解Game的职责:
(1)负责随机分配角色
SetGhostWithRandom()
(2)负责分配词语
SetWordWithRandom(), SetGhostWord(), SetCivilianWord(), SetIdioWord(), HasWord():bool
(3)负责设置发言者
SetStartSpeaker(), SetNextSpeaker(), GetCurrentSpeaker()
(4)负责鬼讨论
isGhostSpeaking():bool, SetGhostSpeaked()
(5)开始轮流发言、记录发言
StartLooping(), RecordSpeak(), FormatSpeak(), GetInter(), GetGhostInter(), AppendLineToInter(), AppendLineToGhostInter()
(6)负责检查此人发言后是否是本轮发言结束
CheckEndLoop(), EndLoop()
(7)负责投票
isVoting(), SetVoting(), SetVoted()
上述七个责任分配还不是很明确,且明显职责多的都要爆炸了,必须在新版中分离。
2. Common:(较为简单不赘述)
WebCommon负责获取各种session
publicstaticclassWebCommon
{publicstaticAudience GetAudienceFromSession()
{returnHttpContext.Current.Session["player"]asAudience;
}publicstaticCivilian GetCivilianFromSession()
{returnHttpContext.Current.Session["player"]asCivilian;
}publicstaticGhost GetGhostFromSession()
{returnHttpContext.Current.Session["player"]asGhost;
}publicstaticvoidRenewPlayerSession(Audience newAudience)
{
HttpContext.Current.Session["player"] =newAudience;
}publicstaticvoidAddPlayerSession(Audience audience)
{
HttpContext.Current.Session.Add("player", audience);
}publicstaticvoidRemovePlayerSession()
{
HttpContext.Current.Session.Remove("player");
}
}
WebCommon
AudienceFilterAttribute负责过滤听众,即区分玩家与旁观者的操作许可和界面显示内容
publicclassAudienceFilterAttribute : ActionFilterAttribute
{publicoverridevoidOnActionExecuting(ActionExecutingContext filterContext)
{
HttpContextBase context=filterContext.HttpContext;if(context.Session["player"] ==null)
{
context.Response.Redirect("/");return;
}base.OnActionExecuting(filterContext);
}
}
AudienceFilterAttribute
过滤效果在Controller中调用
[AudienceFilter]publicclassPlayController : Controller
{//...}publicclassHomeController : Controller
{
[HttpPost]
[AudienceFilter]publicActionResult Logout(){//...}[AudienceFilter]publicActionResult Signout(){//...}[AudienceFilter]publicActionResult Restart(){//...}}
[AudienceFilter]
3. Controllers
(1)HomeController:负责登陆、登出,处理Session问题
HomeController
Logout():“退出报名”或“退出旁观”,回到刚输入昵称进入桌子的界面。
Signout():“完全退出”登陆这个应用程序,回到输入昵称前的界面。
Restart():重新开始一局,保持当前的身份(报名/旁观)
(2)PlayController:负责选择“报名”或“旁观”后的页面
PlayController
其中,获取参与者人数GetCivilians(), 获取旁听者人数GetAudiencesWithoutCivilians(), 获取对话记录GetInter(), 获取游戏是否开始的状态GetGameState(), 获取投票区域GetVoteArea(),上述都是for eventsource——使用HTML5的服务器发送事件技术处理的,提供前台页面定时刷新的内容,当然,有的方法只在特定的时间获取(如获取游戏状态,游戏开始后就不会再调此方法了)。
获取词的方法是GetWord(),采用Ajax完成(只调一次)。
剩下就是发言Speak()和投票Vote()方法了。
也许大家注意到了,为了用户体验稍好一点,除了用户需要点击屏幕操作的发言、投票两个方法,其他都是用了异步获取的方式来完成。
--------------OK,至此,整个项目详细的代码剖析已结束--------------
哪些代码没看懂的,或者哪个类的职责没分清的,或者游戏流程还不清楚的,再或者需要详细贴哪些代码的,都可以留言给我,我都会详细解释,以便为新版的业务对象职责设计有更好的理解。
此外,若读者还发现了我没提及的坏味道问题,请一定一定留言给我,因为从下一篇开始,就是新项目的开始了,我也希望能改的更完善、更极致。因为本周比较忙,争取一周内做好业务对象的优化——也就是对此篇中坏味道的优化设计。
新项目我会放在我的svn上,到Models代码出来后再公布上来。(其实是我还没熟悉Github和TSF,所以只能用最熟悉的svn了><,我会努力的~)
感谢阅读!尤其是近几日来一直跟随此系列的朋友们,你们的阅读量是我最大的动力、鼓励与压力,是的,如果没有你们,如果我只是打开word写给自己,那么我估计我也懒得再思考这么详细了。
再次感谢!