源码剖析:TheManaWorld_资源管理系统

 

 

TheManaWorld是一个开源2D MMORPGhttps://www.themanaworld.org/ 以下简称TMW,它的资源管理比较典型:

1 基于引用计数使资源自动归还

2 各种资源在加载时根据类型做分派

 

先从Resouce与ResouceManager这两个类开始,Resouce主要提供了引用计数的功能,ResouceManager基于Resouce提供的抽象接口编写资源管理的方法。

class Resource
{
    friend class ResourceManager;

    public:
        enum OrphanPolicy {
            DeleteLater,
            DeleteImmediately
        };

        Resource():
            mRefCount(0)
        {}

        /**
         * Increments the internal reference count.
         */
        void incRef() { ++mRefCount; }

        /**
         * Decrements the reference count. When no references are left, either
         * schedules the object for deletion or deletes it immediately,
         * depending on the \a orphanPolicy.
         */
        void decRef(OrphanPolicy orphanPolicy = DeleteLater);

        /**
         * Return the path identifying this resource.
         */
        const std::string &getIdPath() const
        { return mIdPath; }

    protected:
        virtual ~Resource() {}

    private:
        std::string mIdPath; /**< Path identifying this resource. */
        time_t mTimeStamp;   /**< Time at which the resource was orphaned. */
        unsigned mRefCount;  /**< Reference count. */
};

OrphanPolicy指明了在引用计数降为0时资源回收的策略,马上回收与延后回收。延后回收的策略非常有用,比如游戏中常有同造型的怪删除后又马上创建出来。

void Resource::decRef(OrphanPolicy orphanPolicy)
{
    // Reference may not already have reached zero
    if (mRefCount == 0) {
        logger->log("Warning: mRefCount already zero for %s", mIdPath.c_str());
        assert(false);
    }

    --mRefCount;

    if (mRefCount == 0)
    {
        ResourceManager *resman = ResourceManager::getInstance();

        switch (orphanPolicy)
        {
            case DeleteLater:
            default:
                resman->release(this);
                break;
            case DeleteImmediately:
                resman->remove(this);
                delete this;
                break;
        }
    }
}

ResouceManager::getInstance()说明资源管理器采用的是单例模式,比较直观。因为游戏只需要一个资源管理器,且为其他地方的代码直接访问资源管理器提供了方便。

然后跟踪Reslese和remove两种方法有何不同

class ResouceManager
{
    ...
        typedef std::map<std::string, Resource*> Resources;
        typedef Resources::iterator ResourceIterator;

        Resources mOrphanedResources;
        Resources mResources;

}

void ResourceManager::release(Resource *res)
{
    ResourceIterator resIter = mResources.find(res->mIdPath);

    // The resource has to exist
    assert(resIter != mResources.end() && resIter->second == res);

    timeval tv;
    gettimeofday(&tv, NULL);
    time_t timestamp = tv.tv_sec;

    res->mTimeStamp = timestamp;
    if (mOrphanedResources.empty())
        mOldestOrphan = timestamp;

    mOrphanedResources.insert(*resIter);
    mResources.erase(resIter);
}

void ResourceManager::remove(Resource *res)
{
    mResources.erase(res->mIdPath);
}

ResouceManager维护了两个列表mOrphanedResources和mResources,

延后删除的资源从mResources中移除后放到mOrphanedResources中时时保存起来,并且记录下时间戳.时间戳主要作用就是判断mOrphanedResources中的资源是否长期不引用到了应该彻底回收的时候

void ResourceManager::cleanOrphans()
{
    timeval tv;
    gettimeofday(&tv, NULL);
    // Delete orphaned resources after 30 seconds.
    time_t oldest = tv.tv_sec;
    time_t threshold = oldest - 30;

    if (mOrphanedResources.empty() || mOldestOrphan >= threshold)
        return;

    ResourceIterator iter = mOrphanedResources.begin();
    while (iter != mOrphanedResources.end())
    {
        Resource *res = iter->second;
        time_t t = res->mTimeStamp;
        if (t >= threshold)
        {
            if (t < oldest)
                oldest = t;
            ++iter;
        }
        else
        {
            logger->log("ResourceManager::release(%s)", res->mIdPath.c_str());
            ResourceIterator toErase = iter;
            ++iter;
            mOrphanedResources.erase(toErase);
            delete res; // delete only after removal from list, to avoid issues in recursion
        }
    }

    mOldestOrphan = oldest;
}

从当前时间点往前推30秒threshold,如果在这个时间之前release的则应该回收,之后的保留,并且统计出保留的资源里最老的时间戳mOldestOrphan,如果下次清理资源的时候发现mOldestOrphan还在threshold之后,那说明没有资源需要回收,可以避免不必要的遍历.那么cleanOrphans()何时调用呢?这个因各个游戏的资源管理策略而定,TMW是在每次请求资源的时候调用一次.

那外部如何从资源管理器请求资源,请求的资源又如何从硬盘加载?我们从上层逻辑代码往下跟.

怪物,NPC,玩家都是类Being,Being的构造函数调用setSubtype()->setupSpriteDisplay


