Cocos游戏设计心得(四)

哈喽大家好,我们又碰面了,不知觉已经发布3期相关内容了,也不知道有多少人从中获得了帮助。前方的还很遥远,我将用的我的一生,囚你无期!

我们继续上期内容接着讲。

我们上期已经说到了分包设计,已经讲解boot中的大致结构。那么下一步我们要考虑的就是我们如何去设计main包呢?
我们在期初之前需要想一下这块 到底我们需要哪些才能满足我们的需求。
在我想肯定需要这几项:

1.logic逻辑模块
2.moudels通用模块(只需要一个这样的模块)
3.uis游戏界面控制模块

这里logic因为我们是在设计框架,所以暂时无需去考虑游戏中的具体逻辑。同样的uis模块也是一样。所以我们这里最要注意的是moudles通用模块

#1.log游戏日志模块,主要管理游戏输出的日志。(这个模块其实可以放在boot包中)这里我将大概的代码给出
其中fileMgr就是对cocos中的FileUtils封装类让后通过lua绑定就可以使用了

utils = utils or {}
--[[
    http GET调用
]]
function utils.http_get(url, onComplete, resptype)
    local xhr = cc.XMLHttpRequest:new()
    xhr.responseType = resptype or cc.XMLHTTPREQUEST_RESPONSE_STRING
    xhr:open("GET", url)
    local function onReadyStateChanged()
        if onComplete then 
            if xhr.readyState == 4 and (xhr.status >= 200 and xhr.status < 207) then
                onComplete(true,xhr.response)
            else
                onComplete(false)
            end
        end
        xhr:unregisterScriptHandler()
    end
    xhr:registerScriptHandler(onReadyStateChanged)
    xhr:send()
end

-- 获得真实lua文件
function utils.get_real_luafile(filename)
    local finfo = io.pathinfo(filename)
    local luafile = finfo.dirname .. finfo.basename .. ".luac"
    if fileUtils:isFileExist(luafile) then
        return luafile
    end
    luafile = finfo.dirname .. finfo.basename .. ".lua"
    if fileUtils:isFileExist(luafile) then
        return luafile
    end
end

--[[
    日志管理器
--]]
local THIS_MODULE = ...

-- 最大日志缓冲 默认1M
local C_BUFFERSIZE = 1024 * 1024

-- 日志等级
local C_LEVEL = {
    VERBOSE = 1,    -- 啰嗦
    DEBUG   = 2,    -- 调试
    INFO    = 3,    -- 信息
    WARN    = 4,    -- 警告
    ERROR   = 5,    -- 错误
    SILENT  = 6,    -- 寂静,用于关闭日志
}

-- 日志等级标识
local C_LFLAG = {
    [C_LEVEL.VERBOSE]   = "V",
    [C_LEVEL.DEBUG]     = "D",
    [C_LEVEL.INFO]      = "I",
    [C_LEVEL.WARN]      = "W",
    [C_LEVEL.ERROR]     = "E",
    [C_LEVEL.SILENT]    = "S",
}

local LogManager = class("LogManager")

LogManager.LEVEL = C_LEVEL  -- 日志等级

-- 获得单例对象
local instance = nil
function LogManager:getInstance()
    if instance == nil then
        instance = LogManager:create()
        
        -- 定义错误处理
        __G__TRACKBACK__ = function(msg)
            local msg = debug.traceback(msg, 3)
            instance:error("*", 
                "\n---------------------------------ERROR--------------------------------\n" .. 
                "%s" .. 
                "\n----------------------------------------------------------------------",
                msg)
            return msg
        end
    end
    return instance
end

