Système d'entité et rendu

11

Okey, ce que je sais jusqu'à présent; L'entité contient un composant (stockage de données) qui contient des informations telles que; - Texture / sprite - Shader - etc

Et puis j'ai un système de rendu qui dessine tout cela. Mais ce que je ne comprends pas, c'est comment le rendu doit être conçu. Dois-je avoir un composant pour chaque "type visuel". Un composant sans shader, un avec shader, etc?

Vous avez juste besoin de quelques informations sur la "bonne façon" de le faire. Conseils et pièges à surveiller.

hayer
la source
2
Essayez de ne pas rendre les choses trop génériques. Il semblerait étrange d'avoir une entité avec un composant Shader et non un composant Sprite, donc peut-être que le Shader devrait faire partie du composant Sprite. Naturellement, vous n'aurez alors besoin que d'un seul système de rendu.
Jonathan Connell

Réponses:

8

Il est difficile de répondre à cette question car chacun a sa propre idée de la manière dont un système de composants d'entité doit être structuré. Le mieux que je puisse faire est de partager avec vous certaines des choses que j'ai trouvées les plus utiles pour moi.

Entité

J'adopte l'approche ECS pour les matières grasses, probablement parce que je trouve que les méthodes de programmation extrêmes sont très inefficaces (en termes de productivité humaine). À cette fin, une entité est pour moi une classe abstraite à hériter de classes plus spécialisées. L'entité a un certain nombre de propriétés virtuelles et un simple indicateur qui me dit si cette entité doit exister ou non. Donc, par rapport à votre question sur un système de rendu, voici à quoi Entityressemble:

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

Composants

