HGE系列之六 管中窥豹(资源管理)

HGE系列之六 管中窥豹(资源管理)

记的上次浮光掠影的讲了一些HGE中的基础类别,不知大家了解了多少,仔细看过的朋友肯定知道当时在讲述一个类别的构造函数时我打了个马虎,直接略过了,原因说的好像是比较繁琐,想起来了吗?对,就是 hgeStringTable ,为了免去“避重就轻”的嫌疑,我想这次也该好好的将他说一说了(但说实话,讲解这个函数确实比较繁琐……),而 hgeStringTable 是HGE资源管理中的一个基础(Base)类别,所以我想顺着讲述一下HGE的资源管理实现,相信这种叙述顺序还是比较符合逻辑的 :)不过在开始我们又一次HGE源码之旅时,我必须提醒一句:这次旅行可没上次那般轻松惬意了……什么,你不相信?那么不妨来看看我们旅程的“第一站”,也就是那个 hgeStringTable “神秘”的构造函数吧:

hgeStringTable::hgeStringTable(const char *filename)

{

// 一些变量声明

int i;

void *data;

DWORD size;

char *desc, *pdesc;

// 上次分析过的 NamedString 结构

NamedString *str;

char str_name[MAXSTRNAMELENGTH];

char *str_value, *pvalue;

// 创建HGE

hge=hgeCreate(HGE_VERSION);

// 设置链表头结点为空

strings=0;

// 使用HGE提供的Resource_Load接口加载文件

// 注意返回的 void* 指针所指向的便是文件中的内容

// 相关文件大小则存放在size中

data=hge->Resource_Load(filename, &size);

// 如果data为空,则加载失败,直接返回

if(!data) return;

// 将文件内容全部拷贝至动态申请的desc数组中,以进行之后的解析

desc = new char[size+1];

memcpy(desc,data,size);

desc[size]=0;

// 释放由之前Resource_Load函数分配的内存(即data所指向的区域)

hge->Resource_Free(data);

// 查看文件头(是否是“[HGESTRINGTABLE]”)

if(memcmp(desc, STRHEADERTAG, sizeof(STRHEADERTAG)-1))

{

// 如果不是则使用System_Log接口将错误记录至日志文件,

// 并释放之前分配的desc资源

hge->System_Log(STRFORMATERROR, filename);

delete[] desc;

return;

}

// 将pdesc设置为desc偏移sizeof(STRHEADERTAG)位置,即实际的数据位置

pdesc=desc+sizeof(STRHEADERTAG);

// 用于存储字符串值的动态数组

str_value=new char[8192];

for(;;)

{

// 跳过空白

while(isspace(*pdesc)) pdesc++;

// 如果pdesc指向元素为空('/0'),则跳出循环

if(!*pdesc) break;

// 跳过注释,即以 ';' 开头的行

if(*pdesc==';')

{

while(*pdesc && *pdesc != '/n') pdesc++;

pdesc++;

continue;

}

// 获取字符串的名字

i=0;

// 注意循环条件

while(pdesc[i] && pdesc[i]!='=' && !isspace(pdesc[i]) && i<MAXSTRNAMELENGTH)

{

str_name[i]=pdesc[i];

i++;

}

// 设置字符串结束符

str_name[i]=0;

// 当然,不能忘了设置pdesc的偏移

pdesc+=i;

// 由于有 MAXSTRNAMELENGTH 的字符串名称长度限制,

// 所以需要处理超出的名称字符

while(*pdesc && *pdesc!='=' && !isspace(*pdesc)) pdesc++;

if(!*pdesc) break;

// 跳过 '=' 之前的空白符号

while(isspace(*pdesc)) pdesc++;

// 如果所指不是 '=' ,那么调用Sysytem_Log记录至日志文件,并跳出循环

if(*pdesc!='=') { hge->System_Log(STRFORMATERROR, filename); break; }

pdesc++;

// 跳过 ' " ' 符号之前的空格

while(isspace(*pdesc)) pdesc++;

// 如果所指不是 ' " ', 那么调用Sysytem_Log记录至日志文件,并跳出循环

if(*pdesc!='"') { hge->System_Log(STRFORMATERROR, filename); break; }

pdesc++;

// 解析字符串的实际内容(至最后的 ' " ' 符号)

// 注意一下特殊符号的处理

pvalue=str_value;

// 注意一下循环条件

while(*pdesc && *pdesc!='"')

{

// 处理 '/n' 以及 '/r'

if(*pdesc=='/n' || *pdesc=='/r')

{

while(isspace(*pdesc)) pdesc++;

pvalue--;

while(pvalue>=str_value && isspace(*pvalue)) pvalue--;

pvalue++; *pvalue=' '; pvalue++;

continue;

}

// 处理 '/' 转义字符

if(*pdesc=='//')

{

pdesc++;

if(!*pdesc) continue;

if(*pdesc=='n') *pvalue='/n';

else *pvalue=*pdesc;

pvalue++;

pdesc++;

continue;

}

// 将pdesc的内容复制入 pvalue 中

*pvalue=*pdesc; pvalue++;

pdesc++;

}

// 设置结束标志

*pvalue=0;

// 至此我们已经成功解析了一个字符串“定义”,将其添加入链表中(链表头部)

str=new NamedString;

strcpy(str->name, str_name);

str->string=new char[strlen(str_value)+1];

strcpy(str->string, str_value);

str->next=strings;

strings=str;

if(!*pdesc) break;

pdesc++;

}// end of for(;;)

// 不要忘了释放缓存资源

delete[] str_value;

delete[] desc;

}

