Utilisation d'une architecture de système d'entité avec parallélisme basé sur les tâches

9

Contexte

J'ai travaillé sur la création d'un moteur de jeu multithread pendant mon temps libre et j'essaie actuellement de décider de la meilleure façon d'intégrer un système d'entité dans ce que j'ai déjà créé. Jusqu'à présent, j'ai utilisé cet article d'Intel comme point de départ pour mon moteur. Jusqu'à présent, j'ai implémenté la boucle de jeu normale à l'aide de tâches, et je passe maintenant à l'intégration de certains systèmes et / ou systèmes d'entité. J'ai utilisé quelque chose de similaire à Artemis dans le passé, mais le parallélisme me déstabilise.

L'article d'Intel semble préconiser la possession de plusieurs copies des données d'entité et la modification interne de chaque entité à la fin d'une mise à jour complète. Cela signifie que le rendu sera toujours une image derrière, mais cela semble être un compromis acceptable compte tenu des avantages de performances qui devraient être obtenus. En ce qui concerne un système d'entités comme Artemis, cependant, la duplication de chaque entité pour chaque système signifie que chaque composant devra également être dupliqué. C'est faisable, mais il me semble que cela consommerait beaucoup de mémoire. Les parties du document Intel qui en discutent sont principalement 2.2 et 3.2.2. J'ai fait quelques recherches pour voir si je pouvais trouver de bonnes références pour intégrer les architectures que je cherchais, mais je n'ai pas encore pu trouver quoi que ce soit d'utile.

Remarque: J'utilise C ++ 11 pour ce projet, mais j'imagine que la plupart de ce que je demande devrait être assez indépendant du langage.

Solution possible

Avoir un EntityManager global qui est utilisé pour créer et gérer des Entités et EntityAttributes. Autorisez leur accès en lecture uniquement pendant la phase de mise à jour et stockez toutes les modifications dans une file d'attente par thread. Une fois toutes les tâches terminées, les files d'attente sont combinées et les modifications de chacune sont appliquées. Cela pourrait avoir des problèmes avec plusieurs écritures dans les mêmes champs, mais je suis sûr qu'il pourrait y avoir un système prioritaire ou un horodatage pour trier cela. Cela me semble être une bonne approche car les systèmes peuvent être notifiés des changements aux entités assez naturellement pendant la phase de distribution des changements.

Question

Je recherche des commentaires sur ma solution pour voir si elle a du sens. Je ne mentirai pas et ne prétendrai pas être un expert du multithreading, et je le fais en grande partie pour la pratique. Je peux prévoir des désordres compliqués découlant de ma solution où plusieurs systèmes lisent / écrivent plusieurs valeurs. La file d'attente de modifications que j'ai mentionnée peut également être difficile à formater de manière à ce que tout changement possible puisse être facilement communiqué lorsque je ne travaille pas avec POD.

Tout commentaire / conseil serait très apprécié! Merci!

Liens

Ross Hays
la source

Réponses:

12

Fork-Join

Vous n'avez pas besoin de copies séparées des composants. Utilisez simplement un modèle de jointure en fourche, qui est (extrêmement mal) mentionné dans cet article d'Intel.

Dans un ECS, vous avez effectivement une boucle quelque chose comme:

while in game:
  for each system:
    for each component in system:
      update component

Changez ceci en quelque chose comme:

while in game:
  for each system:
    divide components into groups
    for each group:
      start thread (
        for each component in group:
          update component
      )
    wait for all threads to finish

La partie délicate est le bit "diviser les composants en groupes". Pour les graphiques, il n'y a presque pas besoin de données partagées, c'est donc simple (divisez les objets pouvant être rendus uniformément par le nombre de threads de travail disponibles). Pour la physique et l'IA, vous voulez trouver des "îlots" logiques d'objets qui n'interagissent pas et les assembler. Moins il y a d'interaction entre les composants, mieux c'est.

