Wie sollten Entitäten in einem Spiel aufeinander verweisen?

7

Ich habe viel über Designmuster gelesen, aber bei der Verwendung dieser Muster komme ich immer wieder auf eine Frage zurück. Wie sollen meine Entitäten auf Informationen über einander zugreifen?

Angenommen, ich verwende ein einfaches Muster, bei dem jedes Objekt aktualisiert und selbst gezeichnet wird:

class Monster extends GameEntity {
    public void update(double delta) {}
    public void draw(Graphics g) {}
}

Ich schreibe meine Update-Schleife, aber mir ist klar, dass Monster Kenntnisse darüber benötigt, wo sich der Spieler befindet, wo sich Städte auf der Hauptkarte befinden oder andere zufällige Daten.

Wie kommt Monster dazu? Initialisiere ich es mit einem Verweis auf alles Mögliche, was es braucht?

class Monster extends GameEntity {
    public Monster(Player player, City[] cityList) {
        this.player = player;
        this.cityList = cityList;
    }
}

Das sieht ziemlich chaotisch aus. Oder habe ich eine Art Hauptmanager, der Zugriff auf alles hat und das an alles weitergibt?

class Monster extends GameEntity {
    public Monster(GiantGlobalManager manager) {
        this.manager = manager;
    }

    public void update(double delta) {
        cities = this.manager.getCities();
    }
}

Oder vielleicht den Manager als Singleton? Wir könnten diese Manager auch in ihre Problembereiche einteilen.

class Monster extends GameEntity {
    public void update(double delta) {
        cities = CityManager.getInstance().getCities();
        player = PlayerManager.getInstance().getPlayer();

        guiManager.getInstance().createMenu();
    }
}

Aber das fühlt sich immer noch nicht ganz richtig an. Jetzt muss ich all diese Manager warten, und es scheint falsch, wenn meine Objekte all diese statischen Aufrufe tätigen. Zweitens sollten viele dieser Objekte nicht auf alles zugreifen können, was von diesen Klassen bereitgestellt wird. Alles auf alles zugreifen zu lassen, fühlt sich für mich wie eine Ausrede an.

Unabhängig davon, welche Muster ich verwende - ob ich Objekte in Komponenten zerlege oder nur meine Zeichnungs- und Aktualisierungsschleifen in separate Objekte verschiebe - brauche ich immer noch eine Möglichkeit, damit sie kommunizieren können. Was ist ein guter Weg, um dies zu tun? Was fehlt mir hier?

Sollte dies alles durch Ereignisse geschehen? Aber eine Liste der Städte zu bekommen ist nicht wirklich ein Ereignis, sondern nur Daten, die das Monster benötigt.

Aelast
quelle

Antworten:

6

Wie Sie bemerkt haben, hat jedes Architekturschema Vor- und Nachteile. Jedes Muster hat seine eigenen Auswirkungen darauf, wie und wann Objekte interagieren.

Zunächst würde ich eine kurze Auffrischung des SOLID-Designs empfehlen .

Was wir nun mit einer guten Architektur erreichen wollen, ist genug Abstraktion, dass wir nicht vom System behindert werden, wenn wir ein Feature hinzufügen möchten. Wenn wir das mit ein paar Singletons oder globalen Methoden auf eine Weise erreichen können, die sich auf die zukünftige Entwicklung skaliert , großartig! Es könnte sogar schneller enden.

Der objektorientierte Ansatz verfügt normalerweise über eine oder mehrere Fabriken , die entweder generische Entitäten erzeugen, denen Sie Komponenten hinzufügen, oder bestimmte Entitäten in einem Nicht-ECS-Design. Diese Fabriken befinden sich normalerweise in der Nähe der Kernklasse des Spiels. Als Beispiel für die Nicht-ECS-Version:

class Monster extends GameEntity {
    // Called by factory
    public Monster(EntityService service) {
        // store a private reference to the service
    }
    public GameEntity Find( /* parameters depend on game's needs */ ) {
        // call methods on service to locate entities given these conditions
    }
    public void Destroy() {
        myService.Remove(this);
    }

}

class MonsterFactory extends GameEntityFactory {
    private EntityService myService;
    public MonsterFactory(EntityService service) { /* ... */ }
    public GameEntity Spawn()
    {
        GameEntity entity = new Monster(myService);
        myService.Add(entity);
        return entity;
    }
}

class Physics {
    private EntityService myService;
    public Physics(EntityService service) {
        /* retain service in order to provide updates to entities */
    }
    public void update(double delta) {
        // make changes to each applicable entity in
        // "myService," which the entity will refine
        // in its own update method
    }
}

