cocos2d-x+lua代码热载入(Hot Swap)的研究

        代码热载入跟自己主动更新无关,主要目的是在程序执行的时候动态的替换代码,从而实现不重新启动程序而更新代码的目的。最理想的情况当然是我改动完代码并保存。然后就能够直接在游戏中看到改动后的效果,这个在实际开发过程中会大大提高效率。 即便达不到理想情况,我们也希望能够实现部分热载入,从而简化操作。比如我们能够只对配置文件、消息文件、界面文件实现热载入,这样策划更新数据后能够直接在游戏中看结果,而不须要又一次打开client去跑任务。

        热载入主要原理事实上非常easy,lua require文件都会缓存在package.loaded里面,当又一次载入文件的时候,把这个置空。然后又一次require相应文件就能够了。

        实际应用中会有很多其它须要考虑的因素。所以全然的代码热载入非常复杂(原理非常easy,可是实现非常复杂。须要关注的因素非常多)。

        CocosIDE展示了代码热载入的效果:编辑场景中图片的位置并保存,然后图片自己主动放置到新的位置上面了。 这个效果看着非常奇妙,可是实际上并没有什么有用价值。由于它的热载入,事实上就是又一次require文件(基于上面提到的原理)的过程,这个过程中会又一次require 'main.lua'。从而整个游戏都会被又一次启动。

当我们仅仅有一个简单的场景的时候。就能够实现看起来非常完美的热载入。

然而,由于实际游戏client项目会比这个复杂非常多,我们会涉及到多场景、多界面、多状态的维护,所以想实现没有Bug的热载入是非常困难的。

        如今仅仅研究了一部分。初步可行。后期完好了会更加有用。

1、按R键又一次载入全部的lua脚本。这个后面能够做非常多优化。

比方windows下检測文件变化,而不须要手动按键。仅仅又一次载入改变的文件而不是全部文件都遍历一遍。

local listener = cc.EventListenerKeyboard:create();

            listener:registerScriptHandler(function(keycode, evt)
            	--print(keycode)
                if keycode == 138 then
                    -- 按R又一次载入代码
                    reload_script_files();

                    -- 逻辑代码  又一次载入全部的配置
                    -- 逻辑代码 关闭并又一次打开当前已打开的窗体
                end
            end, cc.Handler.EVENT_KEYBOARD_RELEASED);

            local eventDispatcher = cc.Director:getInstance():getEventDispatcher();
            eventDispatcher:addEventListenerWithSceneGraphPriority(listener, scene);

2、又一次载入脚本的实现。这个会递归遍历这个脚本全部依赖的子脚本。

所以普通情况下我们仅仅须要载入一个main.lua就足够了。当然后面优化后就能够载入特定的文件而无需从main.lua一直遍历下去

-- 外部库 登记
local package_list = package_list or {
    bit = true,
    lfs = true,
    cjson = true,
    pb = true,
    socket = true,
}

-- 全局性质类/或禁止又一次载入的文件记录
local ignored_file_list = ignored_file_list or {
    global = true ,
}

--已又一次载入的文件记录
local loaded_file_list = loaded_file_list or {}

--视图排版控制
function leading_tag( indent )
    -- body
    if indent < 1 then
        return ''
    else
        return string.rep( '    |',  indent - 1  ) .. '    '
    end
end

