Cómo evitar que los objetos del juego se eliminen accidentalmente en C ++

20

Digamos que mi juego tiene un monstruo que puede explotar kamikaze en el jugador. Vamos a elegir un nombre para este monstruo al azar: un Creeper. Entonces, la Creeperclase tiene un método que se parece a esto:

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

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

Los eventos no están en cola, se envían de inmediato. Esto hace que el Creeperobjeto se elimine en algún lugar dentro de la llamada a postEvent. Algo como esto:

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

Debido a que el Creeperobjeto se elimina mientras el kamikazemétodo aún se está ejecutando, se bloqueará cuando intente acceder this->location().

Una solución es poner los eventos en cola en un búfer y enviarlos más tarde. ¿Es esa la solución común en los juegos de C ++? Se siente como un truco, pero eso podría deberse a mi experiencia con otros idiomas con diferentes prácticas de administración de memoria.

En C ++, ¿existe una mejor solución general para este problema en el que un objeto se elimina accidentalmente de uno de sus métodos?

Tom Dalling
fuente
66
¿Qué tal si llamas postEvent al FINAL del método kamikaze en lugar de al principio?
Hackworth
@Hackworth que funcionaría para este ejemplo específico, pero estoy buscando una solución más general. Quiero poder publicar eventos desde cualquier lugar y no tener miedo de causar accidentes.
Tom Dalling
También puede echar un vistazo a la implementación de autoreleaseen Objective-C, donde las eliminaciones se detienen hasta por "solo un poco".
Chris Burt-Brown

Respuestas:

40

No borrar this

Incluso implícitamente.

- Nunca -

Eliminar un objeto mientras una de sus funciones miembro todavía está en la pila es pedir problemas. Cualquier arquitectura de código que resulte en que eso suceda ("accidentalmente" o no) es objetivamente mala , es peligrosa y debe ser refactorizada de inmediato . En este caso, si se le permitirá a su monstruo llamar a 'World :: handleEvent', ¡en ningún caso elimine el monstruo dentro de esa función!

(En esta situación específica, mi enfoque habitual es hacer que el monstruo coloque una bandera 'muerta' sobre sí mismo y hacer que el objeto Mundial, o algo así, pruebe esa bandera 'muerta' una vez por cuadro, eliminando esos objetos de su lista de objetos en el mundo, y ya sea eliminándolo o devolviéndolo a un grupo de monstruos o lo que sea apropiado. En este momento, el mundo también envía notificaciones sobre la eliminación, para que otros objetos en el mundo sepan que el monstruo tiene dejó de existir, y puede soltar cualquier puntero que pueda estar reteniendo. El mundo hace esto en un momento seguro, cuando sabe que actualmente no se están procesando objetos, por lo que no tiene que preocuparse de que la pila se desenrolle en un punto donde el puntero 'this' apunta a la memoria liberada).

Trevor Powell
fuente
66
La segunda mitad de su respuesta entre paréntesis fue útil. Sondear una bandera es una buena solución. Pero, estaba preguntando cómo evitar hacerlo, no si era algo bueno o malo. Si una pregunta pregunta " ¿Cómo puedo evitar hacer X accidentalmente? ", Y su respuesta es " Nunca haga X nunca, incluso accidentalmente " en negrita, eso en realidad no responde la pregunta.
Tom Dalling
77
Respaldo los comentarios que hice en la primera mitad de mi respuesta, y creo que responden completamente la pregunta tal como fue redactada originalmente. El punto importante que repetiré aquí es que un objeto no se elimina a sí mismo. Nunca . No llama a nadie más para eliminarlo. Nunca . En cambio, debe tener algo más, fuera del objeto, que sea el dueño del objeto y que sea responsable de darse cuenta cuando el objeto necesita ser destruido. Esto no es una cosa de "justo cuando un monstruo muere"; Esto es para todo el código C ++ siempre, en todas partes, para siempre. Sin excepciones.
Trevor Powell el
3
@TrevorPowell No estoy diciendo que estés equivocado. De hecho, estoy de acuerdo contigo. Solo digo que en realidad no responde la pregunta que se hizo. Es como si me preguntaras " ¿Cómo puedo incluir el audio en mi juego? " Y mi respuesta fue: " No puedo creer que no tengas audio. Pon el audio en tu juego ahora mismo " . pon " (Puedes usar FMOD) ", que es una respuesta real.
Tom Dalling
66
@TrevorPowell Aquí es donde te equivocas. No es "solo disciplina" si no conozco ninguna alternativa. El código de ejemplo que di es puramente teórico. Ya sé que es un mal diseño, pero mi C ++ está oxidado, así que pensé en preguntar por mejores diseños aquí antes de codificar lo que quiero. Entonces vine a preguntar sobre diseños alternativos. " Agregar un indicador de eliminación " es una alternativa. " Nunca lo hagas " no es una alternativa. Solo me dice lo que ya sé. Se siente como si hubiera escrito la respuesta sin leer la pregunta correctamente.
Tom Dalling
44
@Bobby La pregunta es "Cómo NO hacer X". Simplemente decir "NO hagas X" es una respuesta inútil. SI la pregunta hubiera sido "He estado haciendo X" o "Estaba pensando en hacer X" o cualquier variante de eso, entonces cumpliría con los parámetros de la meta discusión, pero no en su forma actual.
Joshua Drake
21

