Как избежать случайного удаления игровых объектов в C ++

20

Допустим, в моей игре есть монстр, который может взорвать камикадзе на игрока. Давайте наугад выберем имя для этого монстра: Creeper. Итак, у 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
ну, как насчет вызова postEvent в конце метода kamikaze, а не в начале?
Хакворт,
@ Хэкворт, который подойдет для этого конкретного примера, но я ищу более общее решение. Я хочу иметь возможность публиковать события из любого места и не бояться вызвать сбои.
Том Даллинг
Вы также можете взглянуть на реализацию autoreleaseв Objective-C, где удаления задерживаются до "чуть-чуть".
Крис Бёрт-Браун

Ответы:

40

Не удаляйте this

Даже неявно.

- Когда-либо -

Удаление объекта, пока одна из его функций-членов все еще находится в стеке, напрашивается на неприятности. Любая архитектура кода, которая приводит к такому случаю («случайно» или нет), объективно плоха , опасна и должна быть немедленно подвергнута рефакторингу . В этом случае, если вашему монстру будет разрешено вызывать World :: handleEvent, ни при каких обстоятельствах не удаляйте монстра внутри этой функции!

(В этой конкретной ситуации мой обычный подход состоит в том, чтобы монстр установил на себе флаг «мертвый», а объект World - или что-то в этом роде - проверял этот флаг «мертвый» один раз за кадр, удаляя эти объекты. из своего списка объектов в мире, и либо удаляя его, либо возвращая его в пул монстров, либо что угодно. В это время мир также рассылает уведомления об удалении, поэтому другие объекты в мире знают, что монстр имеет прекратил существование и может отбросить на него любые указатели, которые они могут содержать. Мир делает это в безопасное время, когда он знает, что в данный момент не обрабатываются никакие объекты, поэтому вам не нужно беспокоиться о том, что стек разворачивается в точку где указатель «this» указывает на свободную память.)

Тревор Пауэлл
источник
6
Вторая половина вашего ответа в скобках была полезной. Голосование за флаг является хорошим решением. Но я спрашивал, как избежать этого, а не было ли это хорошо или плохо. Если вопрос спрашивает « Как я могу избежать выполнения X случайно? », А ваш ответ « Никогда не делайте X никогда, даже случайно » жирным шрифтом, это на самом деле не отвечает на вопрос.
Том Даллинг
7
Я поддерживаю комментарии, которые я сделал в первой половине моего ответа, и я чувствую, что они полностью отвечают на вопрос в том виде, как он был сформулирован. Важный момент, который я повторю здесь, заключается в том, что объект не удаляется сам. Когда - либо . Это не вызывает кого-то еще, чтобы удалить его. Когда - либо . Вместо этого вам нужно иметь что-то еще, кроме объекта, которое владеет объектом и отвечает за уведомление, когда объект должен быть уничтожен. Это не просто «когда монстр умирает»; это для всего кода C ++ всегда, везде, на все времена. Без исключений.
Тревор Пауэлл
3
@TrevorPowell Я не говорю, что вы не правы. На самом деле я согласен с вами. Я просто говорю, что это на самом деле не отвечает на вопрос, который был задан. Это как если бы вы спросили меня « Как я могу добавить звук в мою игру? », И мой ответ был « Я не могу поверить, что у вас нет звука. Вставьте звук в вашу игру прямо сейчас». Затем в скобках внизу я поставить « (вы можете использовать FMOD) », который является фактическим ответом.
Том Даллинг
6
@TrevorPowell Это где вы не правы. Это не просто дисциплина, если я не знаю альтернатив. Код примера, который я привел, является чисто теоретическим. Я уже знаю, что это плохой дизайн, но мой C ++ ржавый, поэтому я подумал, что мне стоит спросить о лучших проектах, прежде чем я на самом деле кодирую то, что хочу. Поэтому я пришел спросить об альтернативных проектах. « Добавить флаг удаления » является альтернативой. « Никогда не делай этого » не является альтернативой. Это просто говорит мне то, что я уже знаю. Такое ощущение, что вы написали ответ, не прочитав вопрос должным образом.
Том Даллинг
4
@Bobby Вопрос «Как НЕ делать X». Просто сказать «НЕ делай Х» - бесполезный ответ. Если бы вопрос был: «Я делал X» или «Я думал о том, чтобы сделать X» или любой другой вариант этого, тогда он соответствовал бы параметрам мета-обсуждения, но не в его нынешнем виде.
Джошуа Дрейк
21

Вместо того, чтобы ставить в очередь события в буфере, ставьте в очередь удаления в буфере. Отложенное удаление может значительно упростить логику; вы можете на самом деле освободить память в конце или начале кадра, когда вы знаете, что с вашими объектами ничего интересного не происходит, и удалить из любого места.

