自己写三国杀之架构分析

本文分析的是三国杀标准包+EX扩展包所带的功能如何在我的程序里实现的,不包括其他的扩展包。实际的程序可以支持2至10人在控制台下进行网络对战。博主仅仅凭兴趣写的,设计有缺陷,请多多包涵。微笑

代码下载地址(素将版,没有实现具体武将,有具体武将的会在2、3月,比赛评比完毕后公布):

http://download.csdn.net/detail/ilovevista/3514209

刚才看了大家下载后的回复,让我倍受鼓舞,谢谢你们!


三国杀实体牌分游戏牌、体力牌、身份牌、武将牌,进行游戏时以玩家的座次顺序来依次进行游戏,大家都共同遵守同样的游戏规则。程序的设计就以此为切入点设计。


一、游戏牌(Poker)

所有的游戏牌主动打出时大致的流程就是:确定使用->指定目标->进入结算流程(生效或失效),被动打出则较为简单。所以定义一个基类,保存牌的基本信息以及执行牌的通用动作,例如存取牌的数字、花色、名称等;不同的牌继承自这个基类(基本牌、锦囊牌、装备牌),具体实现各自不同的“确定使用” “指定目标” “进入结算流程”的函数。在代码中这三个函数分别是Poker::selected()、Poker::use()、TrickPoker::effect()

1,、杀、闪、桃。继承自基本牌,类名分别为Slash、Jink、Peach。闪不能主动使用,桃的使用也比较简单,杀的使用就较为复杂了,涉及目标合法性检查、武器技能发动(每个武器的技能有不同的发动时机)、对方防具技能的选择发动等等,需要在设计时充分考虑后再动手编码,以免有遗漏导致实现出来的功能不正确。

2、各种武器、防具、马。继承自装备牌,类名与中文的对应见各自的头文件注释。装备牌的特点是在使用后要确定装备于玩家的装备区,所以每个装备牌在初始化时就有一个变量:这张牌应该处于什么位置(enum EquipPoker::Location)。

3、锦囊牌,分为延时锦囊和即时锦囊。它们在使用后要在全场询问无懈可击,在没有人打出或者一共打出偶数个无懈可击后才生效,进入结算。

4、虚牌。它用于武将技能发动、卡牌转换,在代码中它包含一个子卡的集合、记录相关信息。比如制衡、仁德,就是将相应的卡牌作为一张虚牌的子牌,提交处理;或者大乔的国色、甘宁的奇袭,是将方片牌或者黑色牌转换成乐不思蜀或者过河拆桥进行结算。

二、牌堆(deskPoker)

牌堆分为未用牌堆、弃牌堆、正在结算牌堆。其中要注意考虑武将技能对牌堆的影响,比如诸葛亮的观星。正在结算牌堆,在结算杀闪、五谷丰登等等的时候所有的牌先置于这里,结算完毕时这个牌堆里的牌统一清理到弃牌堆。

在具体实现时,我一开始用自己写的链表去保存相应的数据(按照《数据结构》书上讲的方法)。结果在测试时,发现如果对这个链表进行上万次操作后它会随机崩溃,我拿着代码请教老师,他也说不出来什么错误。最后我在程序里,所有包含多个数据的东西用的是vector来做,比如玩家的手牌、装备、判定区等等。

三、服务(Service)

在游戏里,这个服务扮演裁判的角色。计算玩家之间的距离、进行杀结算流程的一部分、在玩家打出锦囊时进行询问无懈可击、处理转换卡牌(前面的虚卡)等等,最重要的功能是指导游戏进行流程(Service::setPhase):在游戏开始时,服务通知N号位玩家进行他的回合的某一阶段(回合开始阶段、判定阶段、摸牌阶段、出牌阶段、弃牌阶段、回合结束阶段),玩家返回这个阶段是怎样结束的(是正常结束还是跳过某一阶段,亦或是触发胜利条件),服务对这个返回值检查并处理之后,再通知N+1位玩家进行他的回合。