-- 构造函数
function LogManager:ctor()
    self._loglevel = C_LEVEL.VERBOSE                                -- 日志输出等级
    self._flushlevel = C_LEVEL.ERROR                                -- 日志刷新缓冲等级
    self._buffsize = C_BUFFERSIZE                                   -- 日志缓冲大小
    self._logformat = handler(self,LogManager._defaultLogFormat)    -- 日志格式函数
    self._logpath = fileMgr:getWritablePath() .. DIRECTORY.LOG .. "/"
    self._logfile = self._logpath .. "/" .. os.date("%Y%m%d%H%M%S") .. ".log"
    self._logbuffer = { logs = {}, size = 0, }                      -- 日志缓冲
    
    fileMgr:createDirectory(self._logpath)
end

--[[
    默认日志格式化函数
    config  
        level   等级
        tag     标签
        thread  线程
    content     日志内容
]]
function LogManager:_defaultLogFormat(config, content)
    return string.format("%s %s %s%s - ", 
        os.date("%m-%d %H:%M:%S"), 
        config.thread or "[MAIN]", 
        C_LFLAG[config.level], 
        config.tag and ("/" .. config.tag) or "") .. content
end

-- 设置日志输出等级
function LogManager:setLogLevel(level)
    self._loglevel = level
end

-- 设置刷新缓冲区等级
function LogManager:setFlushLevel(level)
    self._flushlevel = level
end

-- 设置日志缓冲区大小
function LogManager:setBufferSize(size)
    self._buffsize = size
end

--[[
    设置日志格式化函数
    format      格式化函数
]]
function LogManager:setLogFormat(format)
    self._logformat = format
    if not format then
        self._logformat = handler(self,LogManager._defaultLogFormat)
    end
end

--[[
    写入日志
    config  
        level   等级
        tag     标签
        thread  线程  
    fmt         格式字符串
    ...         日志参数
]]
function LogManager:_writeLog(config, fmt, ...)
    if config.level >= self._loglevel then
        local content = string.format(fmt, ...)
        local log = self._logformat(config, content)
        self:_writeContent(config.level, log)
    end
end

--[[
    写入日志内容
    log     日志字符串
]]
function LogManager:_writeContent(level, log)
    print(log)
    table.insert(self._logbuffer.logs, log)
    self._logbuffer.size = self._logbuffer.size + #log
    if level >= self._flushlevel or self._logbuffer.size >= self._buffsize then
        self:flushLog()
    end
end

-- 完全写入日志缓冲
function LogManager:flushLog()
    if self._logbuffer.size > 0 then
        fileMgr:appendStringToFile(table.concat(self._logbuffer.logs, "\n") .. "\n", self._logfile)
        self._logbuffer = { logs = {}, size = 0, }
    end
end

-- 输出 VERBOSE 日志
function LogManager:verbose(config, fmt, ...)
    if type(config) ~= "table" then
        config = { tag = config }
    end
    config.level = C_LEVEL.VERBOSE
    self:_writeLog(config, fmt, ...)
end

-- 输出 DEBUG 日志
function LogManager:debug(config, fmt, ...)
    if utils.isDebug() then
        if type(config) ~= "table" then
            config = { tag = config }
        end
        config.level = C_LEVEL.DEBUG
        self:_writeLog(config, fmt, ...)
    end
end

-- 输出 INFO 日志
function LogManager:info(config, fmt, ...)
    if type(config) ~= "table" then
        config = { tag = config }
    end
    config.level = C_LEVEL.INFO
    self:_writeLog(config, fmt, ...)
end

-- 输出 WARN 日志
function LogManager:warn(config, fmt, ...)
    if type(config) ~= "table" then
        config = { tag = config }
    end
    config.level = C_LEVEL.WARN
    self:_writeLog(config, fmt, ...)
end

-- 输出 ERROR 日志
function LogManager:error(config, fmt, ...)
    if type(config) ~= "table" then
        config = { tag = config }
    end
    config.level = C_LEVEL.ERROR
    self:_writeLog(config, fmt, ...)
end

return LogManager

#config 游戏配置模块,主要提供游戏本地和远程配置功能。

-- 标志
FLAG = {
    ENABLEREMOTE    = true,     -- 远程配置
    ENABLEUPDATE    = true,     -- 使能更新
    HTTPSVERIFY     = true,     -- https验证
}