En lugar de poner en cola eventos en un búfer, ponga en cola las eliminaciones en un búfer. La eliminación retrasada tiene el potencial de simplificar masivamente la lógica; en realidad puede liberar memoria al final o al principio de un cuadro cuando sabe que nada interesante le está sucediendo a sus objetos, y eliminarlos desde cualquier lugar.

John Calsbeek
fuente
1
Es curioso que menciones esto, porque estaba pensando en lo agradable que NSAutoreleasePoolsería un Objective-C en esta situación. Puede que tenga que hacer un DeletionPoolcon plantillas de C ++ o algo así.
Tom Dalling
@TomDalling Lo único que debe tener cuidado si hace que el búfer sea externo al objeto es que un objeto podría querer eliminarse por múltiples razones en un solo marco, y sería posible intentar eliminarlo varias veces.
John Calsbeek
Muy cierto. Tendré que mantener los punteros en un std :: set.
Tom Dalling el
55
En lugar de un búfer de objetos para eliminar, también puede establecer una bandera en el objeto. Una vez que empiece a darse cuenta de cuánto desea evitar llamar a nuevo o eliminar durante el tiempo de ejecución y pasar a un grupo de objetos, será más simple y más rápido.
Sean Middleditch
4

En lugar de permitir que el mundo maneje la eliminación, puede permitir que la instancia de otra clase sirva como un depósito para almacenar todas las entidades eliminadas. Esta instancia particular debe escuchar los ENTITY_DEATHeventos y manejarlos de manera que los ponga en cola. La Worldentonces iterar sobre estos casos y puede realizar operaciones de post-muerte después de la trama se ha prestado y 'claro' este cubo, que a su vez realizar la eliminación real de instancias de entidades.

Un ejemplo de tal clase sería así: http://ideone.com/7Upza

Vite Falcon
fuente
+1, esa es una alternativa a marcar las entidades directamente. Más directamente, solo tenga una lista viva y una lista muerta de entidades directamente en la Worldclase.
Laurent Couvidou
2

Sugeriría implementar una fábrica que se use para todas las asignaciones de objetos del juego en el juego. Entonces, en lugar de llamarlo nuevo, le diría a la fábrica que cree algo para usted.

Por ejemplo

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

Cada vez que desea eliminar un objeto, la fábrica empuja el objeto en un búfer que se borra el siguiente cuadro. La destrucción retrasada es muy importante en la mayoría de los escenarios.

También considere enviar todos los mensajes con un retraso de un cuadro. Solo hay un par de excepciones en las que debe enviar de inmediato, la gran mayoría de los casos, sin embargo, solo

Millianz
fuente
2

Puede implementar memoria administrada en C ++ usted mismo, de modo que cuando ENTITY_DEATHse llama, todo lo que sucede es que el número de referencias se reduce en uno.

Más tarde, como sugirió @John al comienzo de cada cuadro, puede verificar qué entidades son inútiles (aquellas con cero referencias) y eliminarlas. Por ejemplo, puede usar boost::shared_ptr<T>( documentado aquí ) o si está usando C ++ 11 (VC2010)std::tr1::shared_ptr<T>

Ali1S232
fuente
Simplemente std::shared_ptr<T>, no informes técnicos! - Deberá especificar un eliminador personalizado; de lo contrario, también eliminará el objeto inmediatamente cuando el recuento de referencia llegue a cero.
Leftaroundabout
1
@leftaroundabout realmente depende, al menos necesitaba usar tr1 en gcc. pero en VC no había necesidad de eso.
Ali1S232
2

Use la agrupación y en realidad no elimine objetos. En cambio, cambie la estructura de datos en la que están registrados. Por ejemplo, para renderizar objetos hay un objeto de escena y todas las entidades están registradas de alguna manera para renderizar, detectar colisiones, etc. En lugar de eliminar el objeto, sepárelo de la escena e insértelo en un grupo de objetos muertos. Este método no solo evitará problemas de memoria (como que un objeto se elimine a sí mismo) sino que también puede acelerar su juego si usa el grupo correctamente.

hevi
fuente
1

Lo que hicimos en un juego fue usar colocación nueva

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

EventPool era solo una gran variedad de memoria dividida, y el puntero a cada segmento estaba almacenado. Entonces alloc () devolvería la dirección de un bloque de memoria libre. En nuestro grupo de eventos, la memoria se trató como una pila, por lo que después de enviar todos los eventos, simplemente restablecimos el puntero de la pila al inicio de la matriz.

Debido a la forma en que funcionaba nuestro sistema de eventos, no necesitábamos llamar a los destructores por las noches. Por lo tanto, el grupo simplemente registraría el bloque de memoria como libre y lo asignaría.

Esto nos dio una gran velocidad.

Además ... En realidad, utilizamos grupos de memoria para todos los objetos objetados asignados dinámicamente en el desarrollo, ya que era una excelente manera de encontrar fugas de memoria, si quedaban objetos en los grupos cuando el juego salía (normalmente), entonces era probable que hubiera una fuga de mem.

PhilCK
fuente