如何避免游戏对象在C ++中意外删除自身


20

假设我的游戏中有一个怪物,可以让神风敢死队在玩家身上爆炸。让我们随机选择一个怪物的名字:爬行者。因此,Creeper该类具有一个类似于以下内容的方法:

void Creeper::kamikaze() {
    EventSystem::postEvent(ENTITY_DEATH, this);

    Explosion* e = new Explosion;
    e->setLocation(this->location());
    this->world->addEntity(e);
}

这些事件未排队,将立即分派。这会导致该Creeper对象在调用的内部某处被删除postEvent。像这样:

void World::handleEvent(int type, void* context) {
    if(type == ENTITY_DEATH){
        Entity* ent = dynamic_cast<Entity*>(context);
        removeEntity(ent);
        delete ent;
    }
}

由于Creeper对象在kamikaze方法仍在运行时会被删除,因此在尝试访问时它将崩溃this->location()

一种解决方案是将事件排队到缓冲区中,然后再分派它们。这是C ++游戏中的常见解决方案吗?感觉有点像破解,但这可能只是因为我在具有不同内存管理实践的其他语言上的经验。

在C ++中,对于对象从其方法之一内部意外删除自身的问题,是否有更好的一般解决方案?


6
呃,如何在kamikaze方法的END而不是开始处调用postEvent?
Hackworth '04

@Hackworth适用于此特定示例,但我正在寻找更通用的解决方案。我希望能够从任何地方发布事件,并且不害怕导致崩溃。
Tom Dalling 2012年

您还可以看一下autoreleaseObjective-C 中的实现,在该实现中,删除操作会持续到“稍稍”。
克里斯·伯特·布朗

Answers:


40

不要删除 this

甚至是隐含的。

-曾经-

在对象的成员函数之一仍在堆栈中时删除该对象很麻烦。任何导致这种情况(无论是否“偶然地”发生)的代码体系结构在客观上都是不好的,是危险的,应立即进行重构。在这种情况下,如果将允许您的怪物调用“ World :: handleEvent”,则在任何情况下都不要删除该函数中的怪物!

(在这种特定情况下,我通常的方法是让怪物在其自身上设置一个'dead'标志,并让'World'对象-或类似的东西-每帧测试一次'dead'标志,然后移除这些对象从世界上的对象列表中删除该对象或将其返回到怪物池或其他合适的对象。这时,该世界还会发出有关删除的通知,因此世界上的其他对象都知道该怪物具有停止存在,并可以删除它们可能持有的所有指针。当世界知道它当前没有正在处理的对象时,世界会在安全的时候这样做,因此您不必担心堆栈会逐渐消失“ this”指针指向已释放的内存的位置。)


6
括号中答案的后半部分很有帮助。轮询标志是一个很好的解决方案。但是,我在问如何避免这样做,而不是做一件好事还是坏事。如果一个问题以粗体显示“ 我如何避免意外地使用X? ”,而您的答案是“ 永远也不要意外地使用X ”,那实际上并没有回答问题。
汤姆·达林

7
我支持我在回答的上半部分中所做的评论,并且我确实认为他们完全按照最初的措辞回答了这个问题。我将在这里重复的重点是,对象不会删除自身。 曾经。它不会调用其他人将其删除。 曾经。取而代之的是,您需要在对象外部有其他东西,这些东西拥有该对象,并负责通知何时需要销毁该对象。这不是“怪物死后”。这适用于所有C ++代码,始终无处不在,无时无刻。 没有例外。
特雷弗·鲍威尔

3
@TrevorPowell我并不是说你错了。实际上,我同意你的看法。我只是说它实际上并没有回答所提出的问题。就像您是否问我“我如何在游戏中获取音频? ”,我的回答是“ 我不敢相信您没有音频。请立即在游戏中添加音频。 ”然后在括号中打底输入“ (您可以使用FMOD) ”,这是一个实际答案。
Tom Dalling 2012年

6
@TrevorPowell这是您错的地方。如果我不知道其他选择,那不是“公正的纪律”。我给出的示例代码纯粹是理论上的。我已经知道这是一个糟糕的设计,但是我的C ++生锈了,所以我想在实际编写所需代码之前,我会在这里询问更好的设计。因此,我来​​询问替代设计。“ 添加删除标记 ”是替代方法。“ 从不做不是替代方案。只是告诉我我已经知道的。感觉好像您在未正确阅读问题的情况下写下了答案。
Tom Dalling 2012年

