存储子系统(CategoryStorage)
SDL 的存储 API 是一套高层级接口,旨在屏蔽底层文件操作的跨平台兼容性问题(在 SDL 架构中,该模块基于 Filesystem 和 IOStream 子系统实现)。与传统文件系统 API 相比,它的使用限制更多,主要原因如下:
-
访问对象限制:传统文件系统 API 通常假设所有存储是统一的整体,但许多平台(尤其是游戏主机)对存储类型的访问有严格区分。例如,游戏资源文件和用户数据往往存储在两个独立的存储设备中,具备完全不同的特性(甚至底层 API 都不同)。
-
访问方式限制:应用程序常错误地认为所有存储都可写入——但多数平台将游戏资源和用户数据分离,仅用户数据区可写入,游戏资源区为只读。
-
访问时机限制:文件系统访问最常见的兼容性问题是「时机」——不能假定存储设备始终可用,也不能假定对某设备的访问时长无限制。
反面示例(传统文件操作)
传统文件操作代码因上述假设,存在严重的跨平台兼容性问题:
void ReadGameData(void)
{
extern char** fileNames;
extern size_t numFiles;
for (size_t i = 0; i < numFiles; i += 1) {
FILE *data = fopen(fileNames[i], "rwb");
if (data == NULL) { /* 错误处理 */ }
else { /* 读取逻辑 */ fclose(data); }
}
}
void ReadSave(void)
{
FILE *save = fopen("saves/save0.sav", "rb");
if (save == NULL) { /* 错误处理 */ }
else { /* 读取逻辑 */ fclose(save); }
}
void WriteSave(void)
{
FILE *save = fopen("saves/save0.sav", "wb");
if (save == NULL) { /* 错误处理 */ }
else { /* 写入逻辑 */ fclose(save); }
}
该代码的问题:
- 访问对象:假设游戏资源和存档都在当前工作目录(可能并非游戏安装目录);
- 访问方式:假设资源路径可写入,且存档与资源同目录也可写入;
- 访问时机:假设任何时候都能调用文件操作,存储设备始终可用。
这些问题会导致:
- 游戏安装在只读设备时,资源加载和存档操作均会失败/崩溃;
- 部分平台需显式挂载存储设备,未挂载时无法找到任何文件;
- I/O 未刷新/验证,程序异常可能导致存档丢失/损坏。
正面示例(SDL_Storage 实现)
使用 SDL_Storage 可避免上述问题:
void ReadGameData(void)
{
extern char** fileNames;
extern size_t numFiles;
SDL_Storage *title = SDL_OpenTitleStorage(NULL, 0);
if (title == NULL) { /* 错误处理 */ }
while (!SDL_StorageReady(title)) { SDL_Delay(1); }
for (size_t i = 0; i < numFiles; i += 1) {
void* dst;
Uint64 dstLen = 0;
if (SDL_GetStorageFileSize(title, fileNames[i], &dstLen) && dstLen > 0) {
dst = SDL_malloc(dstLen);
if (SDL_ReadStorageFile(title, fileNames[i], dst, dstLen)) {
/* 读取逻辑 */
} else { /* 错误处理 */ }
SDL_free(dst);
} else { /* 错误处理 */ }
}
SDL_CloseStorage(title);
}
// 读取/写入存档的实现类似,使用 SDL_OpenUserStorage 访问用户存储区
SDL_Storage 的改进点:
- 明确访问对象:根据场景显式访问游戏资源存储(TitleStorage)或用户存储(UserStorage);
- 明确访问方式:根据场景使用读/写函数,区分只读/可写存储区;
- 明确访问时机:使用时打开存储设备,用完关闭,且通过
SDL_StorageReady检查设备可用性。
路径规则说明
存储 API 对路径有严格规范,确保跨平台兼容性:
- 所有路径使用 Unix 风格分隔符(
/),不支持其他分隔符(如\); - 禁止使用相对目录(
.和..); - 文件名支持合法 UTF-8 字符串(排除 NULL 终止符和
/),但底层实现可能限制特殊字符。
函数
- SDL_CloseStorage:关闭已打开的存储设备(释放资源,刷新未完成的 I/O 操作)
- SDL_CopyStorageFile:在存储设备内复制文件(支持跨目录复制,保留文件属性)
- SDL_CreateStorageDirectory:在存储设备中创建目录(仅创建单层,失败返回具体错误)
- SDL_EnumerateStorageDirectory:枚举存储设备中指定目录的所有项(文件/子目录),通过回调处理
- SDL_GetStorageFileSize:获取存储设备中指定文件的大小(字节),目录返回 0
- SDL_GetStoragePathInfo:获取存储设备中指定路径的详细信息(类型、大小、修改时间等)
- SDL_GetStorageSpaceRemaining:获取存储设备的剩余可用空间(字节),用于检查存档写入空间
- SDL_GlobStorageDirectory:按通配符(*、? 等)枚举存储设备中指定目录的文件
- SDL_OpenFileStorage:打开基于本地文件系统的存储设备(适配传统文件路径)
- SDL_OpenStorage:通用存储设备打开函数(自定义存储接口时使用)
- SDL_OpenTitleStorage:打开游戏资源存储设备(只读,用于加载游戏资源)
- SDL_OpenUserStorage:打开用户数据存储设备(可写,用于保存存档/配置,需传入厂商/应用名)
- SDL_ReadStorageFile:从存储设备读取文件内容到内存(原子操作,确保数据完整性)
- SDL_RemoveStoragePath:删除存储设备中的文件/空目录(非空目录删除失败)
- SDL_RenameStoragePath:重命名/移动存储设备中的文件/目录(目标路径已存在则失败)
- SDL_StorageReady:检查存储设备是否就绪(返回非0表示可用,需循环等待直到就绪)
- SDL_WriteStorageFile:将内存数据写入存储设备的文件(原子操作,自动刷新确保数据持久化)
数据类型
- SDL_Storage:存储设备句柄类型(标识已打开的存储设备,所有操作均基于此句柄)
结构体
- SDL_StorageInterface:存储接口结构体(自定义存储实现时使用,包含各类操作的函数指针)
枚举
- (无)
宏
- (无)
FreeBASIC 示例代码
' 引入 SDL 相关声明(需确保 FreeBASIC 已链接 SDL3 库)
#Include "SDL.bi"
' 补充类型/函数声明(FreeBASIC 绑定可能缺失)
Type SDL_Storage As Any Ptr ' 存储设备句柄
Declare Function SDL_OpenTitleStorage CDecl (ByVal path As ZString Ptr, ByVal flags As Integer) As SDL_Storage
Declare Function SDL_OpenUserStorage CDecl (ByVal org As ZString Ptr, ByVal app As ZString Ptr, ByVal flags As Integer) As SDL_Storage
Declare Function SDL_StorageReady CDecl (ByVal storage As SDL_Storage) As Integer
Declare Function SDL_GetStorageFileSize CDecl (ByVal storage As SDL_Storage, ByVal path As ZString Ptr, ByVal size As ULLong Ptr) As Integer
Declare Function SDL_ReadStorageFile CDecl (ByVal storage As SDL_Storage, ByVal path As ZString Ptr, ByVal buffer As Any Ptr, ByVal size As ULLong) As Integer
Declare Function SDL_WriteStorageFile CDecl (ByVal storage As SDL_Storage, ByVal path As ZString Ptr, ByVal buffer As Any Ptr, ByVal size As ULLong) As Integer
Declare Function SDL_CreateStorageDirectory CDecl (ByVal storage As SDL_Storage, ByVal path As ZString Ptr) As Integer
Declare Function SDL_GetStorageSpaceRemaining CDecl (ByVal storage As SDL_Storage, ByVal remaining As ULLong Ptr) As Integer
Declare Sub SDL_CloseStorage CDecl (ByVal storage As SDL_Storage)
Declare Function SDL_RemoveStoragePath CDecl (ByVal storage As SDL_Storage, ByVal path As ZString Ptr) As Integer
' 读取游戏资源文件(只读,TitleStorage)
Sub ReadGameResource(ByVal resourceName As ZString Ptr)
' 打开游戏资源存储设备
Dim As SDL_Storage titleStorage = SDL_OpenTitleStorage(NULL, 0)
If (titleStorage = NULL) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "打开游戏资源存储失败:%s", SDL_GetError())
Exit Sub
End If
' 等待存储设备就绪
While (SDL_StorageReady(titleStorage) = 0)
SDL_Delay(1)
Wend
' 获取文件大小
Dim As ULLong fileSize = 0
If (SDL_GetStorageFileSize(titleStorage, resourceName, @fileSize) = 0) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "获取资源文件大小失败:%s", SDL_GetError())
SDL_CloseStorage(titleStorage)
Exit Sub
End If
If (fileSize = 0) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "资源文件为空:%s", *resourceName)
SDL_CloseStorage(titleStorage)
Exit Sub
End If
' 分配内存并读取文件
Dim As Any Ptr buffer = SDL_malloc(fileSize)
If (buffer = NULL) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "内存分配失败")
SDL_CloseStorage(titleStorage)
Exit Sub
End If
If (SDL_ReadStorageFile(titleStorage, resourceName, buffer, fileSize) = 0) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "读取资源文件失败:%s", SDL_GetError())
Else
SDL_LogInfo(SDL_LOG_CATEGORY_STORAGE, "成功读取资源文件:%s(大小:%llu 字节)", *resourceName, fileSize)
' 此处可处理读取到的资源数据
End If
' 释放资源
SDL_free(buffer)
SDL_CloseStorage(titleStorage)
End Sub
' 保存用户存档(可写,UserStorage)
Sub SaveUserData(ByVal saveName As ZString Ptr, ByVal data As ZString Ptr)
' 打开用户数据存储设备(厂商名/应用名)
Dim As SDL_Storage userStorage = SDL_OpenUserStorage("SDL_Example", "StorageDemo", 0)
If (userStorage = NULL) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "打开用户存储失败:%s", SDL_GetError())
Exit Sub
End If
' 等待存储设备就绪
While (SDL_StorageReady(userStorage) = 0)
SDL_Delay(1)
Wend
' 检查剩余空间
Dim As ULLong remainingSpace = 0
If (SDL_GetStorageSpaceRemaining(userStorage, @remainingSpace) = 0) Then
SDL_LogWarn(SDL_LOG_CATEGORY_STORAGE, "无法获取剩余空间,继续操作...")
Else
Dim As ULLong dataSize = Len(*data)
If (remainingSpace < dataSize) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "存储空间不足(需要:%llu 字节,剩余:%llu 字节)", dataSize, remainingSpace)
SDL_CloseStorage(userStorage)
Exit Sub
End If
End If
' 创建存档目录(如需)
SDL_CreateStorageDirectory(userStorage, "saves")
' 拼接存档路径(必须使用 / 分隔符)
Dim As String savePath = "saves/" & *saveName
Dim As ULLong dataSize = Len(*data)
' 写入存档数据
If (SDL_WriteStorageFile(userStorage, StrPtr(savePath), data, dataSize) = 0) Then
SDL_LogError(SDL_LOG_CATEGORY_STORAGE, "写入存档失败:%s", SDL_GetError())
Else
SDL_LogInfo(SDL_LOG_CATEGORY_STORAGE, "成功写入存档:%s(大小:%llu 字节)", savePath, dataSize)
End If
' 关闭存储设备
SDL_CloseStorage(userStorage)
End Sub
' 主程序
Sub Main()
' 初始化 SDL
If (SDL_Init(0) < 0) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL 初始化失败:%s", SDL_GetError())
Exit Sub
End If
' ========== 1. 读取游戏资源 ==========
SDL_LogInfo(SDL_LOG_CATEGORY_STORAGE, "【1. 读取游戏资源】")
ReadGameResource(StrPtr("assets/config.txt")) ' 假设存在该资源文件
' ========== 2. 保存用户存档 ==========
SDL_LogInfo(SDL_LOG_CATEGORY_STORAGE, vbCrLf & "【2. 保存用户存档】")
Dim As ZString * 100 saveData = "玩家存档数据:等级=10,金币=9999,进度=50%"
SaveUserData(StrPtr("save01.sav"), @saveData)
' 清理 SDL
SDL_Quit()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, vbCrLf & "程序正常退出")
End Sub
' 运行主程序
Main()
核心知识点补充
-
存储设备类型区分:
- TitleStorage:游戏资源存储区,只读,用于加载游戏内置资源(如贴图、音效、配置);
- UserStorage:用户数据存储区,可写,用于保存存档、用户配置等,需传入厂商名/应用名以区分不同应用;
- 两者完全隔离,避免误写游戏资源导致的兼容性问题。
-
设备就绪检查:
- 必须通过
SDL_StorageReady循环等待存储设备就绪,尤其是游戏主机/移动平台,存储设备可能需要挂载时间; - 就绪检查通过后,才能执行文件操作,避免「设备未就绪」导致的操作失败。
- 必须通过
-
路径规范强制要求:
- 仅支持
/分隔符,不支持\(即使 Windows 平台); - 禁止
./..相对路径,所有路径必须是存储设备内的绝对路径; - 示例:
saves/save01.sav合法,./saves/save01.sav非法。
- 仅支持
-
原子操作保障:
SDL_ReadStorageFile/SDL_WriteStorageFile是原子操作,确保数据读写的完整性;- 写入操作会自动刷新缓存,避免程序异常导致的数据丢失(传统 fopen/fwrite 需手动 fflush)。
总结
-
核心优势:
- 严格区分只读/可写存储区,避免跨平台写入权限问题;
- 强制检查存储设备就绪状态,适配需挂载的平台(如游戏主机);
- 原子化读写操作,保障数据完整性,避免存档损坏;
- 统一路径规范,彻底解决跨平台路径分隔符/相对路径问题。
-
使用建议:
- 游戏资源加载使用
SDL_OpenTitleStorage,存档/配置保存使用SDL_OpenUserStorage; - 所有存储操作前必须调用
SDL_StorageReady等待设备就绪; - 写入文件前检查剩余空间(
SDL_GetStorageSpaceRemaining),避免空间不足导致失败; - 存储设备使用完毕后必须调用
SDL_CloseStorage,释放资源并刷新缓存。
- 游戏资源加载使用
-
关键接口:
- 存储设备管理:
SDL_OpenTitleStorage()/SDL_OpenUserStorage()/SDL_StorageReady()/SDL_CloseStorage(); - 文件操作:
SDL_GetStorageFileSize()/SDL_ReadStorageFile()/SDL_WriteStorageFile(); - 辅助操作:
SDL_CreateStorageDirectory()/SDL_GetStorageSpaceRemaining()/SDL_RemoveStoragePath()。
- 存储设备管理:
评论一下?