呼,终于将这个“庞大”的构造函数注释完了,相信大家看的也是够呛,如果实在看的不是太清晰,那也没有多大关系,我再来提纲挈领的讲一讲:

一开始,构造函数调用HGE提供的资源加载接口加载文件资源,然后程序开始解析,首先他会检查该文件是否拥有名为HGESTRINGTABLE 的字段(section),接着他开始解析实际的字符串定义,相应格式为:

字符串名 = "字符串值"

然后么自然将解析的结果构造成一个NamedString,然后添加入链表。接着便是重复执行,知道内容结束。

大体流程就是这样,如果你还不满意,那就请自行研习源码吧,我的讲解便到此为止了,因为我讲述这个函数的目的并不是想细究其每个细节,然后学以致用,恰恰相反,我的目的是“学以不用”,是的,你没有看错,的确是“学以不用”:

首先,这个函数过于“庞大”,这便造成了可读性上的大打折扣(相信你已经有所体会……)以及非常差的可维护性(你可以试试走一走程序的控制流……)

再者,该函数的逻辑相对复杂,本应该将个中功能分解至单一函数来进行一一处理,然而目前的情况却是一股脑儿的凑成了一大碗意大利面条……

最后,程序的目的是解析一种相关的配置文件格式,然而这种格式是完全可以与 hgeStringTable 类相分离的,也就是说,处理这类配置文件的功能完全可以抽象出来形成另外一个辅助类(helper),大可不必写成现在的这幅模样……

所以总的说来,这个函数是一个非常鲜明生动的“反面教材” :)

至此,虽然有些不敢苟同,但hgeStringTable 终算是讲解完毕了,那么稍稍缓口气,让我们继续向 HGE 资源管理模块迈进 :

类名:ResDesc

功能:资源描述类

头文件:hge/hge181/include/hgeresource.h

实现文件:无

老方法,让我们首先来看一看他的头文件:

struct ResDesc

{

// 资源名称

char name[MAXRESCHARS];

// 所属资源的组号

int resgroup;

// 资源“句柄”

DWORD handle;

// 自向指针(看来大抵上又是用来构成单链表的)

ResDesc* next;

// 相对简易的构造函数和析构函数

ResDesc() { hge=hgeCreate(HGE_VERSION); }

~ResDesc() { hge->Release(); }

// 获取资源“句柄”,注意参数

virtual DWORD Get(hgeResourceManager *rm) = 0;

// 释放资源

virtual void Free() = 0;

protected:

// 静态的HGE指针

static HGE *hge;

};

很明显,ResDesc是一个纯虚类,程序中是不能创建该类的对象的,一般而言这种类别也仅是作为基类,然后进行派生,考虑到该类别是一个资源描述类,那么自然想到是不是HGE中的各类资源都继承于他呢?如果你也是这么考虑的,那么恭喜你,回答正确,加十分!

但就目前而言,我们对于这个类别中的一些细节还是有些模糊,譬如Get函数(或者说接口)中的 hgeResourceManager ,好吧,为了解决这些疑问,让我们继续看下去:

类名:hgeResourceManager

功能:资源管理类

头文件:hge/hge181/include/hgeresource.h

实现文件:hge/hge181/src/helpers/hgeresource.cpp

首先来看看他的头文件:

class hgeResourceManager

