可以失效的资源的 RAII
RAII for resources that can be invalidated
我是一个业余爱好者 C++
和 DirectX
程序员,所以我拥有的大部分知识都来自旧的游戏开发书籍,其中的代码设计只是为了得到一些东西 运行 作为演示,即使是最简单的程序也给我留下了很多设计考虑因素。在开发这样一个程序的过程中,我最近了解到 RAII
,所以我决定尝试一下这个设计模式,因为据我了解,一个对象在构造时应该是可用和有效的,这大大简化了对象可以被程序使用。以前,我一直在我的一些对象中使用 create()
& destroy()
模式,这导致在大多数成员函数中进行大量验证检查。
在程序的体系结构中,我有很少的图形对象是 DirectX
资源的包装器,其中之一是 Texture
对象。例如,如果我想渲染瓦片地图,我可以使用指向 AnimatedImage
对象的指针构建 Tile
个对象,这些对象使用指向 Texture
个对象的指针构建。
使用DirectX
的问题是图形设备有时会丢失,例如在程序执行期间更新视频卡的驱动程序。当这些事件发生时,必须释放并重新获取现有的图形资源才能继续正常渲染,包括 Texture
对象的销毁和重建。这使得 RAII
设计模式的使用看起来可能不是最佳选择。我需要重新创建 Texture
个对象,重新创建 AnimatedImage
个对象,然后重新创建 Tile
个对象。这似乎非常麻烦,因为重新创建的某些对象将包含的不仅仅是图像数据。
因此,如果我们从一些示例代码开始(不准确,但可以达到目的):
// Construct graphics objects into some structure we will pass around.
graphics.pDevice = new GraphicsDevice(windowHandle, screenWitdth, screenHeight);
graphics.pTexture1 = new Texture(graphics.pDevice, width1, height1, pPixelData1);
graphics.pTexture2 = new Texture(graphics.pDevice, width2, height2, pPixelData2);
graphics.pSpriteBuffer = new SpriteBuffer(graphics.pDevice, maxSprites);
在为瓦片地图构建对象的程序中的其他地方:
// Construct some in-game animations.
images.pGrass = new AnimatedImage(graphics.pTexture1, coordinates1[4], duration1);
images.pWater = new AnimatedImage(graphics.pTexture2, coordinates2[4], duration2);
// Construct objects to display the animation and contain physical attributes.
thisMap.pMeadowTile = new Tile(images.pGrass, TILE_ATTRIBUTE_SOFT);
thisMap.pPondTile = new Tile(images.pWater, TILE_ATTRIBUTE_SWIMMABLE);
然后在渲染过程中:
while (gameState.isRunning())
{
graphics.pSpriteBuffer->clear();
thisMap.bufferSprites(graphics.pSpriteBuffer);
graphics.pSpriteBuffer->draw();
if (graphics.pDevice->present() == RESULT_COULD_NOT_COMPLETE)
{
// Uh oh! The device has been lost!
// We need to release and recreate all graphics objects or we cannot render.
// Let's destruct the sprite buffer, textures, and graphics device.
// But wait, our animations were constructed with textures, the pointers are
// no longer valid and must be destructed.
// Come to think of it, the tiles were created with animations, so they too
// must be destructed, which is a hassle since their physical attributes
// really are unrelated to the graphics.
// Oh no, what other objects have graphical dependencies must we consider?
}
}
我是否在这里遗漏了一些设计概念,或者这是 RAII
工作的情况之一,但如果存在大量对象到对象的依赖性,则成本过高?是否有任何已知的设计模式专门适用于这种情况?
以下是我想到的一些方法:
为图形对象配备recreate()
方法。优点是任何指向纹理的对象都可以保留该指针而不会被销毁。缺点是如果重新获取失败,我会留下一个僵尸对象,它并不比 create()
& destroy()
模式好。
使用所有图形对象的注册表添加间接级别,这些对象将 return 指向 Texture
指针或指向 Texture
指针的索引这样就不需要销毁依赖图形的现有对象。优点和缺点与上面相同,但增加了间接开销的缺点。
存储程序的当前状态并回退直到图形对象被重新获取,然后以它所在的状态重建程序。我想不出真正的优势,但似乎最 RAII
合适。缺点是在不太常见的情况下实现它的复杂性。
将对象的所有视觉表示与其物理表示完全分离。优点是实际上只有需要重新创建的对象是有效的,这可以使程序的其余部分保持有效状态。缺点是物理和视觉对象仍然需要以某种方式相互了解,这可能会导致一些臃肿的对象管理代码。
中止程序执行。优点是这很容易,并且很少为不经常发生的事情花费很少的工作。缺点是使用该程序的任何人都会感到沮丧。
第一个解决方案
对于 2000 年代后期(至少对于桌面图形而言),这是一个很好的问题。在 2015 年,你最好忘记 DirectX 9,因为它的设备丢失了,转而使用 DirectX 11(甚至即将推出的 DirectX 12)。
第二种解决方案
如果您仍然想坚持使用已弃用的 API(或者如果您同时在移动设备上使用类似 OpenGL ES 的东西,其中上下文丢失是常见事件),有一种方法非常有效好吧(等等)。基本上都是你的混搭
Equip the graphics objects with a recreate() method
和
Add a level of indirection using a registry of all graphics objects
这里是:
使用 Factory pattern 重构您的代码:强制用户使用函数分配新资源(包装 new
、std::make_shared
或您在其中使用的任何内容)
auto resource = device->createResource(param0, param1);
让工厂以某种方式记住资源
std::vector<IResourcePtr> resources;
ResourcePtr Device::createResource(T param0, U param1)
{
auto resource = std::make_shared<Resource>(this, param0, param1);
resources.push_back(resource);
return resource;
}
让资源记住它的参数(如果需要,可以在运行时更改它们,但也应该保存。对于大的或昂贵的参数对象,使用 Proxy pattern)
Resource::Resource(IDevice* device, T param0, U param1)
: m_device(device)
, m_param0(param0)
, m_param1(param1)
{
create(); // private
}
在设备丢失事件中,释放所有对象,然后重新创建它们
while (rendering)
{
device->fixIfLost();
...
}
void Device::fixIfLost()
{
if(isLost())
{
for(auto&& resource: resources)
resource->reload();
}
}
void Resource::reload()
{
release(); // private
create(); // private
}
您可以在此基础上构建更复杂、更智能的系统。
相关说明:
The disadvantage is that if the reacquisition fails, I would be left
with a zombie object
它不是特定于设备丢失事件。在放弃对用户的控制权之前立即处理资源失败,这与您第一次创建资源并失败时所做的方式相同(通过抛出异常(以便用户可以处理它),或者通过使用占位符资源或通过关闭应用程序,或其他任何东西——你来决定)
Completely segregate all visual representations of objects from their
physical representations.
必须有。甚至不是要讨论的问题,除非你正在构建俄罗斯方块。使用 MVC or modern stuff like ECS。切勿将 Mesh
存储在 Player
中,将 ParticleEmitter
存储在 Fireball
中。永远不要让他们互相认识。
Store the current state of the program and unwind back until the
graphics objects have been reacquired, then rebuild the program in the
state it was in
非常有用。您所描述的是 "save game" / "load game" 机制。它还可以用于实现 "replay" 功能和游戏引擎影片。请注意(添加到第 2 点),您的保存数据将永远不会包含视觉表示(除非您想要数 GB 的保存文件)。
不要overengineer。大多数游戏根本不会打扰。他们处理设备丢失的方式与用户处理设备丢失的方式相同 "Save game" -> "Exit" -> "Load game",适当设计启动设施。
另一种单独使用或与工厂结合使用的方法是Lazy initialization:让你的资源验证它本身是否有效以及设备是否丢失。
void Resource::apply()
{
if((!isValid()) || (!device->isValid()))
{
}
// apply resource here
}
每次访问资源时都会增加一些开销,但它是一种安全且实施起来非常简单的方法,可确保您的资源在需要时启动
我是一个业余爱好者 C++
和 DirectX
程序员,所以我拥有的大部分知识都来自旧的游戏开发书籍,其中的代码设计只是为了得到一些东西 运行 作为演示,即使是最简单的程序也给我留下了很多设计考虑因素。在开发这样一个程序的过程中,我最近了解到 RAII
,所以我决定尝试一下这个设计模式,因为据我了解,一个对象在构造时应该是可用和有效的,这大大简化了对象可以被程序使用。以前,我一直在我的一些对象中使用 create()
& destroy()
模式,这导致在大多数成员函数中进行大量验证检查。
在程序的体系结构中,我有很少的图形对象是 DirectX
资源的包装器,其中之一是 Texture
对象。例如,如果我想渲染瓦片地图,我可以使用指向 AnimatedImage
对象的指针构建 Tile
个对象,这些对象使用指向 Texture
个对象的指针构建。
使用DirectX
的问题是图形设备有时会丢失,例如在程序执行期间更新视频卡的驱动程序。当这些事件发生时,必须释放并重新获取现有的图形资源才能继续正常渲染,包括 Texture
对象的销毁和重建。这使得 RAII
设计模式的使用看起来可能不是最佳选择。我需要重新创建 Texture
个对象,重新创建 AnimatedImage
个对象,然后重新创建 Tile
个对象。这似乎非常麻烦,因为重新创建的某些对象将包含的不仅仅是图像数据。
因此,如果我们从一些示例代码开始(不准确,但可以达到目的):
// Construct graphics objects into some structure we will pass around.
graphics.pDevice = new GraphicsDevice(windowHandle, screenWitdth, screenHeight);
graphics.pTexture1 = new Texture(graphics.pDevice, width1, height1, pPixelData1);
graphics.pTexture2 = new Texture(graphics.pDevice, width2, height2, pPixelData2);
graphics.pSpriteBuffer = new SpriteBuffer(graphics.pDevice, maxSprites);
在为瓦片地图构建对象的程序中的其他地方:
// Construct some in-game animations.
images.pGrass = new AnimatedImage(graphics.pTexture1, coordinates1[4], duration1);
images.pWater = new AnimatedImage(graphics.pTexture2, coordinates2[4], duration2);
// Construct objects to display the animation and contain physical attributes.
thisMap.pMeadowTile = new Tile(images.pGrass, TILE_ATTRIBUTE_SOFT);
thisMap.pPondTile = new Tile(images.pWater, TILE_ATTRIBUTE_SWIMMABLE);
然后在渲染过程中:
while (gameState.isRunning())
{
graphics.pSpriteBuffer->clear();
thisMap.bufferSprites(graphics.pSpriteBuffer);
graphics.pSpriteBuffer->draw();
if (graphics.pDevice->present() == RESULT_COULD_NOT_COMPLETE)
{
// Uh oh! The device has been lost!
// We need to release and recreate all graphics objects or we cannot render.
// Let's destruct the sprite buffer, textures, and graphics device.
// But wait, our animations were constructed with textures, the pointers are
// no longer valid and must be destructed.
// Come to think of it, the tiles were created with animations, so they too
// must be destructed, which is a hassle since their physical attributes
// really are unrelated to the graphics.
// Oh no, what other objects have graphical dependencies must we consider?
}
}
我是否在这里遗漏了一些设计概念,或者这是 RAII
工作的情况之一,但如果存在大量对象到对象的依赖性,则成本过高?是否有任何已知的设计模式专门适用于这种情况?
以下是我想到的一些方法:
为图形对象配备
recreate()
方法。优点是任何指向纹理的对象都可以保留该指针而不会被销毁。缺点是如果重新获取失败,我会留下一个僵尸对象,它并不比create()
&destroy()
模式好。使用所有图形对象的注册表添加间接级别,这些对象将 return 指向
Texture
指针或指向Texture
指针的索引这样就不需要销毁依赖图形的现有对象。优点和缺点与上面相同,但增加了间接开销的缺点。存储程序的当前状态并回退直到图形对象被重新获取,然后以它所在的状态重建程序。我想不出真正的优势,但似乎最
RAII
合适。缺点是在不太常见的情况下实现它的复杂性。将对象的所有视觉表示与其物理表示完全分离。优点是实际上只有需要重新创建的对象是有效的,这可以使程序的其余部分保持有效状态。缺点是物理和视觉对象仍然需要以某种方式相互了解,这可能会导致一些臃肿的对象管理代码。
中止程序执行。优点是这很容易,并且很少为不经常发生的事情花费很少的工作。缺点是使用该程序的任何人都会感到沮丧。
第一个解决方案
对于 2000 年代后期(至少对于桌面图形而言),这是一个很好的问题。在 2015 年,你最好忘记 DirectX 9,因为它的设备丢失了,转而使用 DirectX 11(甚至即将推出的 DirectX 12)。
第二种解决方案
如果您仍然想坚持使用已弃用的 API(或者如果您同时在移动设备上使用类似 OpenGL ES 的东西,其中上下文丢失是常见事件),有一种方法非常有效好吧(等等)。基本上都是你的混搭
Equip the graphics objects with a recreate() method
和
Add a level of indirection using a registry of all graphics objects
这里是:
使用 Factory pattern 重构您的代码:强制用户使用函数分配新资源(包装
new
、std::make_shared
或您在其中使用的任何内容)auto resource = device->createResource(param0, param1);
让工厂以某种方式记住资源
std::vector<IResourcePtr> resources; ResourcePtr Device::createResource(T param0, U param1) { auto resource = std::make_shared<Resource>(this, param0, param1); resources.push_back(resource); return resource; }
让资源记住它的参数(如果需要,可以在运行时更改它们,但也应该保存。对于大的或昂贵的参数对象,使用 Proxy pattern)
Resource::Resource(IDevice* device, T param0, U param1) : m_device(device) , m_param0(param0) , m_param1(param1) { create(); // private }
在设备丢失事件中,释放所有对象,然后重新创建它们
while (rendering) { device->fixIfLost(); ... } void Device::fixIfLost() { if(isLost()) { for(auto&& resource: resources) resource->reload(); } } void Resource::reload() { release(); // private create(); // private }
您可以在此基础上构建更复杂、更智能的系统。
相关说明:
The disadvantage is that if the reacquisition fails, I would be left with a zombie object
它不是特定于设备丢失事件。在放弃对用户的控制权之前立即处理资源失败,这与您第一次创建资源并失败时所做的方式相同(通过抛出异常(以便用户可以处理它),或者通过使用占位符资源或通过关闭应用程序,或其他任何东西——你来决定)
Completely segregate all visual representations of objects from their physical representations.
必须有。甚至不是要讨论的问题,除非你正在构建俄罗斯方块。使用 MVC or modern stuff like ECS。切勿将
Mesh
存储在Player
中,将ParticleEmitter
存储在Fireball
中。永远不要让他们互相认识。Store the current state of the program and unwind back until the graphics objects have been reacquired, then rebuild the program in the state it was in
非常有用。您所描述的是 "save game" / "load game" 机制。它还可以用于实现 "replay" 功能和游戏引擎影片。请注意(添加到第 2 点),您的保存数据将永远不会包含视觉表示(除非您想要数 GB 的保存文件)。
不要overengineer。大多数游戏根本不会打扰。他们处理设备丢失的方式与用户处理设备丢失的方式相同 "Save game" -> "Exit" -> "Load game",适当设计启动设施。
另一种单独使用或与工厂结合使用的方法是Lazy initialization:让你的资源验证它本身是否有效以及设备是否丢失。
void Resource::apply() { if((!isValid()) || (!device->isValid())) { } // apply resource here }
每次访问资源时都会增加一些开销,但它是一种安全且实施起来非常简单的方法,可确保您的资源在需要时启动