-- 目录
DIRECTORY = {
    TEMP        = "temp",       -- 临时
    DOWNLOAD    = "downloads",  -- 下载
    LOG         = "logs",       -- 日志
}
-- 解决系统require的时候,可能定义全局变量,而又禁止定义
local g_cenable = true
function cc.safe_require(packpath)
    local r = nil
    local oge = g_cenable
    if not oge then
        cc.enable_global(true)
    end
    r = require(packpath)
    if not oge then
        cc.enable_global(false)
    end
    return r
end
--[[
    游戏配置模块
]]
local THIS_MODULE = ...
local C_LOGTAG = "GameConfig"

-- 游戏配置表
local GAMECONFIG = {}

local cjson = cc.require("cjson")
local utils = cc.safe_require("utils")
local GameConfig = class("GameConfig", GAMECONFIG)

-- 获得单例对象
local instance = nil
function GameConfig:getInstance()
    if instance == nil then
        instance = GameConfig:create()
    end
    return instance
end

-- 解密配置数据
function GameConfig:decryptData(data)
    if not self._aesok then
        self._aesok,self._aeskey,self._aesiv = utils.getConfAESKey()
    end
    if data and self._aesok then
        local decok,decdata = utils.AES_decrypt(self._aeskey,self._aesiv,data)
        if not decok then
            error("config decrypt failure")
        else
            return decdata
        end
    end
end

--[[
    加载配置文件
    confile  配置文件
    onComplete  完成回调
        END     配置读取完成
        REMOTE  加载远程配置
]]
function GameConfig:loadConfig(confile,onComplete)
    self:clear()
    if fileMgr:isFileExist(confile) then
        local lcdata = self:decryptData(fileMgr:getDataFromFile(confile))
        if lcdata then
            table.merge(GAMECONFIG,cjson.decode(lcdata))

            -- 读取远程配置
            if self:isRemoteReadable() then
                if onComplete then onComplete("REMOTE") end
                
                GAMECONFIG.remoteurl = io.pathinfo(GAMECONFIG.remoteconfig).dirname

                logMgr:info(C_LOGTAG, "remote config file : %s", GAMECONFIG.remoteconfig)
                logMgr:info(C_LOGTAG, "remote url : %s", GAMECONFIG.remoteurl)
                
                return utils.http_get(GAMECONFIG.remoteconfig,function (result,data)
                    if result then
                        local rcdata = self:decryptData(data)
                        if rcdata then
                            table.merge(GAMECONFIG,cjson.decode(rcdata))
                        end
                    end
                    if onComplete then onComplete("END") end
                end)
            end
        end
    end
    if onComplete then onComplete("END") end
end

-- 清除配置
function GameConfig:clear()
    for _,key in ipairs(table.keys(GAMECONFIG)) do
        GAMECONFIG[key] = nil
    end
end

-- 远程配置是否可以读取
function GameConfig:isRemoteReadable()
    return FLAG.ENABLEREMOTE and GAMECONFIG.enableremote and GAMECONFIG.remoteconfig
end

-- 包是否可以更新
function GameConfig:isPackUpdatable()
    return FLAG.ENABLEUPDATE and GAMECONFIG.enableupdate and GAMECONFIG.remoteurl and GAMECONFIG.packs
end

return GameConfig

-- 包
PACK = {
    FORMAT = ".zip",           -- 包格式
    BASES = { "main" },         -- 基础包
    BOOT = {},                  -- 启动包
    LOADED = {},                -- 加载包
}

#pack 游戏包模块,主要管理游戏包的更新和加载。

--[[
    包 更新器
]]
local THIS_MODULE = ...
local C_LOGTAG = "PackUpdater"

local C_DOWNINFO = "down.info"  -- 下载信息
local C_DOWNING_MAX = 3         -- 最大下载数
local C_REDOWN_COUNT = 3        -- 重新下载次数

local cjson = cc.require("cjson")
local utils = cc.safe_require("utils")
local PackUpdater = class("PackUpdater")

