來咯!因為章節較多,先做小部分回顧,熟悉的朋友就直接跳過吧。
-----------回顧分割線-----------
此系列旨在開發類似“誰是臥底+殺人游戲”的捉鬼游戲在線版,記錄從分析游戲開始的開發全過程,通過此項目讓自己熟悉面向對象的SOLID原則,提高對設計模式、重構的理解。
系列索引點這里,游戲整體流程介紹與技術選型點這里,之前做了一半的版本點這里(圖文)
在設計業務對象與對象職責劃分(1)中對之前做的版本進行了截圖說明部分views的細節,相信大家已對游戲流程更熟悉了(我也回顧了一遍,包括游戲技巧很有意思~),本第(2)篇主講MVC的M和C,深入剖析游戲業務在代碼中的體現。如索引篇所說,大家的技術性吐槽與質疑是對其他讀者和我最大的幫助;如上一篇所言,這個做了一半的版本壞味道很多,尤其從設計模式的角度看就更明顯了(這個系列做完,我計劃再寫一個俄羅斯方塊系列,俄羅斯方塊是已經做好的C#+WinForm,但同樣是壞味道嚴重,拓展不易,也才更需要update),所以才有了重寫一遍捉鬼游戲,并想把整個項目建設過程同步更新到博客園,以給和我一樣的初學者一個共同思考和進步的途徑。我始終相信,知其然還要知其所以然,甚至和項目開發者同步思考過、犯過錯、再一起探討與改正過,會獲得更深刻的經驗。也歡迎朋友們勇敢的在最后把代碼直接拿去說這是我和一位博客園朋友一期開發的小項目![自豪] 筆者對“開源”一詞的理解還不太深刻,但我相信無償的提供代碼、寫代碼的心得,甚至寫代碼的全程思考會讓大家比直接拿商用代碼更有價值。
-----------回顧結束分割線-----------
-----------本篇開始分割線-----------
1. Models
業務類目錄
從類目錄可看出一共7個類,先從最輔助性的英雄(啊不是英雄,是類)Setting開始(已在上篇貼過代碼)
(1)Setting(設置類)負責從Web.config獲取游戲人數的設定(標配9人,開發測試3人),Setting作為全局訪問點采用了singleton單例模式。
public class Setting { PRivate int _civilianCount; private int _GhostCount; private int _idioCount; public int IdioCount { get { return _idioCount; } } public int GhostCount { get { return _ghostCount; } } public int CivilianCount { get { return _civilianCount; } } public int GetTotalCount() { return IdioCount + GhostCount + CivilianCount; } // singleton private static Setting _instance; private Setting() { 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"]); } public static Setting CreateInstance() { if (_instance == null) { _instance = new Setting(); } return _instance; } }
(2)Subject(題目類)負責從題庫出題(題庫系統未做,要做也是普通的增刪改查即可,先暫寫死在程序里),也是單例模式。
public class Subject { // singleton private static Subject _instance; private Subject() { // Todo: get subject from dictionary this._civilianWord = "周星馳"; this._idiotWord = "孫悟空"; } public static Subject CreateInstance() { if (_instance == null) { _instance = new Subject(); } return _instance; } private string _civilianWord; private string _idiotWord; public string IdioWord { get { return _idiotWord; } } public string CivilianWord { get { return _civilianWord; } } }
(3)Table(游戲桌類)負責數桌上的人數以及開始或重開游戲【壞味道:職責過多】
代碼過多,可先從貼圖看起
Table游戲桌類
先看屬性:可見也是單例模式(_instance),且存在與Game類的關聯關系(_game),以及維護有一個audiences列表(_audiences,List<Audience>類型)
再看方法:先看第二個CheckTotalNum()——處理每次有人點擊報名后,都核查一遍報名總人數是否與Setting設置類中的游戲人數相等,若想等則調用GameStart()方法。其他方法都是Add或Get開頭的,想必看方法名就能理解了:都是獲取所維護的_audiences列表中各個類型的角色的集合,舉個帶Without的GetAudiencesWithoutCivilians來說,就是獲取除了Civilian子類中的其他Audiences,也就是旁聽的有幾人。
壞味道很明顯:職責過多。(注意不是方法過多,而是類負責的職責過多。區別是:可能存在只有一個職責,但需要多個private私有方法來配合完成,這也是允許的)
代碼還是貼一下吧,助于具體方法的理解。
public class Table { // singleton private static Table _instance; private Table() { } public static Table CreateInstance() { if (_instance == null) { _instance = new Table(); } return _instance; } // field private List<Audience> _audiences = new List<Audience>(); private Game _game; public Game GetGame() { return this._game; } public void AddAudience(Audience audience) { if (!this.GetAudiences().Contains(audience)) { if (audience is Civilian) { this.GetAudiences().Add(audience as Civilian); CheckTotalNum(); return; } this.GetAudiences().Add(audience); } } public void RemoveAudience(Audience audience) { this._audiences.Remove(audience); } private void CheckTotalNum() { if (this.GetCivilians().Count >= Setting.CreateInstance().GetTotalCount()) { GameStart(); } } // 獲取在線者 public List<Audience> GetAudiences() { return this._audiences; } // 獲取聽眾 public List<Audience> GetAudiencesWithoutCivilians() { List<Audience> result = new List<Audience>(); foreach (Audience a in this._audiences) { if (!(a is Civilian)) { result.Add(a); } } return result; } // 獲取參與者 public List<Civilian> GetCivilians() { List<Civilian> result = new List<Civilian>(); foreach (Audience a in this._audiences) { if (a is Civilian) { result.Add(a as Civilian); } } return result; } // 獲取好人 public List<Civilian> GetCiviliansWithoutGhosts() { List<Civilian> result = new List<Civilian>(); foreach (Civilian a in this.GetCivilians()) { if (!(a is Ghost)) { result.Add(a); } } return result; } // 獲取鬼 public List<Ghost> GetGhosts() { List<Ghost> result = new List<Ghost>(); foreach (Audience a in this._audiences) { if (a is Ghost) { result.Add(a as Ghost); } } return result; } // game start private void GameStart() { this._game = new Game(); this.GetGame().Start(); } // restart public void Restart() { _instance = null; } }
(4)Audience(聽眾類)、Civilian(好人類)、Ghost(鬼類)存在繼承關系
三個參與者身份類之間的繼承
public class Audience { private string _nickname; public string Nickname { get { return _nickname; } } public Audience(string nickname) { this._nickname = nickname; } private Table table; public Table Table { get { return table; } set { table = value; } } public static Audience CreateFromCivilian(Civilian civilian) { return new Audience(civilian.Nickname) { Table = civilian.Table }; } }
public class Civilian : Audience { private string _word; public string Word { get { return _word; } set { _word = value; } } private bool _isAlive = true; public bool IsAlive { get { return _isAlive; } } public void SetDeath() { this._isAlive = false; } public Civilian(string nickname) : base(nickname) { } public static Civilian CreateFromAudience(Audience audience) { return new Civilian(audience.Nickname) { Table = audience.Table }; } public string Speak(string speak) { return Table.CreateInstance().GetGame().RecordSpeak(speak.Replace("/r/n", " "), this); } }
public class Ghost : Civilian { public Ghost(string nickname) : base(nickname) { } public static Ghost CreateFromCivilian(Civilian civilian) { return new Ghost(civilian.Nickname); } }
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的拿走。
1 public class Game 2 { 3 private Table table = Table.CreateInstance(); 4 5 // start 6 public void Start() 7 { 8 SetGhostWithRandom(); 9 SetWordWithRandom(); 10 AppendLineToInter("鬼正在指定開始發言的順序..."); 11 AppendLineToGhostInter("已開啟鬼內討論,此時的發言只有鬼能看見..."); 12 AppendLineToGhostInter("請指定首發言者..."); 13 } 14 15 private StringBuilder _inter = new StringBuilder(); 16 private StringBuilder _ghostInter = new StringBuilder(); 17 private bool _isGhostSpeaking = true; 18 private bool _isVoting = false; 19 private Civilian _speaker; 20 private Civilian _loopStarter; 21 private bool _isFirstLoop = true; 22 23 public void SetStartSpeaker(Ghost ghost, Civilian speaker) 24 { 25 this._speaker = speaker; 26 this._loopStarter = speaker; 27 SetGhostSpeaked(); 28 AppendLineToGhostInter(string.Format("{0} 選擇了 {1} 作為首發言人", ghost.Nickname, speaker.Nickname)); 29 } 30 private void SetGhostSpeaked() 31 { 32 if (isGhostSpeaking()) 33 { 34 this._isGhostSpeaking = false; 35 StartLooping(); 36 } 37 } 38 39 private void StartLooping() 40 { 41 ClearInter(); 42 AppendLineToInter("從 " + this.GetCurrentSpeaker().Nickname + " 開始發言..."); 43 } 44 45 public void SetNextSpeaker() 46 { 47 List<Civilian> list = this.GetAliveCivilian(); 48 for (int i = 0; i < list.Count; i++) 49 { 50 Civilian current = list[i]; 51 if (current == this.GetCurrentSpeaker()) 52 { 53 if (i == list.Count - 1) 54 { 55 this._speaker = list[0]; 56 CheckEndLoop(); 57 return; 58 } 59 this._speaker = list[i + 1]; 60 CheckEndLoop(); 61 return; 62 } 63 } 64 } 65 66 private void CheckEndLoop() 67 { 68 if (GetCurrentSpeaker() == this._loopStarter) 69 EndLoop(); 70 } 71 72 private void EndLoop() 73 { 74 Thread.Sleep(30000); 75 if (this._isFirstLoop) 76 { 77 this._isFirstLoop = false; 78 AppendLineToInter("第二輪開始"); 79 return; 80 } 81 AppendLineToInter("30秒后開始投票"); 82 ClearInter(); 83 SetVoting(); 84 } 85 86 public Civilian GetCurrentSpeaker() 87 { 88 return this._speaker; 89 } 90 91 public bool isGhostSpeaking() 92 { 93 return this._isGhostSpeaking; 94 } 95 96 public bool isVoting() 97 { 98 return this._isVoting; 99 }100 101 private void SetVoting()102 {103 this._isVoting = true;104 AppendLineToInter("開始投票");105 }106 107 private void SetVoted()108 {109 this._isVoting = false;110 }111 112 public string RecordSpeak(string speak, Civilian civilian)113 {114 if (isGhostSpeaking())115 {116 if (civilian is Ghost)117 {118 AppendLineToGhostInter(FormatSpeak(civilian.Nickname, speak));119 return "ok";120 }121 return "天黑請閉眼!";122 }123 AppendLineToInter(FormatSpeak(civilian.Nickname, speak));124 SetNextSpeaker();125 return "ok";126 }127 128 private string FormatSpeak(string name, string speak)129 {130 return string.Format("【{0}】:{1}", name, speak);131 }132 133 public List<Civilian> GetAliveCivilian()134 {135 return Table.CreateInstance().GetCivilians().Where(c => c.IsAlive).ToList();136 }137 138 public string GetGhostInter()139 {140 return this._ghostInter.ToString();141 }142 143 public string GetInter()144 {145 return this._inter.ToString();146 }147 148 private void AppendLineToInter(string line)149 {150 this._inter.AppendLine(line);151 }152 private void AppendLineToGhostInter(string line)153 {154 this._ghostInter.AppendLine(line);155 }156 157 private void ClearInter()158 {159 this._inter.Clear();160 this._ghostInter.Clear();161 }162 163 private void SetGhostWithRandom()164 {165 Random rd = new Random();166 while (true)167 {168 for (int i = 0; i < Setting.CreateInstance().GetTotalCount(); i++)169 {170 Civilian c = table.GetCivilians()[i];171 if (c is Ghost) continue;172 int role = rd.Next(1, 4); // 1/3幾率173 if (role == 1 && table.GetGhosts().Count < Setting.CreateInstance().GhostCount)174 {175 Ghost ghost = Ghost.CreateFromCivilian(c);176 table.RemoveAudience(c);177 table.GetAudiences().Insert(i, ghost);178 }179 }180 181 if (table.GetGhosts().Count >= Setting.CreateInstance().GhostCount)182 break;183 }184 }185 // set word186 private void SetWordWithRandom()187 {188 Random rd = new Random();189 foreach (Civilian c in table.GetCivilians())190 {191 if (c is Ghost)192 {193 SetGhostWord(c as Ghost);194 continue;195 }196 197 int role = rd.Next(4, 10);198 if (role <= 7)199 {200 // 4,5,6,7201 SetCivilianWord(c);202 }203 else204 {205 // 8,9206 SetIdioWord(c);207 }208 }209 }210 private void SetGhostWord(Ghost g)211 {212 if (HasWord(g)) return;213 g.Word = string.Format("鬼({0}字)", Subject.CreateInstance().CivilianWord.Length);214 }215 private void SetCivilianWord(Civilian civilian)216 {217 if (HasWord(civilian)) return;218 string word = Subject.CreateInstance().CivilianWord;219 List<Civilian> list = table.GetCiviliansWithoutGhosts();220 if (list.Where(c => c.Word == word).Count() < Setting.CreateInstance().CivilianCount)221 {222 civilian.Word = word;223 return;224 }225 SetIdioWord(civilian);226 }227 private void SetIdioWord(Civilian civilian)228 {229 if (HasWord(civilian)) return;230 List<Civilian> list = table.GetCivilians();231 string word = Subject.CreateInstance().IdioWord;232 if (list.Where(c => c.Word == word).Count() < Setting.CreateInstance().IdioCount)233 {234 civilian.Word = word;235 return;236 }237 SetCivilianWord(civilian);238 }239 private bool HasWord(Civilian c)240 {241 return !string.IsNullOrEmpty(c.Word);242 }243 }
按游戲順序理解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
public static class WebCommon { public static Audience GetAudienceFromSession() { return HttpContext.Current.Session["player"] as Audience; } public static Civilian GetCivilianFromSession() { return HttpContext.Current.Session["player"] as Civilian; } public static Ghost GetGhostFromSession() { return HttpContext.Current.Session["player"] as Ghost; } public static void RenewPlayerSession(Audience newAudience) { HttpContext.Current.Session["player"] = newAudience; } public static void AddPlayerSession(Audience audience) { HttpContext.Current.Session.Add("player", audience); } public static void RemovePlayerSession() { HttpContext.Current.Session.Remove("player"); } }
AudienceFilterAttribute負責過濾聽眾,即區分玩家與旁觀者的操作許可和界面顯示內容
public class AudienceFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { HttpContextBase context = filterContext.HttpContext; if (context.Session["player"] == null) { context.Response.Redirect("/"); return; } base.OnActionExecuting(filterContext); } }
過濾效果在Controller中調用
[AudienceFilter]public class PlayController : Controller{ //...}public class HomeController : Controller{ [HttpPost] [AudienceFilter] public ActionResult Logout(){//...} [AudienceFilter] public ActionResult Signout(){//...} [AudienceFilter] public ActionResult Restart(){//...}}
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寫給自己,那么我估計我也懶得再思考這么詳細了。
再次感謝!
新聞熱點
疑難解答