Jak uniknąć przypadkowego usunięcia obiektów gry w C ++

20

Powiedzmy, że moja gra ma potwora, który może eksplodować kamikadze na odtwarzaczu. Wybierzmy losowo nazwę tego potwora: Pnącze. Tak więcCreeper klasa ma metodę, która wygląda mniej więcej tak:

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

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

Zdarzenia nie są w kolejce, są wysyłane natychmiast. Powoduje to, że Creeperobiekt zostaje usunięty gdzieś w wywołaniupostEvent . Coś takiego:

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

Ponieważ Creeper obiekt zostanie usunięty, gdy kamikazemetoda jest nadal uruchomiona, zawiesi się podczas próby uzyskania dostępu this->location().

Jednym z rozwiązań jest umieszczenie zdarzeń w kolejce w buforze i wysłanie ich później. Czy to jest powszechne rozwiązanie w grach C ++? To trochę hack, ale może to wynikać z mojego doświadczenia z innymi językami z różnymi praktykami zarządzania pamięcią.

Czy w C ++ istnieje lepsze ogólne rozwiązanie tego problemu, w którym obiekt przypadkowo usuwa się z jednej ze swoich metod?

Tom Dalling
źródło
6
A może nazywasz postEvent na KONIEC metody kamikadze zamiast na początku?
Hackworth,
@Hackworth, które by działały w tym konkretnym przykładzie, ale szukam bardziej ogólnego rozwiązania. Chcę móc publikować wydarzenia z dowolnego miejsca i nie bać się powodować awarii.
Tom Dalling
Możesz także rzucić okiem na implementację autoreleasew Objective-C, gdzie usuwanie jest wstrzymywane do „tylko trochę”.
Chris Burt-Brown,

Odpowiedzi:

40

Nie usuwaj this

Nawet domyślnie.

- Zawsze -

Usunięcie obiektu, gdy jedna z jego funkcji składowych jest nadal na stosie, prosi o kłopoty. Każda architektura kodu, która powoduje, że tak się dzieje („przypadkowo” lub nie) jest obiektywnie zła , jest niebezpieczna i należy ją natychmiast zrewidować . W takim przypadku, jeśli twój potwór będzie mógł nazywać „World :: handleEvent”, , w żadnym wypadku nie usuwaj potwora z tej funkcji!

(W tej konkretnej sytuacji moje zwykłe podejście polega na tym, aby potwór ustawił na sobie flagę „martwą” i zlecił obiektowi Świat - lub coś w tym stylu - przetestowanie tej flagi „martwej” raz na klatkę, usuwając te obiekty z listy obiektów na świecie i albo usuwając ją, albo zwracając do puli potworów lub cokolwiek innego. W tym czasie świat wysyła również powiadomienia o usunięciu, aby inne obiekty na świecie wiedziały, że potwór ma przestał istnieć i może upuścić dowolne wskaźniki, które mogliby trzymać. Świat robi to w bezpiecznym czasie, gdy wie, że żadne obiekty nie są obecnie przetwarzane, więc nie musisz się martwić, że stos odwija ​​się do punktu gdzie wskaźnik „ten” wskazuje na uwolnioną pamięć).

