游戏本质上是一种多媒体体验,因此载入和管理多媒体是游戏引擎的必备能力。在大型的游戏开发中,资源管理非常重要,这些资源包括纹理位图、模型数据、动画视频、音频音效等等。为了有效的利用设备内存,这些资源在加载到内存的时候,一般只会保存一份,然后在所有需要使用的地方进行引用。

如何管理这些资源其实是游戏引擎的工作,只不过对于小游戏来说,这个问题其实很容易被忽略,因为小游戏使用的多媒体素材很少,一般只有图片素材和简单音频文件,如果在程序中使用,直接载入使用即可。今天这篇文章主要讲一讲使用 SDL 开发小游戏过程中可能预计的两个资源加载问题。

1. 如何加载游戏素材

如果你使用 C++ 语言,你可以直接将资源管理设置为一个单例对象,每种资源可以按照类别进行归类,然后为每种资源确定一个唯一的标记位,并且可以起一个人类容易记忆的名称,如果用数据结构描述,最接近这种东西是 std::map 对象。类似下面这种定义:

std::map<std::string, UUID> resources;

使用的时候直接 resources["wall"] 获取相关的游戏资源即可。当然如果资源复杂,你甚至可以按照一定的比例、关卡、结构专门设计游戏资源的管理方式,不过这里我们只是准备简单地介绍一下使用 SDL 开发小游戏的时候如果加载资源。

首先,你需要了解如何将资源加载到游戏。我们常见的游戏资源包括图片、音频还有字体,这些资源全部以文件的形式保存在计算机的硬盘上。加载这三种资源,SDL 分别为我们提供了不同的函数接口:

  • 加载图片
SDL_Texture* IMG_LoadTexture(SDL_Renderer *renderer, const char *file);
  • 加载音频
Mix_Music* Mix_LoadMUS(const char *file);
Mix_Chunk* Mix_LoadWAV(const char *file);
  • 加载字体
TTF_Font* TTF_OpenFont(const char *file, int ptsize);

在 C语言里没有类的概念,如果资源的数量不多,推荐你直接使用全局变量,简单方便。如果资源很多,最好还是专门的定义一个类似 std::map 结构的容器,确保可以随时找到相关的资源数据,当然你也可以使用其它的数据结构,只要你觉得使用简单,读取和保存的效率可以接受即可。

除了上面提到以文件形式进行加载之外,SDL 还支持直接从内存中加载资源。对应的接口如下:

  • 加载图片
SDL_Texture* IMG_LoadTexture_RW(SDL_Renderer *renderer, SDL_RWops *src, int freesrc);
  • 加载音频
Mix_Music* Mix_LoadMUS_RW(SDL_RWops *src, int freesrc);
Mix_Chunk* Mix_LoadWAV_RW(SDL_RWops *src, int freesrc);
  • 加载字体
TTF_Font* TTF_OpenFont_RW(SDL_RWops *src, int freesrc, int ptsize);

可以看到从内存中加载的函数名称后面统一包含 _RW 后缀,函数的返回值和从文件中加载基本一致,和文件中加载有所不同的是每个函数都多了两个参数,分别是一个 SDL_RWops* 对象和一个 int 对象。这两个对象,前者表示一个可以读写的指针,类似 Windows 中的文件句柄,后者是内存中需要加载资源文件的大小。SDL_RWops* 对象可以通过 SDL 提供的内存加载函数获取,函数原型如下:

SDL_RWops* SDL_RWFromConstMem(const void *mem, int size);

这个函数的两个参数分别是数据资源所在内存中的位置和大小,比较容易理解。如果想要加载某些资源,可以直接嵌套这两个函数获取。

2. 如何从资源文件中加载

上面简单的讲解了 SDL 加载资源的方式,接下里讲解本文的重点,在 Windows 操作系统中如何从资源文件中加载素材。

在 Windows 编程中,为了方便项目中的资源统一管理,引入了一个资源文件的概念。你可以用它来管理程序所需要的各种资源,它的名称一般用 .rc 来做后缀,并且在程序编译期间,可以将这个 .rc 的资源描述文件进行编译,最终,将编译后的二进制整合到程序中,这样你可以让程序变成一个简单的单一文件,不依赖任何外部文件。

