Laden...

Type Parameter, der ein Interface implementieren muss

Erstellt von Garzec vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.870 Views
G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren
Type Parameter, der ein Interface implementieren muss

Hallo,
ich versuche ein ganz minimalistisches Entity Component System aufzubauen. Ich könnte etwas fertiges nehmen, aber wollte es mal selbst versuchen. Zumal ich am Ziel bin, sobald diese Frage geklärt ist.

Ich erstelle ein Dictionary, das als Komponenten Pool dient. Dieser Pool hat als Key die ID der Entität (Entitäten sind bei mir nur GUIDs) und als Value die Komponente. Somit stellt jeder Pool einen einzelnen Komponententypen dar. Übergeordnet sollen die Pools in einer "Collection" liegen, damit man über diese Collection anhand des Komponententyps und der ID der Entität sehr schnell an die Komponente kommt, um Schleifen zu vermeiden.

Nun gibt es bereits die fertige Klasse KeyedByTypeCollection, die anscheinend darauf achten wird, dass jeder Pool in dieser Auflistung einzigartig bleibt.

Jede Komponente muss das Interface IComponent implementieren. Daher kann ich beim Hinzufügen, Suchen und Entfernen von Pools generische Typen erstellen, die das Interface erfordern.

        private KeyedByTypeCollection<Dictionary<Guid, IComponent>> componentPools = new KeyedByTypeCollection<Dictionary<Guid, IComponent>>();

        public Dictionary<Guid, TComponent> GetComponentPool<TComponent>() where TComponent : IComponent
        {
            return componentPools[typeof(TComponent)];
        }

        public void AddComponentPool<TComponent>() where TComponent : IComponent
        {
            componentPools.Add(new Dictionary<Guid, TComponent>());
        }

        public void RemoveComponentPool<TComponent>() where TComponent : IComponent
        {
            componentPools.Remove(typeof(TComponent));
        }

Nun ergibt sich das Problem, dass ich nicht

KeyedByTypeCollection<Dictionary<Guid, IComponent>>

schreiben kann, sondern eigentlich sowas haben müsste

KeyedByTypeCollection<Dictionary<Guid, Type where Type : IComponent>>

Hat jemand eine Idee, wie ich das Ganze fixen könnte?

16.806 Beiträge seit 2008
vor 5 Jahren

Versteh nicht ganz, worauf Du hinaus willst.

Aber wenn jede Komponente das Interface implementieren muss, dann passt doch

KeyedByTypeCollection<Dictionary<Guid, IComponent>>

Wieso solltest Du das nicht schreiben können? 🤔 Was genau meinst Du damit?

Oder willst Du auf sowas raus?

    public class MyBag<TComponent> where TComponent : IComponent
    {
        private KeyedByTypeCollection<Dictionary<Guid, TComponent>> componentPools = new KeyedByTypeCollection<Dictionary<Guid, TComponent>>();
    }
G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

Mit dem aktuellen Code erhalte ich aber 2 Fehler

GetComponentPool: Cannot implicitly convert IComponent to TComponent

AddComponentPool: Cannot convert from TComponent to IComponent

und da weiß ich nicht, wie ich das Ganze dann fixen muss.

T
461 Beiträge seit 2013
vor 5 Jahren

Hallo,