-- 获得单例对象
local instance = nil
function PackUpdater:getInstance()
    if instance == nil then
        instance = PackUpdater:create()
    end
    return instance
end

-- 构造函数
function PackUpdater:ctor()
    self._downpath = fileMgr:getWritablePath() .. DIRECTORY.DOWNLOAD .. "/"
    self._downinfo = self._downpath .. "/" .. C_DOWNINFO
    self._temppath = fileMgr:getWritablePath() .. DIRECTORY.TEMP .. "/"
    fileMgr:removeDirectory(self._temppath)
    fileMgr:createDirectory(self._temppath)
end

--[[
    下载配置
    verscode        版本
    config          配置
    onComplete      完成回调
        R   {true/false}    结果
        P   {float}         进度
]]
function PackUpdater:download(verscode,config,onComplete)
    if fileMgr:isFileExist(self._downinfo) then
        local downconf = cjson.decode(fileMgr:getDataFromFile(self._downinfo))
        if downconf.verscode ~= verscode or downconf.path ~= config.path then
            fileMgr:removeDirectory(self._downpath)
        end
    end
    fileMgr:createDirectory(self._downpath)
    if not fileMgr:isFileExist(self._downinfo) then
        fileMgr:writeStringToFile(cjson.encode({
            verscode = verscode,
            path = config.path,
        }),self._downinfo)
    end
    
    local function _onComplete(...)
        if onComplete then onComplete(...) end
    end

    -- 下载任务配置
    local downtasks = {}
    for i = 1, config.count do
        local downfile = string.format(config.format, i-1) 
        downtasks[i] = {
            remotef = gameConfig.remoteurl .. config.path .. "/" .. downfile,   -- 远程文件
            localf = self._downpath .. downfile,                                -- 保存的本地文件
            progress = 0,                                                       -- 进度
            redowncount = 0,                                                    -- 重新下载次数
        }
    end
    local downindex = 1                                             -- 下载任务索引
    local taskprog = 1 / config.count                               -- 每个任务进度
    local downingmax = gameConfig.downingmax or C_DOWNING_MAX       -- 最大正在下载数量
    local downingnum = 0                                            -- 当前正在下载数量
    local redowncount = gameConfig.redowncount or C_REDOWN_COUNT    -- 重新下载次数
    local downloader = cc.Downloader.new()                          -- 下载器
    local updateProgress = nil                                      -- 进度更新
    local downloadNexts = nil                                       -- 下载剩下的文件

    updateProgress = function(testend)
        local prog = 0
        local complete = true
        local success = true
        for _,downtask in pairs(downtasks) do
            prog = prog + downtask.progress * taskprog
            if testend and downindex > #downtasks then
                if downtask.state == nil then
                    complete = false
                elseif downtask.state == false then
                    success = false
                end
            end
        end

        _onComplete("P", prog)

        if testend and downindex > #downtasks then
            if complete then
                if success then
                    _onComplete("R", true)
                else -- failure
                    _onComplete("R", false)
                end
            end
        end
    end

    downloadNexts = function (downfail)
        while downindex <= #downtasks do
            local downtask = downtasks[downindex]
            if downfail then
                downindex = downindex + 1
                downtask.progress = 0
                downtask.state = false
                updateProgress(true)
            else
                if fileMgr:isFileExist(downtask.localf) then
                    downindex = downindex + 1
                    downtask.progress = 1
                    downtask.state = true
                    updateProgress(true)
                else
                    if downingnum >= downingmax then
                        break       -- 到达最大下载数量
                    end
                    downloader:createDownloadFileTask(downtask.remotef, downtask.localf, tostring(downindex));
                    downindex = downindex + 1
                    downingnum = downingnum + 1
                end
            end
        end
    end

    -- 下载文件
    downloader:setOnFileTaskSuccess(function (task)
        local downtask = downtasks[tonumber(task.identifier)]
        downtask.progress = 1
        downtask.state = true
        updateProgress(true)
        downingnum = downingnum - 1
        downloadNexts()
    end)
    downloader:setOnTaskProgress(function (task, received, totalReceived, totalExpected)
        downtasks[tonumber(task.identifier)].progress = totalReceived / totalExpected
        updateProgress(false)
    end)
    downloader:setOnTaskError(function (task, errorCode, errorCodeInternal, errorStr)
        local downtask = downtasks[tonumber(task.identifier)]
        if downtask.redowncount < redowncount then
            downtask.redowncount = downtask.redowncount + 1
            logMgr:info(C_LOGTAG, "retry download %s : %d", downtask.remotef, downtask.redowncount)
            downtask.progress = 0
            updateProgress(false)
            downloader:createDownloadFileTask(downtask.remotef, downtask.localf, task.identifier);
        else
            logMgr:info(C_LOGTAG, "download %s failure : %s", downtask.remotef, errorStr)
            downtask.state = false
            downingnum = downingnum - 1
            updateProgress(true)
            downloadNexts(true)
        end
    end)
    downloadNexts()