Les composants sont "stupides" en ce qu'ils ne font rien ou ne savent rien. Ils n'ont aucune référence à d'autres composants, et ils n'ont généralement pas de fonctions (je travaille en C #, donc j'utilise des propriétés pour gérer les getters / setters - s'ils ont des fonctions, ils sont basés sur la récupération des données qu'ils contiennent).

Les systèmes

Les systèmes sont moins "stupides", mais restent des automates stupides. Ils n'ont aucun contexte du système global, n'ont aucune référence à d'autres systèmes et ne contiennent aucune donnée à l'exception de quelques tampons dont ils peuvent avoir besoin pour effectuer leur traitement individuel. Selon le système, il peut avoir une méthode Updateou une Drawméthode spécialisée , ou dans certains cas, les deux.

Interfaces

Les interfaces sont une structure clé de mon système. Ils sont utilisés pour définir ce qu'un Systemprocessus peut traiter et ce dont un Entityest capable. Les interfaces pertinentes pour le rendu sont: IRenderableet IAnimatable.

Les interfaces indiquent simplement au système quels composants sont disponibles. Par exemple, le système de rendu doit connaître le cadre de sélection de l'entité et l'image à dessiner. Dans mon cas, ce serait le SpatialComponentet le ImageComponent. Il ressemble donc à ceci:

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

Le système de rendu

Alors, comment le système de rendu dessine-t-il une entité? C'est en fait assez simple, donc je vais juste vous montrer la classe dépouillée pour vous donner une idée:

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

En regardant la classe, le système de rendu ne sait même pas ce qu'est un Entity. Tout ce qu'il sait, c'est IRenderableet on lui donne simplement une liste à dessiner.

Comment tout cela fonctionne

Il peut également être utile de comprendre comment je crée de nouveaux objets de jeu et comment je les alimente dans les systèmes.

Création d'entités

Tous les objets de jeu héritent de l'entité et de toutes les interfaces applicables qui décrivent ce que cet objet de jeu peut faire. À peu près tout ce qui est animé à l'écran ressemble à ceci:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Nourrir les systèmes

Je garde une liste de toutes les entités qui existent dans le monde du jeu dans une seule liste appelée List<Entity> gameObjects. Chaque cadre, je passe ensuite au crible cette liste et copie les références d'objet vers d'autres listes basées sur le type d'interface, telles que List<IRenderable> renderableObjects, et List<IAnimatable> animatableObjects. De cette façon, si différents systèmes doivent traiter la même entité, ils le peuvent. Ensuite, je remets simplement ces listes à chacun des systèmes Updateou Drawméthodes et je laisse les systèmes faire leur travail.

Animation

Vous pourriez être curieux de savoir comment fonctionne le système d'animation. Dans mon cas, vous voudrez peut-être voir l'interface IAnimatable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

L'élément clé à noter ici est que l' ImageComponentaspect de l' IAnimatableinterface n'est pas en lecture seule; il a un setter .

Comme vous l'avez peut-être deviné, le composant d'animation ne contient que des données sur l'animation; une liste d'images (qui sont des composants d'image), l'image actuelle, le nombre d'images par seconde à dessiner, le temps écoulé depuis le dernier incrément d'image et d'autres options.

Le système d'animation tire parti du système de rendu et de la relation entre les composants d'image. Il modifie simplement le composant d'image de l'entité en incrémentant l'image de l'animation. De cette façon, l'animation est rendue indirectement par le système de rendu.

Chiffrer
la source
Je devrais probablement noter que je ne sais pas vraiment si cela est même proche de ce que les gens appellent un système à composants d'entité . Dans ma tentative d'implémenter une conception basée sur la composition, je me suis retrouvé à tomber dans ce modèle.
Cypher du
Intéressant! Je ne suis pas trop intéressé par la classe abstraite de votre entité, mais l'interface IRenderable est une bonne idée!
Jonathan Connell
5

Voir cette réponse pour voir le type de système dont je parle.

Le composant doit contenir les détails de ce qu'il faut dessiner et comment le dessiner. Le système de rendu prendra ces détails et dessinera l'entité de la manière spécifiée par le composant. Ce n'est que si vous utilisiez des technologies de dessin sensiblement différentes que vous disposeriez de composants distincts pour des styles distincts.

MichaelHouse
la source
3

La principale raison de séparer la logique en composants est de créer un ensemble de données qui, lorsqu'elles sont combinées dans une entité, produisent un comportement utile et réutilisable. Par exemple, séparer une entité en un composant physique et un composant rendu est logique car il est probable que toutes les entités n'auront pas la physique et certaines entités pourraient ne pas avoir Sprite.

Afin de répondre à votre question, vous devez regarder votre architecture et vous poser deux questions:

  1. Est-il judicieux d'avoir un Shader sans texture
  2. La séparation de Shader de Texture me permettra-t-elle d'éviter la duplication de code?

Lorsque vous divisez un composant, il est important de poser cette question, si la réponse à 1. est oui, vous avez probablement un bon candidat pour créer deux composants distincts, l'un avec un shader et l'autre avec la texture. La réponse à 2. est généralement oui pour des composants comme Position où plusieurs composants peuvent utiliser position.

Par exemple, la physique et l'audio peuvent utiliser la même position, plutôt que les deux composants stockant des positions en double, vous les refactorisez en un seul PositionComponent et exigez que les entités qui utilisent PhysicsComponent / AudioComponent aient également un PositionComponent.

Sur la base des informations que vous nous avez données, il ne semble pas que votre RenderComponent soit un bon candidat pour se diviser en TextureComponent et ShaderComponent car les shader dépendent entièrement de Texture et de rien d'autre.

En supposant que vous utilisez quelque chose de similaire à T-Machine: Entity Systems, un exemple d'implémentation d'un RenderComponent & RenderSystem en C ++ ressemblerait à ceci:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}
Jake Woods
la source
C'est complètement faux. L'intérêt des composants est de les séparer des entités, et non de faire en sorte que les systèmes de rendu parcourent les entités pour les trouver. Les systèmes de rendu doivent contrôler entièrement leurs propres données. PS Ne mettez pas std :: vector (en particulier avec les données d'instance) dans les boucles, c'est horrible (lent) C ++.
snake5
@ snake5 vous avez raison sur les deux points. J'ai tapé le code du haut de ma tête et il y a eu des problèmes, merci de les avoir signalés. J'ai corrigé le code affecté pour qu'il soit moins lent et pour utiliser correctement les idiomes du système d'entités.
Jake Woods du
2
@ snake5 Vous ne recalculez pas les données à chaque image, getComponents renvoie un vecteur appartenant à m_manager qui est déjà connu et ne change que lorsque vous ajoutez / supprimez des composants. C'est un avantage lorsque vous avez un système qui souhaite utiliser plusieurs composants de la même entité, par exemple un PhysicsSystem qui souhaite utiliser PositionComponent et PhysicsComponent. D'autres systèmes voudront probablement la position et en ayant un PositionComponent vous n'aurez pas de données en double. Il résout principalement le problème de la façon dont les composants communiquent.
Jake Woods du
5
@ snake5 La question n'est pas de savoir comment le système EC doit être présenté ni ses performances. La question concerne la configuration du système de rendu. Il existe plusieurs façons de structurer un système EC, ne vous laissez pas entraîner par les problèmes de performances les uns par rapport aux autres ici. Le PO utilise probablement une structure CE complètement différente de l'une ou l'autre de vos réponses. Le code fourni dans cette réponse est juste destiné à mieux montrer l'exemple, à ne pas être critiqué pour ses performances. Si la question portait sur les performances, cela rendrait peut-être la réponse «non utile», mais ce n'est pas le cas.
MichaelHouse
2
Je préfère de loin le design présenté dans cette réponse que dans Cyphers. Il est très similaire à celui que j'utilise. Les composants plus petits sont mieux imo, même s'ils n'ont qu'une ou deux variables. Ils devraient définir un aspect d'une entité, comme mon composant "Damagable" aurait 2, peut-être 4 variables (max et courant pour chaque santé et armure). Ces commentaires deviennent de plus en plus longs, passons au chat si vous souhaitez en discuter plus.
John McDonald du
2

Piège # 1: code sur-conçu. Réfléchissez si vous avez vraiment besoin de tout ce que vous implémentez, car vous devrez vivre avec pendant un certain temps.

Piège n ° 2: trop d'objets. Je n'utiliserais pas un système avec trop d'objets (un pour chaque type, sous-type et autres) car cela rend le traitement automatisé plus difficile. À mon avis, il est beaucoup plus agréable que chaque objet contrôle un certain ensemble de fonctionnalités (par opposition à une fonctionnalité). Par exemple, la création de composants pour chaque bit de données inclus dans le rendu (composant de texture, composant de shader) est trop divisée - vous auriez généralement besoin d'avoir tous ces composants ensemble de toute façon, n'êtes-vous pas d'accord?

Piège n ° 3: contrôle externe trop strict. Préférez changer les noms en objets shader / texture car les objets peuvent changer avec le rendu / type de texture / format shader / peu importe. Les noms sont de simples identificateurs - c'est au moteur de rendu de décider quoi faire de ceux-ci. Un jour, vous voudrez peut-être avoir des matériaux au lieu de simples shaders (ajoutez des shaders, des textures et des modes de mélange à partir de données, par exemple). Avec une interface textuelle, il est beaucoup plus facile de l'implémenter.

Quant au rendu, il peut s'agir d'une interface simple qui crée / détruit / maintient / rend des objets créés par des composants. La représentation la plus primitive pourrait être quelque chose comme ceci:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

Cela vous permettrait de gérer ces objets à partir de vos composants et de les garder suffisamment loin pour vous permettre de les rendre comme vous le souhaitez.

snake5
la source