四、玩家(Player)

玩家的设计曾经困扰我很长时间,直到现在的设计也不是让我很满意。在只有标准包+EX时,我认为玩家所用武将就是“素将+技能”,而且我认为“玩家=素将”,所以我在Player类里面实现了所有素将的功能,具体的武将从素将继承,并改写相关函数实现武将技能。

在这里我说一点,在三国杀“神话再临”版本里,武将与技能的关系不再是一一对应的,也就是说有可能你用的是“孙权”,但是你的技能有可能不再是“制衡”和“救援”,所以技能和武将设计时一定要解耦,我的实现方式直接决定我不能实现“神话再临”里面的武将。

Player里面最重要的函数是Player::setPhase()和Player::askLeadPoker()。前者在自己回合内通过接收Service的传值知道自己处于什么阶段,并提示玩家进行相应操作、选择,比如玩家在出牌阶段要出什么牌,是在这个函数里程序向玩家询问的;后者在玩家回合外提示玩家被要求出什么牌,比如别的玩家杀自己、打出南蛮入侵、万箭齐发时,这个函数通知自己要打出“杀”或者"闪"。其他函数是为了配合对应牌的功能而实现的,比如丢弃牌、亮出牌、交出一张牌等等。

具体武将的类名是武将名称的拼音,基本上是重写Player::setPhase()和Player::askLeadPoker()。


五、服务器内部消息传递

在struct.h里面定义了一些消息结构体,比如用于传递“杀”消息的SlashEffectStruct、用于锦囊消息的TrickEffectStruct等等。主要传递的消息就是“来源”、“目标”、“牌”。其中来源一定是单一的,但是目标和牌有可能是单一的,也有可能是多个。存在多个目标时一定要按照游戏规则的结算顺序来依次结算。


六、服务器/客户端消息传递

在开始时我编写的代码是为了单机玩的,所以没有设计通信协议,基本上都是cout一段中文提示让用户进行一种选择,再cin一个数字,进行检查合法后继续流程。在单机版写完后突然想加入网络功能,这时候改中文重新设计通信协议已经来不及了。我的折中办法就是将客户端做成一个“屏幕”,客户端仅仅是用于显示文字并接受用户的输入,将数据回传至服务器进行相关检查、操作等等,也就是说客户端根本没有关于游戏的内部数据,不知道本玩家手里有什么牌。

原来的所有cin cout,有的东西是要求所有玩家都知道的、有的东西只能让一个人知道,消息一共有三种类型:向所有人通知、向单个人通知、向单个人询问。前两个客户端仅仅需要接收数据并显示,最后一个要求在显示数据之后让用户输入一个选择之后回传至服务器。

在实现上,我写了一个GameServer类,用于网络通信,采用TCP方式连接。因为TCP是流式协议,服务器/客户端内部用了string用于接发数据,也方便进行格式转换,比如让用户进行选择时,用户只需要输入一个数字作为选择,服务器接受的“数字”其实是字符串格式的,用StringStream可以很方便地进行字符串/数字类型的转换。


七、健壮性

1、输入输出健壮性

这个程序的输出都是字符串格式的,不会出现什么错误。所有输入仅仅是一个整数作为选择,在服务器接受用户做出某种选择时,用StringStream去接收,再进行转换,比如:

string recv="4";//接收到的字符串格式的数据

StringStream ss;//用于转换的字符串流

int a=0;//真正能用于服务器内部处理的int型变量

while(输入不合法){

ss<<recv;//字符串导入字符串流的缓冲区

ss>>a;//从字符串流中读取一个int型数据并保存

if(a在范围内)  输入合法跳出循环

}

这样就能保证即使输入错误也不会导致程序崩溃。

2、网络通信健壮性

这方面比较遗憾,因为时间紧迫所以没有做更好的处理,在代码里规定,如果网络通信出现中断,则所有人断开与主机联系。


到此为止,这个三国杀的设计思想差不多介绍完了。


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值