Comment éviter que des objets de jeu ne se suppriment accidentellement en C ++

20

Disons que mon jeu a un monstre qui peut exploser kamikaze sur le joueur. Choisissons un nom pour ce monstre au hasard: un Creeper. Ainsi, la Creeperclasse a une méthode qui ressemble à ceci:

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

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

Les événements ne sont pas mis en file d'attente, ils sont envoyés immédiatement. Cela provoque la Creepersuppression de l' objet quelque part dans l'appel à postEvent. Quelque chose comme ça:

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

Étant donné que l' Creeperobjet est supprimé pendant que la kamikazeméthode est toujours en cours d'exécution, il se bloque lorsqu'il essaie d'accéder this->location().

Une solution consiste à mettre les événements en file d'attente dans un tampon et à les envoyer ultérieurement. Est-ce la solution courante dans les jeux C ++? Cela ressemble à un peu de piratage, mais cela pourrait être dû à mon expérience avec d'autres langues avec différentes pratiques de gestion de la mémoire.

En C ++, existe-t-il une meilleure solution générale à ce problème lorsqu'un objet se supprime accidentellement de l'intérieur de l'une de ses méthodes?

Tom Dalling
la source
6
euh, que diriez-vous d'appeler postEvent à la fin de la méthode kamikaze au lieu du début?
Hackworth
@Hackworth qui fonctionnerait pour cet exemple spécifique, mais je cherche une solution plus générale. Je veux pouvoir publier des événements de n'importe où et ne pas avoir peur de provoquer des plantages.
Tom Dalling
Vous pouvez également jeter un œil à l'implémentation de autoreleaseObjective-C, où les suppressions sont suspendues jusqu'à "juste un peu".
Chris Burt-Brown

Réponses:

40

Ne supprimez pas this

Même implicitement.

- Déjà -

La suppression d'un objet alors que l'une de ses fonctions membres est toujours sur la pile demande des ennuis. Toute architecture de code qui se traduit par ce qui se passe ("accidentellement" ou non) est objectivement mauvaise , dangereuse et doit être refactorisée immédiatement . Dans ce cas, si votre monstre va être autorisé à appeler 'World :: handleEvent', ne supprimez en aucun cas le monstre à l'intérieur de cette fonction!

