使用 PureMVC 构建游戏项目 Part IV 之场景管理

许多游戏引擎已经提供了原生的场景管理功能,例如 Cocos2d-x 的 CCDirector.replaceScene() 或者 Unity3d 的 Application.loadLevel() 。场景切换的本质是将原有的对象删除并从内存中释放,腾出空间然后加载新的场景资源。但这并不能满足当下的一些需求,简单的从将一个场景销毁,进入下一个场景,而想要点击返回的时候,回退到原来的界面,并不是简单重新加载一下原来的界面就可以了,还需要还原当时现场的一些关键状态(详细的需求见 Part I)。

所谓的关键状态就是,例如:
你在进入装备界面,默认显示所有装备(type = all),然后你点击界面上的分类按钮,按类型B(type = B)进行筛选。然后你从装备界面跳转到关卡界面(准备去相应的关卡打装备),打完后你想回到装备界面时,它还按类型 B 而不是默认 all 进行筛选。那么 type 就是一个关键状态。

在手机游戏中,场景指的是那些独占整个屏幕的模块;此外还有一些模块是浮动于场景之间的,类似于层,可以弹出和关闭。在 Part III 的 Mediator 部分,我描述了两种类型的视图组件(ViewComponent):一种是 Scene;另一种是 Layer。在 Part I 中,我将这两种类型的 View 区别开来:NestMediator 和 SubNestMediator。由于我觉得这些 Mediators 是一种嵌套关系(类似 CCNode),所以在做场景恢复的时候,我认为等 Mediator 创建并加载 ViewComponent 后遍历子节点,逐层恢复就可以了。但这导致 Part II 中提到的 Mediators 相互引用,并且有大量场景切换和还原的逻辑被放在 NestMediator 中。

后来我重新审视了这个设计,发现虽然可行,但有太多不合理的地方。现将新的方案整理如下:将状态和场景树转移到 Contexts 中储存,使用 Commands 来处理 Mediator 和 ViewComponent 的注册与销毁,而场景实际切换过程交给 SceneManager 处理。

0x00 Context & ContextProxy

Context

Part II 中我提出了使用 Context 来储存场景状态以及嵌套关系:Context 描述了一个场景或者层使用哪个 Mediator 和 ViewComponent 创建节点,包含哪些关键状态(data),并实现了简单的嵌套关系。

Context = class("Context")

function Context:ctor(data)
    self.mediatorClass = data.mediatorClass
    self.viewComponentClass = data.viewComponentClass
    self.data = {}

    self.parent = nil
    self.children = {}
end

function Context:addChild(context)
    assert(isa(context, Context), "should be an instance of Context")
    assert(context.parent == nil, "context already has parent")
    
    context.parent = self;
    table.insert(self.children, context)
end

function Context:removeChild(context)
    assert(isa(context, Context), "should be an instance of Context")

    for i, v in ipairs(self.children) do
        if v == context then
            context.parent = nil
            return table.remove(self.children, i)
        end
    end

    return nil
end

用以下代码可以描述「打开一个主场景,带有一个弹出的邮件界面」:

local mainContext = Context.new({
    mediatorClass = MainMediator,
    viewComponentClass = MainScene
})

local mailboxContext = Context.new({
    mediatorClass = MailboxMediator,
    viewComponentClass = MailboxLayer
})

mainContext:addChild(mailboxContext)

game.facade:sendNotification(GAME.LOAD_SCENE, mainContext)

ContextProxy

每打开一个场景,就将场景树的根结点放到 ContextProxy 中,当点击返回按钮的时候,将当前场景 Context 出栈,再取出下一个 Context 重新加载,即可以实现返回场景。所以我们需要实现一个栈式结构的 Proxy 来维护场景栈。

ContextProxy = class('ContextProxy', Proxy)

