概述
游戏引擎必须必须具备载入和管理多种资源的能力。包括纹理贴图、网格、音频、场景布局等。
游戏引擎应该确保在同一时间,每个媒体文件只可载入一份,避免重复以节省内存空间。
游戏引擎会采用某种类型的资源管理器来载入和管理所需资源。资源管理器会大量使用文件系统。
有时游戏引擎会封装原生的文件系统API,来实现跨平台。
第一部分:文件系统
一、文件名和路径
路径是一种字符串,用于描述文件系统层次中文件或者目录的位置。
不同的操作系统路径格式可能不一样,但是本质上有相同结构。
卷/目录/目录/.../目录或者文件名
不同操作系统的差异:
略。见《游戏引擎架构》原书。
绝对路径和相对路径:
相对于根目录的称为绝对路径。相对于其他目录的称为相对路径。
在UNIX和Windows下,绝对路径的首字符为路径分隔符(
\
或者/
),二相对路径则不会以路径分隔符作为首字符。
在Windows中,绝对路径和相对路径都可以加入卷标,不加入卷标则表示当前工作卷。
搜寻路径:
搜寻路径是含有一串路径的字符串,各路径之间用特殊字符(冒号或者分号)分开,找文件时就从这些路径进行搜寻。
例如在命令行中执行程序,操作系统会首先在当前目录下找该可执行文件,如果没有就在PATH环境变量中搜寻该可执行文件。
路径API:
路径应该要比简单的字符串复杂得多。因为我们通常需要对路径进行多种操作,例如分离目录或文件名或扩展名、绝对路径相对路径转换、路径规范化等。所以我们通常需要含有丰富功能的路径API。
Windows提供了一组API,由shlwapi.dll动态链接库实现,并提供shlwapi.h头文件。索尼也为PS3提供了类似API。
若要开发跨平台引擎就不能直接使用平台的API,而且也用不到这些平台API的所有功能。所以游戏引擎一般会实现轻量化的路径处理API来符合引擎专门需求。(可以是从零实现,也可以是平台API包装)
二、基本文件IO
一般有两种API以供开启/读取/写入文件内容。
一组是低阶的IO接口,无缓冲。(操作系统函数)
一组是高阶的IO接口,有缓冲。(标准C语言库函数)
操作 | 标准C | 操作系统 |
---|---|---|
开启文件 | fopen() | open() |
关闭文件 | fclose() | close() |
读取文件 | fread() | read() |
写入文件 | fwrite() | write() |
移动访问位置 | fseek() | seek() |
… | … | … |
cnblog清清飞扬:open 是系统调用 返回的是文件句柄(文件描述符),文件的句柄是文件在文件描述副表里的索引,fopen是C的库函数,返回的是一个指向文件结构的指针。一般用fopen打开普通文件,用open打开设备文件。
cnblog清清飞扬:我认为fopen和open最主要的区别是fopen在用户态下就有了缓存,在进行read和write的时候减少了用户态和内核态的切换,而open则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列快;如果随机访问文件open要比fopen快。
在UNIX中,标准C程序库的无缓冲IO函数是原生的操作系统调用。在Windows中,这些函数仅仅是底层API的包装。(例如Win32API的CreateFile()/ReadFile()/WriteFile())
相对于标准C语言库函数,使用低阶系统调用的优点是能运用原生文件系统的所有细节功能。(例如使用Windows原生API可以询问和改变文件的安全属性而C标准库不行)
包装IO的API:
游戏引擎可以使用标准C的IO函数,也可以使用操作系统原生API。
但是最好是把操作系统的API函数包装为自定义的IO函数。有以下好处:
- 就算不同平台的原生程序库不一样,也可保证这些自定义API在不同平台都有相同的行为。
- API可以轻量化只实现实际需要的函数。
- 可以实现功能延申扩展,可以实现处理不同媒体上的文件。(例如硬盘、光盘、记忆棒、网络上的文件等)
同步文件IO:
两种文件IO库都是同步的。即IO请求发出后,程序必须等待读写数据完成后才能继续运行。
示例:
#define U8 unsigned char
bool ReadFileSync(const char* filePath, U8* buffer, size_t bufferSize, size_t& rBytesRead)
{
FILE * handle = fopen(filePath, "rb");
if (handle) {
//在此阻塞,直到所有数据读取完毕
size_t bytesRead = fread(buffer, 1, bufferSize, handle);
int err = ferror(handle); //如果出错获取错误码
fclose(handle);
if (0 == err)
{
rBytesRead = bytesRead;
return true;
}
return false;
}
}
int main()
{
U8 testBuffer[512];
size_t bytesRead = 0;
bool result = ReadFileSync("G:\\testFile.bin", testBuffer, sizeof(testBuffer), bytesRead);
if (result)
{
printf("success: read %u bytes\n", bytesRead);
}
}
异步文件IO:
流(streaming)是指在后台载入数据,而主程序同时继续运行。
为了实现无缝的游戏体验,很多游戏都在游戏进行的同时使用流载入一些数据。
最常见的流数据类型是音频和纹理。但其他数据例如图形、关卡场景、动画片段也可使用流。
有些平台自带提供异步IO库。若目标平台不提供异步IO库,但提供线程相关的功能,可以自行实现异步文件IO功能。
Creedon: C#中有
System.IO.FileStream.BeginRead()
等异步IO函数。
伪代码示例:
U8 g_asyncBuffer[256];
void main(){
AsyncRequestHandle hRequest = ASYNV_INVALID_HANDLE;
AsyncFileHandle hFile = asyncOpen("C:\\testfile.bin");
if(hFile)
{
//该函数做读取请求,非阻塞
hRequest = asyncReadFile()
hFile, //文件句柄
g_asyncBuffer, //输入缓冲
sizeof(g_asyncBuffer), //缓冲大小
NULL //无回调函数
;
}
//模拟:异步读取时做一点工作
for(int i = 0; i < 10; ++1)
{
Sleep(50);
}
//数据预备好之前不能继续 需要等待
asyncWait(hRequest);
if(asyncWasSuccessful(hRequest))
{
//...
}
}
优先权(Priority):
异步IO操作常有不同的优先权。异步IO系统必须能暂停低优先权请求,让较高优先权的IO请求有机会在时限内完成。
异步IO的原理:
异步文件IO是利用另一个线程处理IO请求的。
主线程调用异步函数时,会把请求放入一个队列,并立刻返回。同时,IO线程从队列中取出请求,并用会阻塞的普通IO函数(例如read和fread)处理这些请求,完成时就会调用主线程提供的回调函数。
若主线程选择等待完成IO操作,就会使用信号量(semaphore)处理。(每个请求对应一个信号量,主线程休眠,等待IO线程完成请求工作后通知信号量)
(END)