so wie ich das verstehe, versuchst du das Dictionary mit einem anderen Typen zu erstellen als es vorgegeben ist: (TComponent


// Vorgabe:
KeyedByTypeCollection<Dictionary<Guid, IComponent>>
// Versuch:
new KeyedByTypeCollection<Dictionary<Guid, TComponent>>()

Grüße

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

@Thomas

Das habe ich leider schon probiert. Aber es ist ja auch logisch, wieso es nicht funktioniert. TComponent ist ja nur ein generischer Typparameter, den ich bei den Methoden definiert habe. Außerhalb der Methoden ist diese Variable natürlich nicht vorhanden.

T
461 Beiträge seit 2013
vor 5 Jahren

Hallo,

ok hab mich falsch ausgedrückt, es war eine Feststellung daß das wegen den unterschiedlichen Typen nicht funktionieren kann.

Die Lösung müßte anders ausehen.
Wie, kann ich leider nicht sagen, da ich mom. viel am Hut hab...

Grüße

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

Also ich werde den EntityManager wahrscheinlich nicht statisch machen, weil statisch ist ja eigentlich böse ...

Aber ich dachte mir ich zeige einfach mal den Code, weil er sehr kurz ist, vielleicht verschafft das einen guten Überblick.

    internal interface IComponent
    {
    }

    internal interface ISystem
    {
        void Run();
    }

    internal static class EntityManager
    {
        private static List<Guid> activeEntities = new List<Guid>();

        public static IReadOnlyList<Guid> ActiveEntities { get { return activeEntities; } }

        private static KeyedByTypeCollection<Dictionary<Guid, IComponent>> componentPools = new KeyedByTypeCollection<Dictionary<Guid, IComponent>>();

        public static KeyedByTypeCollection<ISystem> Systems { get; } = new KeyedByTypeCollection<ISystem>();

        public static Guid CreateEntity()
        {
            Guid entityId = Guid.NewGuid();
            activeEntities.Add(entityId);
            return entityId;
        }

        public static void DestroyEntity(Guid entityId)
        {
            activeEntities.Remove(entityId);

            for (int i = 0; i < componentPools.Count; i++)
            {
                componentPools[i].Remove(entityId);
            }
        }

        public static Dictionary<Guid, TComponent> GetComponentPool<TComponent>() where TComponent : IComponent
        {
            return componentPools[typeof(TComponent)];
        }

        public static void AddComponentPool<TComponent>() where TComponent : IComponent
        {
            componentPools.Add(new Dictionary<Guid, TComponent>());
        }

        public static void RemoveComponentPool<TComponent>() where TComponent : IComponent
        {
            componentPools.Remove(typeof(TComponent));
        }

        public static void AddComponentToEntity(Guid entityId, IComponent component)
        {
            componentPools[component.GetType()].Add(entityId, component);
        }

        public static void RemoveComponentFromEntity<TComponent>(Guid entityId) where TComponent : IComponent
        {
            componentPools[typeof(TComponent)].Remove(entityId);
        }
    }

Momentan liegt es nur noch am oben genannten Problem. Aber ich hätte gerne eine Liste von Pools, die darauf achtet, dass jeder Pool einzigartig ist und die Pools selbst halten zur GUID (EntityID) die dazugehörige Komponente.

Durch die Dictionaries möchte ich einfach lange Suchen vermeiden.

3.170 Beiträge seit 2006
vor 5 Jahren

Hallo,

ThomasE. hat völlig recht. Was Du da machst, kann so nicht funktionieren.

KeyedByTypeCollection<Dictionary<Guid, IComponent>>

hier sagst Du, dass Deine Collection Dictionary<Guid, IComponent> enthält.

Dictionary<Guid, TComponent> GetComponentPool<TComponent>() where TComponent : IComponent

hier willst DU aber Dictionary<Guid, TComponent> zurückgeben. Daher der erste Konvertierungsfehler. Hier müsstest Du auch Dictionary<Guid, IComponent> zurückgeben.

componentPools.Add(new Dictionary<Guid, TComponent>());

genauso müsstest Du hier nicht Dictionary<Guid, TComponent> erstellen und hinzufügen sondern ebenfalls ein new Dictionary<Guid, IComponent>(). Daher der zweite Konvertierungsfehler.

Ob dann der Zweck so wie gedacht weiterhin erfüllt wird und/oder der Indexer von KeyedByTypeCollection richtig funktioniert, kann ich so nicht sagen.

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

Danke für deine Hilfe, deine genannten Fehler sind mir bereits aufgefallen, aber ich weiß nicht, ob ich einen Dictionary Cast erzwingen muss und ob dieser überhaupt so möglich ist. Ich ging davon aus, dass er bei where TComponent : IComponent weiß, dass dieses Element vom Typ des Interfaces ist.

Ich scheue mich aber ein wenig vor einem Cast, da dies wahrscheinlich etwas unperformant ist, von daher hat vielleicht noch jemand eine einfachere Idee? Damit meine ich einen ganz anderen Weg.

16.806 Beiträge seit 2008
vor 5 Jahren

Wenn Deine Collection verschiedene Implementierungen von IComponent halten soll, dann kommst Du so oder so nicht um einen Cast herum; dann brauchst aber eigentlich kein generischen Ansatz.
TComponent über eine generische Einschränkung wiederum nimmt nur einen einzigen Typ an, der IComponent implementiert.

G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

@Abt

Ok, ich würde es dann casten. Aber könntest du deine anderen Sätze noch weiter erläutern? Wenn ich den generischen Typen nehme, habe ich die Möglichkeit genau vorzugeben, dass nur etwas rein darf, was das Interface implementiert und eben diesen angegebenen Typen auch wieder rauszubekommen.

Als Beispiel hier mal ein Pseudo Movement System, wie das Ganze dann hinterher abläuft

    internal class Movement : ISystem
    {
        public void Run()
        {
            for (int i = 0; i < EntityManager.ActiveEntities.Count; i++)
            {
                Guid entityId = EntityManager.ActiveEntities[i];

                if (EntityManager.GetComponentPool<Position>().TryGetValue(entityId, out Position positionComponent))
                {
                    if (EntityManager.GetComponentPool<MovementSpeed>().TryGetValue(entityId, out MovementSpeed movementSpeedComponent))
                    {
                        positionComponent.X += movementSpeedComponent.Value;
                        positionComponent.Y += movementSpeedComponent.Value;
                    }
                }
            }
        }
    }
16.806 Beiträge seit 2008
vor 5 Jahren

Wenn ich den generischen Typen nehme, habe ich die Möglichkeit genau vorzugeben, dass nur etwas rein darf, was das Interface implementiert und eben diesen angegebenen Typen auch wieder rauszubekommen.

Pauschal: nein.

Wenn sich der generische Ansatz auf die Klasse bezieht, dann kann die Instanz dazu auch nur einen einzigen Typen halten => konkreter Typ.
Bezieht sich der generische Ansatz auf eine Methode, dann kann darin natürlich mehr landen - hast aber keine konkrete Typen mehr.

Dein Entity Manager ist nach Zweitem implementiert.

Ich bin mir nicht sicher, ob ich wirklich bei dem Ansatz getrennte Pools machen würde... sehe absolut keinen Vorteil.

G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

Du meinst du würdest einen großen Pool mit sämtlichen Komponenten halten und wenn du alle Positions Komponenten haben willst, danach filtern? Also alle Dictionary Values vom Typ Position?

16.806 Beiträge seit 2008
vor 5 Jahren

Ja, da ich keinen Vorteil in der Trennung sehe - selbst auf technischer Ebene nicht.

G
Garzec Themenstarter:in
50 Beiträge seit 2016
vor 5 Jahren

Bei der Performance bin ich mir unsicher, ob ein permanentes Filtern eines großen Dictionaries schneller wäre, als ein Casten eines kleineren Dictionaries.

W
955 Beiträge seit 2010
vor 5 Jahren

Ein Dictionary hat einen O(1)-Aufwand bei Zugriff auf ein Objekt. Am Besten mal die Aufwandsklassen anschauen.

16.806 Beiträge seit 2008
vor 5 Jahren

Bei der Performance bin ich mir unsicher,

premature optimization is the root of all evil

als ein Casten eines kleineren Dictionaries.

Casten musst so oder so - das ist es ja; jedenfalls so, wie Deine Architektur hier ist.
Der Unterschied is nur die Zugriffsgröße, siehe Kommentar von witte.