
·您现在的位置: 云翼网络 >> 文章中心 >> 网站建设 >> 网站建设开发 >> ASP.NET网站开发 >> 在线捉鬼游戏开发之二-设计业务对象与对象职责划分(2)
来咯!因为章节较多,先做小部分回顾,熟悉的朋友就直接跳过吧。
-----------回顾分割线-----------
此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的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;
}
}
Setting
(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; }
}
}
Subject
(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;
}
}
Table
(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 };
}
}
Audience
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);
}
}
Civilian
public class Ghost : Civilian
{
public Ghost(string nickname) : base(nickname) { }
public static Ghost CreateFromCivilian(Civilian civilian)
{
return new Ghost(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的拿走。
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 word
186 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,7
201 SetCivilianWord(c);
202 }
203 else
204 {
205 // 8,9
206 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
按游戏顺序理解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");
}
}
WebCommon
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);
}
}
AudienceFilterAttribute
过滤效果在Controller中调用
[AudienceFilter]
public class PlayController : Controller
{
//...
}
public class HomeController : Controller
{
[HttpPost]
[AudienceFilter]
public ActionResult Logout(){//...}
[AudienceFilter]
public ActionResult Signout(){//...}
[AudienceFilter]
public ActionResult 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写给自己,那么我估计我也懒得再思考这么详细了。
再次感谢!