function ContextProxy:getCurrentContext()
    return self.data[#self.data]
end

function ContextProxy:pushContext(context)
   table.insert(self.data, context)
end

function ContextProxy:popContext()
    return table.remove(self.data)
end

function ContextProxy:cleanContext()
    self.data = {}
end

function ContextProxy:getContextCount()
    return #self.data
end

0x01 GameMediator

游戏启动后,会注册一个 GameMediator 作为场景的控制中枢,实际上它是一个工厂,根据场景的名字创建 Context 实例并把相应的 Mediator 与 ViewComponent 关联起来:

local GameMediator = class('GameMediator', Mediator)

function GameMediator:listNotificationInterests()
    return {
        GAME.GO_TO_SCENE
    }
end

function GameMediator:handleNotification(note)
    local name = note:getName()
    local data = note:getBody()

    local context = Context.new()

    -- load context data
    for k, v in pair(data)
        context.data[k] = v
    end

    if name == GAME.GO_TO_SCENE then
        local type = note:getType()

        if type == SCENE.MAIN then
            context.mediatorClass = MainMediator
            context.viewComponentClass = MainScene

        elseif type == SCENE.EQUIPMENT then
            context.mediatorClass = EquipmentMediator
            context.viewComponentClass = EquipmentScene

        -- elseif type == ... then
        --     context.mediatorClass = ...
        --     context.viewComponentClass = ...
        --
        -- ...

        end
    end

    self:sendNotification(GAME.LOAD_SCENE, context)
end

在需要切换场景的时候,广播以下消息即可:

game.facade:sendNotification(GAME.GO_TO_SCENE, {
    type = "all"
}, SCENE.EQUIPMENT)

其中第二个参数会被合并到 context.data 中,作为状态被传递到 Mediator 使用,使用 context.data 在场景之间传递信息非常方便。在 EquipmentMediator 要使用 context.data 中的信息只要访问 self.contextData 即可。最后会广播并调用 LoadSceneCommand 来创建和切场景。

0x02 Commands

控制场景的命令主要有这几个:

  • LoadSceneCommand - 清空当前场景树,并加载新场景树
  • LoadLayersCommand - (递归)加载除根节点(场景)外的所有子节点(层)
  • RemoveLayersCommand - (递归)移除根节点(场景)外的所有子节点(层)
  • BackSceneCommand - 返回前一场景

与之对应的消息如下:

  • GAME.LOAD_SCENE
  • GAME.LOAD_LAYERS
  • GAME.REMOVE_LAYERS
  • GAME.GO_BACK

LoadSceneCommand

LoadSceneCommand 只处理 Mediators 和 ViewComponents 的创建和销毁。而把 ViewComponents 的切换过程交给 SceneManager 处理,并以异步的方式串连起来。切换场景的过程如下:

  1. 首先是取出当前场景 Context,按层序遍历场景树,然后逆序逐个删除,由于删除层结点的过程有可能存在动画效果,所以这个过程是异步的。这部分的代码由 RemoveLayersCommand 配合 SceneManager 实现,最终留下当前场景的根结点;
  2. 场景的切换可以有不同的方式(详见 SceneManager 部分),以其中一种方式载入下一个场景的根节点;
  3. 对新场景的根结点(context)进行层序遍历,逐一还原每一个层结点,由于加载层结点的过程可能存在动画效果,所以这个过程也是异步的。这部分的代码由 LoadLayersCommand 配合 SceneManager 实现;
  4. 切换场景过程完成。
LoadSceneCommand = class('LoadSceneCommand', SimpleCommand)

function LoadSceneCommand:execute(note)
    local data = note:getBody()
    local context = data.context
    assert(isa(context, Context), "should be an instance of Context")

    local viewComponent = context.viewComponentClass.new()
    assert(isa(viewComponent, BaseView), "should be an instance of BaseView: " .. viewComponent.__cname)

    local function onTransFinish(viewComponent)
        local mediator = context.mediatorClass.new(viewComponent)
        mediator:setContextData(context.data)
        self.facade:registerMediator(mediator)

        self:sendNotification(GAME.LOAD_LAYERS, {
            context = context
        })
    end

    local contextProxy = getProxy(ContextProxy)
    local fromViewComponent;

    local function nextScene()
        if context.transType == Context.TRANS_TYPE.ONE_BY_ONE then
            SceneManager.transOneByOne(fromViewComponent, viewComponent, onTransFinish)
        else
            SceneManager.transCross(fromViewComponent, viewComponent, onTransFinish)
        end

        if context.cleanStack then
            contextProxy:cleanContext()
        end

        contextProxy:pushContext(context)
    end

    local function onRemoved()
        function()
            local prevMediator = self.facade:removeMediator(prevContext.mediator.__cname)
            fromViewComponent = prevMediator:getViewComponent()
            nextScene()
        end
    end

    local prevContext = data.prevContext or contextProxy:getCurrentContext()
    if prevContext ~= nil then
        assert(isa(prevContext, Context), "should be an instance of Context")

        self:sendNotification(GAME.REMOVE_LAYERS, {
            context = prevContext,
            onFinish = onRemoved
        })
    else
        nextScene()
    end
end

RemoveLayersCommand

RemoveLayersCommand 有两个参数,一个是要进行移除的 context 对象。当 context 是一个场景结点时(context.parent == nil),只对其子结点进行逆层序移除;否则连同 context 对应的层结点一起移除。另一个参数是一个回调函数,在清除完成后被调用。

local RemoveLayersCommand = class('RemoveLayersCommand', SimpleCommand)

function RemoveLayersCommand:execute(note)
    local data = note:getBody()
    local context = data.context
    assert(isa(context, Context), "should be an instance of Context")

    -- Breadth-First-Search
    local open = { context }
    local close = {}
    while #open > 0 do
        local context = table.remove(open, 1) -- FIFO

        table.insert(close, context)

        for _, v in ipairs(context.children) do
            table.insert(open, v)
        end
    end

    if context.parent == nil then
        -- do not remove root context
        table.remove(close, 1)
    else
        context.parent:removeChild(context)
    end

    -- remove context tree
    local removeState, status, iter
    removeState = coroutine.create(function()
        while #close > 1 do
            local context = table.remove(close) -- LIFO
            local subMediator = self.facade:removeMediator(context.mediator.__cname)

            local removed = false
            local function onRemoved()
                coroutine.resume(removeState)
                removed = true
            end

            SceneManager.remove(subMediator:getViewComponent(), onRemoved)
            if not removed then coroutine.yield() end
        end

        -- do the callback
        if data.callback then data.callback() end
    end)

    -- start remove
    coroutine.resume(removeState)    
end

这里使用 Lua 的协程来组织异步场景卸载,如果用 javascript 的话,可以考虑 async.js 或者将来使用 es6 提供的 generator 。

LoadLayersCommand

LoadLayersCommand 有三个参数,一个是要进行层序加载的 context 对象,一个是可选的 parentContext 。当 context 是场景结点时(parentContext = nil)只对其子结点进行载入;否则连同 context 一起加载。另外还有个是完成时的回调函数。

local LoadLayersCommand = class('LoadLayersCommand', SimpleCommand)

function LoadLayersCommand:execute(note)
    local data = note:getBody()
    local context = data.context
    assert(isa(context, Context), "should be an instance of Context")

    local open = {}

    local restoreLayers, status, iter
    restoreLayers = coroutine.create(function()
        -- restore context tree

        while #open > 0 do
            local context = table.remove(open, 1)
            for _, v in ipairs(context.children) do
                table.insert(open, v)
            end

            local parentContext = context.parent
            local parentMediator = self.facade:retrieveMediator(parentContext.mediator.__cname)
            local parentViewComponent = parentMediator:getViewComponent()

            local viewComponent = context.viewComponent.New()
            local finish = false
            local function onFinish()
                local mediator = context.mediator.New(viewComponent)
                mediator:setContextData(context.data)
                self.facade:registerMediator(mediator)
                coroutine.resume(restoreLayers)
                finish = true
            end

            SceneManager.overlay(parentViewComponent, viewComponent, onFinish)
            if not finish then coroutine.yield() end
        end

        if data.callback then data.callback() end
    end)

    local parentContext = data.parentContext
    if parentContext ~= nil then
        assert(isa(parentContext, Context), "should be an instance of Context")

        if parentContext:getContextByMediator(context.mediator) then
            print("Mediator already exist: " .. context.mediator.__cname)
            return
        end

        table.insert(open, context)
        parentContext:addChild(context)
    else
        for _, v in ipairs(context.children) do
            table.insert(open, v)
        end
    end

    -- start restore
    coroutine.resume(restoreLayers)    
end

BackSceneCommand

在 ContextProxy 部分,我们说到「将当前场景 Context 出栈,再取出下一个 Context 重新加载,即可以实现返回场景。」,除此之外,我们还可以在返回时传递一些 context.data 。这便是 BackSceneCommand 所要做的全部事情:

local BackSceneCommand = class('BackSceneCommand', SimpleCommand)

function BackSceneCommand:execute(note)
    local data = note:getBody()

    local contextProxy = getProxy(ContextProxy)
    if contextProxy:getContextCount() > 1 then
        local currentContext = contextProxy:popContext()
        local prevContext = contextProxy:popContext()
        prevContext:extendData(data)

        self:sendNotification(GAME.LOAD_SCENE, {
            prevContext = currentContext,
            context = prevContext
        })
    end
end

0x03 Base View Component

在 Commands 中随处可见 SceneManager 的踪影。但是在介绍 SceneManager 之前,首先要了解一下基本的 ViewComponent 的生命周期是什么样的,然后再把两个 ViewComponent 的生命周期重叠或连接,就可以表现场景切换。

一般而言,我们会把一个模块做成一个场景或层,使用 UI 编辑器将其打包成一个独立的资源。然后将它作为 ViewComponent 的一个资源载入使用。所以我们并不需要在 ViewComponent 层面区分场景或层。

Part I 的设计不同的是,现在的 ViewComponent 并不是一个引擎中的结点(Unity3d 的 GameObject 或者 Cocos2d 的 CCNode)。引擎中的结点只是 ViewComponent 的一个引用。通过这个引用,我们需要维护这个资源的生命周期:

  1. load() 开始加载资源(方法)
  2. LOADING 加载进度(事件)
  3. ON_LOADED 加载完成(事件)
  4. attach() 绑定父结点(方法)
  5. init() 初始化(方法)
  6. enter() 进场并播放进场动画(方法)
  7. DID_ENTER 进场完成(事件)
  8. onEnter() 进场完成后回调(方法)
  9. exit() 退场并播放退场动画(方法)
  10. onExit() 退场前回调(方法)
  11. DID_EXIT 退场完成(事件)
  12. detach() 从父结点移除(方法)

我们需要一种方式,将这些事件推送给 SceneManager,这样当两个场景进行切换的时候,就能自如控制。在 Part III 中的 View 部分,我提到的 LuaNotify 或 emitter.js 就是用来做这件事情的。以下是 BaseViewComponent 的参考实现:

BaseViewComponent = class("BaseViewComponent")
local Event = require('Framework.notify.event')

-- 用于场景切换的事件
BaseViewComponent.LOADED = "BaseViewComponent:LOADED"
BaseViewComponent.DID_ENTER = "BaseViewComponent:DID_ENTER"
BaseViewComponent.DID_EXIT = "BaseViewComponent:DID_EXIT"

-- 返回事件
BaseViewComponent.ON_BACK = "BaseViewComponent:ON_BACK"

-- 层关闭事件
BaseViewComponent.ON_CLOSE = "BaseViewComponent:ON_CLOSE"

function BaseViewComponent:Ctor()
    self.event = Event.New() -- 事件
    self._isLoaded = false -- 是否加载完成
    self._go = nil -- game object 引用
    self._tf = nil -- transform 引用
end

-- 获取 UI 名称,由子类实现
function BaseViewComponent:getUIName()
    return nil
end

-- 开始加载
function BaseViewComponent:load()
    -- 获取 UI 名称
    local uiName = self:getUIName()
    if uiName ~= nil then
        self:LoadUI(uiName, "OnUILoaded")
    else
        self:OnUILoaded()
    end
end

-- 是否加载完成
function BaseViewComponent:isLoaded()
    return self._isLoaded
end

-- UI 加载完成后被调用
-- @param go 来自 Unity3d 的 Game Object
function BaseViewComponent:OnUILoaded(go)

    -- 加载完成
    self._go = go
    self._tf = go and go.transform

    self._isLoaded = true

    -- 执行一些初始化工作
    self:init()

    -- 通知加载完成
    self.event:emit(BaseViewComponent.LOADED)
end

-- 由子类实现的一些初始工作
function BaseViewComponent:init() end

-- 加载完成后进入场景
function BaseViewComponent:enter()

    -- 子类可以覆盖该方法,播放一段动画
    -- 然后通知完成入场
    self.event:emit(BaseViewComponent.DID_ENTER)

    -- 通知后会执行 mediator 的 onRegister

    -- 入场后执行的一些操作
    self:onEnter()
end

-- 由子类实现的入场后操作
function BaseViewComponent:onEnter() end

-- 由子类实现的出场前操作
function BaseViewComponent:onExit() end

-- 退出场景
function BaseViewComponent:exit()

    -- 执行一些退场前操作
    self:onExit()

    -- 子类可以覆盖该方法,播放一段动画
    -- 然后通知退场完成
    self.event:emit(BaseViewComponent.DID_EXIT)

    self:detach()
end

-- 添加到父结点
function BaseViewComponent:attach(parent)
    if self._tf ~= nil and parent._tf ~= nil then
        self._tf:SetParent(parent._tf, false)
    end
end

-- 从父结点删除
function BaseViewComponent:detach(parent)

    -- 销毁 Game Object
    if self._go ~= nil then
        Object.Destroy(self._go)
    end

    self._go = nil
    self._tf = nil
    self._isLoaded = false

    -- 清空事件队列
    self.event:clear()
end

0x04 SceneManager

SceneManager 是一个很重要组件,而且因为责任的分工已经相当合理,所以这部分的代码并不多,也不难理解。

当 ViewComponent 的生命周期可控后,给出两个 ViewComponents 要实现他们的切换就变得简单了。然而切换的过程有很多种形式,可以根据自己的需要进行扩展,一般而言有以下两种:

  • 在当前场景加载下一场景,穿叉过场动画,然后删除前一场景;
  • 播放当前场景的退场动画,加载下一场景(loading),播放下一场景进场动画。

除了切换场景外,由于场景中的结点是可以嵌套的,所以SceneManager 还要处理在一个 ViewComponent 上增加/删除另一个 ViewComponent 的操作。实际上就是半个切换场景的过程。


SceneManager = {}

function SceneManager.transCross(from, to, onFinishCallback)

    -- TO load()
    -- TO loading
    -- TO loaded
    -- FROM exit()
    -- FROM didExit
    -- TO enter()
    -- TO didEnter
    -- callback()

    local function didEnter(e)
        onFinishCallback(to)
    end

    local function didExit(e)
        if to ~= nil then
            to.event:connect(BaseViewComponent.DID_ENTER, didEnter)
            to:enter()
        else
            didEnter()
        end
    end

    local function onLoaded(e)
        if from ~= nil then
            from.event:connect(BaseViewComponent.DID_EXIT, didExit)
            from:exit()
        else
            didExit()
        end
    end

    if to == nil or to:isLoaded() then
        onLoaded()
    else
        to.event:connect(BaseViewComponent.LOADED, onLoaded)
        to:load()
    end
end

function SceneManager.transOneByOne(from, to, onFinishCallback)

    -- FROM exit()
    -- FROM didExit
    -- TO load()
    -- TO loading
    -- TO loaded
    -- TO enter()
    -- TO didEnter
    -- callback()

    local function didEnter(e)
        onFinishCallback(to)
    end

    local function onLoaded(e)
        if to ~= nil then
            to.event:connect(BaseViewComponent.DID_ENTER, didEnter)
            to:enter()
        else
            didEnter()
        end
    end

    local function didExit(e)
        if to == nil or to:isLoaded() then
            onLoaded()
        else
            to.event:connect(BaseViewComponent.LOADED, onLoaded)
        end
    end

    if from ~= nil then
        from.event:connect(BaseViewComponent.DID_EXIT, didExit)
        from:exit()
    else
        didExit()
    end
end

function SceneManager.overlay(parent, child, onFinish)

    -- CHILD load()
    -- CHILD loading
    -- CHILD loaded
    -- CHILD attach()
    -- CHILD enter()
    -- CHILD didEnter
    -- callback()

    local function didEnter(e)
        onFinish(child)
    end

    local function onLoaded(e)
        if child ~= nil then
            child:attach(parent)
            child.event:connect(BaseViewComponent.DID_ENTER, didEnter)
            child:enter()
        else
            didEnter()
        end
    end

    if child == nil or child:isLoaded() then
        onLoaded()
    else
        child.event:connect(BaseViewComponent.LOADED, onLoaded)
        child:load()
    end
end

function SceneManager.remove(viewComponent, onFinishCallback)

    -- CHILD exit()
    -- CHILD didExit
    -- callback()

    if viewComponent ~= nil then
        if onFinishCallback then
            viewComponent.event:connect(BaseViewComponent.DID_EXIT, onFinishCallback)
        end

        viewComponent:exit()
    end
end

0x05 Base Mediator

整篇文章似乎都没有其它 Mediator 什么事。但是我们发现 GameMediator 只实现了场景级的 Mediator 的切换,如果想在指定的 Mediator 上嵌套子 Mediator,还需要做一些工作:

BaseMediator = class('BaseMediator', Mediator)

function BaseMediator:ctor(viewComponent)
    BaseMediator.super.ctor(self, nil, viewComponent)
end

-- 注册 Mediator
function BaseMediator:onRegister()
    -- 事件队列
    self.event = {}

    self:bind(BaseViewComponent.ON_BACK, function(e)
        self:sendNotification(GAME.GO_BACK)
    end)

    self:bind(BaseViewComponent.ON_CLOSE, function(e)
        local contextProxy = getProxy(ContextProxy)
        local currentContext = contextProxy:getCurrentContext()
        local parentContext = currentContext:getContextByMediator(self.class)
        self:sendNotification(GAME.REMOVE_LAYERS, {
            context = parentContext
        })
    end)

    -- 注册时要执行的
    self:didRegister()
end

-- 由子类实现注册时要执行的
function BaseMediator:didRegister() end

-- 设置上下文参数,通过 context.data 获得的参数
function BaseMediator:setContextData(data)
    self.contextData = data
end

-- 订阅来自 UI 的消息
-- @param event 事件名
-- @param callback 回调函数
function BaseMediator:bind(event, callback)
    -- 绑定 UI 事件回调
    self.viewComponent.event:connect(event, callback)

    -- 将事件记录到队列,注销 mediator 时称除
    table.insert(self.event, {
        event = event,
        callback = callback
    })
end

function BaseMediator:onRemove()
    -- 注销时要执行的
    self:willRemove()

    -- 移除关联的 UI 事件
    for _, v in ipairs(self.event) do
        self.viewComponent.event:disconnect(v.event, v.callback)
    end
end

-- 由子类实现注销时要执行的
function BaseMediator:willRemove() end

function BaseMediator:addSubLayers(context)
    local contextProxy = getProxy(ContextProxy)
    local currentContext = contextProxy:getCurrentContext()
    local parentContext = currentContext:getContextByMediator(self.class)

    self:sendNotification(GAME.LOAD_LAYERS, {
        parentContext = parentContext,
        context = context
    })
end

在主界需要弹出邮件子层模块的时候可以这样:

MainMediator = class("MainMediator", BaseMediator)

MainMediator.OPEN_MAIL = "MainUIMediator.OPEN_MAIL"

function MainMediator:register()
    -- ...
    self:bind(MainMediator.OPEN_MAIL, function(e)
        local childContext = Context.new({
            viewComponentClass = MailboxLayer,
            mediatorClass = MailboxMediator            
        })
        
        self:addSubLayers(childContext)
    end)
    -- ...
end

0x06 Summary

将原来混杂在 NestMediator 中的逻辑合理的分配到各个不同的组件。并对 ViewComponent 的生命周期进行详细的描述后,我发现这整个框架除了 ViewComponent 本身是依赖引擎的结点和资源管理以外,其它所有组件都是平台无关的。可以移植到任意平台中使用。至于如何回答 Part III 留下的问题,相信各位也有所领悟。希望这篇文章对你们有所启发 :)

