第14章 文件的读写与数据保存

本章将介绍关于游戏数据保存和读取的相关内容。

文件的读写与数据保存

游戏存档、读档,加载配置文件等等都需要对外部文件进行读写操作。

love和lua中的文件

lua对于文件的控制在io库中,love对文件的控制在love.filesystem库中。
他们在实际的作用上是差不多的,只是love的文件控制库对love本身的目录控制比较方便,而lua由于需要跨平台以及权限问题,所以没有集成lfs系统,所以无法对文件夹进行遍历。

文件的读写范围

love文件系统只允许你读取:

  1. 源码文件夹
  2. app/.exe所在的文件夹
  3. 存档文件夹
    这是为了保证你正常文件读写操作的同时,避免接触不必要的权限。因为一般游戏也不太需要其他位置的文件读取。另外,文件可以从任意的位置拖动到游戏窗口,从而被游戏接收到。这是在不使用外部工具的前提下接触到外部文件的唯一方法。当然你可以用io.open来试图打开其他位置的文件,但你无法遍历文件,因此是单向操作。

写文件比读取需要更高的权限,因此范围也更窄,仅能够在存档文件夹中进行。如果是win系统,存档文件夹可能在C:\Users\user\AppData\Roaming\LOVE中。而在设置中identity可以让你自定义存档文件夹的名字,否则将使用标题的名字(不推荐)。

在love引擎中,默认的lua文件查找顺序为:

  1. 在游戏工程文件夹中
  2. 在游戏存档文件夹中
  3. 在游戏已设置的mount目录中
    查找方式为,寻找以”?.lua;?/init.lua”为规则的文件,换句话就是以?为文件名的,或者以?为目录且下面有init.lua的。
    查找方式也可以自己自定义,比如 “?.lua;?/init.lua;lib/?.lua”,这样就增加了lib目录下的所有同名的lua文件,不需要在每个require中写根目录了。不过不太推荐这种做法。

文件的基本读写

我们暂时不管lua的io库,因为基本做法是一样的。我们仅使用love.filesystem库。
love.filesystem.newFile(name,mode) 创建文件对象
file:open(openMode) 打开文件,打开方式包括 r w a c(关闭)
file:read() 读取文件,返回一个string.
file:close() 关闭文件。
这个跟任何其他语言的文件操作是一样的。注意的是,要习惯性的关闭文件,否则文件将一直处于占用状态直到游戏关闭。
实际上love还提供了两个快捷方式,有了他们就不必管file对象了。
love.filesystem.read(name) 直接读取一个文件,返回一个字符串。
love.filesystem.write(name,string) 直接写字符串到一个文件中。
注意,上述情形都没有考虑到是否读取成功,比如文件不存在,或者被占用。所以最好用exist来判断下。

数据序列化和导入

我们现在来看文件读写的内容。因为写数据只能用string格式,但这个string格式又不限定它的编码规则,所以你可以写任意东西,不过别指能够print他们出来。
存储一些并列格式,或者是已知顺序的序列,只需要存储所有的值,并用逗号,或者你喜欢的其他符号分隔即可。

1
2
3
4
5
6
local data = {"Alexar","2016-12-9",123456789}
local raw = ""
for i,v in ipairs(data) do
raw = raw .. "," .. tostring(v)
end
love.filesystem.write("save.dat",raw)

读取他们,需要用到一个比较常用的方法,下面介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function string.split(str,keyword)
local tab={}
local index=1
local from=1
local to=1
while true do
if string.sub(str,index,index)==keyword then
to=index-1
if from>to then
table.insert(tab, "")
else
table.insert(tab, string.sub(str,from,to))
end
from=index+1
end
index=index+1
if index>string.len(str) then
if from<=string.len(str) then
table.insert(tab, string.sub(str,from,string.len(str)))
end
return tab
end
end
end

看源码似乎有点枯燥,它的作用就是将一个string用某个关键字分割,每一段存入一个表中,并返回。使用起来倒是很简单,我们看下面代码:

1
2
local raw = love.filesystem.read("save.dat")
local data = string.split(raw,",")

对于有着复杂结构的表而言,用这种方法其实也可以,只要你规定好每层的标记符号即可。比如:

1
keywords = {"~","!","#","$","%"}

这个符号是随意定的,我只是用键盘数字键一排的上标来标记的,至于方法相信有点编程基础的应该都会,这里不说了。

上面的方法是针对有序表的,如果是无序表呢?无序表实际上也叫做键值对,存储他们最简单就是键,值,换行这种形式。比如:

1
2
3
4
5
local data = {name = "Alexar",score = 12345}
local raw = ""
for k,v in pairs(data)
raw = raw .. k .. " " .. v .. "\n"
end

读取他们同样用split就行,先找\n换行,然后再找空格分隔就行了。或者使用love.filesystem.lines(迭代器)进行行遍历即可。
最后,对于有序无序混排的,如果希望它是可读的,可以参照笔者git上table.save方法来尝试一下。如果是不可读的,看下面的大招。

lua中有一个很强的函数是string.dump。它能将一个没有上查值的函数序列化变成二进制码。wow,有了他,你看到一个函数就相当于看到一个string是一样的。而再次使用这个函数,只需要loadstring()即可。于是,我们想到了,实际上你把任何数据包裹在一个函数中,再返回他们就实现了数据出入啦。比如:

1
2
3
4
5
6
7
8
local raw = string.dump(funciton()
local data = {"abc",123}
return data
end)
love.filesystem.write("save.dat",raw)
local raw = love.filesystem.read("save.dat")
local data = loadstring(raw,",")()

这里有三点需要注意,一点是一定要保证数据写在函数块里,而不是上查。第二点,由于这个raw实际上是二进制文件,他无法与别的utf8编码的字符进行连接操作(不然会报错的)。第三点,这个函数是有平台限制的,不同版本的lua.dll就会产生不同的二进制文件,所以不能通用,不过给自己用是足够了的。

另外,在做phaser(数据转换器)时,比较重要的函数还有两个:string.format和string.find, 这两个函数的具体用法这里不讲了,设计到lua的一个重要部分——匹配模式(简化的正则表达式)。

有用的库

下面介绍一些有用的库,他们能够帮你对不同形式的文件实现读写或转化
binser 一个数据序列库,导出/读取二进制文件
bintable 一个数据序列库,导出/读取二进制文件
lady 更多的封装,可以直接用来读写游戏存档。
dkjson luatable和json的相互转化。
luaxml luatable和xml的相互转化。