end

-- 检查并返回指定包的更新
function PackUpdater:checkUpdate(packname,packpath)
    local rmpack = gameConfig.packs[packname]
    if rmpack then
        packpath = packpath or (PACKSPATH .. packname .. PACK.FORMAT)
        local lcvers = fileMgr:lookPackVersion(packpath) 
        if rmpack.verscode > lcvers then    -- 远程版本号 > 本地版本号
            local compsize = rmpack.complete.size
            local patchsize = 0
            local patchs = {}
            local rmvern = rmpack.versname
            local curvern = utils.get_version_name(lcvers)
            if rmpack.patchs then
                local function findPatch(vernbegin)
                    for pname,pconf in pairs(rmpack.patchs) do
                        local bgvern,edvern = unpack(string.split(pname,"_"))
                        if vernbegin == bgvern then
                            return edvern,pconf
                        end
                    end
                end
                while curvern ~= rmvern do
                    local edvern,patch = findPatch(curvern)
                    if not edvern then
                        break
                    end
                    patchs[#patchs + 1] = patch
                    patchsize = patchsize + patch.size
                    curvern = edvern
                end
            end
            if rmvern == curvern and #patchs > 0 and patchsize < compsize then
                return {
                    type = "P",                 -- 补丁包更新配置
                    lcvers = lcvers,
                    rmvers = rmpack.verscode,
                    size = patchsize,
                    patchs = patchs,
                    path = packpath,
                    pack = packname,
                }
            else
                return {
                    type = "C",                 -- 整包更新配置
                    lcvers = lcvers,
                    rmvers = rmpack.verscode,
                    size = compsize,
                    complete = rmpack.complete,
                    path = packpath,
                    pack = packname,
                }
            end
        elseif lcvers > rmpack.verscode then    -- 本地版本比服务器更高
            logMgr:warn(C_LOGTAG, "[%s] pack local version(%s) > remote version(%s)", 
                packname, utils.get_version_name(lcvers), utils.get_version_name(rmpack.verscode))
        end
    end
end

--[[
    更新包
    configs                 包配置数组,checkUpdate返回组成的数组
    onComplete
        R   {true/false}    结果
        P   {float}         进度
        C   {config}        当前配置
]]
function PackUpdater:updatePacks(configs,onComplete)
    
    local function _onComplete(...)
        if onComplete then onComplete(...) end
    end

    table.asyn_walk_sequence(function ()
        _onComplete("P",1)
        _onComplete("R",true)
    end,configs,
    function (updateNext,config,index)
        local update = handler(self,config.type == "C" and PackUpdater.completeUpdate or PackUpdater.patchUpdate)
        _onComplete("C",config)
        update(config,function (ctype,result)
            if ctype == "P" then
                _onComplete("P",(1/#configs) * (index - 1 + result))
            elseif ctype == "R" then
                if result then
                    updateNext()
                else
                    _onComplete("R", false, config)
                end
            end
        end)
    end)
end

--[[
    整包更新
    config  整包配置
    onComplete  完成回调
        R   {true/false}    结果
        P   {float}         进度
]]
function PackUpdater:completeUpdate(config,onComplete)
    logMgr:info(C_LOGTAG, "update pack [%s] by complete from %s to %s, size : %s" ,
        config.pack, utils.get_version_name(config.lcvers), utils.get_version_name(config.rmvers), 
        utils.format_store_size(config.size))

    local function _onComplete(...)
        if onComplete then onComplete(...) end
    end

    local function downEnd()
        local destpack = io.pathinfo(config.path).dirname .. "temp.pack"
        utils.mergeFile(self._downpath,destpack,config.complete.format,true)
        _onComplete("P", 0.9)

        if fileMgr:isFileExist(config.path) then
            fileMgr:removeFile(config.path)
        end
        fileMgr:renameFile(destpack,config.path)

        logMgr:info(C_LOGTAG, "[%s] update success", config.pack)

        _onComplete("P", 1)
        _onComplete("R",true)
    end

    self:download(config.rmvers,config.complete,function (ctype,result)
        if ctype == "P" then
            _onComplete("P", 0.8 * result)
        elseif ctype == "R" then
            if result then
                downEnd()
            else
                _onComplete("R",false)
            end
        end
    end)
end

--[[
    补丁更新
    config      补丁配置
    onComplete  完成回调
        R   {true/false}    结果
        P   {float}         进度
]]
function PackUpdater:patchUpdate(config,onComplete)
    logMgr:info(C_LOGTAG, "update pack [%s] by patchs from %s to %s, size : %s" ,
        config.pack, utils.get_version_name(config.lcvers), utils.get_version_name(config.rmvers), 
        utils.format_store_size(config.size))
    
    local function _onComplete(...)
        if onComplete then onComplete(...) end
    end

    local function downEnd()
        local destpack = io.pathinfo(config.path).dirname .. "pack.temp"
        local temppack = self._temppath .. "pack.temp"
        local function swappath()
            local tmp = destpack
            destpack = temppack
            temppack = tmp
        end
        if #config.patchs % 2 == 0 then
            swappath()
        end

        -- 打第一个补丁
        if not utils.patchH(destpack,config.path,self._temppath .. "1.patch") then
            _onComplete("R",false)
            error(string.format("%s patch 1 failure", config.pack))
        end
        _onComplete("P", 0.8 + 0.1 * (1/#config.patchs))
        
        -- 打剩余补丁
        for i = 2, #config.patchs do
            swappath()
            if not utils.patchH(destpack,temppack,self._temppath .. tostring(i) .. ".patch") then
                _onComplete("R",false)
                error(string.format("%s patch %d failure", config.pack, i))
            end
            _onComplete("P", 0.8 + 0.1 * (i/#config.patchs))
        end
        
        if fileMgr:isFileExist(config.path) then
            fileMgr:removeFile(config.path)
        end
        fileMgr:renameFile(destpack,config.path)

        logMgr:info(C_LOGTAG, "[%s] update success", config.pack)

        _onComplete("P", 1)
        _onComplete("R",true)
    end

    -- 下载所有补丁
    table.asyn_walk_sequence(downEnd,config.patchs,function (downNext, patch, index)
        self:download(config.rmvers,patch,function (ctype,result)
            if ctype == "P" then
                _onComplete("P", (0.8 / #config.patchs) * (index - 1 + result))
            elseif ctype == "R" then
                if result then
                    utils.mergeFile(self._downpath,self._temppath .. tostring(index) .. ".patch", patch.format,true)
                    downNext()
                else
                    _onComplete("R",false)
                end
            end
        end)
    end)
end

return PackUpdater

今天就到这里太多了,下期继续喽!(Cocos游戏设计心得(五)已经出炉慢慢品尝)

###留下个人博客以供学习taoqy666.com,生活视频模块已添加

5赞