Unity Lua与本地存档之我见我想我写

在几天前(30+天),已经对Sqlite3和pb来制作游戏存档做了讲解。
Unity 通过Sqlite3和lua-protubuf制作游戏存档

今天来讲一下,在Lua中的实用。

我不知道其他独立游戏工作室是如何制作存档的,也不知道他们是用C#还是Lua来做存档。我只是因为项目需要,使用Lua来做对存档做存储和读取。如果有更好的做法,欢迎交流。

接下里是废话,如果不感兴趣,可以直接跳过,直接去看下一个标题。

这个最终做法是经过一周的尝试,改了又改,删了又删,最终找到的一种对我而言,算是比较安全便捷的写法。

一开始的时候,所有都是硬夯上去的。很干脆的对转出来的存档数据进行改写,然后存储。
不过写着写着,想到这样不太好。因为LuaTable是引用类型,要是在某个业务逻辑中不小心改了某一个值又存储了,就会导致整个存档在这里出现问题。
比如说一个装备的等级在展示的时候要显示最高级,按理说代码应该是这样的:


function EquipShowPanel:InitData(equip, levelTextComp)
	...
	--等级文本组件
	self.levelTextComp = levelTextComp
	...
	--最高等级数值
	self.maxLevel = EquipMgr:GetMaxLevel(equip.equipId)
	...
end

function EquipShowPanel:SetUIShow()
	...
	--设置等级本文显示
	self.levelTextComp.text = self.maxLevel
	...
end

但是可能一个不小心就搞成了这样:

function EquipShowPanel:InitData(equipData, levelTextComp)
	...
	--等级文本组件
	self.levelTextComp = levelTextComp
	...
	--更新此界面所需数据
	equipData.level = EquipMgr:GetMaxLevel(equipData.equipId)
	self.equipData = equipData
	...
end

function EquipShowPanel:SetUIShow()
	...
	--设置等级本文显示
	self.levelTextComp.text = self.equipData.level
	...
end

这个功能是没有问题的,但是实际上这个equipData是直接指向存档的数据的。这就相当于暗改了存档数据。
这个对于不清楚值类型和引用类型的人来说,甚至一些有经验的人来说,都是可能发生的。
因此这个做法不够安全。

因此就要换一个做法。
首先就是先把存档的内容变成只读表,可以保证存档数据的安全性,不会出现暗改的问题。只读表的做法会在下面有提及。
但是新的问题又出现了,只读表因为其做法的原因,不能被pb.encode正确的读取并转为二进制流。
所以只能再通过深拷贝对存档数据(sourceTable)进行拷贝(copyTable)并做只读表。然后把copyTable作为各个Mgr的数据区。当有确定要修改存档数据时,先对存档进行修改,然后再深拷贝重新赋值给Mgr的数据区。管理器再做相关处理。
听起来可能有点乱,让我用简单代码说明一下:

---存档管理类
GameSaveMgr = {
    
    }
--存档数据
local GameSaveData = {
    
    }
--加载存档数据
function GameSaveMgr:LoadGameSaveData()
	local saveBytes = CS.GameSave.GetData()
	GameSaveData = pb.decode(".Save", saveBytes)
end

--获取存档数据
function GameSaveMgr:GetGameSaveData()
	return GameSaveData
end

--保存存档数据
function GameSaveMgr:SaveGameSaveData()
	CS.GameSave.SetData(GameSaveData)
end

---初始化数据区
_G.InitMgrData = function()
	local copySaveData = table.copy(GameSaveMgr:GetGameSaveData())
	local readOnlySaveData = table.readObly(copySaveData)
	BagMgr:LoadBagData(readOnlySaveData.bagData)
end
---背包管理器类
BagMgr = {
    
    }
--背包数据区
local BagData = {
    
    }
--加载背包数据
function BagMgr:LoadBagData(bagData)
	BagData = bagData
end

--修改数据区道具数量
function BagMgr:SetItemCount(itemId, itemCount)
	for _, item in ipairs(BagData) do
		if item.item_id == itemId then
			item = table.readOnly({
    
    
				item_id = itemId,
				item_count = itemCount.
			})
		end
	end
	
	--通知UI修改显示,当然并不推荐能直接调用BagPanel这样,为了直观才这么写。
	BagPanel:SetItemCountShow(itemId, itemCount)
end
---背包数据存储类
BagDBSet = {
    
    }
--修改存档背包道具数量
function BagDBSet:SetItemCount(itemId, itemCount)
	local saveData = GameSaveMgr:GetGameSaveData()
	for _, item in ipairs(saveData.bagData) do
		if item.item_id == itemId then
			item.item_count = itemCount
		end
	end
	GameSaveMgr:SaveGameSaveData()
	BagMgr:SetItemCount(itemId, itemCount)
end
---背包界面
...
--通知存储类要修改道具数量
function BagPanel:SendSetItemCount(itemId, itemCount)
	BagDBSet:SetItemCount(itemId, itemCount)
end

--修改道具数量显示
function BagPanel:SetItemCountShow(itemId, itemCount)
	...
end
...
---主函数
GameSaveMgr:LoadGameSaveData()
InitMgrData()
BagPanel:SendSetItemCount(1, 100)

可能代码还是很晕,我还是上图吧。
各个模块的主要方法与数据
逻辑流程
就看这个弯弯绕绕的,就知道很麻烦。(我光作图就觉得麻烦的要死)
除此之外,还要注意保存存档数据完成后,通知修改数据区道具数量的时候,一定要注意再做一次只读表。
复杂度可显而知,而且调来调去这么多,维护起来也是十分的要命。

