zhouqijie

概述

游戏引擎必须必须具备载入和管理多种资源的能力。包括纹理贴图、网格、音频、场景布局等。

游戏引擎应该确保在同一时间,每个媒体文件只可载入一份,避免重复以节省内存空间。

游戏引擎会采用某种类型的资源管理器来载入和管理所需资源。资源管理器会大量使用文件系统。

有时游戏引擎会封装原生的文件系统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函数。有以下好处:

  1. 就算不同平台的原生程序库不一样,也可保证这些自定义API在不同平台都有相同的行为。
  2. API可以轻量化只实现实际需要的函数。
  3. 可以实现功能延申扩展,可以实现处理不同媒体上的文件。(例如硬盘、光盘、记忆棒、网络上的文件等)

同步文件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)