(Dans cette situation spécifique, mon approche habituelle consiste à faire en sorte que le monstre mette un drapeau `` mort '' sur lui-même, et que l'objet Monde - ou quelque chose comme ça - teste ce drapeau `` mort '' une fois par image, en supprimant ces objets de sa liste d'objets dans le monde, et soit en le supprimant, soit en le retournant dans un pool de monstres ou tout ce qui est approprié. À ce moment, le monde envoie également des notifications sur la suppression, afin que d'autres objets dans le monde sachent que le monstre a a cessé d'exister et peut y déposer tous les pointeurs qu'ils pourraient contenir. Le monde le fait à un moment sûr, lorsqu'il sait qu'aucun objet n'est en cours de traitement, vous n'avez donc pas à vous soucier de la pile se déroulant en un point où le pointeur «ceci» pointe vers la mémoire libérée.)

Trevor Powell
la source
6
La deuxième moitié de votre réponse entre parenthèses a été utile. Interroger un drapeau est une bonne solution. Mais, je demandais comment éviter de le faire, pas si c'était une bonne ou une mauvaise chose à faire. Si une question demande " Comment puis-je éviter de faire X accidentellement? ", Et votre réponse est " Ne faites jamais X jamais, même accidentellement " en gras, cela ne répond pas réellement à la question.
Tom Dalling
7
Je souscris aux commentaires que j'ai faits dans la première moitié de ma réponse et je pense qu'ils répondent pleinement à la question telle que formulée à l'origine. Le point important que je répéterai ici est qu'un objet ne se supprime pas lui-même. Jamais . Il n'appelle personne d'autre pour le supprimer. Jamais . Au lieu de cela, vous devez avoir autre chose, en dehors de l'objet, qui est le propriétaire de l'objet et qui est responsable de remarquer quand l'objet doit être détruit. Ce n'est pas une chose "juste quand un monstre meurt"; c'est pour tout le code C ++ toujours, partout, pour toujours. Aucune exception.
Trevor Powell
3
@TrevorPowell Je ne dis pas que vous vous trompez. En fait, je suis d'accord avec vous. Je dis simplement que cela ne répond pas vraiment à la question qui a été posée. C'est comme si vous me demandiez " Comment puis-je intégrer l'audio dans mon jeu? " Et ma réponse était " Je ne peux pas croire que vous n'ayez pas d'audio. Mettez l'audio dans votre jeu en ce moment. " Puis entre parenthèses en bas, je mettre " (Vous pouvez utiliser FMOD) ", qui est une vraie réponse.
Tom Dalling
6
@TrevorPowell C'est là que vous vous trompez. Ce n'est pas "juste de la discipline" si je ne connais pas d'alternative. L'exemple de code que j'ai donné est purement théorique. Je sais déjà que c'est un mauvais design, mais mon C ++ est rouillé, alors j'ai pensé que je demanderais de meilleurs designs ici avant de coder ce que je veux. Je suis donc venu pour poser des questions sur les conceptions alternatives. " Ajouter un indicateur de suppression " est une alternative. " Ne jamais faire " n'est pas une alternative. Cela me dit simplement ce que je sais déjà. C'est comme si vous aviez écrit la réponse sans avoir lu correctement la question.
Tom Dalling
4
@Bobby La question est "Comment NE PAS faire X". Dire simplement "NE PAS faire X" est une réponse sans valeur. SI la question avait été "j'ai fait X" ou "je pensais faire X" ou n'importe quelle variante de cela, alors elle répondrait aux paramètres de la méta discussion, mais pas dans sa forme actuelle.
Joshua Drake
21

Au lieu de mettre les événements en file d'attente dans un tampon, placez les suppressions dans un tampon. La suppression différée a le potentiel de simplifier massivement la logique; vous pouvez réellement libérer de la mémoire à la fin ou au début d'une image lorsque vous savez qu'il ne se passe rien d'intéressant à vos objets et les supprimer de n'importe où.

John Calsbeek
la source
1
C'est drôle que vous mentionniez cela, parce que je pensais à quel point un NSAutoreleasePoolobjectif d'Objectif-C serait bien dans cette situation. Pourrait avoir à faire un DeletionPoolavec des modèles C ++ ou quelque chose.
Tom Dalling
@TomDalling La seule chose à laquelle vous devez faire attention si vous rendez le tampon externe à l'objet est qu'un objet pourrait vouloir être supprimé pour plusieurs raisons sur une seule image, et il serait possible d'essayer de le supprimer plusieurs fois.
John Calsbeek
Très vrai. Je vais devoir garder les pointeurs dans un std :: set.
Tom Dalling
5
Plutôt qu'un tampon d'objets à supprimer, vous pouvez également simplement définir un indicateur dans l'objet. Une fois que vous aurez réalisé combien vous voulez éviter d'appeler new ou delete pendant l'exécution et de passer à un pool d'objets, ce sera à la fois plus simple et plus rapide.
Sean Middleditch
4

Au lieu de laisser le monde gérer la suppression, vous pouvez laisser l'instance d'une autre classe servir de compartiment pour stocker toutes les entités supprimées. Cette instance particulière doit écouter les ENTITY_DEATHévénements et les gérer de telle sorte qu'ils les mettent en file d'attente. Le Worldpeut ensuite parcourir ces instances et effectuer des opérations post-mort après le rendu de la trame et «effacer» ce compartiment, qui à son tour effectuerait la suppression réelle des instances d'entités.

Un exemple d'une telle classe serait comme ceci: http://ideone.com/7Upza

Vite Falcon
la source
+1, c'est une alternative au marquage direct des entités. Plus directement, il suffit d'avoir une liste vivante et une liste morte d'entités directement dans la Worldclasse.
Laurent Couvidou
2

Je suggérerais d'implémenter une fabrique utilisée pour toutes les allocations d'objets de jeu dans le jeu. Ainsi, au lieu d'appeler nouveau vous-même, vous diriez à l'usine de créer quelque chose pour vous.

Par exemple

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

Chaque fois que vous souhaitez supprimer un objet, la fabrique pousse l'objet dans un tampon qui est effacé la trame suivante. La destruction retardée est très importante dans la plupart des scénarios.

Pensez également à envoyer tous les messages avec un retard d'une trame. Il n'y a que quelques exceptions où vous devez envoyer immédiatement, la grande majorité des cas cependant juste

Millianz
la source
2

Vous pouvez implémenter vous-même la mémoire gérée en C ++, de sorte que lorsqu'il ENTITY_DEATHest appelé, tout ce qui se passe est que le nombre de ses références soit réduit de un.

Plus tard, comme @John l'a suggéré au début de chaque trame, vous pouvez vérifier quelles entités sont inutiles (celles avec zéro référence) et les supprimer. Par exemple, vous pouvez utiliser boost::shared_ptr<T>( documenté ici ) ou si vous utilisez C ++ 11 (VC2010)std::tr1::shared_ptr<T>

Ali1S232
la source
Juste std::shared_ptr<T>, pas des rapports techniques! - Vous devrez spécifier un suppresseur personnalisé, sinon il supprimera également l'objet immédiatement lorsque le nombre de références atteint zéro.
leftaroundabout
1
@leftaroundabout cela dépend vraiment, au moins j'avais besoin d'utiliser tr1 dans gcc. mais dans VC, cela n'était pas nécessaire.
Ali1S232
2

Utilisez le regroupement et ne supprimez pas réellement les objets. Au lieu de cela, modifiez la structure de données dans laquelle ils sont enregistrés. Par exemple, pour le rendu des objets, il existe un objet de scène et toutes les entités qui y sont en quelque sorte enregistrées pour le rendu, la détection de collision, etc. Au lieu de supprimer l'objet, détachez-le de la scène et insérez-le dans un pool d'objets morts. Cette méthode permettra non seulement d'éviter les problèmes de mémoire (comme un objet qui se supprime), mais peut également accélérer votre jeu si vous utilisez correctement le pool.

hevi
la source
1

Ce que nous avons fait dans un jeu était d'utiliser le placement nouveau

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

le eventPool n'était qu'un grand tableau de mémoire qui a été découpé, et le pointeur de chaque segment a été stocké. Donc alloc () retournerait l'adresse d'un bloc de mémoire libre. Dans notre eventPool, la mémoire était traitée comme une pile, donc après que tous les événements aient été envoyés, nous venions de réinitialiser le pointeur de pile au début du tableau.

En raison de la façon dont notre système d'événements fonctionnait, nous n'avions pas besoin d'appeler les destructeurs des evetns. Ainsi, le pool enregistrerait simplement le bloc de mémoire comme libre et l'allouerait.

Cela nous a donné une vitesse énorme.

Aussi ... Nous avons en fait utilisé des pools de mémoire pour tous les objets alloués dynamiquement en développement, car c'était un excellent moyen de trouver des fuites de mémoire, s'il restait des objets dans les pools lorsque le jeu se terminait (normalement), il était probable une fuite de mem.

PhilCK
la source