Джон Калсбек
источник
1
Забавно, что вы упомянули об этом, потому что я думал о том, как хорошо в NSAutoreleasePoolObjective-C будет в этой ситуации. Возможно, придется сделать DeletionPoolс C ++ шаблоны или что-то.
Том Даллинг
@TomDalling Единственное, на что следует обратить внимание, если вы сделаете буфер внешним по отношению к объекту, это то, что объект может быть удален по нескольким причинам в одном кадре, и можно будет попытаться удалить его несколько раз.
Джон Калсбек
Очень верно. Мне придется хранить указатели в std :: set.
Том Даллинг
5
Вместо буфера объектов для удаления вы также можете просто установить флаг в объекте. Как только вы начнете понимать, сколько вы хотите избежать вызова new или delete во время выполнения и перейти к пулу объектов, это будет и проще, и быстрее.
Шон Мидлдич
4

Вместо того, чтобы позволить миру обрабатывать удаление, вы могли бы позволить экземпляру другого класса служить в качестве корзины для хранения всех удаленных объектов. Этот конкретный экземпляр должен прослушивать ENTITY_DEATHсобытия и обрабатывать их так, чтобы он ставил их в очередь. Затем они Worldмогут выполнить итерации по этим экземплярам и выполнить операции после смерти после рендеринга кадра и «очистить» этот сегмент, который, в свою очередь, будет выполнять фактическое удаление экземпляров объектов.

Пример такого класса будет выглядеть так: http://ideone.com/7Upza

Vite Falcon
источник
+1, это альтернатива помечению сущностей напрямую. Точнее, просто имейте живой список и мертвый список сущностей прямо в Worldклассе.
Лоран Кувиду
2

Я бы предложил реализовать фабрику, которая используется для всех распределений игровых объектов в игре. Таким образом, вместо того, чтобы называть себя новым, вы бы сказали фабрике создать что-то для вас.

Например

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

Всякий раз, когда вы хотите удалить объект, фабрика помещает объект в буфер, который очищается в следующем кадре. Задержка уничтожения очень важна в большинстве сценариев.

Также рассмотрите возможность отправки всех сообщений с задержкой в ​​один кадр. Существует только пара исключений, когда вам нужно отправить немедленно, в подавляющем большинстве случаев, однако просто

Millianz
источник
2

Вы можете реализовать управляемую память в C ++ самостоятельно, поэтому при ENTITY_DEATHвызове все, что происходит, - это уменьшение количества ссылок на него.

Позже, как @John предложил в начале каждого кадра, вы можете проверить, какие объекты бесполезны (те, с нулевыми ссылками), и удалить их. Например, вы можете использовать boost::shared_ptr<T>( задокументировано здесь ) или если вы используете C ++ 11 (VC2010)std::tr1::shared_ptr<T>

Ali1S232
источник
Просто std::shared_ptr<T>не технические отчеты! - Вам нужно будет указать пользовательский удалитель, иначе он также удалит объект немедленно, когда счетчик ссылок достигнет нуля.
оставил около
1
@ leftaroundabout, это действительно зависит, по крайней мере, мне нужно было использовать tr1 в gcc. но в ВК это не было необходимости.
Ali1S232
2

Используйте пул и на самом деле не удаляйте объекты. Вместо этого измените структуру данных, в которой они зарегистрированы. Например, для рендеринга объектов есть объект сцены и все сущности, каким-то образом зарегистрированные в нем для рендеринга, обнаружения столкновений и т. Д. Вместо удаления объекта отсоедините его от сцены и вставьте в пул мертвых объектов. Этот метод не только предотвратит проблемы с памятью (например, удаление самого объекта), но также может ускорить вашу игру, если вы правильно используете пул.

Hevi
источник
1

В игре мы использовали новое размещение

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

eventPool был просто большим массивом памяти, который был разделен, и указатель на каждый сегмент был сохранен. Так что alloc () вернет адрес свободного блока памяти. В нашем eventPool память обрабатывается как стек, поэтому после отправки всех событий мы просто сбрасываем указатель стека обратно в начало массива.

Из-за того, как работает наша система событий, нам не нужно было вызывать деструкторов на evetns. Таким образом, пул просто зарегистрирует блок памяти как свободный и выделит его.

Это дало нам огромную скорость.

Кроме того ... Мы фактически использовали пулы памяти для всех динамически выделяемых объектов в процессе разработки, так как это был отличный способ обнаружения утечек памяти, если в пуле оставались какие-либо объекты при выходе из игры (обычно), то, скорее всего, утечка памяти.

PhilCK
источник