游戏设计模式系列(三)—— 策划变心太快?也许可以使用组合

  作为一个饱经风霜的程序员,你一定早就习惯了游戏开发中的反复。记得刚来公司的时候就听师兄讲过一个故事:策划在国庆节前突然想模仿微信做一个红包系统,还没等他实现完毕,红包二期的案子就已经写好了。不巧,这个幸运的师兄碰巧要去同学聚会,就暂时没有做。等他请假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的任务交给了我,都说做成事情的第一步就是开始动手,在和大家进行了简单的讨论后,我们得到了几个结论:

  1.很多功能在不同的战斗中是相同的,比如方向盘,不管在哪都是上下左右

  2.功能模块之间互相不需要交互,比如方向盘和血条技能什么的没有任何关系

  3.我们需要它变得更灵活,可以很方便地在某种战斗中增加一个新的自定义UI类型

  这仿佛就是在告诉我们:你快用组件啊,就像堆砌乐高积木一样去创造你的游戏世界!是的,用组件模式来重构真的再适合不过了

 

  大概是这个样子,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

  addComponent和管理组件的方法写在一个基类ui_game_battle_base中,因为我们不关心组件具体的内容,只需要对它进行初始化和更新就行了,以下代码中的OnUpdate由游戏引擎的时钟驱动,然后分别调用组件的更新。

function ui_game_battle_base:AddComponent(args)
    local component = args.component
    local zoder     = args.zoder
    local layerTag  = args.layerTag

    local componentName = component.__cname
    local layer = self[tag_to_layer[layerTag]]
    if layer then
        layer:addChild(component, zoder)
        if self.ComponentList[componentName] ~= nil then
            self.ComponentList[componentName]:removeFromParent()
        end
        self.ComponentList[componentName] = component
    else
        echoError("[ui_game_battle_base][AddComponent] layer error" .. tostring(layerTag))
    end
end

function ui_game_battle_base:OnUpdate()
    for k,v in pairs(self.ComponentList) do
        if v.OnUpdate then
            v:OnUpdate()
        end
    end
end

  如此这般的设计,使代码比原来灵活许多,逻辑也变得清晰,我们先实现一堆component,再用各种方式组合,形成类似ui_game_battle_normal这样的实体,不仅代码得到了解耦,以后策划再要增加一个新的功能也变得异常简单,要么是修改其中的一个组件,要么就是直接新建一个组件,然后把它add到实体中就可以了。重构完成后,看着简单的目录结构,内心觉得很舒服,LZ再也不怕策划改这块代码了!

  不过除了让人舒服的特性,在实现的过程中也遇到了一些小麻烦。因为某种原因,倒计时模块需要通知血条模块进行变化,当时有点傻眼,组件之间到底应该怎么交互呢?之前的结构貌似完全没有考虑过这个问题,如果我们随便去获取其他组件,本来清晰的结构不是又要乱掉了吗?组件之间会互相依赖!fuck!

  《游戏编程模式》中提出了3种解决方案,和之前公司分享时讲到的方法几乎完全相同,看来这是经过无数人实践得出的优质答案:

  1.由容器储存公用的变量,组件间可以通过访问这个容器中的变量进行通信(缺点:可能有些容器中不需要这个变量,但是通用的容器还是要定义,浪费内存)

  2.保存另一个组件的引用。如果我们确定两个组件之间有关系,可以在初始化的时候就传入需要的组件的引用。(缺点:容器之间的关系很可能变得很复杂,甚至初始化顺序都要有要求)

  3.在容器中实现通用的消息机制,每个组件可以通过容器发送广播,感兴趣的组件自己监听对应的消息就可以了(缺点:事件太多之后,你会往返于事件之间,搞不清楚谁的更新依赖谁)

  这3种方式各有优缺点,比如说一个对象的位置信息是很常见的,完全就可以使用第一种方式,把它定义在容器中,所有组件都可以访问。这时候后面两种就显得很不合适。具体要使用哪种方案要根据实际的需要进行分析,选取最优的。就像《游戏编程模式》中说到的:意料之外的是,没有哪个选择是最好的。你最终有可能将上述所说的三种方法都用到

  最后的最后,发表一下对组合的感叹,这是一种非理性的感叹:组合是创造之魂,它符合世界本身的运行规律,就像质子、中子、电子组成分子,细胞组成人,各种不同的硬件组成了你的台式电脑。组合存在着无数的可能性,来来来,尝试把这两个家伙放在一起,看看会发生什么!

  PS:感觉关于具体的实现没有讲好,强烈推荐一篇文章,在这里你可以更容易理解如何去实现它:http://job.17173.com/content/2009-08-07/20090807104220649,1.shtml

posted @ 2016-11-28 10:12  破晓べ  阅读(1129)  评论(3编辑  收藏  举报