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 Creeper
obiekt 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 kamikaze
metoda 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?
źródło
autorelease
w Objective-C, gdzie usuwanie jest wstrzymywane do „tylko trochę”.Odpowiedzi:
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ęć).
źródło
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.
źródło
NSAutoreleasePool
byłoby w Objective-C w tej sytuacji. Może trzeba zrobićDeletionPool
z szablonami C ++ lub coś takiego.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ępnieWorld
mogą 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
źródło
World
klasie.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
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
źródło
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>
źródło
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.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ę.
źródło
To, co zrobiliśmy w grze, to użycie nowego miejsca
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.
źródło