Як уникнути ігрових об’єктів, випадково видаливши себе в 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 в кінці методу камікадзе, а не на початку?
Хакворт

@Hackworth, який би працював для цього конкретного прикладу, але я шукаю більш загальне рішення. Я хочу мати можливість публікувати події з будь-якого місця і не боятися спричиняти збої.
Том Даллінг

Ви також можете поглянути на реалізацію autoreleaseв Objective-C, де видалення затримано до "лише трохи".
Кріс Берт-Браун

Відповіді:


40

Не видаляйте this

Навіть неявно.

- Колись -

Видалення об'єкта, поки одна з його функцій-членів все ще знаходиться у стеку, викликає проблеми. Будь-яка архітектура коду, яка призводить до того, що трапляється ("випадково" чи ні), об'єктивно погана , небезпечна , і її слід негайно відновити . У цьому випадку, якщо вашому монстрові буде дозволено дзвонити "Світ :: handleEvent", ні в якому разі не видаляйте монстра всередині цієї функції!

(У цій конкретній ситуації мій звичайний підхід полягає в тому, щоб монстр встановив «мертвий» прапор на себе, а об’єкт «Світ» - або щось подібне до нього - протестував цей «мертвий» прапор один раз на кадр, видаляючи ці об’єкти зі свого списку об'єктів у світі, або видалення або повернення його до пулу монстрів або будь-якого іншого. У цей час світ також надсилає сповіщення про видалення, тому інші об’єкти у світі знають, що монстр має припинила існування і може кинути на нього будь-які вказівники, які вони могли б утримувати. Світ це робить у безпечний час, коли він знає, що жоден об’єкт не обробляє в даний час, тож вам не доведеться турбуватися про те, щоб стек розгорнувся до точки де вказівник "цей" вказує на звільнену пам'ять.)


6
Друга половина вашої відповіді в дужках була корисною. Опитування прапора є хорошим рішенням. Але я питав, як уникнути цього, а не чим це добре чи погано робити. Якщо питання задає питання " Як я можу уникнути випадкового використання X? ", А ваша відповідь " Ніколи не роби X, навіть випадково ", виділеною жирним шрифтом, це насправді не відповідає на питання.
Том Даллінг

7
Я стою за коментарями, які я зробив у першій половині своєї відповіді, і я відчуваю, що вони повністю відповідають на питання як спочатку сформульовані. Важливим моментом, який я повторю тут, є те, що об’єкт не видаляє себе. Колись . Не закликає когось іншого видалити його. Колись . Натомість, вам потрібно мати щось інше, поза об’єктом, який є власником об'єкта і який відповідає за те, щоб помітити, коли об’єкт потрібно знищити. Це не річ "просто, коли монстр помирає"; це для всіх кодів C ++ завжди, скрізь, на всі часи. Немає винятків.
Тревор Пауелл

3
@TrevorPowell Я не кажу, що ви помиляєтесь. Насправді я з вами згоден. Я просто кажу, що насправді не відповідає на запитання, яке було задано. Це так, як якщо б ви запитали мене " Як я можу отримати звук у своїй грі? ", А моя відповідь була " Я не можу повірити, що у вас немає аудіо. Введіть аудіо в свою гру прямо зараз ". Потім в дужках внизу я поставити " (Можна використовувати FMOD) ", що є фактичною відповіддю.
Том Даллінг

6
@TrevorPowell Тут ви помиляєтесь. Це не "просто дисципліна", якщо я не знаю жодної альтернативи. Приклад коду, який я дав, чисто теоретичний. Я вже знаю, що це поганий дизайн, але мій C ++ іржавий, тому я подумав, що запитаю тут про кращі конструкції, перш ніж я дійсно кодую те, що хочу. Тож я прийшов запитати про альтернативні конструкції. " Додати прапор видалення " - це альтернатива. " Ніколи не роби цього " не є альтернативою. Це просто розповісти мені те, що я вже знаю. Схоже, що ви написали відповідь, не читаючи запитання належним чином.
Том Даллінг

4
@Bobby Питання "Як НЕ зробити X". Просто сказати "НЕ робіть Х" - ​​це нікчемна відповідь. Якби питання було "Я робив X" або "Я думав зробити X" або будь-який варіант цього, він би відповідав параметрам мета-дискусії, але не в її теперішній формі.
Джошуа Дрейк

21

Замість того, щоб чергувати події в буфері, чергуйте видалення в буфері. Затримка-видалення може потенційно значно спростити логіку; ви можете фактично звільнити пам'ять в кінці або на початку кадру, коли вам відомо, що з вашими об’єктами нічого цікавого не відбувається, і видалити з будь-якого місця.


1
Смішно ви згадуєте про це, тому що я думав, як приємно було б в NSAutoreleasePoolкомпанії "Об'єктив-С". Можливо, потрібно створити DeletionPoolшаблони C ++ чи щось таке.
Том Даллінг

@TomDalling Одне слід бути обережним, якщо ви робите буфер зовнішнім об’єктом, це те, що об’єкт може захотіти видалити з кількох причин на одному кадрі, і його можна буде спробувати видалити кілька разів.
Джон Калсбек

Дуже правильно. Мені доведеться тримати покажчики в std :: set.
Том Даллінг

5
Замість того, щоб буфер об’єктів видаляти, ви також можете просто встановити прапор в об'єкті. Як тільки ви почнете розуміти, наскільки ви хочете уникати виклику нових або видалення під час виконання та переходите до пулу об'єктів, це буде і простішим, і швидшим.
Шон Міддлічч

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 це дійсно залежить, принаймні мені потрібно було використовувати tr1 в gcc. але в VC в цьому не було потреби.
Ali1S232

2

Використовуйте об'єднання та фактично не видаляйте об'єкти. Натомість змініть структуру даних, до якої вони зареєстровані. Наприклад, для візуалізації об'єктів є об'єкт сцени, і всі об'єкти, якимось чином зареєстровані в ньому для візуалізації, виявлення зіткнень тощо. Замість видалення об'єкта від'єднайте його від місця події та вставіть у пул мертвих об’єктів. Цей метод не тільки запобігає проблемам із пам'яттю (наприклад, видалення самого об'єкта), але також може прискорити гру, якщо ви правильно використовуєте пул.


1

Що ми робили в грі - це використання місця розташування нове

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

eventPool був просто великим масивом пам'яті, яка була вирізана, і вказівник на кожен сегмент зберігався. Отже, alloc () повертає адресу вільного блоку пам'яті. У нашому eventPool пам'ять трактувалася як стек, тому після відправлення всіх подій ми просто скинули вказівник стека назад до початку масиву.

Через те, як працювала наша система подій, нам не потрібно було викликати деструкторів на евенах. Таким чином пул просто зареєстрував би блок пам'яті як безкоштовний і розподілив би його.

Це дало нам величезну швидкість.

Крім того ... Ми фактично використовували пули пам'яті для всіх динамічно розподілених об'єктів, що заперечуються в процесі розвитку, оскільки це був чудовий спосіб пошуку витоків пам'яті, якщо в пулах залишилися якісь об'єкти, коли гра виходила (як правило), то, швидше за все, це було пам’ятний витік.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.