[转]从头开始思考游戏的数据管理系统(二)

上回谈及一些游戏数据管理的初部分析,但有些思绪还没有整理好,写得颇乱。今次就直接谈笔者在几个月前设计的Mil Universe Database(MUD),从中再阐述当中的一些想法。

基本的数据流程

首先,一个项目中,引擎会使用的所有数据,都放进一个完整的数据库里。现时笔者的设计一个数据库会储存成一个.mud文件。

外部文件可以汇入数据库,也可以汇出。但汇出的数据会失去一些资讯(如稍后说的依存关系)。

数据库最后会转换为平台特定的格式,如转换Endianness、转换模型和纹理格式、转换XML到二进制格式、编译脚本等等。这步骤的主要目的是加快游戏读数据的速度。此外,也可以把关卡或人物的数据(如三维模型、纹理、动画等)包装成一个数据块(data chunk),优点是进一步加快了读数据的速度(尤其是光碟,因为寻轨慢),缺点是一笔数据可能出现在多个数据块中,增加了游戏的容量,而且可能会读到已载入的数据。

如果多人参与同一项目,该数据库会分为中央和本地的数据库,透过版本控制同步及提交更新。这里描述的是一个可离线编辑的版本控制系统,笔者也和伙伴讨论过其他方式,可见《使用Database Server Source Control Server构建游戏开发用数据文件的可行性分析》

功能需求

构想MUD时考虑了以下的需求。

完整单一数据库

上回说到一个游戏项目会有数万以上笔数据。如果每笔数据以独立文件储存,会产生很多问题。笔者觉得缺乏完整性(integrity)是最大的问题。所以MUD是以单一的数据库储(.mud文件)存整个项目的数据。

Key

使用整数作为每笔数据在永续时的标识符(identifier),称为Key。 Key只是系统才知道的,制作人员应该从来不需要知道这概念。

层阶式文件系统

每笔数据会有路径和文件名称,但这只是给使用者「看」而已,游戏引擎始终是使用Key作为唯一标识符。数据库转换成平台格式时,这些路径文件名会被忽略,不存在于游戏的执行版本。

依存关系

数据和数据间的依存关系会记绿下来。如一个三维模使用数个材质;每个材质又使用数个纹理。当一笔数据被删掉时,可提醒使用者该数据被那些数据使用,如果确定删去,也会通知其他数据删去那些使用参考。

脚本也是用Key去挷定数据。例如一个脚本会在执行期,在某些情况下改变它的游戏物件的三维模型,那么该脚本会定义它需要一个三维模型的(成员)变数。关卡设计师会把脚本挷定在一个游戏物件上,并设定它需要的三维模型。这游戏物件永续时储存了三维模型的Key,而在执行期脚本则拿到了三维模型的物件(指针)。这可以解决把数据(如路径)直接写在脚本所产生的问题。

此外,依存关系也可以找出及清除一些孤立的数据(没有其他数据会依存的数据)。当然,要做到这一点,还要定义一个(或一些)根节点。

版本控制

游戏的数据通常由很多人参与制作,版本的控制是十分重要的。笔者未详细考虑怎样实现MUD的版本控制。简单的方案是使用SVN之类的文件版本控制软件的API,把Key转为文件去存取。但仍要考虑数据以外的东西怎么作版本处理(文件名、依存关系等等)。

其他扩展功能
  • 资产管理(Asset Management): 除了储存游戏编辑工具和游戏引擎会直接使用的文件,亦储存来源文件(.max、.psd),并有workflow自动转换这些文件,例如纹理的大小和mip -map设定。
  • 压缩: 加快文件的读写、减少磁碟使用空间。
  • 加密: 加强安全性(开发时期的泄漏、出版后被反组译等等)
Schema设计

笔者之前设计的第一个Mud版本应该是以C++直接实作,但schema用一般数据库形式画出来可能比较容易明白:

最重要的是Key table,它记录每个key所指向的数据的大小、类型、和储存在数据库中的文件偏移(offset)。

File、Folder和Dependency都是制作期的数据库才有的,这些tables在最终运行版的数据库会被删去。

API 设计

以下是Mud API的最近版本,当中示有Retail的函数是游戏正式版需要的函数:

01
class MudFile

