《DFQ》开发随录——资源管理

  欢迎参与讨论,转载请注明出处。
  本文转载自https://musoucrow.github.io/2018/07/27/dfq_res/

前言

  在游戏开发的领域里,游戏资源的管理可谓一个很重要的基础功能,在一些强大的游戏引擎会为其配备一套解决方案。而LÖVE很不幸的再次没有提供,好在即使没有也起码做好了内存管理的工作,那么即便自己动手做一套也不是什么困难的事了,本文便记录其中心得。
  资源管理模块本质上只做了两件事:

  • 生命周期管理:保证多次加载时资源的复用,在无需该资源时进行销毁。
  • 配置接口:提供加载资源的API,以及外部化的资源配置。

      接下来便围绕以上两点展开说明其中的要点。

生命周期管理

  如上文所言,生命周期管理要做的事即:保证多次加载时资源的复用,在无需该资源时进行销毁。资源复用的实现思路非常的简单,使用哈希表将资源以路径-对象为映射关系进行存储即可,然后每次加载资源时进行一次检查,若存在表内则直接获取,否则再进行读取。

local poor = {}

local function GetResource(path)
    if (poor[path]) then
        return poor[path]
    end

    local res = open(path)
    --... load resource.
    poor[path] = res

    return res
end

  接下来是第二个问题:在无需该资源时进行销毁。说得具体点便是:当外部没有对象引用该资源时,将其从资源池里移除。如此只需要使用弱引用即可,在Lua里即是建立弱表(weak table)

local poor = {}
setmetatable(poor, {__mode = 'v'})

  如此当poor内存在外部无引用的对象时,在垃圾回收时便会将其移除。如此资源的生命周期管理便算完成了。

配置接口

  资源文件按照性质可以划分为两种:数据文件(二进制为主,如图片、声音等),配置文件(可编辑、可序列化的变量对象)。对于配置文件,Lua可以很方便地直接使用本体:

return {
    x = 1,
    y = 2
}

  只要将其读取后使用loadstring(text)()函数便可将其序列化,在其他引擎也有自定义配置格式以编辑器加持的形式解决,如Unity。现实情况中,一般数据文件会通过配置文件进行加载,即在配置文件提供对应资源的路径,然后由代码进行加载处理。

return {
    image = "glow",
    ox = 5,
    oy = 5,
    color = {
        r = 255,
        g = 255,
        b = 255,
        a = 127
    }
}

  如上配置所示,此配置的image项将会由代码根据配置提供的路径glow进行读取对应目录下的image/glow.png文件。如此便可看出,资源与资源之间存在很强的联动性,它们就像是一棵树,节节相扣。对于那些较上层的配置文件而言,往往会从上到下牵涉巨多资源。这么做是很棒的,一加载便将所有相关的资源都加载了,只要在恰当的场合进行资源加载(如切换地图),核心游戏过程中则几乎不会涉及到加载了。

配置的健壮性

  在没有编辑器加持的情况下,单纯的配置文件健壮性是有限的,最突出的两个需求便是:

  • 快捷定位路径:如位于sprite/test/1.cfg的配置文件想要读取位于同路径、不同分类下的image/test/1.png文件,如果没有一些辅助手段,那么只能傻傻的输入全路径,十分愚蠢。
  • 参数注入:倘若存在一些大体相似,少部分不同的配置需求,若没有参数注入,那么只好傻傻的批量复制修改,也是十分的愚蠢。

      当然以上两点若是存在编辑器,自然可以无视并通过自动化手段之类达到相同的效果。但目前项目暂无编辑器,于是采用了替换文本的方案。

---sprite/test/1.cfg
return {
    image = "$A",
    sx = $1,
    sy = $2,
}

  如上配置所示,$A便是代表当前资源分类下的路径,即替换为test/1,如此便可快速定位至image/test/1.png,算是一种语法糖吧。至于$1 $2则代表第1、第2个注入的参数,在调用的API的时候会以{1.2, 1}的形式作为参数填入。如此便会将$1替换为1.2,同理$2替换为1。当然这种注入了参数的配置在资源池的key是不能使用路径的(不是标准的),会在其后加入参数值成为:test/1|1.2|1

配置的只读性

  由于资源对象往往都是独一一份,到处引用,倘若哪处不小心对其进行了修改,那么便会引起连锁反应影响全局。所以有必要考虑将资源对象设置为只读的:

local function _PrivateTips()
    assert(nil, "The table is const.")
end

function _TABLE.NewConst(tab)
    local tabMt = getmetatable(tab)

    if (not tabMt) then
        tabMt = {}
        setmetatable(tab, tabMt)
    end

    local const = tabMt.__const

    if (not const) then
        const = {}
        tabMt.__const = const

        local constMt = {
            __index = tab,
            __newindex = _PrivateTips,
            __const = const
        }

        setmetatable(const, constMt)
    end

    for k, v in pairs(tab) do
        if (type(v) == "table") then
            tab[k] = _TABLE.NewConst(v)
        end
    end

    return const
end

  只要将对象拿去处理后,试图修改该对象时将会报错。当然这样做是有代价的:pairs()table.getn函数将会变得无法直接使用,需要取出其元表方可使用。所以需要配备专门函数:

function _TABLE.Len(tab)
    local meta = getmetatable(tab)

    if (meta and meta.__index) then
        return #meta.__index
    else
        return #tab
    end
end

function _TABLE.Pairs(tab)
    local meta = getmetatable(tab)

    if (meta and meta.__index) then
        return pairs(meta.__index)
    else
        return pairs(tab)
    end
end

  这样子使用起来虽然麻烦了点,不过的确将资源对象和一般对象作出了明显的区分。另外只读处理的时机也需要考量的,一般得在整个资源对象处理完毕后才进行。

配置的个性化

  对于一些普遍的资源文件(图片、声音、精灵、动画等),一般配备专属的处理函数即可。但是到了业务层面情况往往会繁杂许多,将会存在许多个性化的配置格式。这时候便需要将业务对象和资源对象进行绑定:

return {
    script = "$A",
    tagMap = {
        attack = true,
        attackRate = true,
        autoPlay = true
    },
    stopTime = 200,
    endTime = 300,
    nextState = "stay",
    frameaniPath = "$0attack1",
    actor = "bullet/throwstone",
    bulletPos = {
        x = 20,
        y = 0,
        z = -60
    }
}

  如上配置所示,这是一个哥布林的投掷状态,这里的配置便需要提供子弹资源以及发射坐标了。关于这些个性化的配置项,将会如此解决:

local function _NewStateData(path, keys)
    local data, path = _RESOURCE.ReadConfig(path, "config/actor/state/%s.cfg", keys)

    data.class = require("actor/state/" .. data.script)
    data.script = nil

    if (data.class.HandleData) then
        data.class.HandleData(data)
    end

    return data
end

  可以看到,通过配置的script项找到对应的脚本业务对象,然后调用其对象的HandleData函数进行解析。如此便解决了个性化的问题。

后记

  还是如上篇一般,这个问题对于流行的大引擎而言已经提供了成熟的解决方案。上了贼船呀,只能走到黑了。不过造造轮子也是有益技术的提升的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值