动态流,加载,卸载和共享游戏资产

时间:2015-12-08 14:50:28

标签: java multithreading opengl resources game-engine

我目前正在设计一个在游戏引擎中处理游戏资产的系统,我只是在寻找一些关于最佳方式的输入/讨论。我目前的系统非常手动和传统,我正在寻找更自动的东西来替换它。我的目标是实现一个系统:

  1. 动态加载和卸载数据,例如在导航大型开放世界时。
  2. 在重用相同数据的资产之间共享数据,确保数据仅在不再有任何用户的情况下从内存中卸载。
  3. 管理质量变更/部分或预览或资产的LOD版本。
  4. 到目前为止,我的新系统看起来像这样:

    我目前的方法是抽象出资产加载和卸载的概念,只需让代码引用'使用'资产,或“不使用”'它和资产内部我保留一个整数来跟踪资产的用户数量。当资产达到0个用户时,它将从相关字典中删除。

    我使用特定的定制Manager类作为每种类型资产的中央控件,例如TextureManager或MeshManager。每个AssetManager都有一个在其中运行的线程,用于处理来自游戏主线程外部的流式资产。单独的AssetStreamerThreads在后台持续运行,当没有工作要做时阻塞,等待Jobs从主线程的队列中完成。当有Job要做的时候,他们抓住它,完成它,然后将它返回到一个完整的队列,主线程将在其中接收下一次更新。

    这些线程中的每一个都处理从硬盘上的文件(或理论上任何地方,甚至下载?)解码资产并将其放入内存,准备好在几个OpenGL调用中简单地上传到GPU。数据存储在来回传递的作业内。

    管理员保存已加载文件路径的每个资产的字典,等同于已经在内存中的资产对象的引用。

    伪代码:

    // A simple structure to store job data, Data would be tailored to each type of Asset
    class AssetLoadJob {
        boolean complete = false;
        String filepath;
        Asset targetAsset;
        Data data;
    }
    
    // The Asset Manager
    class AssetManager {
        Dictionary<String,Asset> assets;
        Queue<AssetLoadJob> jobsList;
        Queue<AssetLoadJob> jobsComplete;
    
        // Startup and run the streamer thread.
        public AssetManager {
            AssetStreamerThread streamer = new AssetStreamerThread(this);
            streamer.run();
        }
    
        // To fetch an asset to use. Note, the Asset returned may not be loaded, but will be eventually.
        public Asset use(String filepath) {
            if(assets.exists(filepath)) {
                assets.addUser();
                return assets.get(filepath);
            }
            else {
                Asset newAsset = new Asset();
                jobsList.add(new AssetLoadJob(newAsset, filepath));
            }
        }
    
        // Don't need it anymore? Let the manager know.
        public void unuse(String filepath) {
            assets.get(filepath).removeUser();
            if(assets.get(filepath).getUsers() < 1) {
                assets.get(filepath).unload();
                assets.remove(filepath);
            }
        }
    
        // Called before each update() loop in the game engine.
        public void processJobs() {
            foreach(jobsComplete as finishedJob) {
                finishedJob.targetAsset.receiveData(finishedJob.data);
                jobsComplete.remove(finishedJob);
            }
        }
    
        // Accessed by worker thread
        public AssetLoadJob getJob() {
            return jobsList.remove();
        }
    
        // Accessed by worker thread
        public void returnJob(AssetLoadJob job) {
            jobsComplete.add(job);
        }
    }
    
    // The worker thread which handles loading content.
    class AssetStreamerThread {
    
        AssetManager mgr;
    
        public AssetStreamerThread(AssetManager mgr) {
            this.mgr = mgr;
        }
    
        // The out of main thread loop which runs forever.
        public void run() {
            while(forever) {
                AssetLoadJob job = mgr.getJob(); // Blocking until returns valid job.
                // Load job data.. 
                mgr.returnJob(job);
            }
        }
    }
    
    // An abstract example of an Asset. In practice, this might be instead a Texture, Mesh, Sound object, etc.
    class Asset {
    
        private int users = 0;
        private boolean loaded = false;
    
        // Since we can't access users integer directly, these next two methods control increments/decrements.
        public void addUser() {
            users++;
        }
    
        public void removeUser() {
            users--;
        }
    
        public int getUsers() {
            return users;
        }
    
        // The method to Bind an Asset for rendering, such as Binding a texture before drawing an object with it.
        public void bind() {
            if(loaded) {
                // Use loaded data on GPU
            } else {
                // Use placeholder for missing data or just use 'empty' data. Eg: a checkerboard texture for missing textures or solid black 1px x 1px texture, or whatever. A question mark shape for meshes, or simply nothing at all.
            }
        }
    
        // This method is called by the Manager to give the Asset it's data when it's loaded.
        public void receiveData(data) {
            // Upload data to GPU
            loaded = true;
        }
    
        // Called by Manager, informs the Asset it can release resources.
        public void unload() {
            // Unload data from GPU
            loaded = false;
        }
    }
    

    优点:

    • 使用这样的系统游戏代码非常简单,可以通过简单调用来加载模型:ModelManager.use(&#34; resources / models / model1.m&#34;);并在最后从内存中释放模型,调用ModelManager.unuse(&#34; resources / models / model1.m&#34;);

    • 系统是多线程的,以避免在加载大型资产时出现帧率错误。

    • 在代码中实现非常简单。

    缺点:

    • 忘记致电&#39;不使用&#39;在资产上将导致资产的用户&#39;计数永远保持在0以上,所以即使它没有被使用,它也永远不会被卸载。虽然不是一个主要的问题,因为资产保留在字典中并且不会被加载两次,对于大的开放世界太大而无法融入内存,这可能是一个问题。 这个系统是否因设计错误或我应该忍受它?我应该添加一个&#39; memoryPurge&#39;在游戏状态之间经常被调用的方法?不确定如何处理这个问题。

    • 纹理和网格物体要么完全加载,要么根本不加载。中间不会有任何阶段。我甚至不确定如何实现这样的部分负载。我希望能够加载一个对象的不同LOD质量,并逐渐加载一个对象的更高质量,因为它在一个场景中更接近,例如远处的建筑物。我也希望能够以类似的方式控制资产质量,例如纹理。如果玩家在主菜单中更改了纹理的质量设置,那么如果我的Asset系统自动处理增加或降低加载到内存中的Assets的质量,那将会很好。 我应该如何融入“质量”概念?进入我的资产?

    • 我的加载线程中突然出现的一大堆作业可能会长时间堵塞它。如玩家意外地传送到游戏的新区域,或者由于物理系统中的错误等而被空中甩开等等。这是我应该设法防范的吗?如果是这样,我应该在我的游戏代码(例如:最大播放器速度)或我的流媒体线程中这样做吗?

    • 我看到一个人设计了他们的纹理流系统,以便在应用程序启动时加载每个纹理的4px x 4px版本,然后将4px版本的纹理用作任何纹理的占位符。 #39; t还没有加载,所以没有任何东西永远不会丢失,只是总是以最差的质量存储。 值得研究,如果是这样,您将如何实施这样的系统?

      这个概念首先想到的是,加载像数千个单独文件那样的微小数据并通过每个文件进行jpeg解压缩是否过于迟钝? 特殊&#39; texture_preview.cache&#39;文件是一个好主意,用于存储该数据并将其已经解压缩直接加载到内存中以便快速加载?

    结论:

    这是迄今为止我能想到的最好的,但我脑子里还有很多未知数。我至少在正确的轨道上吗?我知道我已经问了很多问题,但我并不是在寻找每一个问题的答案。任何解决我的部分或大部分疑问的答案都将被接受。在我开始实施之前,我主要只是寻找新的方向来考虑完成这个概念。或者是经验丰富的大师的警告,他们在我面前走过这条路,知道龙在哪里。

1 个答案:

答案 0 :(得分:0)

你真的在这里问了太多问题,虽然这些问题很好。

我可以回答的一个方面是您的参考计数 - 不要这样做。利用垃圾收集器和Java已经完成的所有工作。

让集中存储为加载的数据保留SoftReferenceWeakReference。如果该引用已经为null,那么下次您被要求时它将需要重新加载它。 Java将自动垃圾收集需要内存时不需要的任何内容。你不需要引用计数或其他任何一个。