การออกแบบคลาส ResourceManager


17

ฉันตัดสินใจว่าฉันต้องการเขียนคลาส ResourceManager / ResourceCache สำหรับเอนจิ้นเกมอดิเรกของฉัน แต่ฉันมีปัญหาในการออกแบบรูปแบบแคช

แนวคิดก็คือ ResourceManager มีเป้าหมายนุ่มนวลสำหรับหน่วยความจำทั้งหมดที่ใช้โดยทรัพยากรทั้งหมดของเกมรวมกัน คลาสอื่น ๆ จะสร้างออบเจ็กต์ทรัพยากรซึ่งจะอยู่ในสถานะไม่โหลดและส่งไปยัง ResourceManager จากนั้น ResourceManager จะตัดสินใจเมื่อใดที่จะโหลด / ยกเลิกการโหลดทรัพยากรที่กำหนดโดยคำนึงถึงขีด จำกัด ซอฟต์

เมื่อต้องการทรัพยากรโดยชั้นเรียนอื่นคำขอจะถูกส่งไปยัง ResourceManager สำหรับมัน (โดยใช้ id ของสตริงหรือตัวระบุที่ไม่ซ้ำกัน) หากโหลดทรัพยากรแล้วการอ้างอิงแบบอ่านอย่างเดียวไปยังทรัพยากรจะถูกส่งผ่านไปยังฟังก์ชั่นการโทร หากไม่ได้โหลดทรัพยากรผู้จัดการจะทำเครื่องหมายวัตถุที่จะโหลดในโอกาสถัดไป (โดยปกติจะอยู่ที่ส่วนท้ายของการวาดเฟรม)

โปรดทราบว่าแม้ว่าระบบของฉันจะทำการนับการอ้างอิงบางอย่างจะนับเฉพาะเมื่อมีการอ่านทรัพยากร (ดังนั้นจำนวนการอ้างอิงอาจเป็น 0 แต่เอนทิตีอาจยังคงติดตาม uid อยู่)

นอกจากนี้ยังเป็นไปได้ที่จะทำเครื่องหมายทรัพยากรเพื่อการโหลดที่ดีก่อนการใช้งานครั้งแรก นี่เป็นภาพร่างของชั้นเรียนที่ฉันใช้อยู่:

typedef unsigned int ResourceId;

// Resource is an abstract data type.
class Resource
{
   Resource();
   virtual ~Resource();

   virtual bool load() = 0;
   virtual bool unload() = 0;
   virtual size_t getSize() = 0; // Used in determining how much memory is 
                                 // being used.
   bool isLoaded();
   bool isMarkedForUnloading();
   bool isMarkedForReload();
   void reference();
   void dereference();
};

// This template class works as a weak_ptr, takes as a parameter a sub-class
// of Resource. Note it only hands give a const reference to the Resource, as
// it is read only.
template <class T>
class ResourceGuard
{
   public:
     ResourceGuard(T *_resource): resource(_resource)
     {
        resource->reference();
     }

     virtual ~ResourceGuard() { resource->dereference();}
     const T* operator*() const { return (resource); }
   };

class ResourceManager
{
   // Assume constructor / destructor stuff
   public:
      // Returns true if resource loaded successfully, or was already loaded.
      bool loadResource(ResourceId uid);

      // Returns true if the resource could be reloaded,(if it is being read
      // it can't be reloaded until later).
      bool reloadResource(ResourceId uid)

      // Returns true if the resource could be unloaded,(if it is being read
      // it can't be unloaded until later)
      bool unloadResource(ResourceId uid);

      // Add a resource, with it's named identifier.
      ResourceId addResource(const char * name,Resource *resource);

      // Get the uid of a resource. Returns 0 if it doesn't exist.
      ResourceId getResourceId(const char * name);

      // This is the call most likely to be used when a level is running, 
      // load/reload/unload might get called during level transitions.
      template <class T>
      ResourceGuard<T> &getResource(ResourceId resourceId)
      {
         // Calls a private method, pretend it exits
         T *temp = dynamic_cast<T*> (_getResource(resourceId));
         assert(temp != NULL);
         return (ResourceGuard<T>(temp));
      }

      // Generally, this will automatically load/unload data, and is called
      // once per frame. It's also where the caching scheme comes into play.
      void update();

};

ปัญหาคือเพื่อให้การใช้งานข้อมูลโดยรวมมีการวนรอบ / ต่ำกว่าขีด จำกัด ซอฟต์แวร์ผู้จัดการจะต้องมีวิธีที่ชาญฉลาดในการกำหนดว่าวัตถุใดที่จะทำการขนถ่าย

ฉันกำลังคิดที่จะใช้ระบบการจัดลำดับความสำคัญบางอย่าง (เช่นลำดับความสำคัญชั่วคราวลำดับความสำคัญที่ใช้บ่อยลำดับความสำคัญถาวร) รวมกับเวลาของการเพิกถอนครั้งสุดท้ายและขนาดของทรัพยากรเพื่อกำหนดว่าจะลบเมื่อใด แต่ฉันไม่สามารถนึกถึงรูปแบบที่เหมาะสมที่จะใช้หรือโครงสร้างข้อมูลที่ถูกต้องซึ่งจำเป็นในการจัดการอย่างรวดเร็ว

คนที่ใช้ระบบเช่นนี้สามารถให้ภาพรวมของการทำงานของพวกเขาได้ไหม มีรูปแบบการออกแบบที่ชัดเจนที่ฉันพลาดไปหรือไม่? ฉันทำสิ่งนี้ซับซ้อนเกินไปหรือไม่ เป็นการดีที่ฉันต้องการระบบที่มีประสิทธิภาพและยากที่จะละเมิด ความคิดใด ๆ