在大型游戏中,这种资源的管理方式并不是很好用。但是当做小游戏的时候,这种资源管理的方式不失为一种好的做法,因为使用资源加载方式制作的小游戏可以确保拷贝一个 exe 文件,既可以在别的 Windows 上直接运行,无须寻找其它素材资源。

那如何将游戏中的素材打包到程序中呢?

首先,将资源加入到工程中,你可以在工程的右键菜单中找到加载资源的选项:

添加资源

资源包含很多默认类型,例如位图、图标、字体等等。

资源类型

直接点击导入,导入我们自己的资源即可。这里演示导入一个字体文件:

资源导入

导入后会弹出一个自定义资源的对话框,在资源类型中可以定义自己的资源类型名称,这里起名为 FONT。点击确认后,会弹出字体文件二进制展示画面:

资源浏览

直接按 Ctrl+S,保存导入的字体资源。Windows 资源编辑器会自动的添加该资源的 ID,以及路径等相关信息,所有这些信息都默认保存在一个叫做 game.rc 的文件中(game 是当前项目的工程名称,所以会是 game.rc)。

你可以手动编辑这个文件内容,直接在该文件上右键,选择查看代码,默认的文件内容下:

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

......

/////////////////////////////////////////////////////////////////////////////
//
// Font
//

IDR_FONT1               FONT                    "DS-DIGIB.TTF"

#endif    // 中文(简体,中国) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED


你可以在代码中看到你所添加的资源内容,第一列 IDR_FONT1 表示 Windows 自动生成的资源标识,第二列是资源的类型,第三列是资源的路径。

知道了这些之后,我们现在就使用 Windows 提供的 API 接口,将上述的字体加载到内存中,具体的套路如下:

 //获取资源数据
SDL_RWops* getResourceData(HINSTANCE hinst, LPCWSTR lpName, LPCWSTR lpType)
{
	//查找资源
	HRSRC hr = FindResource(hinst, lpName, lpType);
	if (hr == NULL) {
		return NULL;
	}

	//获取资源大小
	DWORD size = SizeofResource(hinst, hr);
	if (size == 0) {
		return NULL;
	}

	//加载资源
	HGLOBAL hResData = LoadResource(hinst, hr);
	if (hResData == NULL) {
		return NULL;
	}

	//获取资源指针
	LPVOID pData = LockResource(hResData);
	if (pData == NULL) {
		return NULL;
	}

	//读写操作指针
	SDL_RWops* rwOps = SDL_RWFromConstMem(pData, size);
	if (rwOps == NULL) {
		return NULL;
	}

	return rwOps;
}

上面的函数,可以将加载指定的资源,并返回 SDL 从内存中加载资源函数的第一个参数指针。这里先介绍一下如何使用这个函数:

getResourceData(hInstance, MAKEINTRESOURCE(IDR_FONT1), RT_FONT);

第一个参数是程序主函数 WinMain 的第一个参数,代表当前程序的实例句柄。第二个参数是资源的标识,这里需要使用 Windows 提供的 MAKEINTRESOURCE 进行包裹。第三个参数是资源的类型,字体其实是 Windows 资源中的一种默认类型,所以这里使用了 RT_FONT,如果你使用其它资源,第三个参数直接传入你前面输入类型名称,例如如果你添加的资源是 PNG 图片,并且你将类型起名为 PNG,则可以使用下面的代码加载:

getResourceData(hInstance, MAKEINTRESOURCE(IDB_PNG1), TEXT("PNG"));

介绍完资源加载的使用方法,我们简单的讲解一下,Windows 资源加载的固定流程:

  1. 查找资源
  2. 返回资源的大小
  3. 加载资源
  4. 锁定资源
  5. 使用资源

步骤都是固定的,对应的 API 名称就是上面函数中使用的名称,大致了解一下就好,如果在使用中函数返回 NULL,可以调用 GetLastError 之类的接口获取错误代码,去 MSDN 查找相关资料即可。