有了游戏源码pc端web上线项目源码还需要什么步奏?

扫一扫,随时随地挑选人才和公司
无线工作台,管理更高效更自由
微信公众号
随时掌握一手资讯
需求发布后1小时内收到服务商响应每个需求平均有10个服务商参与95%以上的需求得到了圆满解决所有需求不向雇主收取任何佣金网站抢先做,开年省心用好站
质量:0.00
速度:0.00
态度:0.00
近三月收入:0元 0 笔
让玩家“看着爽”、“玩着爽”,3D街机的完美一直,全新捕鱼游戏,熟悉的西游记故事情节,搭配任务机制,解决玩家存留率的苦恼
捕鱼想玩家买账,一定要做好“视觉体验”和“数值体验”。通俗点讲就是“看着爽”和“玩着爽”。
捕鱼网页游戏开发,大话捕鱼开发出售,泊众捕鱼源码出售,可按需定制各类棋牌游戏开发
游戏运营利器,大话捕鱼(泊众三缺一出品)助抢占市场先机
大话捕鱼加入了大量的原画设计以及故事感,游戏中都是采用3D粒子动画,视觉冲击更强烈。
街机捕鱼网页游戏开发,大话捕鱼开发出售,泊众捕鱼源码出售
手机扫码下单,便捷优惠
数量件
泊众在棋牌游戏开发行业已经10年啦,之前也有非常多的产品发布,这次新发售的《大话捕鱼-女儿国》有什么特别之处呢?捕鱼游戏5年之前就已经流行了。但在我的产品理念中:捕鱼想玩家买账,一定要做好“视觉体验”和“数值体验”。通俗点讲就是“看着爽”和“玩着爽”。 那怎么让玩家“看着爽”“玩着爽”的呢?近几年行业的“创新”五花八门,行业公司都是元素拼凑的“搬运工”式创新。我们则加入了大量的原画设计以及故事感,游戏中都是采用3D粒子动画,视觉冲击更强烈。熟悉的故事场景、人物也会出现,比如:女儿国国王会给你任务,去降服会“变身”的蝎子精。因为捕鱼和传统棋牌不一样,他的发源地是“街机场地”,也可以理解为台湾和日本,要知道,日本可是RPG王国。没有故事感的游戏是没法深入人心的。当街机游戏进入了棋牌行业之后,大多数公司还用传统思维去做捕鱼,是没有办法传神的,更没办法留住玩家的心。捕鱼源自“街机”,所以它的数值感一直是行业所追捧的,但是大多都是噱头。我们先前为了做出理想的奖池也是费尽心思。这次换了一个思路,我们的运营商中有很多街机厅老板、街机经销商,他们给我们提供了方案,源自一家叫做“昌盛”的街机厂商,这让我们的奖池方案有了质的飞跃。不过街机一般是4到8人,这种奖池还是有局限性的,场地和网络捕鱼存在很大差异,所以我们在方案的基础上加入了这些年对奖池研发摸索中的一些经验。  市场上捕鱼产品都千篇一律的,那泊众怎么帮运营商提升竞争力?为了能让运营商能最大展现特点,泊众开辟了新业务模式:“鱼阵定制”和“主题定制”,运营商可以在捕鱼游戏中增加自己的鱼阵,比如平台名称,再就是运营商有主题创意,我们也可以帮助运营商实现。不过就现在来说,我们后续还有几个主题会陆续面世,运营商还有很多选择,不用担心没有特色。& & & & &&“棋牌游戏开发——上线运营——售后服务流程”有一下三步骤:一、泊众棋牌、捕鱼游戏开发服务内容:1、选择一款棋牌平台。如:我公司的“三缺一”或者“面对面”“紫金游”,结合您需要的地方游戏或者特色捕鱼游戏。 (备注:如果您有特殊规则和游戏玩法,泊众可专门根据您的需求开发定制,由我们的游戏开发工程师评估游戏开发可执行性和时间)2、准备服务器和域名。3、资质准备。文网文、ICP,备案,资料可参“泊众网科技“公众微信号”的棋牌资质办理文章,有详细说明4、组建团队。维护人员和客服人员二、泊众开发棋牌游戏服务流程:1、客户确定需求,签定合同2、支付首款3、项目立项4、资源提交5、项目实施部署6、产品培训(3-5天)7、测试验收,完成交付8、支付尾款三、泊众售后服务保障承诺:1、游戏BUG永久免费修正。2、提供五天免费棋牌系统运维培训。3、一年免费技术服务支撑。4、7×8小时专职在线客服服务。5、系统安装一条龙服务。泊众特色业务:开发定制地方性棋牌游戏、开发可立即上线运营棋牌游戏、棋牌源码出售、捕鱼游戏开发、麻将,斗地主,德州扑克,牛牛等休闲棋牌游戏开发,街机捕鱼网页游戏开发,大话捕鱼开发出售,泊众捕鱼源码出售,可按需定制各类棋牌游戏开发,泊众十年,只出精品!
服务宝,全程保障交易安全
验收合格后,再付款给服务商
所有服务商,100%实名认证
交易出问题,可获保证金赔付
承诺不向雇主收取任何费用
担保交易,交易全程护航
交易遇到以上问题&&雇主可获保证金赔付
内容声明:
猪八戒网为第三方交易平台及互联网信息服务提供者,猪八戒网(含网站、客户端等)所展示的商品/服务的标题、价格、详情等信息内容系由店铺经营者发布,其真实性、准确性和合法性均由店铺经营者负责。猪八戒网提醒用户购买商品/服务前注意谨慎核实。如用户对商品/服务的标题、价格、详情等任何信息有任何疑问的,请在购买前店铺经营者沟通确认;猪八戒存在海量店铺,如用户发现店铺内有任何违法/侵权信息,请立即向猪八戒网举报并提供有效线索。
&&诚信度:
本店其他官方套餐
正在加载类似服务...
本店其它服务
正在加载类似服务...文 | 李勤来自雷锋网(leiphone-sz)的报道雷锋网消息,据 IT 之家 1 月 2 日消息称,&跳一跳&居然可以利用漏洞自己改分数,甚至连微信小程序、小游戏的源代码都可以直接下载,只需要知道appid和版本号,就可以直接构造URL下载后缀为wxapkg的源码包,不需要任何验证。1 月 2 日下午,著名安全团队&盘古&的安全专家向雷锋网宅客频道证实,微信小游戏&跳一跳&改分漏洞仍在,此前流传的&微信已补漏洞&是指已经修补了微信小游戏&跳一跳&的源代码下载漏洞。这意味着,改分依然可行。到底怎么回事?雷锋网从盘古旗下的 Janus 威胁情报平台了解到了详情。一、复现一个为该平台投稿的独立开发者朱鹏飞称&我看完文章之后立马根据他的思路方法测试了一下,然后下载了十几个微信官方的小游戏源码单纯研究学习,截止我目前推送文章的时候( 日23:50分 ),微信官方已经修复了这个漏洞,但是我感觉文章还是可以分享出来给诸位开发者,安全问题真的不容忽视呢。&雷锋网还注意到,朱表示,有些老版本的微信还是可以抓包,获取包地址。朱称,一大早起来刷 V2EX,看到一个帖子《微信跳一跳 可以直接更改分数, POST 请求没有校验&》好奇点进去看了。发现不但跳一跳小游戏可以直接改分数,甚至连微信小程序、小游戏的源代码都可以直接下载,只需要知道 appid 和 版本号,就可以直接构造 URL 下载后缀为 wxapkg 的源码包,不需要任何验证。虽然下载来的源码包是加密的,但是解密方法已经被 V2EXer 发现,并且写了一个解密的 Python 脚本,运行即可把源码包解开为文件夹。第一步,我先试着用帖子作者拼接好的跳一跳源码包地址测试,发现能够下载,不需要任何验证,只需要知道这个地址,直接任意浏览器或者下载工具打开都可以下载。第二步,再用帖子中的解包 Python 脚本把源码包解压成源代码。第三步,在本地微信开发者工具中新建一个空白的小程序或小游戏的项目,不要选择快速启动模板。第四步,把刚才解压出来的源代码复制到刚刚创建的项目目录中,开发者工具会提示编译出错,这个只需要新建一个game.json文件即可。文件内容不能为空,写一对大括号进去,或者加上deviceOrientation的配置,这句话的意思是游戏竖屏玩。保存后你发现游戏还是编译不通过,还需要修改最后一项,点击开发者工具右上角详情按钮,把调试基础库改成game。好了,运行起来了:此外, Janus 为雷锋网出具了一份最新改分攻略:二、改分关键步骤电脑安装抓包软件,手机设置https代理到电脑通过抓包软件,抓包拿到微信的sesseion_id将sesseion_id写入改分脚本,提交改分请求以下为详细教程(1)部署代理环境(mac环境)mac下选用charles作为抓包代理软件(https://www.charlesproxy.com/),windows用户可以用
fiddler(https://www.telerik.com/fiddler)。安装好后运行起来,查看本机IP地址,以及软件提供的远程代理端口号:charles 的顶部菜单 Proxy-&Proxy settings将手机接入与电脑同一个局域网的wifi,然后在wifi的代理设置中,选择手动指定代理,代理服务器为电脑的ip及代理软件提供的端口号(如图中的8888)。电脑端会提示是否运行远程接入,点allow即可。设置好了以后可以尝试浏览器打开baidu.com,如果可以正常打开,并且在抓包软件中可以看到请求记录就完成了手机的代理设置。由于是https的请求,需要在手机端安装证书才可以解密请求,charles的证书可通过手机浏览器安装,手机浏览器访问http://chls.pro/ssl,点继续访问此网站,会提示安装证书。iOS 10.3以上的系统版本,需要在 设置&通用&关于本机&证书信任设置 里面启用完全信任Charles证书。配置好证书后可以打开https://baidu.com看看是否能解开百度的首页源代码。(2)获取session idhttps配置完毕后,打开微信的跳一跳小程序,就可以看到抓包历史有一个带有session id的请求:https://mp.weixin.qq.com/wxagame/wxagame_init在request部分就可以复制到session_id了。(3)将sesseion_id写入改分脚本,提交改分请求目前开源的脚本是nodejs写的,git地址:https://gist.github.com/feix/6dd1f62a54c5efa10f1e1c24f8efc417具体的步骤:新建个目录,比如:wxt1t,然后将脚本源码保存到这里,比如hack.js。然后安装nodejs,可以通过官网下载安装包安装:https://nodejs.org/en/然后在命令行cd到当前项目文件夹(wct1t),运行:npm init --ynpm install crypto-js request-promise然后用文本编辑器打开hack.js,修改里面的score(分数)和session_id变量的值即可。命令行运行node hack.js,出现2018! Happy new year! 就代表成功了。上述盘古专家还提醒:&虽然&跳一跳&的下载代码漏洞已经修补,但是之前官方的源代码已经传播开了,别人拿去改一改,就可以出个自己的&跳一跳&了,比如,可以拿&跳一跳&的代码改一个游戏,叫&跳跳跳&,然后夹带一些私货,发布个新游戏。&如果这样,类似的新游戏是否有夹带恶意代码的风险?专家对雷锋网称:&小程序有审核机制,如果夹带了恶意代码,躲避审核后才能实现。现在小程序审核比较严格,尚不清楚其对恶意代码的审计力度。&参考链接:http://appscan.io/discover-discuss.html?id=
24小时报不停
京东联合腾讯入股万达商业,联手打造无界零售价值组合
2018中国域名大会举行,CN域名新注册量同比增长43%
搜狐第四季度营收5.1亿美元,净亏损3600万美元
小米回应酷派专利诉讼:未收到任何起诉文件
菲律宾正起草加密货币交易相关法规
中国城市级AI首出海:马来西亚引入阿里云ET城市大脑
林芝腾讯认购永辉云创新增注册资本1.875亿元
日媒:旅行青蛙苹果下载1000万,中国占95%日本仅2%
飞猪在线签证新上线11国,今年将再新增30个国家
第四范式获三家国有银行联合战略投资
WeGene获B轮融资,消费级基因检测用户超十万
IDG资本、阿里、富士康联合领投小鹏汽车22亿B轮融资
机器之心完成 A 轮融资,坚持全球化,探索新产品
聚思教育获青山资本天使轮融资
乐视网复牌后连续第四日跌停,市值蒸发超200亿
泰国新规将允许通过ICO募资
网宿科技否认腾讯入股,股票今日复牌
融慧金科完成近千万美元A轮融资
俄罗斯官方正式发布数字金融资产法律草案
西南财大正筹建区块链研究中心
Visa和万事达卡开始针对购买数字货币额外收费
汽车之家招聘区块链人才,疑似进军区块链
大众汽车首席数字官跳槽区块链行业
聚思教育获青山资本天使轮融资,发力儿童脑智教育市场
用友发布“鲲鹏计划”,与华为和百度展开合作
“咿啦看书”宣布完成4000万元A+轮融资
日本或20年后普及氢燃料汽车,续航里程1000公里
"雪龙"号完成南极特拉诺瓦湾海底精密勘测
刘二海透露李斌将全部资金投入蔚来造车:他还在租房
特斯拉不给马斯克发薪了,除非市值未来10年内能增10倍
央行主管媒体:今后三年是防范化解金融风险的重点时段
北京严查房产中介机构违规经营,累计注销关停超千家
河北发改委:推动腾讯等已入驻雄安新区企业尽早启动建设
量子高科重启收购睿智化学,29日复牌
百华悦邦全资子公司拟3690万元收购凯特信息100%股权
日本虚拟货币交易所承诺赔偿被盗463亿日元资产
新华社:IPO迎来史上最严发审周,通过率降低或成常态
小米公司创始人雷军当选“2017十大经济年度人物”
叮当快药获得新一轮融资,软银中国投资
完美世界转让旗下院线业务,售价约16.65亿元人民币
4次冲击IPO,六个核桃母公司上市在即,依然重营销轻研发
沃尔玛总部宣布裁员1000人
强生在最后关头退出竞购辉瑞旗下消费者健康业务
网宿科技:未与腾讯就股权合作事项进行过洽谈 明日复牌
微博公布对“存在刷榜行为的热门话题及主持人”处罚措施
英媒:中国掀科技人才争夺战,薪资水平直追美国硅谷
暂停新增投放后,北京共享单车总量减15万
北京人大代表建议:银行正规军应进军校园贷
西媒称中国正成人工智能领军者:实力堪与美国竞争
日媒称中国需求让全球机器人市场“盛况空前”
区块链将影响媒体行业?西媒:能帮忙打击虚假新闻
阿里巴巴王坚:机器智能时代到来
多地掀起新能源汽车抢购潮,业内:与补贴退坡有关
菜鸟网络董事长建议:为合规快递三轮车配发车牌
用友发布“鲲鹏计划” 与华为和百度展开合作
商汤科技汤晓鸥:听从你心,现在不一定要学AI
春节档电影“票补”将受限,单张票价不得低于19.9元
新浪微博热搜榜等板块下线整改,至2月3日21时暴雪游戏趣闻:战网隐身登陆功能未实装 缘起代码_图文攻略_全通关攻略_高分攻略_百度攻略
这是在现场的暗月马戏团区进行的小规模的工程师访谈,算是第一次有这种技术向的东西,我觉得很有兴趣,就尝试翻译了下。因为东西很多,我顺序从上往下更新,慢慢翻译。守望先锋组当初为了Chinajoy做了一个展示用客户端,但是制作时忘记和谐血液了。结果就是他们只好顶着很弱的wifi信号来临时修改。当时有人想到代码里有一行语句用来判断你是否击中了其他目标或者环境,就仅仅是一个if。于是他们决定通过修改可执行文件里的一个字节来达成目的。结果就是他们写了一个可执行文件,载入客户端,找到那个字节,翻转之,然后把修改后的客户端输出。后来在CJ上的展示河蟹效果很不错。精彩内容,尽在百度攻略:https://gl.baidu.com德拉诺正式上线的那几天,有一些超出预料的情况。当时硬件资源已经准备到位,但是因为WoW是个老游戏,每一台刀片服务器都负责游戏里的某一部分。工程师们根据他们推断出来的最佳配置对服务器集群进行了优化,只是他们也不清楚负责游戏里某一部分的刀片服务器具体是什么配置。结果就是当时用于创建新副本的服务器并不是高内存型服务器,而你每次进入要塞都会创建一个新的副本(想想看上线那几天会有多少万这样的副本被频繁创建吧)。问题出现后,工程师们在数据中心里待了8个小时,把刀片服务器拔出来,增加内存,在不同服务器集群之间转移。虽然那段时间里这些操作导致了不少“副本未找到”的问题,但是调整结束后一切都稳定多了。之后就只是程序性调整了。风暴英雄的引擎和星际的是同一个。所以每次进行更新的时候,都需要考虑到是否用户创建的mod会被这些修改影响。他们不希望用户mod会因此出bug甚至无法使用。因此,他们设定了许多引擎设计的限制和枷锁,不过大家都挺支持的。所幸的是,Team 1(星际组)的离职率很低,大家都很熟悉这些代码(言下之意……)背包,永远的背包问题。最初的WoW设计师决定用一个数组(array)来记录你的所有物品。数组一开始的几个位置用来放你正在装备的物品,然后就是你的包包里的物品。后来(此时依然是早期)他们想加一个银行进来,于是把银行加到了这个数组的末尾那边(装备-&背包物品-&银行物品)。不过,为了让玩家不能随时随地打开银行(代码会被玩坏的),他们在代码库里的很多地方写了很多额外的逻辑,比如指定数组的某个位置对应到背包里的什么位置,然后银行的位置是从数组里的哪一位开始。这些数值都是hardcode(就是写死一个数字,而不是用类似(数组总长度/10)这种写法)到各种地方的,而且想去修改的话也不是说把这些数据找出来全部改掉就可以。有些逻辑判断可能就需要某个数值写死。如果你想修改默认背包的大小,天晓得你会搞坏些啥。这样的修改非常容易bug或是出现难以名状的错误,而且你十有八九会改出新问题,然后以各种你完全不想看到的姿势炸裂客户端。结果就是,他们需要找一群牛逼的,本来可以去做那些新功能的工程师,去专门一行行代码扫过去成千上万行老代码找到要修改的地方,以及让QA部门去用所有能想到的姿势测试以保证修改上线后不回瞬间爆炸。然后就没有然后了,还是16格到死吧。精彩内容,尽在百度攻略:https://gl.baidu.com制作组比较喜欢在给现有功能增加相关新功能的时候再去动老代码。比如当时在改跨服区域的时候,他们重新设计了把不同玩家分配到不同服务器的逻辑。老代码被完全重写并且易于扩展,结果就是将来想加入新的相关功能的话,不需要担心16格背包那样的问题。一个扩展的例子就是要塞的无缝切换,还有军团资料片里的一些类似功能。战网客户端需要支持多个游戏里的老代码。比如他们想要增加你的战网好友上限时,他们必须去相关游戏里找到所有可能限制这个上限的代码并且加以修改。比如有些游戏的UI限制了你的好友数上限,这些必须要同步修改。每一个新修改都可能需要导致一个多个甚至所有游戏进行更新,所以他们也只能尽力而为。魔兽世界开发者访谈:战网隐身功能正在制作中精彩内容,尽在百度攻略:https://gl.baidu.com隐藏在线状态这个功能迟迟不来也是因为老代码。想要让这个功能和游戏内容保持一致并不简单。技术上来说不难,但是比如说你的朋友隐藏了在线状态但是他在游戏里就站在你前面的话?隐藏在线状态,对我们来说,应该说是“我想去偷偷撸一把D3,而不是和基友们去刷马桶”这样的东西,但是当你们都站在AH的时候。守望先锋组一度非常担心新闻站的数据挖掘。比如最近被挖掘出来的玩家等级,而且blzcon现场的demo里也依然有踪影。实际上我们在beta开始前2天就弃用了这个系统,因为觉得这样一点都不cooooool。只是这个决定来的太快以至于他们没把遗留数据清理干净。
魔兽世界-相关攻略推荐
魔兽世界-种族
魔兽世界-圣骑士
魔兽世界-萨满祭司让游戏程序员在代码面前不再止步a year ago找找看,bug在哪里?这里应该还有如果主角着地将isJumping_设置回false的代码。为了简洁起见,我省略了。我们没有阻止主角“在空中跳跃”——当主角跳起来后持续按下B键。这样会导致她一直飘在空中,简单的修复方法可以是:在Heroine类中添加一个isJumping_布尔值变量来跟踪主角的跳跃,然后这么做:void Heroine::handleInput(Input input)
 if (input == PRESS_B)
  if (!isJumping_)
   isJumping_ =
   // Jump...
接下来,我们想实现主角的闪避动作。当主角站在地面上的时候,如果玩家按下下方向键,则躲避,如果松开此键,则站立。void Heroine::handleInput(Input input)
 if (input == PRESS_B)
  // Jump if not jumping...
 else if (input == PRESS_DOWN)
  if (!isJumping_)
   setGraphics(IMAGE_DUCK);
 else if (input == RELEASE_DOWN)
  setGraphics(IMAGE_STAND);
找找看,bug在哪里?通过上面的代码,玩家可以:1.按下方向键来闪避。2.按B键从闪避的状态直接跳起来。3.玩家还在空中的时候松开下键。此时,当女主角在跳跃状态的时候,显示的是站立的图像。是时候添加另外一个布尔标志位来解决该问题了······void Heroine::handleInput(Input input)
 if (input == PRESS_B)
  if (!isJumping_ && !isDucking_)
   // Jump...
 else if (input == PRESS_DOWN)
  if (!isJumping_)
   isDucking_ =
   setGraphics(IMAGE_DUCK);
 else if (input == RELEASE_DOWN)
  if (isDucking_)
   isDucking_ =
   setGraphics(IMAGE_STAND);
接下来,如果我们的主角可以在跳起来的过程中,按下方向键进行一次俯冲攻击那就太酷了,代码如下:void Heroine::handleInput(Input input)
 if (input == PRESS_B)
  if (!isJumping_ && !isDucking_)
   // Jump...
 else if (input == PRESS_DOWN)
  if (!isJumping_)
   isDucking_ =
   setGraphics(IMAGE_DUCK);
   isJumping_ =
   setGraphics(IMAGE_DIVE);
 else if (input == RELEASE_DOWN)
  if (isDucking_)
   // Stand...
你崇拜一些程序员,他们总是看起来会编写完美无瑕的代码,然而他们并非超人。相反,他们有一种直觉会意识到哪种类型的代码容易出错,然后避免编写出这种代码。复杂的分支和可变的状态——随时间变化的字段,这是两种容易出错的代码,上面的例子就是这样。又到寻找bug的时间了。找到了吗?我们发现主角在跳跃状态的时候不能再跳,但是在俯冲攻击的时候却可以跳跃。又要添加一个成员变量······很明显,我们的这种做法有问题。每次我们添加一些功能的时候,都会不经意地破坏已有代码的功能。而且,我们还有很多“行走”等动作没有添加。如果我们还是采用类似的做法,那bug可能会更多。7.2 救星:有限状态机为了消除你心中的疑惑,你可以准备一张纸和一支笔,让我们一起来画一张流程图。对于女主角能够进行的动作画一个“矩形”:站立、跳跃、躲避和俯冲。当你可以按下一个键让主角从一个状态切换到另一个状态的时候,我们画一个箭头,让它从一个矩形指向另一个矩形。同时在箭头上面添加文本,表示我们按下的按钮。恭喜,你刚刚已经成功创建了一个有限状态机。有限状态机借鉴了计算机科学里的自动机理论(automata theory)中的一种数据结构(图灵机)思想。有限状态机(FSMs)可以看作是最简单的图灵机(如图7-1所示)。图7-1 一张状态机的图表其表达的是:关于有限状态机我最喜欢的比喻就是它是像Zork一样的古老的文字冒险游戏。游戏中有着由出口连接着的一些房间。你可以通过输入像“往北前进”这样的命令来进行探索。这其实就是一个状态机:每一个房间是一个状态。你所在的房间就是当前的状态。每个房间的出口就是它的转换,导航命令就是输入。你拥有一组状态,并且可以在这组状态之间进行切换。比如:站立、跳跃、躲避和俯冲。状态机同一时刻只能处于一种状态。女主角无法同时跳跃和站立。事实上,防止同时存在两个状态是我们使用有限状态机的原因。状态机会接收一组输入或者事件。在我们这个例子中,它们就是按钮的按下和释放。每一个状态有一组转换,每一个转换都关联着一个输入并指向另一个状态。当有一个输入进来的时候,如果输入与当前状态的其中一个转换匹配上,则状态机便会转换状态到输入事件所指的状态。在我们的例子中,在站立状态的时候如果按下向下方向键,则状态转换到躲避状态。如果在跳跃状态的时候按下向下方向键,则会转换到俯冲攻击状态。如果对于每一个输入事件没有对应的转换,则这个输入就会被忽略。简而言之,整个状态机可以分为:状态、输入和转换。你可以通过画状态流程图来表示它们。不幸的是,编译器并不认识状态图,所以,我们接下来要介绍如何实现。GoF的状态模式是一种实现方法,但是让我们先从更简单的方法开始。7.3 枚举和分支一个问题是,Heroine类有一些布尔类型的成员变量:isJumping_和isDucking_,但是这两个变量不应该同时为true。当你有一系列的标记成员变量,而它们只能有且仅有一个为true时,这表明我们需要把它们定义成枚举(enum)。在这个例子当中,我们的有限状态机的每一个状态可以用一个枚举来表示,所以,让我们定义以下枚举:enum State
 STATE_STANDING,
 STATE_JUMPING,
 STATE_DUCKING,
 STATE_DIVING
这里没有大量的标志位,Heroine类只有一个state_成员。我们也需要调换分支语句的顺序。在前面的代码中,我们先判断输入事件,然后才是状态。那种代码可以让我们集中处理每一个按键相关的逻辑,但是,它也让每一种状态的处理代码变得很乱。我们想把它们放在一起来处理,因此,我们先判断状态。代码如下:void Heroine::handleInput(Input input)
 switch (state_)
  case STATE_STANDING:
   if (input == PRESS_B)
    state_ = STATE_JUMPING;
    yVelocity_ = JUMP_VELOCITY;
    setGraphics(IMAGE_JUMP);
   else if (input == PRESS_DOWN)
    state_ = STATE_DUCKING;
    setGraphics(IMAGE_DUCK);
  // Other states...
我们可以像下面设置其他状态:void Heroine::handleInput(Input input)
 switch (state_)
  // Standing state...
  case STATE_JUMPING:
   if (input == PRESS_DOWN)
    state_ = STATE_DIVING;
    setGraphics(IMAGE_DIVE);
  case STATE_DUCKING:
   if (input == RELEASE_DOWN)
    state_ = STATE_STANDING;
    setGraphics(IMAGE_STAND);
这样看起来虽然很普通,但是它却是对前面的代码的一个提升。我们仍然有一些条件分支语句,但是我们简化了状态的处理。所有处理单个状态的代码都集中在一起了。这是实现状态机最简单的方法,而且在某些情况下,这样做也挺好的。重要的是,我们的女主角再也不可能处于一个无效的状态了。通过布尔值标识,会存在一些没有意义的值。但是,使用枚举,则每一个枚举值都是有意义的。你的问题可能也会超过此方案能解决的范围。比如,我们想在主角下蹲躲避的时候“蓄能”,然后等蓄满能量之后可以释放出一个特殊的技能。那么,当主角处于躲避状态的时候,我们需要添加一个变量来记录蓄能时间。如果你猜这是更新方法模式,那么恭喜你,你猜中了!我们可以在Heroine类中添加一个chargeTime_成员来记录主角蓄能的时间长短。假设,我们已经有一个update()方法了,并且这个方法会在每一帧被调用。在那里,我们可以使用如下代码片断能记录蓄能的时间:void Heroine::update()
 if (state_ == STATE_DUCKING)
  chargeTime_++;
  if (chargeTime_ & MAX_CHARGE)
   superBomb();
我们需要在主角躲避的时候重置这个蓄能时间,所以,我们还需要修改handleInput()方法:void Heroine::handleInput(Input input)
 switch (state_)
  case STATE_STANDING:
   if (input == PRESS_DOWN)
    state_ = STATE_DUCKING;
    chargeTime_ = 0;
    setGraphics(IMAGE_DUCK);
   // Handle other inputs...
   // Other states...
总之,为了添加蓄能攻击,我们不得不修改两个方法,并且添加一个chargeTime_成员变量给主角,尽管这个成员变量只有在主角处于躲避状态的时候才有效。其实我们真正想要的是把所有这些和与之相关的数据和代码封装起来。接下来,我们介绍GoF的状态模式来解决这个问题。7.4 状态模式对于熟知面向对象方法的人来说,每一个条件分支都可以用动态分发来解决(换句话说,都可以用C++里面的虚函数来解决)。但是,如果这样做,你可能会把简单问题复杂化。有时候,一个简单的if语句就足够了。状态模式的由来也有一些历史原因。许多面向对象设计的拥护者—— GoF和重构的作者Martin Fowler都是Smalltalk出身。在那里,如果有一个ifThen语句,我们便可以用一个表示true和false的对象来操作。但是,在我们这个例子当中,我们发现面对对象设计也就是状态模式更合适。GoF描述的状态模式在应用到我们的例子中时如下。7.4.1 一个状态接口首先,我们为状态定义一个接口。每一个与状态相关的行为都定义成虚函数。在我们的例子中,就是handleInput()和update()函数。class HeroineState
 virtual ~HeroineState() {}
 virtual void handleInput(Heroine& heroine,
                Input input) {}
 virtual void update(Heroine& heroine) {}
7.4.2 为每一个状态定义一个类对于每一个状态,我们定义了一个类并继承此状态接口。它的方法定义主角对应此状态的行为。换句话说,把之前的switch语句里面的每一个case语句里的内容放置到它们对应的状态类里面去。比如:class DuckingState : public HeroineState
 DuckingState()
 : chargeTime_(0)
 virtual void handleInput(Heroine& heroine,
                Input input) {
  if (input == RELEASE_DOWN)
   // Change to standing state...
   heroine.setGraphics(IMAGE_STAND);
 virtual void update(Heroine& heroine) {
  chargeTime_++;
  if (chargeTime_ & MAX_CHARGE)
   heroine.superBomb();
 int chargeTime_;
注意,我们这里chargeTime_从Heroine类中移到了DuckingState(躲避状态)类中。这样非常好,因为这个变量只是对躲避状态有意义,现在把它定义在这里,正好显式地反映了我们的对象模型。7.4.3 状态委托接下来,我们在主角类中定义一个指针变量,让它指向当前的状态。我们把之前那个很大的switch语句去掉,并让它去调用状态接口的虚函数,最终这些虚方法就会动态地调用具体子状态的相应函数。状态委托看起来很像策略模式和类型对象模式(第13章)。在这三个模式中,你会有一个主对象委托给另外的附属对象。它们三者的区别主要在于目的不同:策略模式的目标是将主类与它的部分行为进行解耦。类型对象模式的目标是使得多个对象通过共享相同类型对象的引用来表现出相似性。状态模式的目标是通过改变主对象代理的对象来改变主对象的行为。class Heroine
 virtual void handleInput(Input input)
  state_-&handleInput(*this, input);
 virtual void update() { state_-&update(*this); }
 // Other methods...
 HeroineState* state_;
为了修改状态,我们需要把state_指针指向另一个不同的HeroineState状态对象。至此,我们的状态模式就讲完了。7.5 状态对象应该放在哪里呢我这里忽略了一些细节。为了修改一个状态,我们需要给state_指针赋值为一个新的状态,但是这个新的状态对象要从哪里来呢?我们之前的枚举方法是定义一些数字。但是,现在我们的状态是类,我们需要获取这些类的实例。通常来说,有两种实现方法。7.5.1 静态状态如果一个状态对象没有任何数据成员,那么它的唯一数据成员便是虚表指针了。那样的话,我们就没有必要创建此状态的多个实例了,因为它们的每一个实例都是相同的。在那种情况下,我们可以定义一个静态实例。即使你有一系列的FSM在同时运转,所有的状态机也能同时指向这一个唯一的实例。如果你的状态类没有任何数据成员,并且只有一个虚函数方法。那么我们还可以进一步简化此模式。我们可以使用一个普通的状态函数来替换状态类。这样的话,我们的state_变量就变成一个状态函数指针。这个就是享元模式。(第3章)你把静态方法放置在哪里,这个由你自己来决定。如果没有任何特殊原因的话,我们可以把它放置到基类状态类中:class HeroineState
 static StandingS
 static DuckingS
 static JumpingS
 static DivingS
 // Other code...
每一个静态成员变量都是对应状态类的一个实例。如果我们想让主角跳跃,那么站立状态应该是这样子:if (input == PRESS_B)
 heroine.state_ = &HeroineState::
 heroine.setGraphics(IMAGE_JUMP);
7.5.2 实例化状态有时候上面的方法可能不行。一个静态状态对于躲避状态而言是行不通的。因为它有一个chargeTime_成员变量,所以这个具体取决于每一个躲避状态下的主角类。如果我们的游戏里面只有一个主角的话,那么定义一个静态类也是没有什么问题的。但是,如果我们想加入多个玩家,那么此方法就行不通了。当你为状态实例动态分配空间时,你不得不考虑碎片化问题了。对象池模式(第19章)可以帮助到你。在那种情况下,我们不得不在状态切换的时候动态地创建一个躲避状态实例。这样,我们的有限状态机就拥有了它自己的实例。当然,如果我们又动态分配了一个新的状态实例,则要负责清理老的状态实例。这里必须相当小心,因为修改状态的函数是在当前状态里面,所以我们需要小心地处理删除的顺序。另外,我们也可以选择在HeroineState类中的handleInput()方法里面可选地返回一个新的状态。当这个状态返回的时候,主角将会删除老的状态并切换到这个新的状态,如下所示:void Heroine::handleInput(Input input)
 HeroineState* state = state_-&handleInput(
      *this, input);
 if (state != NULL)
  delete state_;
  state_ =
那样的话,我们只有在从handleInput方法返回的时候才有可能去删除前面的状态对象。现在,站立状态可以通过创建一个躲避状态的实例来切换状态了。HeroineState* StandingState::handleInput(
      Heroine& heroine, Input input)
 if (input == PRESS_DOWN)
  // Other code...
  return new DuckingState();
 // Stay in this state.
 return NULL;
通常情况下,我倾向于使用静态状态。因为它们不会占用太多的CPU和内存资源。7.6 进入状态和退出状态的行为状态模式的目标就是将每个状态相关的所有的数据和行为封装到相关类里面。万里长征,我们仅仅迈出去了一步,我们还有更多路要走。当主角更改状态的时候,我们也会切换它的贴图。现在,这段代码包含在它要切换的状态的上一个状态里面。当她从躲避状态切换到站立状态时,躲避状态将会修改它的图像:HeroineState* DuckingState::handleInput(
      Heroine& heroine, Input input)
 if (input == RELEASE_DOWN)
  heroine.setGraphics(IMAGE_STAND);
  return new StandingState();
 // Other code...
我们希望的是,每一个状态控制自己的图像。我们可以通过给每一个状态添加一个entey行为。class StandingState : public HeroineState
 virtual void enter(Heroine& heroine)
  heroine.setGraphics(IMAGE_STAND);
 // Other code...
回到Heroine类,我们修改代码来处理状态切换的情况:void Heroine::handleInput(Input input)
 HeroineState* state = state_-&handleInput(
      *this, input);
 if (state != NULL)
  delete state_;
  state_ =
  // Call the enter action on the new state.
  state_-&enter(*this);
这样也可以让我们简化躲避状态的代码:HeroineState* DuckingState::handleInput(
      Heroine& heroine, Input input)
 if (input == RELEASE_DOWN)
  return new StandingState();
 // Other code...
它所做的就是切换到站立状态,然后站立状态会自己设置图像。现在,我们的状态已经封装好了。entry动作的一个最大的好处就是它不用关心上一个状态是什么,它只需要根据自己的状态来处理图像和行为就可以了。大部分的真实状态图里面,我们有多个状态对应同一个状态。比如,我们的女主角会在她俯冲或者跳跃之后站立在地面上。这意味着,我们可能会在每一个状态发生变化的时候重复写很多代码。但是,entry动作帮我们很好地解决了这个问题。当然,我们也可以扩展这个功能来支持退出状态的行为。我们可以定义一个exit函数来定义一些在状态改变前的处理。7.7 有什么收获吗一个有限状态机甚至都不是图灵完备的。自动机理论使用一系列抽象的模型来描述计算,并且每一个模型都比先前的模型更复杂。而图灵机只是这里面最具有表达力的模型之一。“图灵完备”意味着一个系统(通常指的是一门编程语言)是足够强大的,强大到它可以实现一个图灵机。这也意味着,所有图灵完备的编程语言,在某些程度上其表达力是相同的。但有限状态机由于其不够灵活,并不在其中。我已经花了大量的时间来介绍有限状态机。现在我们一起来捋一捋。到目前为止,我跟你讲的所有事情都是对的,有限状态机对于某些应用来讲是非常合适的。但是,最大的优点往往也是最大的缺点。状态机帮助你把千丝万缕的逻辑判断代码封装起来。你需要的只是一组调整好的状态,一个当前状态和一些硬编码的状态切换。如果你想要用一个状态机来表示一些复杂的游戏AI,则可能会面临这个模型的一些限制。幸运的是,我们的前辈们已经发现了一些不错的解决方案。我将会在本章的最后简单地介绍它们。7.8 并发状态机我们决定给我们的主角添加持枪功能。当她持枪的时候,她仍然可以:跑、跳和躲避等。但是,她也需要能够在这些状态过程中开火。如果你执着于传统的有限状态机,那我们可能需要把之前的状态加倍。对于每一个已经存在的状态,我们需要定义另一个状态,它做的事情也差不多,不过就是多了持枪的操作。比如站立状态和站立开火状态,跳跃状态和跳跃开火状态等。如果我们添加更多的武器种类,那么这个状态数量将会急剧增加。而且不仅仅是增加了大量的状态类实例,它还会增加大量的冗余,实际上带不带枪的状态仅有是否包含开火代码的区别而已。这里的问题是,我们把两种状态杂合在一起了。我们把两种不同的状态硬塞到一个状态机里面去了。为所有可能出现的组合建模,我们可能需要为每一种状态准备一组状态。解决方法比较直观,就是分开成两个状态机。如果我们需要为主角定义n种状态和m种它能够携带的武器状态,如果使用一个状态机来表示,那么我们需要n×m个状态。而如果使用两个状态机,那么状态组合仅是n+m。首先我们可以保留原有的状态机的代码和功能不管它。接下来,我们定义一个单独的状态机,用来处理主角携带的武器。现在,我们的主角会有两个状态索引,其中一个看起来如下所示:为了便于示例说明,我们这里使用了完整的状态模式来处理女主角的装备变化。事实上,由于装备目前只有两个状态,我们完全可以只使用一个布尔值变量来替代。class Heroine
 // Other code...
 HeroineState* state_;
 HeroineState* equipment_;
当主角派发输入事件给状态类时,需要给两种状态都派发一下。void Heroine::handleInput(Input input)
 state_-&handleInput(*this, input);
 equipment_-&handleInput(*this, input);
这样每一个状态机都可以响应输入事件并以此切换状态而不用考虑其他状态机的实现细节。当两个状态没什么关系的时候,这种方法工作得很好。功能更加完备的系统可能会让一个状态机来处理输入,以便另外一个状态机不会接收到输入。这样将能防止两个状态机对同一输入进行错误的响应。在实际中,你可能会发现你需要对某些状态处理进行干预。比如,如果主角不能够在跳跃的过程中开火,或者她在装备武器的时候不能俯冲。为了处理这种情况,在代码里面,对于每一个状态,你可能需要做一些简单的if判断并做出特殊处理。虽然这可能不是最好的解决方案,但是至少它可以完成任务。7.9 层次状态机在我们把主角的行为更加具象化以后,她可能会包含大量相似的状态。比如,她可能有站立、走路、跑步和滑动状态。在这些状态中的任何一个状态时按下B键,我们的主角要跳跃;按下下方向键,我们的主角要躲避。如果只是使用一个简单的状态机实现,我们可能会在这些状态中重复不少代码。更好的解决方案是,我们只需要实现一次然后它便可以在所有的状态下都复用。这可能同时带来好坏两种影响。继承是一种强大的代码重用方式,但是,它也会使得子类与基类之间的代码变得紧耦合。它是一个很大的“锤子”,需小心使用才行。如果我们抛开状态机来谈面向对象,有一种共享代码的方式便是继承。我们可以定义一个类来表示“on ground”的状态,它用来处理跳跃状态和躲避状态。站立、走路、跑步和滑行状态从这个“on ground”的状态继承而来,并且在其类里面实现一些特殊行为。这里,我们通常把这种状态机叫做层次状态机。一个状态有一个父状态。当有一个事件进来的时候,如果子状态不处理它,那么沿着继承链传给它的父状态来处理。换句话说,它有点像覆盖继承的方法。实际上,如果我们正在使用状态模式来实现有限状态机,那么我们可以使用继承类来实现继承。我们首先定义一个基类来表示父状态:class OnGroundState : public HeroineState
 virtual void handleInput(Heroine& heroine,
                   Input input)
  if (input == PRESS_B) // Jump...
  else if (input == PRESS_DOWN) // Duck...
然后,每一个子状态都继承至它:class DuckingState : public OnGroundState
 virtual void handleInput(Heroine& heroine,
                   Input input)
  if (input == RELEASE_DOWN)
   // Stand up...
   // Didn't handle input, so walk up hierarchy.
   OnGroundState::handleInput(heroine, input);
当然,这不是实现继承的唯一方式。如果你没有使用GoF的状态模式,这种做法可能并不奏效。不过,你可以在基类中使用状态栈而不是单单一个状态的方法来更加明确地表示父状态的状态链。我们当前的状态总是处于栈顶,栈顶下面的第一个元素是它的父状态,再下一个状态则是它的父状态的父状态,以此类推。如果你要进行一些与状态相关的行为操作,那么首先从栈顶状态开始。如果它不处理,则往下寻找直到找到一个能处理此事件的状态为止(如果找遍整个栈了,还是没能被处理,则将此事件被忽略掉)。7.10 下推自动机还有一种有限状态机的扩展,它们也使用状态栈。容易让人混淆的是,这里的栈代表了完全不同的东西,且用于解决一个完全不同的问题。它要解决的是有限状态机没有历史记录的问题。我们知道当前状态,但是,我们并不知道之前的状态是什么。而且,我们也没有简便的方法可以获取之前的状态。举个例子:之前,让无畏的主角全副武装。当她开枪的时候,我们需要一种新的状态来播放开枪的动画,发射子弹并显示一些特效。因此,我们需要定义一个FiringState,并且所有的状态都可以切换到这个状态,只要有玩家按下开火按键就行了。因为这个行为在许多状态里面都重复了,所以是个使用层次状态机来复用代码的好机会。那么问题来了,当她开完枪后,她要回到什么状态呢?主角可以处于站立、躲避、俯冲和跳跃状态。但开火的动画播放完以后,她应该要回到之前的状态。如果我们仍然坚持使用以前的有限状态机,那么我们将无法获得上一个状态的信息。为了保留上一个状态的信息,我们不得不定义一些几乎对等的状态,比如站立开火状态,跑步开火状态等。这样的话,当我们的开火状态完成以后,就可以切换回之前的状态了。我们需要的仅仅是一种能够让我们可以保存开火前状态的方法,这样在开火状态完成之后可以回去。这里自动机理论再次帮上了我们的忙。相关的数据结构叫做下推自动机(pushdown automata)。本来,有限状态机有一个指向当前状态的指针。而下推自动机则有一个状态栈。在一个有限状态机里面,当有一个状态切进来时,则替换掉之前的状态。下推自动机可以让你这样做,同时它还提供其他选择:你可以把这个新的状态放入栈里面。当前的状态永远存在栈顶,所以你总能转换到当前状态。但是当前状态会将前一个状态压在栈中自身的下面而不是抛弃掉它。你可以弹出栈顶的状态,该状态将被抛弃。与此同时,上一个状态就变成了新的栈顶状态了。图7-2所示就是我们的开火状态所需要的。当开火按钮在任何一种状态下被按下的时候,我们把开火状态push到栈顶。当开火动画结束的时候,我们把这个开火状态pop出去。此时,状态机会自动切换到我们开火前的上一个状态。图7-2 对状态进行push和pop,与pop和lock不同7.11 现在知道它们有多有用了吧即使有了这些通用的状态机扩展,它们的使用范围仍然是有限的。在游戏的AI领域,最近的趋势是越来越倾向于行为树和规划系统。如果你对复杂的AI感兴趣的话,那么本章所有这些内容只是在刺激你的胃口。你可能还想通过阅读其他的书籍来了解它们。但是这并不意味着有限状态机、下推自动机和其他简单的状态机没有用。它们对于解决某些特定的问题是一个很好的建模工具。当你的问题满足以下几点要求的时候,有限状态机将会非常有用:你有一个游戏实体,它的行为基于它的内部状态而改变。这些状态被严格划分为相对数目较少的小集合。游戏实体随着时间的变化会响应用户输入和一些游戏事件。在游戏里,它们被广泛使用在AI里面,但是它们也经常被应用于用户输入处理、浏览菜单屏幕、解析文件、网络协议和其他异步的行为。喜欢看书和分享的朋友可以加群--& 程序员书屋群:221收藏分享举报文章被以下专栏收录推荐靠谱技术书。公众号(程序员书屋)programmer-book{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\u002Fpay.zhihu.com\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&isPending&:false,&contributes&:[{&sourceColumn&:{&lastUpdated&:,&description&:&出版圈里小混混一枚,QQ群: 公众账号:programmer-book 。专攻技术书,为大家推荐最靠谱的书。每天穿梭在微博、微信中、社区中,只要搜索出版圈郭志敏或者程序员书屋或者异步社区郭志敏,都可以找到我。&,&permission&:&COLUMN_PUBLIC&,&memberId&:356440,&contributePermission&:&COLUMN_PUBLIC&,&translatedCommentPermission&:&all&,&canManage&:true,&intro&:&推荐靠谱技术书。公众号(程序员书屋)programmer-book&,&urlToken&:&guozhimin&,&id&:3125,&imagePath&:&v2-9ae8bb14e1caf8e5df146c.jpg&,&slug&:&guozhimin&,&applyReason&:&&,&name&:&出版圈郭志敏&,&title&:&出版圈郭志敏&,&url&:&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fguozhimin&,&commentPermission&:&COLUMN_ALL_CAN_COMMENT&,&canPost&:true,&created&:,&state&:&COLUMN_NORMAL&,&followers&:3180,&avatar&:{&id&:&v2-9ae8bb14e1caf8e5df146c&,&template&:&https:\u002F\u002Fpic4.zhimg.com\u002F{id}_{size}.jpg&},&activateAuthorRequested&:false,&following&:false,&imageUrl&:&https:\u002F\u002Fpic4.zhimg.com\u002Fv2-9ae8bb14e1caf8e5df146c_l.jpg&,&articlesCount&:107},&state&:&accepted&,&targetPost&:{&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-a0f2be1c59cd_r.jpg&,&lastUpdated&:,&imagePath&:&v2-a0f2be1c59cd.png&,&permission&:&ARTICLE_PUBLIC&,&topics&:[,1045],&summary&:&本周推荐一本新书《游戏编程模式》,阅读群体:游戏领域的设计人员、开发人员,还是想要进入游戏开发领域的学生和普通程序员,都可以阅读本书。\u003Cstrong\u003E让游戏程序员在高度复杂的代码库面前不再止步的一本书\u003C\u002Fstrong\u003E 全书共分20章,通过三大部分内容全面介绍了与游戏编程模…&,&copyPermission&:&ARTICLE_COPYABLE&,&translatedCommentPermission&:&all&,&likes&:0,&origAuthorId&:0,&publishedTime&:&T16:35:20+08:00&,&sourceUrl&:&&,&urlToken&:,&id&:1189142,&withContent&:false,&slug&:,&bigTitleImage&:false,&title&:&让游戏程序员在代码面前不再止步&,&url&:&\u002Fp\u002F&,&commentPermission&:&ARTICLE_ALL_CAN_COMMENT&,&snapshotUrl&:&&,&created&:,&comments&:0,&columnId&:3125,&content&:&&,&parentId&:0,&state&:&ARTICLE_PUBLISHED&,&imageUrl&:&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-a0f2be1c59cd_r.jpg&,&author&:{&bio&:&专给程序员推荐优秀技术书的小编&,&isFollowing&:false,&hash&:&d6f5fb574ada362f23f849&,&uid&:60,&isOrg&:false,&slug&:&guozhimin&,&isFollowed&:false,&description&:&出版圈里小混混一枚,专门给大家推荐靠谱技术书。\n个人公众账号:programmer-book\n微博:@异步社区郭志敏 &,&name&:&郭志敏&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fguozhimin&,&avatar&:{&id&:&v2-8ca35ffd866&,&template&:&https:\u002F\u002Fpic1.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&memberId&:356440,&excerptTitle&:&&,&voteType&:&ARTICLE_VOTE_CLEAR&},&id&:434506}],&title&:&让游戏程序员在代码面前不再止步&,&author&:&guozhimin&,&content&:&\u003Cp\u003E本周推荐一本新书《游戏编程模式》,阅读群体:游戏领域的设计人员、开发人员,还是想要进入游戏开发领域的学生和普通程序员,都可以阅读本书。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E让游戏程序员在高度复杂的代码库面前不再止步的一本书\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg src=\&https:\u002F\u002Fpic2.zhimg.com\u002Fv2-a08afe2ecdb0aceeddf76_b.jpg\& data-rawwidth=\&217\& data-rawheight=\&279\& class=\&content_image\& width=\&217\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='217'%20height='279'&&\u002Fsvg&\& data-rawwidth=\&217\& data-rawheight=\&279\& class=\&content_image lazy\& width=\&217\& data-actualsrc=\&https:\u002F\u002Fpic2.zhimg.com\u002Fv2-a08afe2ecdb0aceeddf76_b.jpg\&\u003E\u003C\u002Ffigure\u003E\u003Cp\u003E全书共分20章,通过三大部分内容全面介绍了与游戏编程模式相关的各类知识点。第一部分介绍了基础知识和框架;第二部分深入探索设计模式,并介绍了模式与游戏开发之间的关联;第三部分介绍了13种有效的游戏设计模式。\u003C\u002Fp\u003E\u003Ch2\u003E本书和设计模式有什么联系\u003C\u002Fh2\u003E\u003Cp\u003E任何名字中带有“模式”的编程书籍都和经典图书《设计模式:可复用面向对象软件的基础》有所联系。这本书由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides编著(这4人也称为“Gang of Four”,即本书所提到的“GoF”四人组)。\u003C\u002Fp\u003E\u003Cp\u003E设计模式一书本身也源自前人的灵感。创造一种模式语言来描述问题的开放性解决方案,该想法来自《A Pattern Language》 ,由 Christopher Alexander(和Sarah Ishikawa、Murray Silverstein一起)完成。\u003C\u002Fp\u003E\u003Cp\u003E这是一本关于框架结构的书(就像真正的建筑结构中建筑与墙体和材料之间的关系),作者希望他人能够将其运用作其他领域问题的解决方案。设计模式(Design Patterns)正是GoF在软件领域的一个尝试。\u003C\u002Fp\u003E\u003Cp\u003E本书的英文原名是Game Programming Design Patterns,并不是说GoF的书不适用于游戏。恰恰相反,在本书第2篇中介绍了众多来自GoF著作的设计模式,同时强调了在它们游戏开发中的运用。\u003C\u002Fp\u003E\u003Cp\u003E从另一面说,我觉得这本书也适用于非游戏软件。我也可以把这本书命名为《More Design Patterns》 ,但我认为游戏开发有更多迷人的例子。难道你真的想要阅读的另外一本关于员工记录和银行账户例子的设计模式图书吗?\u003C\u002Fp\u003E\u003Cp\u003E也就是说,尽管这里介绍的模式在其他软件中也是有用的,但我觉得它们特别适合应对游戏工程中普遍会遇到的挑战,例如:\u003C\u002Fp\u003E\u003Cp\u003E时间和顺序往往是一个游戏的架构的核心部分。事情必须依照正确的顺序和正确的时间发生。\u003C\u002Fp\u003E\u003Cp\u003E开发周期被高度压缩。众多程序员必须在不牵涉他人代码、不污染代码库的前提下对一套庞大而错杂的行为体系进行快速的构建与迭代。\u003C\u002Fp\u003E\u003Cp\u003E所有这些行为被定义后,游戏便开始互动。怪物撕咬英雄,药水混合在一起,炸弹炸到敌人和朋友……诸如此类。这些交互必须很好地进行下去,可不能把代码库给搅成一团毛线球。\u003C\u002Fp\u003E\u003Cp\u003E最后,性能在游戏中至关重要。游戏开发者永远在榨取平台性能这件事上赛跑。多削掉一个CPU周期,你的游戏就有可能从掉帧和差评迈入A级游戏和百万销量的天堂。\u003C\u002Fp\u003E\u003Ch2\u003E市面上已有的书籍\u003C\u002Fh2\u003E\u003Cp\u003E目前市面已经有数十多本游戏编程的书籍。为什么还要再写一本?\u003C\u002Fp\u003E\u003Cp\u003E我见过的大多数游戏编程书籍无非两类。\u003C\u002Fp\u003E\u003Cp\u003E关于特定领域的书籍。这些针对性较强的书籍带领你深入地探索游戏开发的一些特定方面。它们会教你3D图形、实时渲染、物理仿真、人工智能或音频处理。这些是众多游戏程序员在自己的职业生涯中所专注的领域。\u003C\u002Fp\u003E\u003Cp\u003E关于整个游戏引擎的书籍。相反,这些图书试图涵盖整个游戏引擎的各个部分。它们的目标是构建一整套适合某个特殊游戏类型的引擎系统,这类通常是3D第一人称射击游戏。\u003C\u002Fp\u003E\u003Cp\u003E我喜欢这两类书,但我觉得它们仍留下了一些空白。讲特定领域的书很少会谈及你的代码块如何与游戏的其他部分交互。你可能擅长物理和渲染,但是你知道如何优雅地将它们拼合起来吗?\u003C\u002Fp\u003E\u003Cp\u003E这种分类讲解风格的另外一个例子,就是广受大家喜爱的\u003Ca href=\&http:\u002F\u002Flink.zhihu.com\u002F?target=http%3A\u002F\u002Fwww.epubit.com.cn\u002Ftag\u002Fdetails\u002F25\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E《游戏编程精粹》\u003C\u002Fa\u003E系列。\u003C\u002Fp\u003E\u003Cp\u003E第二类书籍涵盖了这类问题,但我往往发现这类书通常都太过庞大、太过空泛。特别是随着移动和休闲游戏的兴起,我们正处在众多类型的游戏共同发展的时代。我们不再只是照搬Quake\u003C\u002Fp\u003E\u003Cp\u003E了。当你的游戏不适合这个模型时,这类阐述单一引擎的书籍就不再合适了。\u003C\u002Fp\u003E\u003Cp\u003E相反,这里我想要做的,更倾向于分门别类。本书的每个章节都是一个独立的思路,你可以将它应用到你的代码里。你也可以针对自己制作的游戏来决定以最恰当的方式将它们进行混搭。\u003C\u002Fp\u003E\u003Ch2\u003E作者介绍\u003C\u002Fh2\u003E\u003Cp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg src=\&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-99f0fd698abdc7b5802d7_b.jpg\& data-rawwidth=\&382\& data-rawheight=\&400\& class=\&content_image\& width=\&382\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='382'%20height='400'&&\u002Fsvg&\& data-rawwidth=\&382\& data-rawheight=\&400\& class=\&content_image lazy\& width=\&382\& data-actualsrc=\&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-99f0fd698abdc7b5802d7_b.jpg\&\u003E\u003C\u002Ffigure\u003ERobert Nystrom是一位具备超过20年职业编程经验的开发者,而其中大概一半时间用于从事游戏开发。在艺电(Electronic Arts)的8年时间里,他曾参与劲爆美式足球(Madden)系列这样庞大的项目,也曾投身于亨利o海茨沃斯大冒险(Henry Hatsworth in the Puzzling Adventure)这样稍小规模的游戏开发之中。他所开发的游戏遍及PC、GameCube、PS2、XBox、X360以及DS平台。但最傲人之处在于,他为开发者们提供了开发工具和共享库。他热衷于寻求易用的、漂亮的代码来延伸和增强开发者们的创造力。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003ERobert与他的妻子和两个女儿定居于西雅图,在那里你很有可能会见到他正在为朋友们下厨,或者在为他们上啤酒。\u003C\u002Fp\u003E\u003Ch2\u003E章节欣赏:状态模式(选摘)\u003C\u002Fh2\u003E\u003Cp\u003E“允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像是在修改自身类。”\u003C\u002Fp\u003E\u003Cp\u003E交代一下:我写的有些过头了,我在本章里面添加了太多东西。表面上这一章是介绍状态模式\u003Ca href=\&http:\u002F\u002Flink.zhihu.com\u002F?target=http%3A\u002F\u002Fwww.epubit.com.cn\u002Fbook\u002Fonlinechapter\u002F41170%23anchor71\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E[1]\u003C\u002Fa\u003E的,但是我不能抛开游戏里面的有限状态机(finite state machines,FSM)而单独只谈“状态模式”。不过,当我讲到FSM的时候,我发觉我还有必要再介绍一下层次状态机(hierarchical state machine)和下推自动机(pushdown automata)。\u003C\u002Fp\u003E\u003Cp\u003E因为有太多东西需要讲,所以我试图压缩本章的内容。本章中的代码片断没有涉及很细节的东西,所以,这些省略的部分需要靠读者来脑补。我希望它们仍然足够清楚到能让你掌握关键点(big picture)。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E层次状态机和下推自动机这对术语指的是早期的人工智能。在20世纪50年代和60年代,大部分AI研究关注的是语言处理。许多现在用来解析编程语言的编译器被发明用来解析人类语言。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E如果你从未听说过状态机,也不要感到沮丧。它们对于人工智能领域的开发者和编译器黑客来说非常熟悉,不过在其他编程领域可能不是那么被人熟知了。我觉得它应该被更多的人了解,因此,我将从一个不同的应用领域的视角来介绍它。\u003C\u002Fp\u003E\u003Ch2\u003E7.1 我们曾经相遇过\u003C\u002Fh2\u003E\u003Cp\u003E假设我们现在正在开发一款横版游戏。我们的任务是实现女主角——游戏世界中玩家的图像。我们需要根据玩家的输入来控制主角的行为。当按下B键的时候,她应该跳跃。我们可以这样实现:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n if (input == PRESS_B)\n {\n  yVelocity_ = JUMP_VELOCITY;\n  setGraphics(IMAGE_JUMP);\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E找找看,bug在哪里?\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E这里应该还有如果主角着地将isJumping_设置回false的代码。为了简洁起见,我省略了。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E我们没有阻止主角“在空中跳跃”——当主角跳起来后持续按下B键。这样会导致她一直飘在空中,简单的修复方法可以是:在Heroine类中添加一个isJumping_布尔值变量来跟踪主角的跳跃,然后这么做:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n if (input == PRESS_B)\n {\n  if (!isJumping_)\n  {\n   isJumping_ =\n   \u002F\u002F Jump...\n  }\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E接下来,我们想实现主角的闪避动作。当主角站在地面上的时候,如果玩家按下下方向键,则躲避,如果松开此键,则站立。\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n if (input == PRESS_B)\n {\n  \u002F\u002F Jump if not jumping...\n }\n else if (input == PRESS_DOWN)\n {\n  if (!isJumping_)\n  {\n   setGraphics(IMAGE_DUCK);\n  }\n }\n else if (input == RELEASE_DOWN)\n {\n  setGraphics(IMAGE_STAND);\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E找找看,bug在哪里?\u003C\u002Fp\u003E\u003Cp\u003E通过上面的代码,玩家可以:\u003C\u002Fp\u003E\u003Cp\u003E1.按下方向键来闪避。\u003C\u002Fp\u003E\u003Cp\u003E2.按B键从闪避的状态直接跳起来。\u003C\u002Fp\u003E\u003Cp\u003E3.玩家还在空中的时候松开下键。\u003C\u002Fp\u003E\u003Cp\u003E此时,当女主角在跳跃状态的时候,显示的是站立的图像。是时候添加另外一个布尔标志位来解决该问题了······\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n if (input == PRESS_B)\n {\n  if (!isJumping_ && !isDucking_)\n  {\n   \u002F\u002F Jump...\n  }\n }\n else if (input == PRESS_DOWN)\n {\n  if (!isJumping_)\n  {\n   isDucking_ =\n   setGraphics(IMAGE_DUCK);\n  }\n }\n else if (input == RELEASE_DOWN)\n {\n  if (isDucking_)\n  {\n   isDucking_ =\n   setGraphics(IMAGE_STAND);\n  }\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E接下来,如果我们的主角可以在跳起来的过程中,按下方向键进行一次俯冲攻击那就太酷了,代码如下:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n if (input == PRESS_B)\n {\n  if (!isJumping_ && !isDucking_)\n  {\n   \u002F\u002F Jump...\n  }\n }\n else if (input == PRESS_DOWN)\n {\n  if (!isJumping_)\n  {\n   isDucking_ =\n   setGraphics(IMAGE_DUCK);\n  }\n  else\n  {\n   isJumping_ =\n   setGraphics(IMAGE_DIVE);\n  }\n }\n else if (input == RELEASE_DOWN)\n {\n  if (isDucking_)\n  {\n   \u002F\u002F Stand...\n  }\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cblockquote\u003E\u003Cp\u003E你崇拜一些程序员,他们总是看起来会编写完美无瑕的代码,然而他们并非超人。相反,他们有一种直觉会意识到哪种类型的代码容易出错,然后避免编写出这种代码。\u003C\u002Fp\u003E\u003Cp\u003E复杂的分支和可变的状态——随时间变化的字段,这是两种容易出错的代码,上面的例子就是这样。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E又到寻找bug的时间了。找到了吗?\u003C\u002Fp\u003E\u003Cp\u003E我们发现主角在跳跃状态的时候不能再跳,但是在俯冲攻击的时候却可以跳跃。又要添加一个成员变量······\u003C\u002Fp\u003E\u003Cp\u003E很明显,我们的这种做法有问题。每次我们添加一些功能的时候,都会不经意地破坏已有代码的功能。而且,我们还有很多“行走”等动作没有添加。如果我们还是采用类似的做法,那bug可能会更多。\u003C\u002Fp\u003E\u003Ch2\u003E7.2 救星:有限状态机\u003C\u002Fh2\u003E\u003Cp\u003E为了消除你心中的疑惑,你可以准备一张纸和一支笔,让我们一起来画一张流程图。对于女主角能够进行的动作画一个“矩形”:站立、跳跃、躲避和俯冲。当你可以按下一个键让主角从一个状态切换到另一个状态的时候,我们画一个箭头,让它从一个矩形指向另一个矩形。同时在箭头上面添加文本,表示我们按下的按钮。\u003C\u002Fp\u003E\u003Cp\u003E恭喜,你刚刚已经成功创建了一个有限状态机。有限状态机借鉴了计算机科学里的自动机理论(automata theory)中的一种数据结构(图灵机)思想。有限状态机(FSMs)可以看作是最简单的图灵机(如图7-1所示)。\u003C\u002Fp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg src=\&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-4d13aec9ee00e9d9cca220f_b.jpg\& data-rawwidth=\&616\& data-rawheight=\&364\& class=\&origin_image zh-lightbox-thumb\& width=\&616\& data-original=\&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-4d13aec9ee00e9d9cca220f_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='616'%20height='364'&&\u002Fsvg&\& data-rawwidth=\&616\& data-rawheight=\&364\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&616\& data-original=\&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-4d13aec9ee00e9d9cca220f_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic1.zhimg.com\u002Fv2-4d13aec9ee00e9d9cca220f_b.jpg\&\u003E\u003C\u002Ffigure\u003E\u003Cp\u003E图7-1 一张状态机的图表\u003C\u002Fp\u003E\u003Cp\u003E其表达的是:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E关于有限状态机我最喜欢的比喻就是它是像Zork一样的古老的文字冒险游戏。游戏中有着由出口连接着的一些房间。你可以通过输入像“往北前进”这样的命令来进行探索。\u003C\u002Fp\u003E\u003Cp\u003E这其实就是一个状态机:每一个房间是一个状态。你所在的房间就是当前的状态。每个房间的出口就是它的转换,导航命令就是输入。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cul\u003E\u003Cli\u003E你拥有一组状态,并且可以在这组状态之间进行切换。比如:站立、跳跃、躲避和俯冲。\u003C\u002Fli\u003E\u003Cli\u003E状态机同一时刻只能处于一种状态。女主角无法同时跳跃和站立。事实上,防止同时存在两个状态是我们使用有限状态机的原因。\u003C\u002Fli\u003E\u003Cli\u003E状态机会接收一组输入或者事件。在我们这个例子中,它们就是按钮的按下和释放。\u003C\u002Fli\u003E\u003Cli\u003E每一个状态有一组转换,每一个转换都关联着一个输入并指向另一个状态。当有一个输入进来的时候,如果输入与当前状态的其中一个转换匹配上,则状态机便会转换状态到输入事件所指的状态。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E在我们的例子中,在站立状态的时候如果按下向下方向键,则状态转换到躲避状态。如果在跳跃状态的时候按下向下方向键,则会转换到俯冲攻击状态。如果对于每一个输入事件没有对应的转换,则这个输入就会被忽略。\u003C\u002Fp\u003E\u003Cp\u003E简而言之,整个状态机可以分为:状态、输入和转换。你可以通过画状态流程图来表示它们。不幸的是,编译器并不认识状态图,所以,我们接下来要介绍如何实现。GoF的状态模式是一种实现方法,但是让我们先从更简单的方法开始。\u003C\u002Fp\u003E\u003Ch2\u003E7.3 枚举和分支\u003C\u002Fh2\u003E\u003Cp\u003E一个问题是,Heroine类有一些布尔类型的成员变量:isJumping_和isDucking_,但是这两个变量不应该同时为true。当你有一系列的标记成员变量,而它们只能有且仅有一个为true时,这表明我们需要把它们定义成枚举(enum)。\u003C\u002Fp\u003E\u003Cp\u003E在这个例子当中,我们的有限状态机的每一个状态可以用一个枚举来表示,所以,让我们定义以下枚举:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eenum State\n{\n STATE_STANDING,\n STATE_JUMPING,\n STATE_DUCKING,\n STATE_DIVING\n};\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E这里没有大量的标志位,Heroine类只有一个state_成员。我们也需要调换分支语句的顺序。在前面的代码中,我们先判断输入事件,然后才是状态。那种代码可以让我们集中处理每一个按键相关的逻辑,但是,它也让每一种状态的处理代码变得很乱。我们想把它们放在一起来处理,因此,我们先判断状态。代码如下:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n switch (state_)\n {\n  case STATE_STANDING:\n   if (input == PRESS_B)\n   {\n    state_ = STATE_JUMPING;\n    yVelocity_ = JUMP_VELOCITY;\n    setGraphics(IMAGE_JUMP);\n   }\n   else if (input == PRESS_DOWN)\n   {\n    state_ = STATE_DUCKING;\n    setGraphics(IMAGE_DUCK);\n   }\n   \n\n  \u002F\u002F Other states...\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E我们可以像下面设置其他状态:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n switch (state_)\n {\n  \u002F\u002F Standing state...\n\n  case STATE_JUMPING:\n   if (input == PRESS_DOWN)\n   {\n    state_ = STATE_DIVING;\n    setGraphics(IMAGE_DIVE);\n   }\n   \n\n  case STATE_DUCKING:\n   if (input == RELEASE_DOWN)\n   {\n    state_ = STATE_STANDING;\n    setGraphics(IMAGE_STAND);\n   }\n   \n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E这样看起来虽然很普通,但是它却是对前面的代码的一个提升。我们仍然有一些条件分支语句,但是我们简化了状态的处理。所有处理单个状态的代码都集中在一起了。这是实现状态机最简单的方法,而且在某些情况下,这样做也挺好的。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E重要的是,我们的女主角再也不可能处于一个无效的状态了。通过布尔值标识,会存在一些没有意义的值。但是,使用枚举,则每一个枚举值都是有意义的。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E你的问题可能也会超过此方案能解决的范围。比如,我们想在主角下蹲躲避的时候“蓄能”,然后等蓄满能量之后可以释放出一个特殊的技能。那么,当主角处于躲避状态的时候,我们需要添加一个变量来记录蓄能时间。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E如果你猜这是更新方法模式,那么恭喜你,你猜中了!\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E我们可以在Heroine类中添加一个chargeTime_成员来记录主角蓄能的时间长短。假设,我们已经有一个update()方法了,并且这个方法会在每一帧被调用。在那里,我们可以使用如下代码片断能记录蓄能的时间:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::update()\n{\n if (state_ == STATE_DUCKING)\n {\n  chargeTime_++;\n  if (chargeTime_ & MAX_CHARGE)\n  {\n   superBomb();\n  }\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E我们需要在主角躲避的时候重置这个蓄能时间,所以,我们还需要修改handleInput()方法:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n switch (state_)\n {\n  case STATE_STANDING:\n   if (input == PRESS_DOWN)\n   {\n    state_ = STATE_DUCKING;\n    chargeTime_ = 0;\n    setGraphics(IMAGE_DUCK);\n   }\n\n   \u002F\u002F Handle other inputs...\n   \n\n   \u002F\u002F Other states...\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E总之,为了添加蓄能攻击,我们不得不修改两个方法,并且添加一个chargeTime_成员变量给主角,尽管这个成员变量只有在主角处于躲避状态的时候才有效。其实我们真正想要的是把所有这些和与之相关的数据和代码封装起来。接下来,我们介绍GoF的状态模式来解决这个问题。\u003C\u002Fp\u003E\u003Ch2\u003E7.4 状态模式\u003C\u002Fh2\u003E\u003Cp\u003E对于熟知面向对象方法的人来说,每一个条件分支都可以用动态分发来解决(换句话说,都可以用C++里面的虚函数来解决)。但是,如果这样做,你可能会把简单问题复杂化。有时候,一个简单的if语句就足够了。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E状态模式的由来也有一些历史原因。许多面向对象设计的拥护者—— GoF和重构的作者Martin Fowler都是Smalltalk出身。在那里,如果有一个ifThen语句,我们便可以用一个表示true和false的对象来操作。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E但是,在我们这个例子当中,我们发现面对对象设计也就是状态模式更合适。\u003C\u002Fp\u003E\u003Cp\u003EGoF描述的状态模式在应用到我们的例子中时如下。\u003C\u002Fp\u003E\u003Ch3\u003E7.4.1 一个状态接口\u003C\u002Fh3\u003E\u003Cp\u003E首先,我们为状态定义一个接口。每一个与状态相关的行为都定义成虚函数。在我们的例子中,就是handleInput()和update()函数。\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eclass HeroineState\n{\npublic:\n virtual ~HeroineState() {}\n virtual void handleInput(Heroine& heroine, \n                Input input) {}\n virtual void update(Heroine& heroine) {}\n};\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Ch3\u003E7.4.2 为每一个状态定义一个类\u003C\u002Fh3\u003E\u003Cp\u003E对于每一个状态,我们定义了一个类并继承此状态接口。它的方法定义主角对应此状态的行为。换句话说,把之前的switch语句里面的每一个case语句里的内容放置到它们对应的状态类里面去。比如:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eclass DuckingState : public HeroineState\n{\npublic:\n DuckingState()\n : chargeTime_(0)\n {}\n\n virtual void handleInput(Heroine& heroine, \n                Input input) {\n  if (input == RELEASE_DOWN)\n  {\n   \u002F\u002F Change to standing state...\n   heroine.setGraphics(IMAGE_STAND);\n  }\n }\n\n virtual void update(Heroine& heroine) {\n  chargeTime_++;\n  if (chargeTime_ & MAX_CHARGE)\n  {\n   heroine.superBomb();\n  }\n }\n\nprivate:\n int chargeTime_;\n};\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E注意,我们这里chargeTime_从Heroine类中移到了DuckingState(躲避状态)类中。这样非常好,因为这个变量只是对躲避状态有意义,现在把它定义在这里,正好显式地反映了我们的对象模型。\u003C\u002Fp\u003E\u003Ch3\u003E7.4.3 状态委托\u003C\u002Fh3\u003E\u003Cp\u003E接下来,我们在主角类中定义一个指针变量,让它指向当前的状态。我们把之前那个很大的switch语句去掉,并让它去调用状态接口的虚函数,最终这些虚方法就会动态地调用具体子状态的相应函数。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E状态委托看起来很像策略模式和类型对象模式(第13章)。在这三个模式中,你会有一个主对象委托给另外的附属对象。它们三者的区别主要在于目的不同:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E策略模式的目标是将主类与它的部分行为进行解耦。\u003C\u002Fli\u003E\u003Cli\u003E类型对象模式的目标是使得多个对象通过共享相同类型对象的引用来表现出相似性。\u003C\u002Fli\u003E\u003Cli\u003E状态模式的目标是通过改变主对象代理的对象来改变主对象的行为。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003C\u002Fblockquote\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eclass Heroine\n{\npublic:\n virtual void handleInput(Input input)\n {\n  state_-&handleInput(*this, input);\n }\n\n virtual void update() { state_-&update(*this); }\n\n \u002F\u002F Other methods...\nprivate:\n HeroineState* state_;\n};\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E为了修改状态,我们需要把state_指针指向另一个不同的HeroineState状态对象。至此,我们的状态模式就讲完了。\u003C\u002Fp\u003E\u003Ch2\u003E7.5 状态对象应该放在哪里呢\u003C\u002Fh2\u003E\u003Cp\u003E我这里忽略了一些细节。为了修改一个状态,我们需要给state_指针赋值为一个新的状态,但是这个新的状态对象要从哪里来呢?我们之前的枚举方法是定义一些数字。但是,现在我们的状态是类,我们需要获取这些类的实例。通常来说,有两种实现方法。\u003C\u002Fp\u003E\u003Ch3\u003E7.5.1 静态状态\u003C\u002Fh3\u003E\u003Cp\u003E如果一个状态对象没有任何数据成员,那么它的唯一数据成员便是虚表指针了。那样的话,我们就没有必要创建此状态的多个实例了,因为它们的每一个实例都是相同的。\u003C\u002Fp\u003E\u003Cp\u003E在那种情况下,我们可以定义一个静态实例。即使你有一系列的FSM在同时运转,所有的状态机也能同时指向这一个唯一的实例。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E如果你的状态类没有任何数据成员,并且只有一个虚函数方法。那么我们还可以进一步简化此模式。我们可以使用一个普通的状态函数来替换状态类。这样的话,我们的state_变量就变成一个状态函数指针。\u003C\u002Fp\u003E\u003Cp\u003E这个就是享元模式。(第3章)\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E你把静态方法放置在哪里,这个由你自己来决定。如果没有任何特殊原因的话,我们可以把它放置到基类状态类中:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eclass HeroineState\n{\npublic:\n static StandingS\n static DuckingS\n static JumpingS\n static DivingS\n\n \u002F\u002F Other code...\n};\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E每一个静态成员变量都是对应状态类的一个实例。如果我们想让主角跳跃,那么站立状态应该是这样子:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eif (input == PRESS_B)\n{\n heroine.state_ = &HeroineState::\n heroine.setGraphics(IMAGE_JUMP);\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Ch3\u003E7.5.2 实例化状态\u003C\u002Fh3\u003E\u003Cp\u003E有时候上面的方法可能不行。一个静态状态对于躲避状态而言是行不通的。因为它有一个chargeTime_成员变量,所以这个具体取决于每一个躲避状态下的主角类。如果我们的游戏里面只有一个主角的话,那么定义一个静态类也是没有什么问题的。但是,如果我们想加入多个玩家,那么此方法就行不通了。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E当你为状态实例动态分配空间时,你不得不考虑碎片化问题了。对象池模式(第19章)可以帮助到你。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E在那种情况下,我们不得不在状态切换的时候动态地创建一个躲避状态实例。这样,我们的有限状态机就拥有了它自己的实例。当然,如果我们又动态分配了一个新的状态实例,则要负责清理老的状态实例。这里必须相当小心,因为修改状态的函数是在当前状态里面,所以我们需要小心地处理删除的顺序。\u003C\u002Fp\u003E\u003Cp\u003E另外,我们也可以选择在HeroineState类中的handleInput()方法里面可选地返回一个新的状态。当这个状态返回的时候,主角将会删除老的状态并切换到这个新的状态,如下所示:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Evoid Heroine::handleInput(Input input)\n{\n HeroineState* state = state_-&handleInput(
\n      *this, input);\n if (state != NULL)\n {\n  delete state_;\n  state_ =\n }\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E那样的话,我们只有在从handleInput方法返回的时候才有可能去删除前面的状态对象。现在,站立状态可以通过创建一个躲避状态的实例来切换状态了。\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003EHeroineState* StandingState::handleInput(
\n      Heroine& heroine, I}

我要回帖

更多关于 上线了 小程序源码 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信