4
คำถามที่ชัดเจนคือ "คุณต้องการฟีเจอร์ที่คุณกำหนดให้ใช้งานหรือไม่" หากคุณกำลังใช้งานพีซีการตั้งค่าฝาปิดหน่วยความจำอาจไม่จำเป็นตัวอย่างเช่น หากเกมของคุณแบ่งออกเป็นระดับและคุณสามารถกำหนดได้ว่าจะใช้สินทรัพย์ใดในระดับนั้นเพียงแค่โหลดทุกอย่างตั้งแต่เริ่มต้นและหลีกเลี่ยงการโหลด / ไม่โหลดในระหว่างการเล่นเกม
Tetrad

คำตอบ:


8

ฉันไม่แน่ใจว่าสิ่งนี้เกี่ยวข้องกับคำถามของคุณ 100% หรือไม่ แต่มีเคล็ดลับในการให้คำแนะนำดังนี้

  1. ล้อมรอบทรัพยากรของคุณในที่จับ ทรัพยากรของคุณควรแบ่งออกเป็นสองประเภท: คำอธิบาย (โดยปกติจะเป็น XML) และข้อมูลจริง เอ็นจิ้นควรโหลดคำอธิบายทรัพยากรทั้งหมดเมื่อเริ่มเกมและสร้างตัวจัดการทั้งหมดสำหรับพวกเขา เมื่อส่วนประกอบร้องขอทรัพยากรที่จับจะถูกส่งกลับ ฟังก์ชันนั้นสามารถดำเนินการตามปกติ (พวกเขายังสามารถร้องขอขนาด ฯลฯ ได้) ตอนนี้ถ้าคุณยังไม่ได้โหลดทรัพยากรล่ะ สร้าง 'ทรัพยากรที่ไม่มีค่า' ที่ใช้เพื่อแทนที่ทรัพยากรใด ๆ ที่พยายามจะวาดขึ้น แต่ยังไม่ได้โหลด

มีอีกมากมาย ฉันเพิ่งอ่านหนังสือเล่มนี้ " การออกแบบเครื่องยนต์และการนำเกม " มาใช้และมีส่วนที่ดีมากที่จะใช้งานและออกแบบคลาสตัวจัดการทรัพยากร

หากไม่มีฟังก์ชั่น ResourceHandle และ Memory Budget นี่คือสิ่งที่หนังสือแนะนำ:

typedef enum
{
    RESOURCE_NULL = 0,
    RESOURCE_GRAPHIC = 1,
    RESOURCE_MOVIE = 2,
    RESOURCE_AUDIO = 3,
    RESOURCE_TEXT =4,
}RESOURCE_TYPE;


class Resource : public EngineObject
{
public:
    Resource() : _resourceID(0), _scope(0), _type(RESOURCE_NULL) {}
    virtual ~Resource() {}
    virtual void Load() = 0;
    virtual void Unload()= 0;

    void SetResourceID(UINT ID) { _resourceID = ID; }
    UINT GetResourceID() const { return _resourceID; }

    void SetFilename(std::string filename) { _filename = filename; }
    std::string GetFilename() const { return _filename; }

    void SetResourceType(RESOURCE_TYPE type) { _type = type; }
    RESOURCE_TYPE GetResourceType() const { return _type; }

    void SetResourceScope(UINT scope) { _scope = scope; }
    UINT GetResourceScope() const { return _scope; }

    bool IsLoaded() const { return _loaded; }
    void SetLoaded(bool value) { _loaded = value; }

protected:
    UINT _resourceID;
    UINT _scope;
    std::string _filename;
    RESOURCE_TYPE _type;
    bool _loaded;
private:
};

class ResourceManager : public Singleton<ResourceManager>, public EngineObject
{
public:
    ResourceManager() : _currentScope(0), _resourceCount(0) {};
    virtual ~ResourceManager();
    static ResourceManager& GetInstance() { return *_instance; }

    Resource * FindResourceByID(UINT ID);
    void Clear();
    bool LoadFromXMLFile(std::string filename);
    void SetCurrentScope(UINT scope);
    const UINT GetResourceCount() const { return _resourceCount; }
protected:
    UINT _currentScope;
    UINT _resourceCount; //Total number of resources unloaded and loaded
    std::map<UINT, std::list<Resource*> > _resources; //Map of form <scope, resource list>

private:
};

ขอให้สังเกตว่าฟังก์ชัน SetScope อ้างอิงถึงการออกแบบเอ็นจิ้นฉาก - เลเยอร์ที่ ScopeLevel อ้างถึงฉาก # เมื่อมีการเข้า / ออกฉากทรัพยากรทั้งหมดตามขอบเขตนั้นจะถูกโหลดและสิ่งที่ไม่ได้อยู่ในขอบเขตส่วนกลางจะถูกยกเลิกการโหลด


ฉันชอบแนวคิด NULL Object และความคิดในการติดตามขอบเขต ฉันเพิ่งผ่านห้องสมุดโรงเรียนของฉันเพื่อค้นหา 'การออกแบบเครื่องยนต์และการนำไปใช้งาน' แต่ไม่มีโชค หนังสืออธิบายรายละเอียดว่าจะจัดการงบประมาณหน่วยความจำได้อย่างไร
Darcy Rayner

มันมีรายละเอียดแผนการจัดการหน่วยความจำง่ายๆ ในท้ายที่สุดแม้แต่พื้นฐานก็ควรจะดีกว่า malloc ทั่วไปเนื่องจากมีแนวโน้มที่จะพยายามและดีที่สุดสำหรับทุกสิ่ง
Setheron

ในที่สุดฉันก็เลือกดีไซน์ที่คล้ายกันมาก
Darcy Rayner
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.