Comment supprimer au mieux une entité de ma boucle de jeu lorsqu'elle est morte?

16

Ok, j'ai donc une grande liste de toutes mes entités que je boucle et mets à jour. Dans AS3, je peux stocker cela comme un tableau (longueur dynamique, non typé), un vecteur (typé) ou une liste chaînée (non native). Pour le moment, j'utilise Array, mais je prévois de passer à Vector ou à une liste liée si elle est plus rapide.

Quoi qu'il en soit, ma question, lorsqu'une entité est détruite, comment dois-je la supprimer de la liste? Je pourrais annuler sa position, l'épisser ou simplement mettre un drapeau dessus pour dire "saute-moi, je suis mort". Je mets mes entités en commun, donc une Entité qui est morte est très susceptible d'être de nouveau vivante à un moment donné. Pour chaque type de collection, quelle est ma meilleure stratégie et quelle combinaison de type de collection et de méthode de suppression fonctionnera le mieux?

Iain
la source
La longueur d'un vecteur n'est pas fixe, mais elle est typée, ce qui le rend supérieur à Array. L'inconvénient est qu'il n'y a pas de syntaxe rapide pour définir une liste pré-remplie, mais vous n'en avez pas besoin je pense.
Bart van Heukelom

Réponses:

13

Je voudrais stocker tous les ajouts / suppressions dans des listes distinctes et effectuer ces opérations après avoir parcouru la boucle de mise à jour.

Simon
la source
10

Le framework Flixel utilise l'indicateur mort (en fait plusieurs indicateurs qui déterminent s'il doit être dessiné, mis à jour, etc.). Je dirais que si vous allez faire revivre des entités, et si les performances sont un problème, vous utilisez l'indicateur mort. D'après mon expérience, l'instanciation de nouvelles entités est l'opération la plus coûteuse dans le cas d'utilisation que vous décrivez, et l'épissage ou la suppression d'éléments peut entraîner un gonflement de la mémoire compte tenu de la collecte des ordures parfois squirrely de Flash.

Gregory Avery-Weir
la source
1
+1 pour flixel. Le recyclage deadaide vraiment à la performance.
Snow Blind
3

Bien que certaines techniques soient intrinsèquement plus efficaces que d'autres, cela n'aura d'importance que si vous manquez de cycles sur votre plate-forme cible. Utilisez la technique qui vous permet de terminer votre jeu plus rapidement. Essayez de ne pas compter sur l'implémentation spécifique de vos structures de données de conteneur entre-temps et cela vous aidera à optimiser par la suite si vous en avez besoin.

Juste pour aborder certaines des techniques déjà discutées par d'autres ici. Si l'ordre des entités est important, un indicateur mort peut vous permettre d'épisser pendant votre boucle de mise à jour sur la trame suivante. par exemple. pseudocode très simple:

void updateGame()
{
  // updateEntities()
  Entity* pSrcEntity = &mEntities[0];
  Entity* pDstEntity = &mEntities[0];
  newNumEntities = 0;
  for (int i = 0; i < numEntities; i++)
  {
    if (!pSrcEntity->isDead)
    {
       // could be inline but whatever.
       updateEntity(pDstEntity, pSrcEntity);
       // if entity just died, don't update the pDstEntity pointer, 
       // and just let the next entity updated overwrite it.
       if (!pDstEntity->isDead)
       {
          pDstEntity++;
          newNumEntities++;
       }
    }
    pSrcEntity++;
  }
}
numEntities = newNumEntities;

Ce sont les caractéristiques de ce schéma:

  • compacité naturelle des entités (quoique avec éventuellement 1 latence de trame avant qu'un créneau d'entité puisse être récupéré).
  • aucun problème de réorganisation aléatoire.
  • tandis que les listes doublement liées ont une insertion / suppression O (1), mais sont très difficiles à pré-extraire pour masquer la latence du cache optimale. Les conserver dans un tableau compact permet aux techniques de prélecture de blocs de fonctionner correctement.
  • Dans le cas de la destruction multi-objets, vous n'avez pas besoin de faire des copies shift redondantes pour maintenir l'ordre et la compacité (tout est fait une fois pendant la mise à jour)
  • Vous profitez de toucher des données qui devront déjà être dans le cache lors de la mise à jour.
  • Cela fonctionne bien si vos poitners d'entités source et de destination doivent séparer des tableaux. Vous pouvez ensuite double-tamponner vos tableaux d'entités afin de tirer parti du multicœur / par exemple. un thread met à jour / écrit les entités pour la trame N, tandis qu'un autre thread rend les entités de la trame précédente pour la trame N-1.
  • La compacité signifie qu'il est plus facile de DMA le tout à un processeur hétérogène pour encore plus de déchargement de travail CPU par exemple. SPU ou GPU.
jpaver
la source
+1. J'aime ça. Bien que je n'aie presque jamais besoin de mises à jour commandées dans une piscine, je l'ajouterai au sac de choses à retenir si je rencontre la situation: o)
Kaj
2

Parlant de mon expérience de programmation générale, l'épissage est généralement une opération lente, impliquant le déplacement de tous les éléments existants vers le haut. Je pense que le mettre à null serait la meilleure solution ici ; un indicateur mort fonctionnerait mais vous devriez faire attention de ne pas le laisser rendre votre code en désordre.

En fait, nous parlions simplement de la mise en commun des ressources dans le salon de discussion. C'est une très bonne pratique, et bon d'entendre que vous faites cela. :)

Ricket
la source
1
Si l'ordre de mise à jour n'est pas important, l'épissage doit être aussi simple que de déplacer la dernière entité vers l'index en cours et de diminuer le nombre de pools et l'index d'itérateur.
Kaj
Wow, très bon point Kaj! :)
Ricket
2

Personnellement, j'utiliserais une liste chaînée. Il est rapide de parcourir une liste de favoris, ainsi que d'ajouter et de supprimer des éléments. L'utilisation d'un tableau ou d'un vecteur serait un bon choix si vous avez besoin d'un accès direct aux éléments de la structure (par exemple, l'accès à un index), mais cela ne sonne pas comme si vous en aviez besoin.

Chaque fois que vous supprimez un élément de la liste liée, vous pouvez l'ajouter à un pool d'objets qui peuvent ensuite être recyclés pour économiser sur l'allocation de mémoire.

J'ai utilisé les infrastructures de données polygonales dans plusieurs projets et j'en suis très satisfait.

Edit: Désolé, je pense que la réponse n'était pas très claire en termes de stratégie de suppression: je suggère de supprimer l'élément de la liste, dès qu'il est mort et de l'ajouter directement à la structure de regroupement (recyclage). Étant donné que la suppression d'un élément d'une liste chaînée est très performante, je ne vois aucun problème à le faire.

bummzack
la source
1
Je suppose que vous proposez ici une liste à double liaison? (avant / arrière)? Aussi: Suggérez-vous une sorte de pool sur les éléments de lien ou allouez-vous dynamiquement chaque porte-pointeur dans la liste liée?
Simon
Oui, il faudrait que ce soit une liste à double liaison qui convienne le mieux à cette tâche. Merci d'avoir fait remarquer cela! En ce qui concerne la réutilisation des éléments: je pensais à une classe de regroupement / structure de données spécialisée, qui crée de nouveaux objets à la demande ou utilise des instances existantes s'il y en a dans le pool. Par conséquent, il serait bon de supprimer les éléments "morts" de la liste et de les ajouter au pool pour une utilisation ultérieure.
bummzack
Une liste liée individuellement fera l'affaire. Les listes doublement liées ne donnent que l'avantage d'itérer dans les deux sens. Pour parcourir une liste liée individuellement avec l'option de supprimer l'élément actuel, vous devrez garder une trace de l'entrée précédente.
deft_code
@caspin oui exactement. Si vous utilisez une liste liée unique, vous devez garder une trace des nœuds précédents et lier leur nextpointeur au nœud après celui supprimé. Si vous ne voulez pas les tracas de le faire vous-même, une liste à double lien serait la DataStructure de choix.
bummzack
1

"il suffit de mettre un drapeau dessus pour dire" saute par-dessus moi, je suis mort ". Je rassemble mes entités, donc une entité qui est morte est très susceptible d'être de nouveau en vie à un moment donné"

Je pense que vous avez répondu à votre propre question concernant cette application spécifique. Je m'éloignerais des tableaux si vous envisagez de travailler sur eux autre que push and pop. Les listes chaînées seraient une façon plus intelligente de procéder si vous envisagez d'effectuer des opérations lourdes. Cela dit, si vous prévoyez de réintégrer la même entité dans le jeu, il est logique de ne définir qu'une variable booléenne et de la vérifier pendant les boucles de fonctionnement du jeu.

scape
la source
0

Une solution propre et générale que j'ai trouvée sur une bibliothèque que j'ai utilisée était d'utiliser une carte verrouillable.

vous avez 2 opérations lock() et unlock(), pendant que vous parcourez la carte, vous allez lock(), à partir de ce moment, chaque opération qui modifie la carte ne prend pas effet, elle est simplement poussée dans une CommandQueuequi s'exécutera une fois que vous aurez appelé unlock().

La suppression d'une entité aurait donc le pseudo-code suivant:

void lockableMap::remove(std::string id) {
   if(isLocked) {
       commandQueue.add(new RemoveCommand(id));
   } else {
       //remove element from map
   }

et quand vous unlock()

isLocked = false
commandQueue.execute(this);

La seule chose à considérer est que vous ne supprimerez l'entité qu'après la boucle.

EDIT: c'est la solution proposée par Simon.

GriffinHeart
la source
0

J'ai deux méthodes.

Lorsque vous appelez un objet à supprimer, cela définit vraiment deux indicateurs:

1.Un pour indiquer au conteneur qu'un objet a été supprimé

2.Un pour indiquer au conteneur quels objets ont été demandés pour être supprimés

void object::deleteObject()
{
    container->objectHasBeenDeleted = true;
    isToDelete = true;
}

Un utilisant un vecteur d'objets

std::vector<object*> objects;

Ensuite, dans la fonction de mise à jour, vérifiez si un objet a été supprimé et si c'est le cas, parcourez tous les objets et supprimez ceux qui ont un indicateur de suppression

void container::update()
{
    if (objectHasBeenDeleted)
    {
        std::vector<object*>::iterator ListIterator;
        for(ListIterator=objects.begin(); ListIterator!=objects.end();)
        {
            if( (*ListIterator)->isToDelete )
            {
                ListIterator = objects.erase(ListIterator);
                delete *ListIterator;
            }
            else {
                ++ListIterator;
            }
        }
    objectHasBeenDeleted = false;
    }
}

Deux Utilisation d'un (pointeur vers un) vecteur d'objets.

std::vector<object*> *objects;

Dans la fonction de mise à jour, si un objet doit être supprimé, parcourez les objets et ajoutez ceux qui ne doivent pas être supprimés à un nouveau vecteur. supprimer le vecteur d'objets et placer le pointeur sur le nouveau vecteur

void container::update()
{
    if (objectHasBeenDeleted)
    {
        std::vector<object*> *newVector;
        unsigned long i;
        for (i = 0; i < objects->size(); i++)
        {
            if (!objects->at(i)->isToDelete)
            {
                newVector->push_back(objects->at(i));
            }
        }
        delete objects;
        objects = newVector;
        objectHasBeenDeleted = false;
    }
}

la source