--关键递归又一次载入函数
--filename 文件名称
--indent   递归深度, 用于控制排版显示
function recursive_reload( filename, indent )
    -- body
    if package_list[ filename] then 
        --对于 外部库, 仅仅进行又一次载入, 不做递归子文件
        --卸载旧文件
        package.loaded[ filename] = nil

        --装载信文件
        require( filename )

        --标记"已被又一次载入"
        loaded_file_list[ filename] = true

        --print( leading_tag(indent) .. filename .. "... done" )
        return true
    end

    --普通文件
    --进行 "已被又一次载入" 检測
    if loaded_file_list[ filename] then 
        --print( leading_tag(indent) .. filename .. "...already been reloaded IGNORED" )
        return true
    end

    local fullPath = cc.FileUtils:getInstance():fullPathForFilename(string.gsub(filename, '%.', '/') .. '.lua');
    --print(fullPath)
    --读取当前文件内容, 以进行子文件递归又一次载入
    local file, err = io.open( fullPath )
    if file == nil then 
        print( string.format( "failed to reaload file(%s), with error:%s", fullPath, err or "unknown" ) )
        return false
    end

    print( leading_tag(indent) .. filename)

    -- 缓存文件内容,及时关闭文件。否则文件不可写入
    local data = {}
    local comment = false
    for line in file:lines() do
        line = string.trim(line);
        if string.find(line, '%-%-%[%[%-%-') ~= nil then
            comment = true;
        end

        if comment and (string.find(line, '%]%]') ~= nil or string.find(line, '%-%-%]%]%-%-') ~= nil) then
            comment = false;
        end

        -- 被凝视掉的,和持有特殊标志的require文件不又一次载入
        local linecomment = (line[1] == '-' and line[2] == '-')
        if not comment and not linecomment and string.find(line, '%-%- Ignore Reload') == nil  then
            table.insert(data, line);
        end
    end

    io.close(file)

    local function getFileName(line)
        local begIndex = string.find(line, "'");
        local endIndex = string.find(line, "'", (begIndex or 1) + 1)
        if begIndex == nil or endIndex == nil then
            begIndex = string.find(line, '"');
            endIndex = string.find(line, '"', (begIndex or 1) + 1)
        end

        if begIndex == nil or endIndex == nil then
            return nil;
        end

        return string.sub(line, begIndex + 1, endIndex - 1)
    end

    -- 先解析文件。载入里面的子文件
    for _,line in ipairs(data) do 
        -- 去除空白符
        --line = string.gsub( line, '%s', '' )
        local subFileName = nil 
        if string.find(line, 'require') ~= nil then
            subFileName = getFileName(line);
        elseif string.find(line, 'import') ~= nil then
            -- TODO 兼容import 通过fullPath进行解析
            subFileName = nil
        end

        if subFileName then
            --printInfo('file: %s     subFile: %s', line, subFileName)
            --进行递归 
            local success = recursive_reload( subFileName, indent + 1 )
            if not success then 
                print( string.format( "failed to reload sub file of (%s)", filename ) )
                return false 
            end

        end
        
    end    


    -- "后序" 处理当前文件...
    if ignored_file_list[ filename] then
        --忽略 "禁止被又一次载入"的文件
        print( leading_tag(indent) .. filename .. "... IGNORED" )
        return true
    else

        --卸载旧文件
        package.loaded[ filename] = nil

        --装载新文件
        require( filename )

        --设置"已被又一次载入" 标记
        loaded_file_list[ filename] = true
        --print( leading_tag(indent) .. filename .. "... done" )
        return true
    end
end

--主入口函数
function reload_script_files()
    
    print( "[reload_script_files...]")

    loaded_file_list = {}

    --本项目是以 main.lua 为主文件
    recursive_reload( "MainController", 0 )
    
    print( "[reload_script_files...done]")

    return "reload ok"
end

3、详细逻辑层面的处理

      lua的热载入主要麻烦的地方事实上在逻辑层面的处理上面,一開始写代码的时候就要注意一些问题。比方:

      a、全局变量这样创建     test = test or {}    这样又一次载入文件的时候就不会初始化全局变量了。同理。lua文件作用域内的函数调用也须要类似的判定防止反复执行。

      b、又一次载入配置和又一次打开当前窗体都须要针对自己的逻辑特殊处理。 

      c、理论上我们希望的热载入是对函数实现的替换。所以肯定不会实时的反应改动。比方npc的位置不会由于又一次载入脚本而实时改变。这个是我们载入场景的时候就创建好的,假设须要npc站在新的位置上。须要又一次载入场景或者执行刷新npc位置的函数。

同理。窗体中控件的位置也不会实时改变。须要我们又一次打开窗体。

只是假设我们能够通过代码自己主动执行相关的刷新操作,事实上对终于用户来说是没有什么差别的。相同是能够达到所见即所得的效果。

       d、当我们又一次载入脚本后,全部的脚本内容都会自己主动更新。

可是注冊给cocos2d-x的函数不会,预计是由于tolua已经缓存了相应的函数体。这个临时想不到好的解决方法,由于即便我可以清空tolua中的缓存,也无法找到相应的lua中的新函数。 除非又一次注冊函数,而又一次注冊函数事实上就相当于又一次打开窗体这个过程。


4、实际应用

      这里描写叙述的是相对理想的情况,并且因为肯定是热载入功能为游戏框架服务,而不是反过来游戏框架去适应热载入。所以终于达不到真正理想的无缝载入。只是即便如此,通过上面三步操作也能够大大提高游戏开发效率。

      当我们写了部分界面的功能后。执行程序。查看结果。

发现界面光效位置有偏移,在脚本中改动光效的位置,保存。这个时候光效自己主动在新的位置出现(依赖于自己主动又一次打开窗体或自己主动刷新窗体功能,假设没有这个功能,则须要手动点button打开窗体)。我们能够继续加入新的功能,比方给button绑定函数。保存一下。点击窗体中的button看看效果,发现函数实现有错误,改动之。然后再保存,再点击button看下效果,执行正常。继续开发兴许功能。

posted on 2017-06-05 09:44  wgwyanfs  阅读(284)  评论(0编辑  收藏  举报

导航