Der rote Faden dabei ist, dass jedes Objekt mit einem Verweis auf einen Dienst erstellt wird , der einige Kernfunktionen bereitstellt, bei denen das spezifische Verhalten entsprechend dem Verhalten des Objekts immer abstrahiert wird. Müssen Sie ein Monster erstellen? Beschwöre die Monsterfabrik vom Entitätsdienst. Müssen Sie eine andere Entität finden? Es ist im Entity-Service. Müssen Sie Nachrichten an alle Entitäten eines bestimmten Typs (z. B. physische Entitäten) senden? Filtern Sie sie einfach aus dem Entity-Service heraus. Auf diese Weise können Sie vermeiden, an Verweisen auf "tote" Entitäten festzuhalten.

Dies ist natürlich nur ein Beispiel. In der Praxis bestimmen die Bedürfnisse Ihres Spiels, wie dies zusammenkommen muss.

jzx
quelle
Das hat sehr geholfen, danke. Eine Frage: Enthält der Service in Ihrem Beispiel einen Verweis auf das Werk und das Werk hat auch einen Verweis auf den Service? Ist diese Art der zyklischen Objekthierarchie nicht zu vermeiden?
Aelast
Ja, vor allem, weil Objekte, auf die in der Hierarchie noch verwiesen wird, niemals Müll sammeln und ein Baum leichter im Kopf zu halten ist als ein Diagramm. Es ist jedoch ein notwendiges Übel, da wir einige Mittel haben müssen, um nachzuschlagen. Da der Dienst jedoch alles "besitzt", ist es einfach sicherzustellen, dass keine Referenzen vorhanden sind. Objekte müssen nur auf sich selbst aufpassen und vermeiden, aneinander festzuhalten.
jzx
3

Eine geringfügige Variante der Implementierung von jzx wäre die Verwendung einer Aktualisierungsmethode, die der Vorgehensweise beim Zeichnen ähnelt. In der architektonischen Gestaltung ist es nicht ungewöhnlich, ein Kontextobjekt zu haben, das häufig eine Vielzahl von Zustandsinformationen enthält.

 public interface GameEntity {
   void update(GameContext ctx);
   void draw(RenderContext ctx);
 }

Daher wird GameEntityIhre Aktualisierungsmethode in einer bestimmten Implementierungsklasse möglicherweise als solche geschrieben:

 public void update(GameContext ctx) {
   GameObjectManager goManager = ctx.getGameObjectManager();
   if(!hasTarget()) {
     List<GameEntity> entities = goManager.getVisibleEntitiesWithinRadius(position, 30);
     if(!entities.isEmpty()) {
       setTarget(CollectionUtils.selectRandomFromList(entities));
     }
   }
   // do other stuffs
   SomeGameSystem sgSystem = ctx.getSomeGameSystem();
   sgSystem.process(this, ctx);
 }

Sie können diesen Ansatz invertieren, wenn ein System sowohl als Factory als auch als Ort fungiert, an dem sich Ihre Aktualisierungslogik befindet. Diese Systeme werden in einer deterministischen Reihenfolge aktualisiert und führen kleine Sätze von Operationen an einem großen Eimer von Entitäten basierend auf Kriterien aus. Dies führt häufig zu schnelleren und cachefreundlicheren Vorgängen.

Im Wesentlichen könnte diese obige Aktualisierungslogik in eine Reihe solcher Systeme übertragen werden

 public AISystem implements System {

   public AISystem(SpacialQuerySystem spacialQuerySystem) {

   }

   public AIComponent createComponent(GameEntity entity) {
     AIComponent component = new AIComponent();
     component.setOwner(entity);
     components.put(entity, component);
     return component;
   }

   public AIComponent getComponent(GameEntity entity) {
     return components.get(entity);
   }

   @Override
   public void update(double deltaTime) {
     for(Entry<GameEntity, AIComponent> entry : components.entrySet()) {
       /* do whatever with the entity and use the injected systems */
     }
   }

 }

Letztendlich ist es viel einfacher, diesen Ansatz langfristig aufrechtzuerhalten, da Sie häufig Gründungssysteme erstellen können, die Sie in komplexeren Systemen wiederverwenden, die letztendlich einige Spielanforderungen implementieren. Darüber hinaus werden durch die Verwendung der Konstruktorinjektion Abhängigkeiten in Ihrer Architektur explizit identifiziert, ohne dass Code durchsucht werden muss, um festzustellen, was was benötigt.

Sie müssen nur noch einen Kartenlader oder einen solchen haben, der versteht, wie Sie Ihre Level-Daten lesen und die verschiedenen Systeme aufrufen, um Ihre Entitäten zu erstellen. Wenn Sie Archetypen und Archetypfabriken verwenden, kann dies möglicherweise in jene Fabriken abstrahiert werden, die letztendlich mit diesen Systemen interagieren.

Naros
quelle