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



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

  1. 动态加载和卸载数据,因为它需要在飞行中,如导航大型开放世界。
  2. 在重用相同数据的资产之间共享数据,确保数据只有在不再有任何用户时才从内存中卸载。
  3. 管理资产的质量变更/部分或预览或LOD版本。

到目前为止,我的新系统看起来是这样的:

我目前的方法是抽象资产加载和卸载的概念,并简单地使用代码引用"使用"资产或"不使用"它,并在资产中保留一个整数,跟踪有多少用户使用该资产。当一个资产达到0个用户时,它将从相关字典中删除。

我使用一个特定的定制Manager类作为每种类型的资产的中央控制,例如TextureManager或MeshManager。每个AssetManager都有一个线程在它内部运行,处理来自游戏主线程外部的流媒体资产。独立的AssetStreamerThreads在后台不断运行,当没有工作要做时阻塞,等待任务从主线程队列完成。当有Job要做时,它们抓取它,完成它,然后将它返回到一个已完成的队列中,供主线程在下一次更新时接收。

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

管理器保持每个资产的字典,该字典已经加载了文件路径,相当于对内存中已经存在的资产对象的引用。

伪代码:

// 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("resources/models/model1.m");并在最后从内存中释放模型,调用ModelManager.unuse("resources/models/model1.m");

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

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

缺点:

  • 忘记在一个资产上调用'unuse'将导致该资产的'users'计数永久地停留在0以上,所以它永远不会被卸载,即使它没有被使用。虽然这不是一个主要问题,因为资产仍然在字典中并且不会加载两次,但对于太大而无法装入内存的大型开放世界,这可能是一个问题。这个系统是设计错误还是我应该忍受它?我是否应该添加一个"memoryPurge"方法,以便在游戏状态之间经常调用?不知道该怎么处理

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

  • 我的加载线程突然蜂拥而至的作业可能会阻塞它很长一段时间。比如玩家被传送到一个意想不到的游戏新区域,或者因为物理系统的漏洞而被扔到空中,等等。如果是这样,我应该在我的游戏代码(例如:最大玩家速度)或我的流线程中这样做吗?

  • 我看到有人设计他们的纹理流系统,以便在应用程序启动时加载每个纹理的4px x 4px版本,然后将4px版本的纹理用作尚未加载的任何纹理的占位符,因此没有任何东西会"丢失",只是总是以非常低的质量存储。这是否值得研究,如果值得,您将如何实现这样的系统?

    关于这个概念,首先想到的是,从数千个单独的文件中加载如此微小的数据,并对每个文件进行jpeg解压缩,这太迟了,对吧?会有一个特殊的'texture_preview。缓存文件是一个好主意,用于存储数据和加载它已经未压缩直接到内存快速加载?

结论:

这是目前为止我能想到的最好的,但我脑子里还有很多未知数。至少我的方向是对的吧?我知道我问了很多问题,但我不是在寻找每个问题的答案。任何能回答我部分或大部分问题的答案都将被接受。在我开始执行这个概念之前,我主要是在寻找新的方向来完成这个概念。或者来自经验丰富的大师的警告,他们在我之前走过这条路,知道龙在哪里。

你问的问题太多了,虽然这些问题都是很好的。

我可以回答的一个问题是你的引用计数——不要这样做。利用垃圾收集器和Java已经投入其中的所有工作。

让您的集中式存储为加载的数据保留一个SoftReferenceWeakReference。如果该引用为空,则下次请求时需要重新加载它。当需要内存时,Java将自动对不需要的东西进行垃圾收集。你不需要引用计数或其他任何东西。

最新更新