Trevor Powell
źródło
6
Druga połowa twojej odpowiedzi w nawiasie była pomocna. Odpytywanie o flagę jest dobrym rozwiązaniem. Ale pytałem, jak tego uniknąć, nawet jeśli to była dobra lub zła rzecz do zrobienia. Jeśli pytanie brzmi „ Jak uniknąć przypadkowego wykonania X? ”, A twoja odpowiedź brzmi „ Nigdy nie rób X, nawet przypadkowo ” pogrubioną czcionką, to tak naprawdę nie odpowiada na pytanie.
Tom Dalling
7
Opieram się na komentarzach, które poczyniłem w pierwszej połowie mojej odpowiedzi i czuję, że w pełni odpowiadają na pytanie w pierwotnej formie. Ważną kwestią, którą tu powtórzę, jest to, że obiekt sam się nie usuwa. Ever . Nie wzywa kogoś innego do usunięcia. Ever . Zamiast tego musisz mieć coś poza obiektem, który jest właścicielem obiektu i jest odpowiedzialny za zauważenie, kiedy obiekt musi zostać zniszczony. To nie jest rzecz „kiedy potwór umiera”; dotyczy to całego kodu C ++ zawsze, wszędzie i na zawsze. Bez wyjątków.
Trevor Powell,
3
@TrevorPowell Nie mówię, że się mylisz. W rzeczywistości zgadzam się z tobą. Mówię tylko, że tak naprawdę nie odpowiada na zadane pytanie. To tak, jakbyś zapytał mnie „ Jak włączyć dźwięk do mojej gry? ”, A moja odpowiedź brzmiała „ włączyć Nie mogę uwierzyć, że nie masz dźwięku. Umieść dźwięk w swojej grze. ” Następnie w nawiasach na dole I wpisz „ (możesz użyć FMOD) ”, co jest faktyczną odpowiedzią.
Tom Dalling
6
@TrevorPowell To tutaj się mylisz. To nie jest „tylko dyscyplina”, jeśli nie znam żadnych alternatyw. Przykładowy kod, który podałem, jest czysto teoretyczny. Wiem już, że to zły projekt, ale mój C ++ jest zardzewiały, więc pomyślałem, że zapytam o lepsze projekty, zanim zacznę kodować to, czego chcę. Przyszedłem więc zapytać o alternatywne projekty. „ Dodaj flagę usuwania ” jest alternatywą. „ Nigdy nie rób tegonie jest alternatywą. Po prostu mówi mi to, co już wiem. Wydaje się, że napisałeś odpowiedź bez prawidłowego przeczytania pytania.
Tom Dalling
4
@Bobby Pytanie brzmi „Jak NIE robić X”. Samo powiedzenie „NIE rób X” jest bezwartościową odpowiedzią. JEŻELI pytanie brzmiało: „Robiłem X” lub „Myślałem o zrobieniu X” lub w innym wariancie tego, to spełniałoby parametry meta dyskusji, ale nie w obecnej formie.
Joshua Drake
21

Zamiast umieszczać zdarzenia w kolejce w buforze, kolejkuj usunięcia w buforze. Opóźnione usuwanie może potencjalnie znacznie uprościć logikę; możesz faktycznie zwolnić pamięć na końcu lub na początku ramki, gdy wiesz, że nic ciekawego nie dzieje się z twoimi obiektami i usunąć z absolutnie dowolnego miejsca.

John Calsbeek
źródło
1
Zabawne, że o tym wspominasz, bo myślałem, jak miło NSAutoreleasePoolbyłoby w Objective-C w tej sytuacji. Może trzeba zrobić DeletionPoolz szablonami C ++ lub coś takiego.
Tom Dalling
@TomDalling Jedyną rzeczą, na którą należy uważać, jeśli ustawi się bufor zewnętrzny na obiekt, jest to, że obiekt może chcieć zostać usunięty z wielu powodów na jednej ramce, i można go spróbować kilka razy.
John Calsbeek,
Bardzo prawdziwe. Będę musiał trzymać wskaźniki w std :: set.
Tom Dalling
5
Zamiast bufora obiektów do usunięcia, możesz także ustawić flagę w obiekcie. Gdy zaczniesz zdawać sobie sprawę, ile chcesz uniknąć wywoływania new lub delete podczas uruchamiania i przejścia do puli obiektów, będzie to zarówno prostsze, jak i szybsze.
Sean Middleditch,
4

Zamiast pozwalać światu na usunięcie, możesz pozwolić instancji innej klasy służyć jako wiadro do przechowywania wszystkich usuniętych jednostek. Ta konkretna instancja powinna słuchaćENTITY_DEATH zdarzeń i obsługiwać je w taki sposób, aby umieszczały je w kolejce. Następnie Worldmogą iterować te instancje i wykonywać operacje po śmierci po renderowaniu ramki i „wyczyścić” ten segment, który z kolei wykonałby faktyczne usunięcie instancji jednostek.

