J'implémente une variante de système d'entité qui a:
Une classe d'entité qui n'est guère plus qu'un ID qui lie les composants entre eux
Un tas de classes de composants qui n'ont pas de "logique de composant", uniquement des données
Un tas de classes système (alias "sous-systèmes", "gestionnaires"). Ceux-ci font tout le traitement de la logique d'entité. Dans la plupart des cas de base, les systèmes parcourent simplement une liste d'entités qui les intéressent et effectuent une action sur chacun d'eux
Un objet de classe MessageChannel qui est partagé par tous les systèmes de jeu. Chaque système peut s'abonner à un type spécifique de messages à écouter et peut également utiliser le canal pour diffuser des messages vers d'autres systèmes
La variante initiale de la gestion des messages système était quelque chose comme ceci:
- Exécutez une mise à jour sur chaque système de jeu séquentiellement
Si un système fait quelque chose à un composant et que cette action peut intéresser d'autres systèmes, le système envoie un message approprié (par exemple, un système appelle
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
chaque fois qu'une entité est déplacée)
Chaque système qui s'est abonné au message spécifique obtient sa méthode de gestion des messages appelée
Si un système gère un événement et que la logique de traitement des événements nécessite la diffusion d'un autre message, le message est diffusé immédiatement et une autre chaîne de méthodes de traitement des messages est appelée
Cette variante était OK jusqu'à ce que je commence à optimiser le système de détection de collision (cela devenait vraiment lent à mesure que le nombre d'entités augmentait). Au début, il suffit d'itérer chaque paire d'entités à l'aide d'un simple algorithme de force brute. J'ai ensuite ajouté un "index spatial" qui a une grille de cellules qui stocke des entités qui se trouvent à l'intérieur de la zone d'une cellule spécifique, permettant ainsi de faire des vérifications uniquement sur les entités dans les cellules voisines.
Chaque fois qu'une entité se déplace, le système de collision vérifie si l'entité entre en collision avec quelque chose dans la nouvelle position. Si c'est le cas, une collision est détectée. Et si les deux entités en collision sont des "objets physiques" (elles ont toutes les deux un composant RigidBody et sont censées se repousser pour ne pas occuper le même espace), un système de séparation de corps rigide dédié demande au système de mouvement de déplacer les entités vers certaines positions spécifiques qui les sépareraient. Cela entraîne à son tour le système de mouvement à envoyer des messages informant des positions d'entité modifiées. Le système de détection de collision est censé réagir car il doit mettre à jour son index spatial.
Dans certains cas, cela provoque un problème car le contenu de la cellule (une liste générique d'objets Entity en C #) est modifié pendant leur itération, provoquant ainsi une exception levée par l'itérateur.
Alors ... comment puis-je empêcher le système de collision d'être interrompu pendant qu'il vérifie les collisions?
Bien sûr, je pourrais ajouter une logique "intelligente" / "délicate" qui assure que le contenu des cellules soit correctement itéré, mais je pense que le problème ne réside pas dans le système de collision lui-même (j'ai également eu des problèmes similaires dans d'autres systèmes), mais dans la manière les messages sont traités lorsqu'ils voyagent d'un système à l'autre. Ce dont j'ai besoin, c'est d'un moyen de garantir qu'une méthode de gestion d'événement spécifique fonctionne correctement sans interruption.
Ce que j'ai essayé:
- Files d'attente de messages entrants . Chaque fois qu'un système diffuse un message, le message est ajouté aux files d'attente de messages des systèmes qui l'intéressent. Ces messages sont traités lorsqu'une mise à jour du système est appelée à chaque trame. Le problème : si un système A ajoute un message à la file d'attente B du système, cela fonctionne bien si le système B est destiné à être mis à jour plus tard que le système A (dans le même cadre de jeu); sinon, le message est traité dans la trame de jeu suivante (non souhaitable pour certains systèmes)
- Files d'attente de messages sortants . Lorsqu'un système gère un événement, tous les messages qu'il diffuse sont ajoutés à la file d'attente des messages sortants. Les messages n'ont pas besoin d'attendre le traitement d'une mise à jour du système: ils sont traités "immédiatement" une fois que le gestionnaire de messages initial a terminé son travail. Si la gestion des messages entraîne la diffusion d'autres messages, ils sont également ajoutés à une file d'attente sortante, de sorte que tous les messages sont traités dans la même trame. Le problème: si le système de durée de vie d'entité (j'ai implémenté la gestion de durée de vie d'entité avec un système) crée une entité, il en informe certains systèmes A et B. Alors que le système A traite le message, il provoque une chaîne de messages qui finit par détruire l'entité créée (par exemple, une entité balle a été créée juste là où elle entre en collision avec un obstacle, ce qui provoque l'auto-destruction de la balle). Pendant la résolution de la chaîne de messages, le système B ne reçoit pas le message de création d'entité. Ainsi, si le système B s'intéresse également au message de destruction d'entité, il l'obtient et ce n'est qu'après la résolution de la "chaîne" qu'il obtient le message de création d'entité initial. Cela provoque le message de destruction à ignorer, le message de création à "accepté",
MODIFICATION - RÉPONSES AUX QUESTIONS, COMMENTAIRES:
- Qui modifie le contenu de la cellule pendant que le système de collision le parcourt?
Pendant que le système de collision effectue des vérifications de collision sur une entité et ses voisins, une collision peut être détectée et le système d'entité enverra un message qui sera immédiatement réagi par d'autres systèmes. La réaction au message peut entraîner la création d'autres messages et leur traitement immédiat. Ainsi, un autre système pourrait créer un message que le système de collision devrait ensuite traiter immédiatement (par exemple, une entité a été déplacée de sorte que le système de collision doit mettre à jour son index spatial), même si les vérifications de collision antérieures n'étaient pas encore terminées.
- Vous ne pouvez pas travailler avec une file d'attente de messages sortants globale?
J'ai récemment essayé une seule file d'attente globale. Cela provoque de nouveaux problèmes. Problème: je déplace une entité réservoir dans une entité mur (le réservoir est contrôlé avec le clavier). Ensuite, je décide de changer la direction du char. Pour séparer le réservoir et le mur de chaque cadre, le CollidingRigidBodySeparationSystem éloigne le réservoir du mur de la plus petite quantité possible. La direction de séparation doit être opposée à la direction de mouvement du tank (lorsque le dessin du jeu commence, le tank doit avoir l'air de ne jamais être entré dans le mur). Mais la direction devient opposée à la nouvelle direction, déplaçant ainsi le réservoir vers un côté du mur différent de ce qu'il était initialement. Pourquoi le problème se produit: Voici comment les messages sont traités maintenant (code simplifié):
public void Update(int deltaTime)
{
m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
while (m_messageQueue.Count > 0)
{
Message message = m_messageQueue.Dequeue();
this.Broadcast(message);
}
}
private void Broadcast(Message message)
{
if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
{
// NOTE: all IMessageListener objects here are systems.
List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
foreach (IMessageListener listener in messageListeners)
{
listener.ReceiveMessage(message);
}
}
}
Le code s'écoule comme ceci (supposons que ce n'est pas le premier cadre de jeu):
- Les systèmes commencent à traiter TimePassedMessage
- InputHandingSystem convertit les pressions de touches en action d'entité (dans ce cas, une flèche gauche se transforme en action MoveWest). L'action d'entité est stockée dans le composant ActionExecutor
- ActionExecutionSystem , en réaction à l'action d'entité, ajoute un MovementDirectionChangeRequestedMessage à la fin de la file d'attente de messages
- MovementSystem déplace la position de l'entité en fonction des données du composant Velocity et ajoute le message PositionChangedMessage à la fin de la file d'attente. Le mouvement se fait en utilisant la direction / vitesse du mouvement de l'image précédente (disons au nord)
- Les systèmes arrêtent le traitement de TimePassedMessage
- Les systèmes commencent à traiter MovementDirectionChangeRequestedMessage
- MovementSystem change la vitesse de l'entité / la direction du mouvement comme demandé
- Les systèmes arrêtent le traitement MovementDirectionChangeRequestedMessage
- Les systèmes commencent à traiter PositionChangedMessage
- CollisionDetectionSystem détecte que parce qu'une entité s'est déplacée, elle a rencontré une autre entité (le réservoir est entré à l'intérieur d'un mur). Il ajoute un CollisionOccuredMessage à la file d'attente
- Les systèmes arrêtent le traitement PositionChangedMessage
- Les systèmes commencent à traiter CollisionOccuredMessage
- CollidingRigidBodySeparationSystem réagit à la collision en séparant le réservoir et la paroi. Le mur étant statique, seul le réservoir est déplacé. La direction de mouvement des chars est utilisée comme indicateur de la provenance du char. Il est décalé dans une direction opposée
BOGUE: Lorsque le char a déplacé ce cadre, il s'est déplacé en utilisant le sens de déplacement du cadre précédent, mais lorsqu'il était séparé, le sens de déplacement de CE cadre a été utilisé, même s'il était déjà différent. Ce n'est pas comme ça que ça devrait fonctionner!
Pour éviter ce bug, l'ancienne direction de mouvement doit être enregistrée quelque part. Je pourrais l'ajouter à un composant juste pour corriger ce bogue spécifique, mais ce cas n'indique-t-il pas une manière fondamentalement erronée de gérer les messages? Pourquoi le système de séparation devrait-il se soucier de la direction de mouvement qu'il utilise? Comment puis-je résoudre ce problème avec élégance?
- Vous voudrez peut-être lire gamadu.com/artemis pour voir ce qu'ils ont fait avec Aspects, quel côté résout certains des problèmes que vous voyez.
En fait, je connais Artemis depuis un bon moment maintenant. J'ai enquêté sur son code source, lu les forums, etc. Mais j'ai vu des "Aspects" mentionnés seulement à quelques endroits et, pour autant que je le comprenne, ils signifient essentiellement "Systèmes". Mais je ne vois pas comment le côté Artemis résout certains de mes problèmes. Il n'utilise même pas de messages.
- Voir aussi: "Communication d'entité: file d'attente de messages vs publication / abonnement vs signal / slots"
J'ai déjà lu toutes les questions de gamedev.stackexchange concernant les systèmes d'entités. Celui-ci ne semble pas discuter des problèmes auxquels je suis confronté. Suis-je en train de manquer quelque chose?
- Traitez les deux cas différemment, la mise à jour de la grille n'a pas besoin de s'appuyer sur les messages de mouvement car elle fait partie du système de collision
Je ne sais pas ce que tu veux dire. Les anciennes implémentations de CollisionDetectionSystem vérifiaient simplement les collisions lors d'une mise à jour (lorsqu'un TimePassedMessage était géré), mais je devais minimiser les contrôles autant que possible en raison des performances. J'ai donc opté pour la vérification des collisions lorsqu'une entité se déplace (la plupart des entités de mon jeu sont statiques).