0x07 Sequence Diagram

下图以登录场景切换成主场景为例,展示了交叉场景切换的时序图:

SceneMgr.png

图片使用 plantuml 绘制,另附 sublime text 插件:diagram

标签: puremvc, unity3d, cocos2d

已有 37 条评论

  1. 发现现在写的 lua 满满的回调风味(callback flavour),其实都可以改成协程。

    1. jakey jakey

      发现lua和js差不多。呵呵

      1. 嗯,函数都是第一类值,都支持闭包。

  2. 陈冠希 陈冠希

    感谢分享。
    这个博客没有目录模式的么?看前一篇博文滚到中指疼。

  3. jakey jakey

    请问博主
    BackSceneCommand 的GAME.GO_STATE消息是单独命令处理还是GameMediator处理的?
    根据上下文。我认为,BackSceneCommand需要删除当前场景,以及layers。并且重新绘制前一个场景。为什么要单独发命令到其他地方处理?

    1. GameMediator 只是一个消息中转,不做具体的事务,也没有实际界面。
      BackSceneCommand 里面从 ContextProxy 的栈顶可以拿到当前界面的 Context 以及前一个界面的 Context;然后调用 GoSceneCommand 进行跳转(从 context.mediator 和 context.viewComponent 拿到具体界面);
      返回前一界面的本质就是跳到前一个界面的 Context,并把当前的 Context 丢弃。

      1. jakey jakey

        为什么不发消息到LoadLayersCommand命令,而使用GoSceneCommand?
        只要将传递的参数处理下,LoadLayersCommand命令完全满足要求啊~

        1. 是我打错了,应该是 LoadSceneCommand

  4. jakey jakey

    1,请问博主能否介绍下一些三个命令功能细节?
    GAME.REMOVE_SUB_STATE
    GAME.GO_BACK
    GAME.GO_STATE

    2,BaseViewComponent.ON_BACK和BaseViewComponent.ON_CLOSE有何区别?

    1. 文章太长了,有些代码修订的时候遗漏了,GAME.REMOVE_SUB_STATE 应该改成 GAME .REMOVE_LAYERS;GAME.GO_STATE 改成 GAME. LOAD_SCENE。GAME.GO_BACK 触发执行 BackSceneCommand。BaseViewComponent.ON_BACK 是 Mediator 侦听 UI 按下返回场景按钮并发送 GAME.GO_BACK 消息用的;BaseViewComponent.ON_CLOSE 是 Mediator 侦听 UI 按下关闭层按钮并发送 GAME.REMOVE_LAYERS 消息用的;回头我把消息部分补上。

      1. jakey jakey

        多谢博主贡献的这个系列。你的第一个版本我就开始关注。现在也准备到这个方案。你的文章让我受益匪浅。

  5. liaoer liaoer

    博主,这是最后一篇了么? 还有后续的没

    1. 有可能是最后一篇。你还想探讨哪些方面,我看看有时间的话可以整理整理~

      1. liaoer liaoer

        博主能整理出一份最简单的实践demo么,毕竟看了文章之后自己动手实践起来也会遇到不少问题 = =

  6. jakey jakey

    再次麻烦博主:
    1,(self:LoadUI(uiName, "OnUILoaded"))LoadUI这个接口是实现场景资源加载,界面绘制(也就是);然后将这个cocos或unity对象调用OnUILoaded回调函数?
    2,为什么LoadUI需要uiName,回一个字符串而不是一个函数对象?
    3,LoadUI是子类实现?
    4,按我的理解,load完成的功能是完成cocos或unity对象, enter是加载这个场景,对吗?

    1. 1)没错的。LoadUI 是用来加载界面到场景上的接口(在 Unity 中可以实现为把 assetbundle 里的界面资源加载到场景的 canvas 下面,然后把这个 gameObject 传给回调函数)。
      2/3)LoadUI 是 BaseVewComponent 基类实现,文章中没有写出来,这个接口因项目而异。子类实现 GetUiName 工厂方法,向基类提供要加载的资源名。
      4)load 完成资源加载和调用初始化(init)方法,最终完成加载;enter 是加载后播放场景切换动画,以及调用进场完成(onEnter)方法,最终在这里完成按钮等界面事件的绑定。

      1. jakey jakey

        多谢,清楚了

  7. jakey jakey

    1,attach() 绑定父结点(方法)--是否理解为cocos 中的node.addchild(xx), 将layer曾在场景中显示?
    2,enter() 进场并播放进场动画(方法)--是否理解为 cc.replaceScene(xx) 绘制场景?

    3,如果以上是。那为什么SceneManager.overlay函数还要调用enter,来加载层?

    1. 1)child:attach(parent) 在 cocos 项目中可以是 parentNode.addChild(childNode) 在 unity 项目中可以是 childTransform.SetParent(parentTransform, false);
      2)实际上界面 load() 完的时候,界面已经创建好了;attach() 调用后,界面就已经呈现在画面上了; 但是这时候界面没有绑定事件,调用 enter() 可以根据情况播放一段动画再进入 onEnter() 进行事件绑定,然后界面才真正可用。(如果没有动画就直接调用 onEnter )
      3)overlay 的时候,经常会给弹出的层加一个弹出的效果,可以在 enter 里实现。把事件绑定放到进/出场动画后,可以防止玩家误操作打断界面显示过程。enter 主要是用来处理这一异步过程的函数。

      1. jakey jakey

        意思就是load完成后,场景就已经显示。layer只是创建了node对象。layer是在attach中才进行显示的?
        如果是这样的话,就清楚了

        1. 在我们的 unity 项目里面,LoadUI 完成后,GameObject 就已经在界面上显示了。如果是 cocos2d 的话,可能需要在 SceneManager 里面对 CCScene/CCLayer 作额外的区别处理,具体逻辑在 BaseViewComponent.LOADED 的回调函数里搞定。

      2. jakey jakey

        并且需在LoadUI中区分scene和 layer了?
        load scene直接显示场景画面,load layer就只是加载?

  8. louis louis

    楼主,你的框架是不是不用cc.director来管理scene了?

    1. jakey jakey

      博主使用unity3d

      1. louis louis

        你知道这个框架是如何运行scene的吗?在哪管理的?请指教

        1. louis louis

          我用的是cocox2d-js

          1. jakey jakey

            我也是用的cocox2d-js, 场景切换的部分在 SceneManager完成。具体你可以参考博主文章和评论

      2. louis louis

        最近在学习cocos2d-js,公司项目需要用到,能留个联系方式吗?万分感谢!!!

    2. 这个框架将场景切换的部分封装在 SceneManager 里面了,每个的 Scene 有自己的生命周期:加载、进场、进场完成、退场、退场完成,这些 cocos2d 也都封装好了,把这些消息转发给 SceneManager 就可以了,然后 SceneManager 在适当的时候调用 cc.director 的接口进行场景切换即可。

      1. louis louis

        那这样的话,scene自身的生命周期会不会与SceneManager的生命周期矛盾呢?

        1. 使用 cc.Scene 配合 cc.director 来作场景切换,只会使 SceneManager 会失去一些自由度而已。在 unity3d 中并没有 CCScence 这样的抽象,所有的对象都是在一个场景中,对象的加载、添加到场景、从场景中移除的过程都得需要自己处理,于是更加自由一点。SceneManager 只要去感知什么时候切换开始,什么时候切换结束就行了。

          1. louis louis

            谢谢博主的解答,万分感谢!但对于我这个新手来说可能还需要时间去琢磨。

  9. 风之吹吹 风之吹吹

    木头你这边是如何处理多核框架中 多个系统直接的通讯的;还是说把多核当做单核来使用了

    1. 我只用到单核。我之前考虑过多核,但是发现整个游戏系统对 Model 层的依赖其实是很大的,很多功能模块会用到多个 Model 的数据,而把数据分散到多个核中不利于通讯。

      1. 风之吹吹 风之吹吹

        现在两种方案,一种就是整个游戏都只使用一个facade,另一种是通过截取通知信息使用一个GameFacade管理所有的model和其余的facade。我是新实施这个框架,这两种方式有哪些可能遇到的问题,需要特别注意的地方或者有哪些风险呢

        1. 多核的框架我没有实施过,主要是在做的游戏项目都不大,单核已经足够。而像你说的把游戏数据都移到一个独立的核里,这样并不是很好的模块化的实践,一个完整的模块或者说 core 应该是 MVC 三个部分都建全的。如果要做多核,应该是按单独的功能把 MVC 划分到不同的 core 中,但是游戏项目各种功能的耦合度其实是很大的,而且在开发过程中会有各种改动,多核那样绕着弯子其实加大了修改和理解成本。如果各个功能能够很好的划分到不同的 core 中,我会很乐意去使用多核的,这样还可以实现局部热重启而不影响其它模块,很方便开发的时候修改逻辑。另外在官方的例子里,多核的通讯用的是 Pipes 可以参见: http://www.joshuaostrom.com/2008/06/15/understanding-puremvc-pipes/ ,有兴趣可以研究看看。

          1. 风之吹吹 风之吹吹

            好的 谢谢博主

评论已关闭