{

public:

hgeResourceManager(const char *scriptname=0);

~hgeResourceManager();

// 变换资源脚本

void ChangeScript(const char *scriptname=0);

// 查看给定组别资源是否已经缓存

bool Precache(int groupid=0);

// 清除指定资源号的资源

void Purge(int groupid=0);

// 通用的获取资源函数,注意返回类型为 void*

void* GetResource(const char *name, int resgroup=0);

// 获取资源句柄的函数“群”

HTEXTURE GetTexture(const char *name, int resgroup=0);

HEFFECT GetEffect(const char *name, int resgroup=0);

HMUSIC GetMusic(const char *name, int resgroup=0);

HSTREAM GetStream(const char *name, int resgroup=0);

HTARGET GetTarget(const char *name);

// 获取相关资源的函数“群”

hgeSprite* GetSprite(const char *name);

hgeAnimation* GetAnimation(const char *name);

hgeFont* GetFont(const char *name);

hgeParticleSystem* GetParticleSystem(const char *name);

hgeDistortionMesh* GetDistortionMesh(const char *name);

hgeStringTable* GetStringTable(const char *name, int resgroup=0);

// 资源描述的指针数组

ResDesc* res[RESTYPES];

private:

// 将拷贝构造函数设置为私有,以起到禁用拷贝构造的作用

hgeResourceManager(const hgeResourceManager &);

// 将 赋值 操作设置为私有,以起到“屏蔽”作用

hgeResourceManager& operator= (const hgeResourceManager&);

// 私有的辅助函数

// 移除所有资源

void _remove_all();

// 解析资源脚本

void _parse_script(const char *scriptname=0);

static HGE *hge;

};

看来hgeResourceManager的定义还是比较清晰的,让我们一次来看一看个中函数的实现方法:

首先自然是其的构造函数与析构函数:

hgeResourceManager::hgeResourceManager(const char *scriptname)

{

// 创建HGE

hge=hgeCreate(HGE_VERSION);

// 初始化res数组

for(int i=0;i<RESTYPES;i++) res[i]=0;

// 解析给定的资源脚本

_parse_script(scriptname);

}

hgeResourceManager::~hgeResourceManager()

{

// 释放所有的资源

_remove_all();

// 释放HGE

hge->Release();

}

看来这两个函数还是比较简易的,一个加载,一个释放,比较“中规中矩” :)

那么至此,对于hgeResourceManager的数据结构模型,大家应该能够想像了:

大体上便是这副样子:

接着,然我们顺着来看一看构造函数和析构函数中涉及的_parse_script和_remove_all

void hgeResourceManager::_parse_script(const char *scriptname)

{

ResDesc *rc, *rcnext;

// 如果资源脚本名不为空

if(scriptname)

{

// 使用了RScript函数的Parse函数进行解析

RScript::Parse(this, NULL, scriptname, NULL);

// 释放res[RES_SCRIPT]

rc=res[RES_SCRIPT];

while(rc)

{

rc->Free();

rcnext=rc->next;

delete rc;

rc=rcnext;

}

res[RES_SCRIPT]=0;

}

}

其中RES_SCRIPT是一个宏定义,位于hge/hge181/src/helpers/resources.h

#define RES_SCRIPT 0

另外值得一提的是,该文件中另外还定义了其他的资源类别,有兴趣的朋友可以看一看。

_parse_script函数中除了一个目前对我们来说还是“黑盒”的RScript::Parse外,对于res[RES_SCRIPT](即res[0])的处理也是有些令人费解,不过我们大可不必在此过分纠结,与其停步苦思冥想,不如继续前行,说不定马上会豁然开朗 :)

void hgeResourceManager::_remove_all()

{

int i;

ResDesc *rc, *rcnext;

// 释放从res[0] 至res[RESTYPES] 链表资源

for(i=0;i<RESTYPES;i++)

{

rc=res[i];

while(rc)

{

rc->Free();

rcnext=rc->next;

delete rc;

rc=rcnext;

}

res[i]=0;

}

}

看来 _remove_all 也是不出所料啊,依次遍历并释放资源 :)

接着让我们看看析构函数之后的三个公有函数:

// 变换资源脚本

void hgeResourceManager::ChangeScript(const char *scriptname)

{

_remove_all();

_parse_script(scriptname);

}

不出所料,首先释放,然后便是重新加载

// 查看给定的组别资源是否已经缓存(有效),如果groupid为0则检查所有组别

bool hgeResourceManager::Precache(int groupid)