4
@Bobby问题是“如何不做X”。简单地说“不要做X”是毫无意义的答案。如果问题是“我一直在做X”或“我正在考虑做X”或任何其他变体,那么它将满足meta讨论的参数,但不是现在的形式。
约书亚·德雷克

21

与其将事件排队在缓冲区中,不如将缓冲区中的删除排队。延迟删除有可能大大简化逻辑。当您知道对象没有发生任何有趣的事情时,实际上可以释放帧末尾的内存,并从绝对任何位置删除。


1
有趣的是您提到这一点,因为我在想NSAutoreleasePool在这种情况下来自Objective-C的效果如何。可能必须DeletionPool使用C ++模板之类的东西。
Tom Dalling

@TomDalling如果将缓冲区置于对象外部,要注意的一件事是,出于多种原因,可能希望在单个帧上删除对象,并且可以尝试多次删除它。
John Calsbeek'4

非常真实 我将必须将指针保留在std :: set中。
Tom Dalling'4

5
除了要删除的对象的缓冲区外,您还可以在对象中设置一个标志。一旦开始意识到要避免运行时调用new或delete并转移到对象池的数量,这将变得更加简单和快捷。
肖恩·米德迪奇

4

不必让世界来处理删除,您可以让另一个类的实例充当存储所有删除的实体的存储桶。这个特定的实例应该听ENTITY_DEATH事件并对其进行处理,以便事件排队。在World随后迭代在这种情况下,可以进行后死亡运营框架已经呈现和“清除”这一桶,这反过来将执行实体的实际实例删除后。

此类的示例如下:http : //ideone.com/7Upza


+1,这是直接标记实体的替代方法。更直接地,只需在World类中直接拥有一个活动列表和无效列表的实体。
劳伦·库维杜

2

我建议建立一个工厂,该工厂用于游戏中所有游戏对象的分配。因此,您可以告诉工厂为您创建一些东西,而不必自己叫新东西。

例如

Object* o = gFactory->Create("Explosion");

每当您要删除对象时,工厂都会将该对象推入缓冲区,该缓冲区将清除下一帧。在大多数情况下,延迟销毁非常重要。

还应考虑延迟一帧发送所有消息。您只需要立即发送几个例外,绝大多数情况下


2

您可以自己用C ++实现托管内存,以​​便在 ENTITY_DEATH调用,发生的只是其引用数减少了一个。

稍后@John在每帧的开头建议时,您可以检查哪些实体无用(那些零引用实体)并将其删除。例如,您可以使用boost::shared_ptr<T>在此处记录),或者如果您使用的是C ++ 11(VC2010)std::tr1::shared_ptr<T>


只是std::shared_ptr<T>,不是技术报告!—您将必须指定一个自定义删除器,否则当引用计数达到零时,它也将立即删除该对象。
大约

1
@leftaroundabout确实取决于,至少我需要在gcc中使用tr1。但在VC中,则不需要这样做。
Ali1S232

2

使用池化,实际上不删除对象。而是更改它们注册到的数据结构。例如,对于渲染对象,有一个场景对象,所有实体都以某种方式注册到该对象以进行渲染,碰撞检测等。与其删除该对象,不如将其从场景中分离出来,然后插入一个失效的对象池中。如果正确使用池,此方法不仅可以防止内存问题(例如对象自身删除),而且可以加快游戏速度。


1

我们在游戏中所做的就是使用新的展示位置

SomeEvent* obj = new(eventPool.alloc()) new SomeEvent();

eventPool只是一个很大的内存阵列,它被分割,并存储了指向每个段的指针。因此alloc()将返回空闲内存块的地址。在我们的eventPool中,内存被视为堆栈,因此在发送完所有事件之后,我们只需将堆栈指针重置回数组的开头即可。

由于事件系统的工作方式,我们不需要在evetns上调用析构函数。因此,池将简单地将内存块注册为空闲,并对其进行分配。

这极大地加快了我们的速度。

还...我们实际上对开发中所有动态分配的对象使用了内存池,因为这是查找内存泄漏的好方法,如果游戏退出时(正常情况下)如果池中还有任何对象,那么很可能存在内存泄漏。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.