void ActorSprite::setupSpriteDisplay(const SpriteDisplay &display,
                                     bool forceDisplay)
{
    clear();

    SpriteRefs it, it_end;

    for (it = display.sprites.begin(), it_end = display.sprites.end();
         it != it_end; it++)
    {
        std::string file = paths.getStringValue("sprites") + it->sprite;
        int variant = it->variant;
        addSprite(AnimatedSprite::load(file, variant));
    }

…

setupSpriteDisplay会遍历所需要加载的sprite信息,调用addSprite,addSprite是CompoundSprite类的成员函数,Being继承于CompoundSprite,是Sprite的组合。跟进AnimatedSprite::load(file, variant)看sprite是如何加载的

AnimatedSpritre::load

ResourceManager *resman = ResourceManager::getInstance();

    SpriteDef *s = resman->getSprite(filename, variant);

 


SpriteDef *ResourceManager::getSprite(const std::string &path, int variant)
{
    SpriteDefLoader l = { path, variant };
    std::stringstream ss;
    ss << path << "[" << variant << "]";
    return static_cast<SpriteDef*>(get(ss.str(), SpriteDefLoader::load, &l));
}

Resource *ResourceManager::get(const std::string &idPath, generator fun,
                               void *data)
{
    // Check if the id exists, and return the value if it does.
    ResourceIterator resIter = mResources.find(idPath);
    if (resIter != mResources.end())
    {
        resIter->second->incRef();
        return resIter->second;
    }

    resIter = mOrphanedResources.find(idPath);
    if (resIter != mOrphanedResources.end())
    {
        Resource *res = resIter->second;
        mResources.insert(*resIter);
        mOrphanedResources.erase(resIter);
        res->incRef();
        return res;
    }

    Resource *resource = fun(data);

    if (resource)
    {
        resource->incRef();
        resource->mIdPath = idPath;
        mResources[idPath] = resource;
        cleanOrphans();
    }

    // Returns NULL if the object could not be created.
    return resource;
}

 

struct SpriteDefLoader{    std::string path;    int variant;    static Resource *load(void *v)    {        SpriteDefLoader *l = static_cast< SpriteDefLoader * >(v);        return SpriteDef::load(l->path, l->variant);    }};

注意get资源的第二个参数SpriteDefLoader::load ,参数类型在ResouceManager中有定义typedef Resource *(*generator)(void *) 是一个函数指针,函数指针为完成各种类型资源加载提供了间接层

而SpriteDef继承于Resouce,其成员函数load完成读取xml配置加载动画的任务.加载动画时又会进一步分解为加载帧图片的任务

void SpriteDef::loadImageSet(xmlNodePtr node, const std::string &palettes)
{
    const std::string name = XML::getProperty(node, "name", "");

    // We don‘t allow redefining image sets. This way, an included sprite
    // definition will use the already loaded image set with the same name.
    if (mImageSets.find(name) != mImageSets.end())
        return;

    const int width = XML::getProperty(node, "width", 0);
    const int height = XML::getProperty(node, "height", 0);
    std::string imageSrc = XML::getProperty(node, "src", "");
    Dye::instantiate(imageSrc, palettes);

    ResourceManager *resman = ResourceManager::getInstance();
    ImageSet *imageSet = resman->getImageSet(imageSrc, width, height);

    if (!imageSet)
    {
        logger->error(strprintf("Couldn‘t load imageset (%s)!",
                                imageSrc.c_str()).c_str());
    }

    imageSet->setOffsetX(XML::getProperty(node, "offsetX", 0));
    imageSet->setOffsetY(XML::getProperty(node, "offsetY", 0));
    mImageSets[name] = imageSet;
}

获取帧图片是又会通过ResouceManager提供的获取图片的接口.而getImageSet与getSprite代码几乎一致 也是有个ImageSetLoader配接类型和继承于Resource的ImageSet.

ImageSet是调用一连串的帧图片,也会通过ResourceManager的getImage接口

struct DyedImageLoader
{
    ResourceManager *manager;
    std::string path;
    static Resource *load(void *v)
    {
        DyedImageLoader *l = static_cast< DyedImageLoader * >(v);
        std::string path = l->path;
        std::string::size_type p = path.find(‘|‘);
        Dye *d = NULL;
        if (p != std::string::npos)
        {
            d = new Dye(path.substr(p + 1));
            path = path.substr(0, p);
        }
        SDL_RWops *rw = PHYSFSRWOPS_openRead(path.c_str());
        if (!rw)
        {
            delete d;
            return NULL;
        }
        Resource *res = d ? Image::load(rw, *d)
                          : Image::load(rw);
        delete d;
        return res;
    }
};

Image *ResourceManager::getImage(const std::string &idPath)
{
    DyedImageLoader l = { this, idPath };
    return static_cast<Image*>(get(idPath, DyedImageLoader::load, &l));
}

image的load非常简单,PHYSFSRWOPS_openRead是PHYSFS与SDL_RWops的封装函数.PHYSFS好像是一个提供了挂接解压等功能的跨平台文件库,开源游戏里经常见到,有时间研究下…

致此TMW的资源管理思路就大致摸清了,其他如粒子和音效的资源也是这么管理的,跟踪resourceManager的get就能看到脉络。

TMW的资源管理比较简单,功能也比较单一,只是使用引用计数来跟踪回收。游戏中的资源管理器有着各种各样的策略,因需求而定。有的游戏会分别拟定每种类型的资源占用内存的上限,如果超过上限会单独回收这种资源,还有的会结合内存池的使用,减少堆的碎片。游戏的资源管理总是跟内存管理息息相关,而C++在内存管理方面天生的定制能力提供了优势。页游中的资源管理方式则与此大相径庭的,如AS依赖于垃圾回收又苦于垃圾回收对稳定速度的干扰,带宽的紧张使得资源的释放也不能这么随意等等,思路又是截然不同。

总之,这个资源管理器对于2D小游戏来说足够简单实用,想在上面加功能也是比较容易的。

源码剖析:TheManaWorld_资源管理系统

时间: 2024-10-09 23:44:45

源码剖析:TheManaWorld_资源管理系统的相关文章

菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)

俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中的奥秘,下面我们就一起来研究一下nginx的main函数. 1.nginx的main函数解读 nginx启动显然是由main函数驱动的,main函数在在core/nginx.c文件中,其源代码解析如下,涉及到的数据结构在本节仅指出其作用,将在第二节中详细解释. nginx main函数的流程图如下:

《python源码剖析》笔记 Python虚拟机框架

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1. Python虚拟机会从编译得到的PyCodeObject对象中依次读入每一条字节码指令, 并在当前的上下文环境中执行这条字节码指令. Python虚拟机实际上是在模拟操作中执行文件的过程 PyCodeObject对象中包含了字节码指令以及程序的所有静态信息,但没有包含 程序运行时的动态信息--执行环境(PyFrameObject) 2.Python源码中的PyFrameObject

Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程模型的变种,无论其怎么演化,其核心组件都包含了Reactor实例(提供事件注册.注销.通知功能).多路复用器(由操作系统提供,比如kqueue.select.epoll等).事件处理器(负责事件的处理)以及事件源(linux中这就是描述符)这四个组件.一般,会单独启动一个线程运行Reactor实例来

菜鸟nginx源码剖析数据结构篇(九) 内存池ngx_pool_t[转]

菜鸟nginx源码剖析数据结构篇(九) 内存池ngx_pool_t Author:Echo Chen(陈斌) Email:[email protected] Blog:Blog.csdn.net/chen19870707 Date:Nov 11th, 2014 今天是一年一度的光棍节,还没有女朋友的程序猿童鞋不妨new一个出来,内存管理一直是C/C++中最棘手的部分,远不止new/delete.malloc/free这么简单.随着代码量的递增,程序结构复杂度的提高.今天我们就一起研究一下以精巧著

DICOM医学图形处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求(续)

背景: 上一篇博文中,在对storescp工具源文件storescp.cc和DcmSCP类的源文件scp.cc进行剖析后,得出了两者都可以实现响应C-ECHO和C-STORE(需要对DcmSCP类进行扩展)请求的功能.但是在对DcmSCP类进行扩展,期望模拟实现自己的storescp.exe工具时遇到了问题,客户端提示服务中断链接,而服务端显示保存失败,如下图所示.此次博文通过排除该问题再一次对storescp.cc和scp.cc进行对比,主要从Presentation Context.Abst

通读《STL源码剖析》之后的一点读书笔记

[QQ群: 189191838,对算法和C++感兴趣可以进来] 直接逼入正题. Standard Template Library简称STL.STL可分为容器(containers).迭代器(iterators).空间配置器(allocator).配接器(adaptors).算法(algorithms).仿函数(functors)六个部分. 迭代器和泛型编程的思想在这里几乎用到了极致.模板或者泛型编程其实就是算法实现时不指定具体类型,而由调用的时候指定类型,进行特化.在STL中,迭代器保证了ST

Android多线程研究(1)——线程基础及源码剖析

从今天起我们来看一下Android中的多线程的知识,Android入门容易,但是要完成一个完善的产品却不容易,让我们从线程开始一步步深入Android内部. 一.线程基础回顾 package com.maso.test; public class TraditionalThread { public static void main(String[] args) { /* * 线程的第一种创建方式 */ Thread thread1 = new Thread(){ @Override publi

(升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)

本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课程会涵盖Scala编程详解.Spark核心编程.Spark SQL和Spark Streaming.Spark内核以及源码剖析.性能调优.企业级案例实战等部分.完全从零起步,让学员可以一站式精通Spark企业级大数据开发,提升自己的职场竞争力,实现更好的升职或者跳槽,或者从j2ee等传统软件开发工程

tomcat(12)org.apache.catalina.core.StandardContext源码剖析

[0]README 0)本文部分文字描述转自 "how tomcat works",旨在学习 "tomcat(12)StandardContext源码剖析" 的基础知识: 1)Context实例表示一个具体的web 应用程序,其中包含一个或多个Wrapper实例,每个Wrapper 表示一个具体的servlet定义: 2)Context容器还需要其他组件的支持,如载入器和Session 管理器.本章要intro 的 StandardContext是 catalina