Pour une interaction qui doit exister, les messages différés fonctionnent mieux. Si l'objet A doit dire à l'objet B de subir des dommages, A peut simplement mettre un message en file d'attente dans un pool par thread. Lorsque les threads sont joints, les pools sont tous concaténés en un seul pool. Bien qu'il ne soit pas directement lié au filetage, consultez la série d'événements des développeurs BitSquid (en fait, lisez tout le blog; je ne suis pas d'accord avec tout là-dessus, mais c'est une ressource fantastique).

Notez que «fork-join» ne signifie pas utiliser fork()(qui crée des processus, pas des threads), ni n'implique que vous devez réellement joindre les threads. Cela signifie simplement que vous prenez une seule tâche, que vous la divisez en morceaux plus petits à gérer par votre pool de threads de travail, puis que vous attendez que toutes les parcelles soient traitées.

Procurations

Cette approche peut être utilisée seule ou en combinaison avec la méthode de jointure en fourche pour rendre moins important le besoin d'une séparation stricte.

Vous pouvez être plus convivial pour les threads en interaction en utilisant une approche simple à deux couches. Avoir des entités «faisant autorité» et des entités «mandataires». Les entités faisant autorité ne peuvent être modifiées qu'à partir d'un seul thread qui est le propriétaire clair de l'entité faisant autorité. Les entités mandataires ne peuvent pas être modifiées, seulement lues. À un point de synchronisation dans la boucle de jeu, propagez toutes les modifications des entités faisant autorité aux proxys correspondants.

Remplacez "entités" par "composants" selon le cas. L'essentiel est que vous avez besoin d'au plus deux copies de n'importe quel objet, et il y a des points de "synchronisation" clairs dans votre boucle de jeu lorsque vous pouvez copier de l'un à l'autre dans la plupart des conceptions de moteur de jeu filetées.

Vous pouvez étendre les proxys pour permettre à (un sous-ensemble de) méthodes / messages d'être utilisés simplement en ayant toutes ces choses transférées dans une file d'attente qui est remise à la trame suivante de l'objet faisant autorité.

Notez que l'approche proxy est une conception fantastique à avoir à un niveau supérieur car elle rend la prise en charge réseau super facile.

Sean Middleditch
la source
J'avais lu certaines choses sur la jointure de fourche que vous avez mentionnée auparavant et j'avais l'impression que même si cela vous permet d'utiliser un certain parallélisme, il existe des situations dans lesquelles certains threads de travail peuvent attendre qu'un groupe se termine. Idéalement, j'essaie d'éviter cette situation. L'idée de proxy est intéressante et ressemble un peu à ce sur quoi je travaillais. Une entité a EntityAttributes et ce sont des wrappers pour les valeurs réellement stockées par l'entité. Les valeurs peuvent donc être lues à tout moment mais définies uniquement à certains moments et peuvent contenir une valeur proxy dans l'attribut, n'est-ce pas?
Ross Hays
1
Il y a de fortes chances qu'en essayant d'éviter d'attendre, vous passez tellement de temps à analyser le graphique des dépendances que vous perdez du temps dans l'ensemble.
Patrick Hughes
@roflha: oui, vous pouvez mettre les procurations au niveau EntityAttribute. Ou créez une entité distincte avec un deuxième ensemble d'attributs. Ou abandonnez simplement le concept d'attributs et utilisez une conception de composants moins granulaire.
Sean Middleditch
@SeanMiddleditch Quand je dis attribut, je pense essentiellement à des composants. Les attributs ne sont pas seulement des valeurs uniques comme des flottants et des chaînes si c'est ce que j'ai fait sonner. Ce sont plutôt des classes qui contiennent des informations spécifiques comme un PositionAttribute. Si le composant est le nom accepté pour cela, je devrais peut-être changer. Mais recommanderiez-vous le proxy au niveau de l'entité plutôt qu'au niveau du composant / attribut?
Ross Hays
1
Je recommande tout ce que vous trouvez le plus facile à mettre en œuvre. N'oubliez pas que le point que je pourrais être en mesure d'interroger des proxys sans prendre de verrous, sans utiliser d'atomique et sans blocages.
Sean Middleditch