02
{

03
public:

04
MudFile(const char* path);

05
~MudFile();

06

07
// Mud file manipulation

08
bool Create(uint32_t flags);

09
bool Open();    // Retail

10
bool Close();

11
bool IsOpened() const;

12
bool Check() const;

13
bool Compact(uint32_t flags);

14

15
// Mud file information

16
const char* GetPath() const;

17
uint32_t GetVersion() const;

18
uint32_t GetFlags() const;

19
uint32_t GetKeyCount() const;

20
uint32_t GetFileCount() const;

21
uint32_t GetFolderCount() const;

22
uint64_t GetSize() const;

23
Key GetKey(uint32_t index) const;

24

25
// Transaction

26
void BeginTransaction();

27
void Commit();

28
void Rollback();

29
uint32_t GetTransactionLevel() const;

30

31
// File manipulations

32
Key CreateFile(FileType type);

33
Key CreateFile(FolderID folderID, const char* filename, FileType type);

34
bool DeleteFile(Key key);

35
bool RenameFile(Key key, const char* filename);

36
bool MoveFile(Key key, FolderID folderID);

37

38
// Read/Write

39
bool OpenReadFile(Key key);    // Retail

40
bool OpenWriteFile(Key key);

41
bool CloseFile();   // Retail

42
bool IsFileOpened() const;

43

44
bool ReadBuffer(void* buffer, uint32_t size);    // Retail

45
bool WriteBuffer(const void* buffer, uint32_t size);

46
bool ReadString(std::string& s);    // Retail

47
bool WriteString(const std::string& s);

48

49
bool ReadTextFile(Key key, std::string& s);     // Retail

50
bool WriteTextFile(Key key, const std::string& s);

51

52
// File information

53
bool IsFileExist(Key key);

54
uint32_t GetFileSize(Key key);    // Retail

55
FileType GetFileType(Key key);    // Retail

56
const char* GetFilename(Key key);

57
FolderID GetFileFolder(Key key);

58

59
// Folder manipulations

60
FolderID CreateFolder(FolderID parentFolderID, const char* folderName);

61
bool DeleteFolder(FolderID folderID);

62
bool RenameFolder(FolderID folderID, const char* folderName);

63
bool MoveFolder(FolderID folderID, FolderID parentFolderID);

64

65
// Folder information

66
bool IsFolderExist(FolderID folderID);

67
const char* GetFolderName(FolderID folderID);

68
FolderID GetFolderParent(FolderID folderID);

69

70
// Folder/File Traversal

71
FolderID GetFirstChildFolder(FolderID folderID);

72
FolderID GetNextSiblingFolder(FolderID folderID);

73
Key GetFirstFileInFolder(FolderID folderID);

74
Key GetNextFileInFolder(Key key);

75

76
// Import/Export utility

77
Key ImportFile(const char* path, FolderID folderID, const char* filename, FileType type);

78
bool ExportFile(Key key, const char* path);

79

80
// Dependency

81
bool AddDependency(Key source, Key target);

82
bool RemoveDependency(Key source, Key target);

83
bool GetSources(Key target, KeyList& sources);

84
bool GetTargets(Key source, KeyList& targets);

85

86
static bool IsNameValid(const char* name);

87
};

笔者把这组API尽量设计得简单,减少所需要的类别(如File、Folder类别),使C# (或其他语言)的挷定(bindings)容易实作。在Retail版中,只需要少量函数,实作多个平台的版本也变得容易。

这设计里还有事务(transaction)的考虑,希望可以加强数据库的完整性。例如程式在运作中途异常结束,数据库的内容可以维持一致性(consistency)(不会有只写了一半的数据或数据组)。

后记

这一篇文章其实只谈及一个解决方案,并没有很仔细的分析每个决定,及讨论其他方案。如果有写得不太清楚,或有任何意见,欢迎留言讨论。


本文原来是繁体中文,在2009-03-19发表于http://miloyip.seezone.net/?p=109,本文經過修正。

笔者已实现这个系统的部份功能(约1500行代码),并使用在Mil引擎里。后来想到,另一个可行方案是利用現成的软件如SQLite,实现本地数据库,在最终的数据库才使用这种实现;这么做,修改schema比较容易且有弹性,扩充功能更容易。

当进行做内存管理的refactoring时,std::string会被取代。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值