就在我用这一套逻辑和大佬说了之后,他听了半天终于明白了我的意图。觉得倒是可以,但是真的太麻烦了。而且就以正常联网游戏来说,存档不应该是这么刻意的,每个都要自己写。应该做到一种,我在内存随便改,最后只要统一的存档就行了。“要写起来很爽才对”。
听着好像明白了,但是又没有思路。大佬这时候点了个题,“你的存档数据不是应该保持不变的嘛,你的只读表的原数据,应该也是也和存档数据链着的。”
突然我就明白了,你可能还懵的,那么正文开始。

正文

这个存档,主要为了安全性和便利性。将存档做成只读表,同时将每一层表都与存档数据的表链起来。在模块管理器中,针对数据区只有两类方法。
一类是Get,从数据区的只读表中获取数据,以防止数据被暗改,十分安全。
另一类是Set,从数据区的只读表中获取原始表,直接修改存档数据,直接保存,十分便利。

先来看一下只读表的制作
Lua只读表
这里贴一下代码:

table.readOnly = function(sourceTable)
	for k, v in pairs(sourceTable) do
		if type(v) == 'table' then
			sourceTable[k] = table.readOnly(v)
		end
	end

    return setmetatable({
    
    }, {
    
    
        __index = sourceTable,
        __newindex = function() 
        	print(string.format("试图向只读表中插入或修改值 key[%s] value[%s]", k, v))
         end,
    })
end

不过这和存档的制作有点冲突,因为会把原始表中的表的链破坏掉。
其次,原始表还是有可能插值进去的,所以__index指向一个已经做好的表,显然会有问题。所以要用一个额外的表来记录子表的只读表。注意,这个额外的表不需要记录值类型的数据,不然原始表修改了只读表中读的还是错误的。
所以可以这样改一下,顺便把原始表带进去。

table.readOnly = function(sourceTable)
    local lookupTable = {
    
    }
    return setmetatable({
    
    }, {
    
    
        __index = function(tb, k)
            if type(sourceTable[k]) == "table" then
                if lookupTable[k] == nil then
                    lookupTable[k] = table.readOnly(sourceTable[k])
                end
                return lookupTable[k]
            else
                return sourceTable[k]
            end
        end,
        __newindex = function(tb, k, v)
            Error(string.format("试图向只读表中插入或修改值 key[%s] value[%s]", k, v))
        end,
        __source = sourceTable,
    })
end

这样动态的获取表中的数据,会处理的好一些。
同时也附上获取原始表的代码:

table.getSource = function(targetTable)
    local metaTable = getmetatable(targetTable)
    if metaTable ~= nil then
        return metaTable.__source 
    else
        return targetTable
    end
end

关于只读表差不多就是这样了。

再说一下关于存档的另一件事情。就是初始化存档数据,毕竟有些值是要有初始值的。比如说可能有一个存档创建时间。这个这种数据可以在每次登陆的时候去检查是否存在,但是并不是很好,不太建议冗杂在业务逻辑里。所以可以这样:

function GameMgr:CreateSave()
    GameSaveMgr:CreateGameSave()
    DBDataInit()
end
local function TimeDBInit(initSaveData)
	initSaveData.create_st = TimeHelper.GetCurTime()
end

_G.DBDataInit = function()
    local initSaveData = {
    
    }
    TimeDBInit(initSaveData)
    GameSaveMgr:SetGameSave(initSaveData)
    GameSaveMgr:SaveGameSave()
end

也就是只在创建存档的时候才会初始化这些值,同时还可以集中处理。

接下来就是说一下修改存档方面的内容。
关于读取存档。加载存档的时候,将存档数据转为只读表,然后分别存到各个模块的管理器中。

local saveData = table.readOnly(GameSaveMgr:GetGameSaveData())
BagMgr:LoadBagData(saveData.bag_item_data)

以背包数据为例

---背包管理器类
BagMgr = {
    
    }
--背包数据区
local BagData = {
    
    }
--加载背包数据
function BagMgr:LoadBagData(bagData)
	BagData = bagData
end

--获取道具数量
function BagMgr:GetItemCount(itemId)
	for _, itemData in ipairs(BagData) do
		if itemData.item_id == itemId then
			return itemData.item_count
		end
	end
	return 0
end

--修改道具数量
function BagMgr:SetItemCount(itemId, itemCount)
	--获取数据区对应的数据
	local targetItemData
	for _, itemData in iparis(BagData) do
		if itemData.item_id == itemId then
			targetitemData = itemData
			break
		end
	end

	if targetItemData ~= nil then
		local sourceItemData= table.getSource(targetItemData )
		sourceItemData.item_count = itemCount
	else
		local sourceBagData= table.getSource(BagData)
		table.insert(sourceBagData, {
    
    
			item_id = itemId,
			item_count = itemCount,
		}
	end
	
	--存档
	GameSaveMgr:SaveGameData()
	--通知Panel修改显示
	...
end

看起来就简单直了。
Get类的方法就是直接获取,因为获取的就是只读表,所以也不允许修改或添加值,因而十分的安全。
Set类的方法就是获取对应数据的原始表,这个表直接指向了存档数据,修改的值也是直接修改的存档数据的值。修改好了之后,直接存档,不用在意到底改了什么,也不用怕获取的时候会有问题。

写起来是真的很爽啊。

猜你喜欢

转载自blog.csdn.net/qql7267/article/details/115009890