Przykładem takiej klasy byłoby: http://ideone.com/7Upza

Vite Falcon
źródło
+1, to alternatywa dla bezpośredniego oznaczania jednostek. Mówiąc bardziej bezpośrednio, wystarczy mieć żywą listę i martwą listę bytów bezpośrednio w Worldklasie.
Laurent Couvidou,
2

Proponuję zaimplementować fabrykę używaną do przydzielania wszystkich obiektów w grze. Zamiast samemu nazywać nowego, powiedz fabryce, żeby coś dla ciebie stworzył.

Na przykład

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

Ilekroć chcesz usunąć obiekt, fabryka wypycha obiekt do bufora, który jest usuwany z następnej klatki. Opóźnione zniszczenie jest bardzo ważne w większości scenariuszy.

Rozważ również wysłanie wszystkich wiadomości z opóźnieniem jednej ramki. Jest tylko kilka wyjątków, które należy natychmiast wysłać, a zdecydowana większość przypadków jest jednak sprawiedliwa

Millianz
źródło
2

Możesz samodzielnie zarządzać pamięcią zarządzaną w C ++, aby kiedy ENTITY_DEATH zostanie wywołana, wszystko, co się stanie, to liczba odniesień zostanie zmniejszona o jeden.

Później, jak sugerował @John na początku każdej ramki, możesz sprawdzić, które encje są bezużyteczne (te z zerowymi referencjami) i usunąć je. Na przykład możesz użyć boost::shared_ptr<T>( tutaj udokumentowane ) lub jeśli używasz C ++ 11 (VC2010)std::tr1::shared_ptr<T>

Ali1S232
źródło
Po prostu std::shared_ptr<T>nie raporty techniczne! - Będziesz musiał określić niestandardowy usuwacz, w przeciwnym razie usunie on również obiekt natychmiast, gdy liczba odniesień osiągnie zero.
lewo około
1
@leftaroundabout to naprawdę zależy, przynajmniej potrzebowałem użyć tr1 w gcc. ale w VC nie było takiej potrzeby.
Ali1S232,
2

Użyj puli i nie usuwaj obiektów. Zamiast tego zmień strukturę danych, w której są zarejestrowani. Na przykład do renderowania obiektów istnieje obiekt sceny i wszystkie byty zarejestrowane w nim w celu renderowania, wykrywania kolizji itp. Zamiast usuwać obiekt, odłącz go od sceny i wstaw do puli martwych obiektów. Ta metoda nie tylko zapobiegnie problemom z pamięcią (np. Usunięciem obiektu), ale także może przyspieszyć grę, jeśli prawidłowo wykorzystasz pulę.

hevi
źródło
1

To, co zrobiliśmy w grze, to użycie nowego miejsca

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

eventPool to po prostu duża tablica pamięci, która została wykuta, a wskaźnik do każdego segmentu został zapisany. Więc replace () zwróci adres wolnego bloku pamięci. W naszym eventPool pamięć była traktowana jako stos, więc po wysłaniu wszystkich zdarzeń po prostu zresetowaliśmy wskaźnik stosu z powrotem do początku tablicy.

Ze względu na sposób działania naszego systemu zdarzeń nie musieliśmy wywoływać destruktorów w evetns. Tak więc pula po prostu zarejestruje blok pamięci jako wolny i przydzieli go.

To dało nam ogromne przyspieszenie.

Także ... Właściwie użyliśmy puli pamięci dla wszystkich dynamicznie przydzielanych obiektów w trakcie opracowywania, ponieważ był to świetny sposób na wykrycie wycieków pamięci, jeśli w puli pozostały jakieś obiekty po wyjściu gry (normalnie), prawdopodobnie było to możliwe wyciek mem.

PhilCK
źródło