{

int i;

ResDesc *rc;

bool bResult=true;

// 遍历res[0] 到 res[RESTYPES-1]

for(i=0;i<RESTYPES;i++)

{

rc=res[i];

while(rc)

{

// 注意条件的判断

if(!groupid || groupid==rc->resgroup) bResult=bResult && (rc->Get(this)!=0);

rc=rc->next;

}

}

return bResult;

}

// 清除给定组别的资源,如果groupid为0则清除所有组别

void hgeResourceManager::Purge(int groupid)

{

int i;

ResDesc *rc;

// 遍历res[0] 到 res[RESTYPES-1]

for(i=0;i<RESTYPES;i++)

{

rc=res[i];

while(rc)

{

// 注意条件判断

if(!groupid || groupid==rc->resgroup) rc->Free();

rc=rc->next;

}

}

}

好了,看来这三个函数也并不复杂,接下来剩下的便是一堆获取资源函数,让我们首先来看看其中的GetStringTable,没有错,就是获取字符串表的函数,由于目前来讲我们对于hgeStringTable这个类别比较熟悉,以此函数开始阅览,相信能让我们更快的找到关键点(key point):

hgeStringTable* hgeResourceManager::GetStringTable(const char *name, int resgroup)

{

hgeStringTable *strtable;

RStringTable *resource;

// 使用FindRes函数来查找给定的资源名称

ResDesc *Res=FindRes(this, RES_STRTABLE, name);

// 如果找到,则返回

if(Res) return (hgeStringTable*)Res->Get(this);

// 没有找到

else

{

// 以name新建一个hgeStringTable

strtable=new hgeStringTable(name);

// 如果(加载)成功

if(strtable)

{

// 新建一个RStringTable类型

resource=new RStringTable();

// 设置资源句柄

resource->handle=(DWORD)strtable;

// 设置资源组别

resource->resgroup=resgroup;

// 设置资源名称

strcpy(resource->name, name);

// 设置RStringTable的资源脚本名称,与其资源名称相同

strcpy(resource->filename, name);

// 通过AddRes函数增加资源

AddRes(this, RES_STRTABLE, resource);

return strtable;

}

}

return 0;

}

这个函数看起来有点磕碰,首先我们便遇到了一个没见过的类型:RStringTable,查看代码,其实他就是一个之前ResDesc类别的派生类:

类名:RStringTable

功能:字符串表资源类

头文件:hge/hge181/ src/helpers/resources.h

实现文件:hge/hge181/src/helpers/resources.cpp

struct RStringTable : public ResDesc

{

char filename[MAXRESCHARS];

static void Parse(hgeResourceManager *rm, RScriptParser *sp, const char *name, const char *basename);

virtual DWORD Get(hgeResourceManager *rm);

virtual void Free();

};

相当简单,实现了Get和Free两个自ResDesc继承而来的纯虚函数,并且添加了名为filename的数据成员以及一个静态的Parse函数,然我们就势来看看他们的实现:

DWORD RStringTable::Get(hgeResourceManager *rm)

{

// 如果handle为空,则直接新建返回hgeStringTable(filename)

if(!handle) handle = (DWORD)new hgeStringTable(filename);

// 否则直接返回

return handle;

}

void RStringTable::Free()

{

// 如果handle不为空,则使用delete删除

if(handle) delete (hgeStringTable *)handle;

handle=0;

}

Get和Free看来还是相当简易的,那么那个静态的Parse呢?

void RStringTable::Parse(hgeResourceManager *rm, RScriptParser *sp, const char *name, const char *basename)

{

ScriptParseFileResource(rm, sp, name, basename, new RStringTable(), RES_STRTABLE);

}

可以看到,Parse函数只是一个对ScriptParseFileResource 的简单调用,那么ScriptParseFileResource有时那个函数呢?不用急,其实他也位于hge/hge181/src/helpers/resources.cpp 中,但是对他讲解我们会一下子牵扯到其他许多我们未有接触的内容,所以我在此先暂时放一放,留作下次讲解(当然,如果你实在等不及,那还是那句老话,请打开文件自己观赏……)

好了,然我们回到GetStringTable的讲解( 是不是有点像堆栈调用:) ),现在我们已经大体上了解了一下RStringTable,但接着我们又遇到了一个没见过的函数:FindRes,他又是怎么一回事呢?其实顾名思义,我们也基本能够猜出这个函数的作用:定位资源,但是他的定义又在哪里呢?其实他也在hge/hge181/src/helpers/resources.cpp中,让我们来看一下:

ResDesc *FindRes(hgeResourceManager *rm, int type, const char *name)

