Unity Lua and local archives: I see, I think, I write

A few days ago (30+ days), I have explained how to make game archives with Sqlite3 and pb.
Unity makes game archives through Sqlite3 and lua-protubuf

Let’s talk about the practicality in Lua today.

I don't know how other indie game studios make save files, and I don't know whether they use C# or Lua for save files. I just use Lua to store and read archives because of project needs. If there is a better way, welcome to communicate.

The following is nonsense, if you are not interested, you can skip it and go directly to the next title.

This final method is after a week of trying, modifying and modifying, deleting and deleting, and finally found a way that is safer and more convenient for me.

In the beginning, everything was rammed up hard. Simply rewrite the transferred archive data and store it.
But as I write, I think it's not very good. Because LuaTable is a reference type, if a certain value is accidentally changed and stored in a certain business logic, it will cause problems in the entire archive here.
For example, if the level of an equipment is displayed at the highest level, it stands to reason that the code should be like this:


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

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

But it may be accidentally done like this:

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

There is no problem with this function, but in fact this equipData directly points to the archived data. This is equivalent to secretly changing the archive data.
This is possible for people who are not clear about value types and reference types, and even some experienced people.
Therefore, this approach is not safe enough.

Therefore, it is necessary to change the method.
The first is to turn the archived content into a read-only table, which can ensure the security of the archived data, and there will be no problem of secret modification. The practice of read-only tables will be mentioned below.
But a new problem arises again, the read-only table cannot be correctly read by pb.encode and converted into a binary stream because of its practice.
Therefore, the archive data (sourceTable) can only be copied (copyTable) through deep copy and made into a read-only table. Then use copyTable as the data area of ​​each Mgr. When it is determined to modify the archive data, first modify the archive, and then deep copy and reassign it to the data area of ​​Mgr. The manager will do related processing.
It might sound messy, so let me illustrate with simple code:

---存档管理类
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)

Maybe the code is still very dizzy, so I'll stick to the picture above.
The main methods and data of each module
logical flow
Just by looking at the twists and turns, you know it's very troublesome. (I find it extremely troublesome just to draw a picture.)
In addition, after saving the archived data, you must pay attention to making a read-only table again when notifying the modification of the number of props in the data area.
The complexity is obvious, and there are so many transfers, it is also very terrible to maintain.

After I used this set of logic to talk to the boss, he finally understood my intention after listening for a long time. I think it's possible, but it's really annoying. And as far as normal online games are concerned, archives should not be so deliberate, and each one must be written by itself. One should be done, I can change it in the memory at will, and in the end it only needs to be archived uniformly. "It has to be cool to write."
It sounds like I understand it, but I have no idea. The boss clicked a question at this time, "Shouldn't your archived data remain unchanged? The original data of your read-only table should also be linked with the archived data." Suddenly I understood, you
may Still confused, so the text begins.

text

This archive is mainly for security and convenience. Make the archive into a read-only table, and at the same time link each layer of tables with the table of the archived data. In the module manager, there are only two types of methods for the data area.
One is Get, which obtains data from the read-only table in the data area to prevent the data from being changed secretly, which is very safe.
The other type is Set, which is very convenient to obtain the original table from the read-only table in the data area, directly modify the archived data, and save it directly.

Let’s take a look at how to make a read-only table first. Paste the code here for
a Lua read-only table :

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

However, this conflicts with the production of the archive, because it will destroy the chain of the table in the original table.
Secondly, the original table may still be interpolated, so __index points to an already prepared table, which obviously has problems. So use an extra table to record the read-only table of the child table. Note that this additional table does not need to record data of the value type, otherwise the original table is modified and read in the read-only table is still wrong.
So you can change it like this, and bring the original watch in by the way.

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

In this way, the data in the table can be dynamically obtained, which will be handled better.
Also attach the code to get the original table:

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

That's about it for read-only tables.

One more thing about archiving. It is to initialize the archive data. After all, some values ​​​​must have initial values. For example there may be an archive creation time. This kind of data can be checked for existence every time you log in, but it is not very good, and it is not recommended to be redundant in business logic. So it can be like this:

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

That is, the values ​​are only initialized when the archive is created, and can be processed centrally.

The next step is to talk about modifying the content of the archive.
About reading archives. When loading the archive, the archive data is converted into a read-only table, and then stored in the manager of each module.

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

Take backpack data as an example

---背包管理器类
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

It seems straightforward.
The method of the Get class is directly obtained, because the obtained is a read-only table, so it is not allowed to modify or add values, so it is very safe.
The method of the Set class is to obtain the original table of the corresponding data. This table directly points to the archived data, and the modified value is also the value of the directly modified archived data. After the modification is completed, save it directly, don't care about what has been changed, and don't worry about problems when obtaining it.

It's really cool to write.

Guess you like

Origin blog.csdn.net/qql7267/article/details/115009890
I
"I"
I: