Gestion de plusieurs références de la même entité de jeu à différents endroits à l'aide d'ID

8

J'ai vu de grandes questions sur des sujets similaires, mais aucune n'a abordé cette méthode particulière:

Étant donné que j'ai plusieurs collections d'entités de jeu dans mon jeu [XNA Game Studio], avec de nombreuses entités appartenant à plusieurs listes, j'envisage des moyens de garder une trace de chaque fois qu'une entité est détruite et de la supprimer des listes auxquelles elle appartient .

Beaucoup de méthodes potentielles semblent bâclées / compliquées, mais je me souviens d'une manière que j'ai vue auparavant dans laquelle, au lieu d'avoir plusieurs collections d'entités de jeu, vous avez à la place des collections d' ID d' entité de jeu . Ces ID sont mappés aux entités de jeu via une "base de données" centrale (peut-être juste une table de hachage).

Ainsi, chaque fois qu'un morceau de code veut accéder aux membres d'une entité de jeu, il vérifie d'abord s'il est encore dans la base de données. Sinon, il peut réagir en conséquence.

Est-ce une bonne approche? Il semble que cela éliminerait de nombreux risques / tracas liés au stockage de plusieurs listes, le compromis étant le coût de la recherche chaque fois que vous souhaitez accéder à un objet.

vargonian
la source

Réponses:

3

Réponse courte: oui.

Réponse longue: En fait, tous les jeux que j'ai vus fonctionnaient de cette façon. La recherche de table de hachage est assez rapide; et si vous ne vous souciez pas des valeurs d'identification réelles (c'est-à-dire qu'elles ne sont utilisées nulle part ailleurs), vous pouvez utiliser un vecteur au lieu d'une table de hachage. Ce qui est encore plus rapide.

En prime, cette configuration permet de réutiliser vos objets de jeu, réduisant votre taux d'allocation de mémoire. Lorsqu'un objet de jeu est détruit, au lieu de le "libérer", il vous suffit de nettoyer ses champs et de mettre dans un "pool d'objets". Ensuite, lorsque vous avez besoin d'un nouvel objet, il vous suffit d'en prendre un existant dans le pool. Si de nouveaux objets apparaissent constamment, cela peut sensiblement améliorer les performances.

