游戏资产的动态流式传输、加载、卸载和共享
Dynamic Streaming, Loading, Unloading and Sharing of Game Assets
我目前正在设计一个在游戏引擎中处理游戏资产的系统,我只是在寻找一些 input/discussion 的最佳方法。我目前的系统是非常手动和传统的,我正在寻找更自动化的东西来代替它。我的目标是实现一个系统:
- 根据需要动态加载和卸载数据,例如在大型开放世界中导航时。
- 在重用相同数据的资产之间共享数据,确保数据仅在不再有任何用户时从内存中卸载。
- 管理资产的质量 changes/partial 或预览或 LOD 版本。
到目前为止,我的新系统看起来像这样:
我目前的方法是抽象资产加载和卸载的概念,并且简单地让代码引用 'using' 一个资产,或者 'unusing' 它,并且在资产内部我保留一个整数跟踪资产有多少用户。当资产达到 0 个用户时,它将从相关词典中删除。
我使用特定的定制管理器 class 作为每种类型资源的中央控制,例如 TextureManager 或 MeshManager。每个 AssetManager 内部都有一个线程 运行ning,用于处理来自游戏主线程之外的流式资产。单独的 AssetStreamerThreads 运行 一直在后台运行,当没有工作要做时阻塞,等待主线程队列中的作业完成。当有工作要做时,他们抓住它,完成它,然后 return 它到一个完成的队列,供主线程在下一次更新中接收。
这些线程中的每一个都处理从硬盘上的文件解码资产(或者理论上任何地方真的,甚至下载?)并将其放入内存,准备好通过几次 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("resources/models/model1.m");并在最后从内存中释放模型,调用 ModelManager.unuse("resources/models/model1.m");
系统是多线程的,以避免在加载大型资产时帧率卡顿。
用代码实现很简单
缺点:
忘记对资产调用 'unuse' 将导致资产的 'users' 计数永久停留在 0 以上,因此它永远不会被卸载,即使它不在采用。虽然这不是一个主要问题,因为资产保留在字典中并且不会被加载两次,但对于太大而无法放入内存的大型开放世界,这可能是一个问题。 这个系统是设计有问题还是我应该接受它?我是否应该添加一个 'memoryPurge' 方法以便在游戏状态之间每隔一段时间调用一次? 不确定如何处理。
纹理和网格对象要么完全加载,要么根本不加载。中间没有任何阶段。我什至不确定如何实现这样的部分加载。例如,我希望能够加载一个对象的不同 LOD 质量,并随着对象在场景中越来越近(例如远处的建筑物)而逐渐加载更高质量的对象。我还希望能够以类似的方式控制资产质量,例如纹理。如果播放器在主菜单中更改了纹理的质量设置,那么如果我的资产系统能够自动提高或降低加载到内存中的资产的质量,那就太好了。 我应该如何将 'quality' 的概念融入我的资产中?
突然涌入我的加载线程的作业可能会阻塞很长时间。比如玩家突然传送到游戏的新区域,或者由于物理系统的错误而被抛到空中等等。这是我应该避免的事情吗?如果是这样,我应该在我的游戏代码(例如:最大玩家速度)还是在我的流媒体线程中这样做?
我看到有人设计他们的纹理流系统,以便在应用程序启动时加载每个纹理的 4px x 4px 版本,然后将 4px 版本的纹理用作占位符任何尚未加载的纹理,因此什么都没有 'missing',最坏的情况下总是以非常低的质量存储。 这值得研究吗?如果值得,您将如何实施这样的系统?
关于这个概念,首先想到的是从数千个单独的文件中加载如此微小的数据,然后对每个文件进行 jpeg 解压缩,这太慢了吧? 一个特殊的 'texture_preview.cache' 文件是否是存储该数据并将已解压缩的数据直接加载到内存中以便快速加载的好主意?
结论:
这是迄今为止我能想到的最好的,但我脑子里还有很多未知数。我至少在正确的轨道上吗?我知道我已经问了很多问题,但我并不是在寻找每个问题的答案。任何解决我的部分或大部分查询的答案都将被接受。在我开始实施它之前,我主要只是在寻找新的方向来考虑完成这个概念。或者来自经验丰富的大师的警告,他们在我之前走过这些路,知道龙在哪里。
虽然这些都是很好的问题,但您确实在这里问了太多问题。
我可以回答的一个方面是您的引用计数 - 不要那样做。利用垃圾收集器,所有工作 Java 已经投入其中。
只需让您的中央存储对加载的数据保持 SoftReference
或 WeakReference
。如果该引用已变为 null,那么您将需要在下次被要求时重新加载它。 Java 将在需要内存时自动对不需要的内容进行垃圾回收。您不需要引用计数或任何其他内容。
我目前正在设计一个在游戏引擎中处理游戏资产的系统,我只是在寻找一些 input/discussion 的最佳方法。我目前的系统是非常手动和传统的,我正在寻找更自动化的东西来代替它。我的目标是实现一个系统:
- 根据需要动态加载和卸载数据,例如在大型开放世界中导航时。
- 在重用相同数据的资产之间共享数据,确保数据仅在不再有任何用户时从内存中卸载。
- 管理资产的质量 changes/partial 或预览或 LOD 版本。
到目前为止,我的新系统看起来像这样:
我目前的方法是抽象资产加载和卸载的概念,并且简单地让代码引用 'using' 一个资产,或者 'unusing' 它,并且在资产内部我保留一个整数跟踪资产有多少用户。当资产达到 0 个用户时,它将从相关词典中删除。
我使用特定的定制管理器 class 作为每种类型资源的中央控制,例如 TextureManager 或 MeshManager。每个 AssetManager 内部都有一个线程 运行ning,用于处理来自游戏主线程之外的流式资产。单独的 AssetStreamerThreads 运行 一直在后台运行,当没有工作要做时阻塞,等待主线程队列中的作业完成。当有工作要做时,他们抓住它,完成它,然后 return 它到一个完成的队列,供主线程在下一次更新中接收。
这些线程中的每一个都处理从硬盘上的文件解码资产(或者理论上任何地方真的,甚至下载?)并将其放入内存,准备好通过几次 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("resources/models/model1.m");并在最后从内存中释放模型,调用 ModelManager.unuse("resources/models/model1.m");
系统是多线程的,以避免在加载大型资产时帧率卡顿。
用代码实现很简单
缺点:
忘记对资产调用 'unuse' 将导致资产的 'users' 计数永久停留在 0 以上,因此它永远不会被卸载,即使它不在采用。虽然这不是一个主要问题,因为资产保留在字典中并且不会被加载两次,但对于太大而无法放入内存的大型开放世界,这可能是一个问题。 这个系统是设计有问题还是我应该接受它?我是否应该添加一个 'memoryPurge' 方法以便在游戏状态之间每隔一段时间调用一次? 不确定如何处理。
纹理和网格对象要么完全加载,要么根本不加载。中间没有任何阶段。我什至不确定如何实现这样的部分加载。例如,我希望能够加载一个对象的不同 LOD 质量,并随着对象在场景中越来越近(例如远处的建筑物)而逐渐加载更高质量的对象。我还希望能够以类似的方式控制资产质量,例如纹理。如果播放器在主菜单中更改了纹理的质量设置,那么如果我的资产系统能够自动提高或降低加载到内存中的资产的质量,那就太好了。 我应该如何将 'quality' 的概念融入我的资产中?
突然涌入我的加载线程的作业可能会阻塞很长时间。比如玩家突然传送到游戏的新区域,或者由于物理系统的错误而被抛到空中等等。这是我应该避免的事情吗?如果是这样,我应该在我的游戏代码(例如:最大玩家速度)还是在我的流媒体线程中这样做?
我看到有人设计他们的纹理流系统,以便在应用程序启动时加载每个纹理的 4px x 4px 版本,然后将 4px 版本的纹理用作占位符任何尚未加载的纹理,因此什么都没有 'missing',最坏的情况下总是以非常低的质量存储。 这值得研究吗?如果值得,您将如何实施这样的系统?
关于这个概念,首先想到的是从数千个单独的文件中加载如此微小的数据,然后对每个文件进行 jpeg 解压缩,这太慢了吧? 一个特殊的 'texture_preview.cache' 文件是否是存储该数据并将已解压缩的数据直接加载到内存中以便快速加载的好主意?
结论:
这是迄今为止我能想到的最好的,但我脑子里还有很多未知数。我至少在正确的轨道上吗?我知道我已经问了很多问题,但我并不是在寻找每个问题的答案。任何解决我的部分或大部分查询的答案都将被接受。在我开始实施它之前,我主要只是在寻找新的方向来考虑完成这个概念。或者来自经验丰富的大师的警告,他们在我之前走过这些路,知道龙在哪里。
虽然这些都是很好的问题,但您确实在这里问了太多问题。
我可以回答的一个方面是您的引用计数 - 不要那样做。利用垃圾收集器,所有工作 Java 已经投入其中。
只需让您的中央存储对加载的数据保持 SoftReference
或 WeakReference
。如果该引用已变为 null,那么您将需要在下次被要求时重新加载它。 Java 将在需要内存时自动对不需要的内容进行垃圾回收。您不需要引用计数或任何其他内容。