{

ResDesc *rc;

// 首先定位组别资源

rc=rm->res[type];

while(rc)

{

// 比较资源名称,如果相同则返回

if(!strcmp(name, rc->name)) return rc;

rc=rc->next;

}

// 没有找到,怎返回0

return 0;

}

代码还是相当简易的,但是对于不将这个函数封装成hgeResourceManager的成员函数,我个人是不敢苟同的,相应的,虽然我们还没有遇到AddRes函数,但是也不妨碍我们来也先瞧上一瞧:

void AddRes(hgeResourceManager *rm, int type, ResDesc *resource)

{

// 将resource置于相应资源组别的链表头部

resource->next=rm->res[type];

rm->res[type]=resource;

}

同样道理,这个函数我认为也应该封装成hgeResourceManager的成员函数。

好了,至此, GetStringTable这个函数带给我们的阅读障碍应该已经基本消除,大体的流程便是:首先查找是否存在给定的资源,如果有则直接返回,否则新建相应的hgeStringTable,并根据其构造出一个RStringTable,然后加入hgeResourceManager的资源管理链表中。

不过对于GetStringTable在未有找到资源时采取的添加策略,我个人还是有些微词的,毕竟其显然违反了单一职责原则,而且也不足够灵活,试想,如果我想查找一些暂时没有加入hgeResourceManager的资源时,GetStringTable就不是那么方便了 :)最后一点就是,改函数的名字和干的事情并不匹配,GetStringTable如果可以添加资源的话,那SetStringTable又应该干什么呢……?

呼呼,至此我们终于讲解完了GetStringTable,而剩下的Get* 函数还有很多很多,但是可幸的是,其间的原理并没有多少差异,毕竟我们阅读源码的首要目的不是了解细节,而是解其原理,所以我想说的是,既然我们现在对于Get* 函数的原理有了不错的了解,那么就暂可不必急着查看其他的Get* 函数,慢慢来,相信终归有一天能够了然于胸 :)

好了,这次源码之旅我想是时候停一停了(说实话,我觉得早该停了……),大家也该好好休息整理一下了,当然,HGE的资源管理还不止这些内容(还记得那个神秘的RScript::Parse吗……?),但这些就让我们留在下次“品尝”吧,好了,就这样吧,我呢还是那句老话:下次再见吧 :)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏owent

我们的Lua类绑定机制

最近一个人搞后台,框架底层+逻辑功能茫茫多,扛得比较辛苦,一直没抽出空来写点东西。

2841
来自专栏西枫里博客

解决ThinkPHP升级5.1后输出字符被转义的问题。

最近给博客升级框架,由于从5.0过渡到5.1要修改的地方还蛮多的,加上之前一些代码在实际运行中略显低效,索性就把后端重构一下。把一些关键部位调整到位后刷新页面,...

2266
来自专栏阮一峰的网络日志

asm.js 和 Emscripten 入门教程

Web 技术突飞猛进,但是有一个领域一直无法突破 ---- 游戏。 游戏的性能要求非常高,一些大型游戏连 PC 跑起来都很吃力,更不要提在浏览器的沙盒模型里跑了...

3075
来自专栏SDNLAB

《P4语言规范》parser详解

前言 为深入研究P4语言相关规范及运行操作使用,本系列文章根据P4.org网站给出的《The P4 Language Specification v1.0.2》...

3999
来自专栏酷玩时刻

微信扫码支付(模式一)微信扫码支付(模式一)

官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4

1973
来自专栏Python

python常用模块

 python常用模块 什么是模块?    常见的场景:一个模块就是一个包含了python定义和声明的文件,文件名就是模块名字加上.py的后缀。    但其实i...

33711
来自专栏熊二哥

单例模式深入理解

最近去平安系面试时,遇到了个人技术领域认定的一大偶像吴大师(Cat作者),他随口问了个单例的问题,要求基于Java技术栈,给出几种单例的方案,并给出单元测试代码...

22810
来自专栏北京马哥教育

最详细的 linux grep命令教程

grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印...

1580
来自专栏冰霜之地

高效的数据压缩编码方式 Protobuf

Protocol buffers 是一种语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。

2781
来自专栏nnngu

百度搜索 “Java面试题” 前200页(面试必看)

本文中的题目来源于网上的一篇文章《百度搜索 “Java面试题” 前200页》,但该文章里面只有题目,没有答案。因此,我整理了一些答案发布于本文。本文整理答案的原...

80311

扫码关注云+社区

领取腾讯云代金券