Ça ne fait rien
la source
En général, c'est assez précis, mais nous devons d'abord tenir compte d'autres éléments. S'il y a beaucoup d'objets créés et détruits rapidement et que notre fonction de hachage est bonne, la table de hachage pourrait être mieux adaptée qu'un vecteur. En cas de doute, optez pour le vecteur.
hokiecsgrad
Oui, je suis d'accord: le vecteur n'est pas absolument plus rapide qu'une table de hachage. Mais c'est plus rapide 99% du temps (-8
Nevermind
Si vous souhaitez réutiliser des emplacements (ce que vous voulez définitivement), vous devez vous assurer que les ID non valides sont reconnus. ex. L'objet à l'ID 7 est dans la liste A, B et C. L'objet est détruit et dans le même emplacement un autre objet est créé. Cet objet s'inscrit lui-même dans les listes A et C. L'entrée du premier objet dans la liste B "pointe" maintenant vers l'objet nouvellement créé, ce qui est faux dans ce cas. Cela peut être parfaitement résolu avec le Handle-Manager de Scott Bilas scottbilas.com/publications/gem-resmgr . Je l'ai déjà utilisé dans des jeux commerciaux et cela fonctionne comme un charme.
DarthCoder
3

Est-ce une bonne approche? Il semble que cela éliminerait de nombreux risques / tracas liés au stockage de plusieurs listes, le compromis étant le coût de la recherche chaque fois que vous souhaitez accéder à un objet.

Tout ce que vous avez fait, c'est déplacer la charge d'effectuer le contrôle du moment de la destruction au moment de l'utilisation. Je ne dirais pas que je n'ai jamais fait ça auparavant, mais je ne le considère pas comme «sonore».

Je suggère d'utiliser l'une des nombreuses alternatives plus robustes. Si le nombre de listes est connu, vous pouvez vous en sortir en supprimant simplement l'objet de toutes les listes. Cela est inoffensif si l'objet ne se trouve pas dans une liste donnée et que vous êtes assuré de l'avoir supprimé correctement.

Si le nombre de listes est inconnu ou impraticable, stockez-y des références arrières sur l'objet. L'implémentation typique de ceci est le modèle d'observateur , mais vous pouvez probablement obtenir un effet similaire avec les délégués, qui rappellent de supprimer l'élément de toutes les listes contenant si nécessaire.

Ou parfois, vous n'avez pas vraiment besoin de plusieurs listes. Souvent, vous pouvez conserver un objet dans une seule liste et utiliser des requêtes dynamiques pour générer d'autres collections transitoires lorsque vous en avez besoin. par exemple. Au lieu d'avoir une liste distincte pour l'inventaire d'un personnage, vous pouvez simplement retirer tous les objets où "porté" est égal au personnage actuel. Cela peut sembler assez inefficace, mais c'est assez bon pour les bases de données relationnelles et peut être optimisé par une indexation intelligente.

Kylotan
la source
Oui, dans mon implémentation actuelle, chaque liste ou objet unique qui veut une référence à une entité de jeu doit s'abonner à son événement détruit et réagir en conséquence. Cela pourrait être tout aussi bon ou meilleur à certains égards que la recherche d'ID.
vargonian
1
Dans mon livre, c'est exactement le contraire d'une solution robuste. Comparé à lookup-by-id, vous avez PLUS de code, avec PLUS de places pour les bogues, PLUS d'occasions d'oublier quelque chose et une possibilité de détruire un objet SANS informer tout le monde de démarrer. Avec lookup-by-id, vous avez un peu de code qui ne change presque jamais, un point de destruction unique et une garantie à 100% que vous n'accéderez jamais à un objet détruit.
Nevermind
1
Avec lookup-by-id, vous avez du code supplémentaire à chaque endroit dont vous avez besoin pour utiliser l'objet. Avec une approche basée sur l'observateur, vous n'avez pas de code supplémentaire, sauf lors de l'ajout et de la suppression d'éléments de listes, ce qui est susceptible d'être une poignée d'endroits dans l'ensemble du programme. Si vous référencez des éléments plus que vous ne les créez, les détruisez ou les déplacez entre les listes, l'approche de l'observateur entraînera moins de code.
Kylotan
Il y a probablement plus d'endroits qui référencent des objets que d'endroits qui en créent / détruisent. Cependant, chaque référence ne nécessite qu'un tout petit peu de code, ou pas du tout si la recherche est déjà encapsulée par la propriété (ce qui devrait être la plupart du temps). En outre, vous POUVEZ oublier de vous abonner pour la destruction d'objets, mais vous NE POUVEZ PAS oublier de rechercher un objet.
Nevermind
1

Cela semble être juste une instance plus spécifique du problème "comment supprimer des objets d'une liste, tout en itérant à travers elle", qui est facile à résoudre (l'itération dans l'ordre inverse est probablement le moyen le plus simple pour une liste de style tableau comme C # List).

Si une entité doit être référencée à partir de plusieurs endroits, elle doit de toute façon être un type de référence . Vous n'avez donc pas à passer par un système d'identification compliqué - une classe en C # fournit déjà l'indirection nécessaire.

Et enfin - pourquoi s'embêter à "vérifier la base de données"? Donnez simplement un IsDeaddrapeau à chaque entité et vérifiez-le.

Andrew Russell
la source
Je ne connais pas vraiment C #, mais cette architecture est souvent utilisée en C et C ++ où le type de référence normal du langage (pointeurs) n'est pas sûr à utiliser si l'objet a été détruit. Il existe plusieurs façons de le gérer, mais elles impliquent toutes une couche d'indirection, et c'est une solution courante. (Une liste de backpointers comme Kylotan le suggère est une autre - dont vous choisissez dépend si vous voulez passer de la mémoire ou du temps.)
C # est un ramasse-miettes, donc peu importe si vous abandonnez des instances. Pour les ressources non gérées (c'est-à-dire par le garbage collector), il existe le IDisposablemodèle, qui est très similaire au marquage d'un objet comme IsDead. De toute évidence, si vous avez besoin de regrouper des instances, plutôt que de les faire GC, alors une stratégie plus compliquée est nécessaire.
Andrew Russell
Le drapeau IsDead est quelque chose que j'ai utilisé avec succès dans le passé; J'aime l'idée du crash du jeu si je ne vérifie pas l'état mort plutôt que de continuer allègrement, avec des résultats potentiellement bizarres et difficiles à déboguer. L'utilisation d'une recherche de base de données serait la première; IsDead signale ce dernier.
vargonian
3
@vargonian Avec le modèle jetable, ce qui se produit généralement, c'est que vous vérifiez IsDisposed en haut de chaque fonction et propriété d'une classe. Si l'instance est supprimée, vous lancez ObjectDisposedException(ce qui se "bloque" généralement avec une exception non gérée). En déplaçant la vérification vers l'objet lui-même (plutôt que de vérifier l'objet en externe), vous autorisez l'objet à définir son propre comportement "suis-je mort" et supprimez la charge de la vérification des appelants. Pour vos besoins, vous pouvez implémenter IDisposableou créer votre propre IsDeadindicateur avec des fonctionnalités similaires.
Andrew Russell
1

J'utilise souvent cette approche dans mon propre jeu C # /. NET. Outre les autres avantages (et dangers!) Décrits ici, cela peut également aider à éviter les problèmes de sérialisation.

Si vous souhaitez tirer parti des fonctionnalités de sérialisation binaires intégrées du .NET Framework, l'utilisation d'ID d'entité peut aider à réduire la taille du graphique d'objet qui est écrit. Par défaut, le formateur binaire de .NET sérialise un graphique d'objet entier au niveau du champ. Disons que je veux sérialiser une Shipinstance. Si Shipun _ownerchamp fait référence à Playerqui le possède, cette Playerinstance sera également sérialisée. S'il Playercontient un _shipschamp (de, disons ICollection<Ship>), tous les vaisseaux du joueur seront également écrits, ainsi que tous les autres objets référencés au niveau du champ (récursivement). Il est facile de sérialiser accidentellement un énorme graphique d'objet lorsque vous ne voulez sérialiser qu'une petite partie de celui-ci.

Si, à la place, j'ai un _ownerIdchamp, je peux utiliser cette valeur pour résoudre la Playerréférence à la demande. Mon API publique peut même rester inchangée, la Ownerpropriété effectuant simplement la recherche.

Bien que les recherches basées sur le hachage soient généralement très rapides, la surcharge supplémentaire pourrait devenir un problème pour les très grands ensembles d'entités avec des recherches fréquentes. Si cela devient un problème pour vous, vous pouvez mettre en cache la référence à l'aide d'un champ qui n'est pas sérialisé. Par exemple, vous pouvez faire quelque chose comme ceci:

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

Bien sûr, cette approche peut également rendre la sérialisation plus difficile lorsque vous souhaitez sérialiser un graphe d'objets volumineux. Dans ces cas, vous auriez besoin de concevoir une sorte de «conteneur» pour vous assurer que tous les objets nécessaires sont inclus.

Mike Strobel
la source
0

Une autre façon de gérer votre nettoyage consiste à inverser le contrôle et à utiliser un objet jetable. Vous avez probablement un facteur affectant vos entités, alors passez à l'usine toutes les listes auxquelles il doit être ajouté. L'entité conserve une référence à toutes les listes dont elle fait partie et implémente IDisposable. Dans la méthode dispose, il se supprime de toutes les listes. Il ne vous reste plus qu'à vous préoccuper de la gestion des listes en deux endroits: l'usine (création) et l'élimination. La suppression de l'objet est aussi simple que entity.Dispose ().

La question des noms et des références en C # n'est vraiment importante que pour la gestion des composants ou des types de valeurs. Si vous avez des listes de composants liés à une entité, l'utilisation d'un champ ID comme clé de hachage peut être utile. Si vous n'avez que des listes d'entités, utilisez simplement des listes avec une référence. Les références C # sont sûres et simples à utiliser.

Pour supprimer ou ajouter des éléments de la liste, vous pouvez utiliser une file d'attente ou un certain nombre d'autres solutions pour gérer l'ajout et la suppression de listes.

CodexArcanum
la source