文/破晓べ
作为一个饱经风霜的程序员,你一定早就习惯了游戏开发中的反复。记得刚来公司的时候就听师兄讲过一个故事:策划在国庆节前突然想模仿微信做一个红包系统,还没等他实现完毕,红包二期的案子就已经写好了。不巧,这个幸运的师兄碰巧要去同学聚会,就暂时没有做。等他请假2天回到公司的时候,惊奇地发现红包二期已经被推翻了,变成了红包三期,师兄长舒一口气,我想此刻他的内心是复杂的,甚至复杂到不能用代码表达~~
那我们的代码到底能不能经得起折腾呢?大概有两种类型的代码让我印象深刻:
1.复杂的继承
计算机专业出身的同学对面向对象编程一定不陌生,这基本上是所有学校的计算机必修课,其实它也对很多公司的游戏架构产生了深远的影响,在我们公司的代码库里,也有它的身影。在长期的维护中,这些本身设计良好的继承关系由于需求的变动不断改变着继承关系,最后的结果就是,出现了很深很深的继承,这样的代码很难维护,就像一个人很难一下说出自己应该如何称呼自己的爸爸的爸爸的爸爸的爸爸的儿子,一个需求的改变可能要求你理清这些类之间的关系,是要先调用父类的函数呢,还是先调用父类的父类的函数呢,还是重写它。你甚至会陷入一种旋涡,这样的设计很难让逻辑清晰。
2.单个文件包含多种功能以及特判
这个可以举一个实际的例子,游戏中有多种不同类型的战斗,在战斗的过程中,我需要显示血条、倒计时、连击数、方向盘、技能面板、伤害排行榜、暂停按钮、托管按钮等等等等。但是不巧,每一种战斗需要的内容是不一样的,比如排行榜只在组队模式下需要,而暂停按钮则不能在PK模式中显示,血条在PK模式要换另一种表现方式。一开始这对我们来说很简单,几个ifelse就可以轻松搞定,但是看看维护了一年之后的UI代码吧,3000行的代码和充斥着整个文件的20多种战斗的特判,没有人敢维护这块代码,因为它基本一改就会在别的关卡中出现BUG。
之前的代码大概是这个样子的,是不是感觉似曾相识呢。
- function GameBattleUI:init()
- if gameMode == A then
- hP:setVisible(false)
- skillBoard:setVisible(true)
- joystick:setVisible(true)
- skillBoard.skillA:setVisible(true)
- skillBoard.skillB:setVisible(false)
- --some code
- elseif gameMode == B then
- hP:setVisible(true)
- skillBoard:setVisible(true)
- joystick:setVisible(true)
- skillBoard.skillA:setVisible(true)
- skillBoard.skillB:setVisible(true)
- skillBoard.skillC:setVisible(true)
- elseif gameMode == C then
- --some code
- end
- skillBoard.skillA:addTouchEventListener(
- function (sender, eventType)
- if eventType == ccui.TouchEventType.ended then
- if gameMode == A then
- --do something
- elseif gameMode == B then
- --do something
- end
- end
- end
- )
- end
-
- function GameBattleUI:update()
- --和上面差不多
- if gameMode == A then
- elseif gameMode == B then
- elseif gameMode == C then
- end
- end
大概是这个样子,ui_game_battle_normal是某种战斗的UI,我只需要创建一个空壳,然后把每个需要的组件添加进去就够了,对于ui_game_battle_city_pk这个界面,我只需要倒计时、方向盘和HP,那我就只把他们增加进去。
- function cls_uiGameBattleCity3PK:InitUI()
- local timeComponent = CreateComponentTime()
- self:AddComponent({
- component = timeComponent,
- zoder = 10,
- layerTag = data.battle.UI_TAG_BATTLE_UI_NORMAL
- })
-
- local playerHpComponent = CreateComponentPlayerHPUI()
- self:AddComponent({
- component = playerHpComponent,
- zoder = 10,
- layerTag = data.battle.UI_TAG_BATTLE_UI_NORMAL
- })
-
- local joystickComponent = CreateComponentJoystick()
- self:AddComponent({
- component = joystickComponent,
- zoder = 10,
- layerTag = data.battle.UI_TAG_BATTLE_UI_NORMAL
- })
- end
- })
- layerTag = data.battle.UI_TAG_BATTLE_UI_NORMAL
- zoder = 10,
- component = joystickComponent,
- self:AddComponent({
-
- })
- layerTag = data.battle.UI_TAG_BATTLE_UI_NORMAL
- zoder = 10,
- component = playerHpComponent,
- self:AddComponent({
-
- })
- layerTag = data.battle.UI_TAG_BATTLE_UI_NORMAL
- zoder = 10,
- component = timeComponent,
- self:AddComponent({
- local timeComponent = CreateComponentTime()
不过除了让人舒服的特性,在实现的过程中也遇到了一些小麻烦。因为某种原因,倒计时模块需要通知血条模块进行变化,当时有点傻眼,组件之间到底应该怎么交互呢?之前的结构貌似完全没有考虑过这个问题,如果我们随便去获取其他组件,本来清晰的结构不是又要乱掉了吗?组件之间会互相依赖!fuck!
《游戏编程模式》中提出了3种解决方案,和之前公司分享时讲到的方法几乎完全相同,看来这是经过无数人实践得出的优质答案:
1.由容器储存公用的变量,组件间可以通过访问这个容器中的变量进行通信(缺点:可能有些容器中不需要这个变量,但是通用的容器还是要定义,浪费内存)
2.保存另一个组件的引用。如果我们确定两个组件之间有关系,可以在初始化的时候就传入需要的组件的引用。(缺点:容器之间的关系很可能变得很复杂,甚至初始化顺序都要有要求)
3.在容器中实现通用的消息机制,每个组件可以通过容器发送广播,感兴趣的组件自己对应的消息就可以了(缺点:事件太多之后,你会往返于事件之间,搞不清楚谁的更新依赖谁)
这3种方式各有优缺点,比如说一个对象的位置信息是很常见的,完全就可以使用第一种方式,把它定义在容器中,所有组件都可以访问。这时候后面两种就显得很不合适。具体要使用哪种方案要根据实际的需要进行分析,选取最优的。就像《游戏编程模式》中说到的:意料之外的是,没有哪个选择是最好的。你最终有可能将上述所说的三种方法都用到
最后的最后,发表一下对组合的感叹,这是一种非理性的感叹:组合是创造之魂,它符合世界本身的运行规律,就像质子、中子、电子组成分子,细胞组,各种不同的硬件组成了你的台式电脑。组合存在着无数的可能性,来来来,尝试把这两个家伙放在一起,看看会发生什么!
相关阅读:游戏设计模式实操经验:游戏结算功能实现的两个要点
锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛
- 还没有人评论,欢迎说说您的想法!