Spiel(Programmier)-Logik



  • Danke für deine ausführliche Antwort hat mir weiter geholfen :).

    Bis jetzt habe ich immer neue Gegner und neue Schüsse mit "new" in std::vector gespeichert und mit "delete" und "erease" wieder gelöscht. Wusste nicht (oder hab nicht dran gedacht) dass das ohne zeiger gehen würde.

    Und bei der Bewegung hat die Gegner-Klasse die funktion void aktion() die für das "Leben/Aktion" des Gegners zustängig ist und somit auch seine Bewegung steuert und auch ob er schiessen soll. Nachher kamm dann auch die Kollisionsabfrage damit die Gegner nicht wie Geister durcheinander fliegen:

    void Gegner::aktion(sf::RenderWindow& App, std::vector <Gegner*>& alleGegner){
    
    	// Reaktion
    	if(zeit > reaktion){
    		zeit = 0.f;
    		bewegeDauer = SpielFunktionen::zufallsZahl(1, 10) / 10.f;
    		schuesse = SpielFunktionen::zufallsZahl(0, 3);
    	}
    	zeit += App.GetFrameTime();
    
    	// bewegen...
    	if(bewegeDauer > 0.f){
    		if(gibPosition().x1() > 100){
    
    			bool kollidiert = false;
    			for(unsigned int i=0; i < alleGegner.size(); i++){
    
    				// Kollision - damit die Schiffe einander nicht überfahren ;)
    				if(id != alleGegner[i]->gibId()){
    					Position2d kolliPos	= gibPosition();
    					kolliPos.A.x = kolliPos.A.x - 1;
    					kollidiert = SpielFunktionen::Kollision(kolliPos, alleGegner[i]->gibPosition());
    
    					if(kollidiert)
    						break;
    				}
    			}
    			if(kollidiert == false)
    				bewegeVorwaerts(App.GetFrameTime());
    
    		}
    		else
    			zerstoeren = true;
    
    	}
    	bewegeDauer -= App.GetFrameTime();		
    
    	if(schuesse)
    		schuss(App.GetFrameTime());	
    
    }
    


  • void Gegner::aktion(sf::RenderWindow& App, std::vector <Gegner*>& alleGegner)
    

    Das wäre ein guter Anfang, um Abhängigkeiten zu verkleinern.

    Wozu braucht die Funktion das Fenster, dazu noch mit Schreibzugriff? Lediglich für GetFrameTime() , welche aber const ist. Ich würde daher eher einen Parameter

    float frameTime
    

    deklarieren. Möglicherweise kann man sich dadurch ersparen, den SFML-Header einzubinden, und gewinnt mindestens Kompilierzeit.

    Nun zu

    std::vector <Gegner*>& alleGegner
    

    : Wie gesagt, wäre

    std::vector <Gegner>& alleGegner
    

    schon einmal sinnvoller, aber es bleibt ein weiteres Problem. Ich würde die Kollisionsabfrage nicht in der Gegnerklasse implementieren. Zum Beispiel prüfst du jeden Gegner mit jedem, und zwar unnötigerweise je zwei Mal. Ausserdem braucht nun jeder Gegner eine ID und eine Abfrage, ob es sich beim geprüften Gegner nicht um sich selbst handelt. Das ist unnötig komplex. An zentraler Stelle könntest du die ganze Abfrage symmetrisch und massiv einfacher handhaben – in einer verschachtelten For-Schleife deckst du alle möglichen Kollisionen ab und brauchst keine Sonderbehandlung mehr.

    Noch ein Detail:

    if(kollidiert == false)
    // wird zu ->
    if (!kollidiert)
    


  • @ghostboss/Nexus:
    Wie so vieles in der Software-Entwicklung sind auch bei deinen (->ghostboss) Fragen einige dabei die man mal generell mit "ist Ansichtssache" oder "was besser passt/was weniger Probleme macht" beantworten kann 😉

    Nexus schrieb:

    ghostboss schrieb:

    Das gleiche auch mit dem Schiessen. Soll das Objekt selber schiessen oder nur eine Variable setzen und dann ausserhalb abfragen?

    Hier würde ich eine Funktion schreiben. Die Spiel-Klasse fragt den Gegner, ob er gerade schiesst, und erzeugt falls nötig einen neuen abgefeuerten Schuss.

    // Deklaration (type ist Output-Parameter):
    bool Enemy::Shoots(ProjectileType& type);
    // ...
    // Aufruf (innerhalb verwaltender Klasse):
    ProjectileType type;
    if (myEnemy.Shoots(type)) // verändert evtl. type
        CreateProjectile(type);
    

    Natürlich ist das alles nur eine Designmöglichkeit, aber ich habe mit ihr eigentlich gute Erfahrungen gemacht.

    Also ich würde das Erzeugen von Schüssen auf jeden Fall der Klasse zuordnen die "die Waffe trägt", also in dem Fall dem Spieler-Schiff/Gegner-Schiff/Turret/... .

    Bei deinem Design fehlt z.B. schonmal die Möglichkeit dem Schuss Parameter mitzugeben, wie z.B. die Startposition (die sich nicht mit dem Hot-Spot des schiessenden Objektes decken muss) oder die Richtung in die der Schuss wegfliegt. Natürlich könnte man solche Parameter als zusätzliche Output-Parameter rausgeben. Es können aber immer neue dazukommen. z.B. könnte ein Schuss verschiedene Schadens-Werte haben. Für jede "Stärke" eine neue Klasse zu machen ist IMO gänzlich unsinnig, also schon wieder ein neuer Parameter. Oder es könnte sich um Lenkraketen handeln die ein vom Spieler vordefiniertes Ziel treffen. Schon wieder ein neuer Parameter. Uswusf.

    Dann hast du mit dem Design auch nicht die Möglichkeit mehrere Schüsse auf einmal zu erschaffen (z.B. für klassisches "Spread Fire"). Kann man natürlich auch irgendwie reinquetschen, z.B. indem man die "Shoots" so oft wieder aufruft bis sie irgendwann "false" zurückliefert.

    IMO alles nicht schön oder wünschenswert.

    Ich würde wie gesagt eher das Erschaffen der Schüsse/Raketen/... dem schiessenden Objekt selbst zuordnen. Das schiessenden Objekt weiss ja welche Art Schuss/Rakete/... es erschaffen will, und wie viele, und was diese Schüsse/... für Parameter brauchen, d.h. dieses "Problem" ist schonmal gegessen. Was noch bleibt ist diese Schüsse/... irgendwie in die "Welt" reinzuhängen.

    Wenn man hier mit Polymorphie und Zeigern arbeitet wird das relativ einfach:

    class World;
    
    class GameObject
    {
    public:
        virtual void OnEnterWorld(World* world)
        {
            m_world = world;
        }
    
        virtual void DoLogicTick(/* evtl. double deltaTime */) = 0;
    
        // ...
    
    protected:
        World* m_world;
    };
    
    class World
    {
    public:
        virtual void AddGameObject(GameObject* go)
        {
            try
            {
                m_gameObjects.push_back(go);
                go->OnEnterWorld(this);
            }
            catch (...)
            {
                // ich würde das eher über eine guard-klasse lösen, aber ich will den vermutlich noch etwas "grünen" fragesteller nicht gänzlich überfordern
                if (m_gameObjects.size() > 0 && m_gameObjects.back() == go)
                    m_gameObjects.pop_back();
                delete go;
                throw;
            }
        }
    
        // ...
    
    private:
        std::vector<GameObject*> m_gameObjects;
        // ...
    };
    
    class SomeBullet : public GameObject
    {
    public:
        SomeBullet(double posX, double posY, double speedX, double speedY, double damage);
        // ...
    };
    
    class BattleShip : public GameObject
    {
    public:
        void DoLogicTick(/* evtl. double deltaTime */)
        {
            if (blah)
            {
                for (int i = 0; i < 100; i++) // MASSSIVE spread fire :)
                    m_world->AddGameObject(new SomeBullet(...));
            }
        }
    
        // ...
    };
    

    Ist nur ne grobe Skizze aber hoffentlich ausreichend damit man versteht was ich meine.

    Wenn ein Objekt nun sterben will kann man das hier auch relativ einfach machen. Entweder man verpasst der "Welt" eine Methode "void RequestRemoval(GameObject*)", wo das Objekt selbst seine Entfernung aus der Welt beantragen kann. Oder man verpasst dem GameObjekt eine Methode "bool RemovalRequested() const". (Denkbar ist auch beides zu machen, aber das hielte ich für ungünstig - mehrere Arten zu haben ein und dasselbe zu machen ist meist schlecht.)

    Ebenso sollte dann vermutlich eine "OnLeaveWorld" Funktion dazukommen, in der die Welt das Objekt darüber informiert dass es jetzt entfernt wird.

    ----

    So kann man IMO alles halbwegs schön kapseln. z.B. braucht ein GameObjekt nichts darüber zu wissen ob die Welt nun einen std::vector<GameObjekt*> verwendet, oder eine std::list oder einen ganz anderen Container. Auch kann ein GameObjekt nicht wild in dem Container rumwühlen, sondern nur über definierte Funktionen der Welt mit dieser interagieren.



  • Nexus schrieb:

    Nun zu

    std::vector <Gegner*>& alleGegner
    

    : Wie gesagt, wäre

    std::vector <Gegner>& alleGegner
    

    schon einmal sinnvoller

    Hm. Finde ich nicht, wie man vermutlich aus meinem vorigen Posting entnehmen kann 🙂
    Gerade std::vector hat hier einige Nachteile:
    * man kann sich nirgends Zeiger/Iteratoren auf Objekte merken
    * Objekte müssen kopierbar sein (widerspricht grundsätzlich dem was ich unter einem Game-Object verstehe -- etwas was eine "Identität" hat sollte IMO nicht kopierbar sein)
    * man braucht für jede (Sub-)Klasse einen eigenen Vektor

    Die von dir auch erwähnten Pointer-Container oder Smart-Pointer (+normale Container) sind dagegen sicher eine gute Sache.

    Allerdings kommt man vermutlich auch ohne das (gut) aus. Die Ownership ist denkbar einfach: alles gehört der "Welt" Klasse. Einzig wenn man Objekte hat die Zeiger auf andere Objekte halten, könnte es "schwierig" werden. Wobei auch das gut lösbar ist -> Observer Pattern.



  • hustbaer schrieb:

    Bei deinem Design fehlt z.B. schonmal die Möglichkeit dem Schuss Parameter mitzugeben, wie z.B. die Startposition (die sich nicht mit dem Hot-Spot des schiessenden Objektes decken muss) oder die Richtung in die der Schuss wegfliegt. Natürlich könnte man solche Parameter als zusätzliche Output-Parameter rausgeben. Es können aber immer neue dazukommen. z.B. könnte ein Schuss verschiedene Schadens-Werte haben. Für jede "Stärke" eine neue Klasse zu machen ist IMO gänzlich unsinnig, also schon wieder ein neuer Parameter. Oder es könnte sich um Lenkraketen handeln die ein vom Spieler vordefiniertes Ziel treffen. Schon wieder ein neuer Parameter. Uswusf.

    Dann hast du mit dem Design auch nicht die Möglichkeit mehrere Schüsse auf einmal zu erschaffen (z.B. für klassisches "Spread Fire"). Kann man natürlich auch irgendwie reinquetschen, z.B. indem man die "Shoots" so oft wieder aufruft bis sie irgendwann "false" zurückliefert.

    Eine andere Möglichkeit wäre z.B. einen Container mit allen erzeugten Projektilen zurückzugeben (oder als Output-Parameter zu haben). Oder einen Output-Iterator zu verwenden. Hat natürlich alles auch Nachteile, aber diese Alternativen sind mir gerade eingefallen.

    Deine Idee ist auch gut, vor allem da man durch den Funktionsaufruf beliebig Waffen hinzufügen kann. Mich stört halt ein wenig, dass sich Welt und Gegner gegenseitig kennen. Aber falls das tatsächlich ein Problem darstellen sollte, könnte man immer noch ein Zwischenmodul (Waffenverwalter oder so) einbauen... 😉

    hustbaer schrieb:

    Gerade std::vector hat hier einige Nachteile:
    [...]
    * Objekte müssen kopierbar sein (widerspricht grundsätzlich dem was ich unter einem Game-Object verstehe -- etwas was eine "Identität" hat sollte IMO nicht kopierbar sein)

    Das ist ein interessanter Punkt. Ich habe mich bisher meist an die C++-Philosophie gehalten, relativ viel Wertsemantik einzusetzen und zum Teil auch Objekte zu kopieren, wenn semantisch gesehen gar keine Kopie stattfindet. Findest du dieses Vorgehen schlecht?

    Die Alternative hat zwar einige Vorteile: Man kopiert nicht "aus Versehen" und hat wahrscheinlich effizienteren Code, zudem erlaubt die Indirektion, Zeiger und Referenzen auf die Objekte über längere Zeit zu speichern. Aber man verbarrikadiert sich auch einige Möglichkeiten, wie z.B. die direkte Verwendung von STL-Containern, oder falls tatsächlich eine Kopie benötigt wird. Und man verliert womöglich Zeit durch die Heap-Allokationen für einzelne kleine Objekte.

    Ab wann hat für dich ein Objekt eine Identität, sodass du es unkopierbar machst?

    hustbaer schrieb:

    * man braucht für jede (Sub-)Klasse einen eigenen Vektor

    Das sehe ich eher als Vorteil an. Bei verschiedenen Containern hat man statische Typsicherheit und muss nicht alles über Laufzeitpolymorphie lösen. Man kann so z.B. für alle Gegner eine Aktion durchführen, ohne (implizite oder explizite) Typunterscheidungen zu verwenden. Beispielsweise habe ich in einem Spiel eine Render-Klasse, welche eine Draw() -Methode für unterschiedliche Spielelemente überlädt und so spezifisch zeichnet.

    Wenn man alles in einen Container wirft, kann die dynamische Verwaltung relativ kompliziert werden. Z.B. durch Double-Dispatch für Kollisionsabfragen. Und Aktionen, die nur für einen Teil der Spielelemente sinnvoll sind, muss man als virtuelle Funktionen implementieren, welche in den meisten Klassen einfach nichts tun. Man kann natürlich auch schöne Patterns wie Visitor anwenden, aber teilweise finde ich den statischen Ansatz einfacher und naheliegender.



  • "Alles in einem Vektor" und "für jede Subklasse einen eigenen Vektor" schließen sich auch nicht aus.
    Einige Dinge sollen mit allen Objekten gemacht werden (tick() aufrufen zum Beispiel), andere wiederum nur für Objekte einer einzelnen Subklasse.



  • Die Welt-Klasse finde ich gut 🙂 , aber da hab ich eine Frage. Die Klasse hat ja std::vector Objekte von Typ GameObject ( std::vector<GameObject*> m_gameObjects ), neue Objekte werden also da gespeichert und die Klasse kann sie auch löschen. Doch neben diesen std::vector muss ich ja auch noch ausserhalb eine anderen std::vector habe, dass das vollständige Objekt beinhaltet.

    Wenn z.B. die Klasse "BattleShip" noch weitere Funktionen hat dann kann man ja nicht über die Welt-Klasse auf diese Funktionen zugreifen weil in der Welt-Klasse ja der Typ GameObjekt ist. Oder ist das ein Schreibfehler? Wenn nicht dann muss ich ja zwei std::vectoren verwalten. Oder habe ich da was übersehen 😕.

    EDIT:
    Hab das mal kurz nachgebaut...
    Die Funktionen in BatleShip kann ich ja alle virtualisieren (also in GameObject alle Funktionen mit virtual deklarieren).



  • Entweder du hast nur einen Container mit allen möglichen Elementen und schaffst es, diese durchgehend generisch zu behandeln. Das kann aber wie erwähnt mühsam werden. In diesem Fall empfehle ich boost::ptr_vector<GameObject> statt std::vector<GameObject*> , der ist nämlich im Bezug auf besitzende Zeiger um einiges komfortabler und sicherer. Die Boost-Bibliothek könntest du dir ohnehin einmal anschauen, falls du das bisher noch nicht getan hast. 😉

    Oder aber du hast mehrere Container, jeden für einen Klassentyp. Dann ist der Typ zur Kompilierzeit bekannt – du brauchst keine Fallunterscheidungen und kannst spezifische Aktionen direkt ausführen. Vielleicht kann es sich lohnen, einen zusätzlichen Container mit passiven Zeigern auf alle möglichen Objekte (aus verschiedenen Containern) zu haben, allerdings hast du dann wieder zusätzlichen Verwaltungsaufwand, weil du alles konsistent halten musst. Für die besitzenden Container könntest du std::vector<Enemy> bzw. std::vector<Projectile> nehmen, für den passiven allenfalls std::vector<GameObject*> .



  • @ghostboss:
    Hm.
    Dir fehlen da ein paar Grundlagen. Vererbung, virtuelle Funktionen, rauf/runter casten etc.

    Entweder du verwendest ein Design wo du ohne diese Dinge auskommst, oder du wirst dich diesbezüglich wohl noch etwas schlau lesen müssen.



  • Evtl. willst du die ja mal den DMC Sourcecode anschauen, wie wir das da machen? Bei uns gibts da z.b. in der GPIngame Klasse die std::list< IGameObject* > m_liGameObjects; f'`8k

    Gruß, TGGC (der kostenlose DMC Download)



  • Hallo liebe C++ Coummunity,

    ich bin gerade dabei ein Spiel zu schreiben und bin jetzt soweit, dass ich mir um die Collisiondetection Gedanken mache. Ich würde gerne Wissen wie ich dies am besten bewerkstelligen kann. Ist es besser viele vectoren mit mit jeweils einer Klasse zu haben und diese dann mit einander zu überprüfen oder wie hier schon erwähnt worden ist, einen vector der auf Boost basiert und somit die Datentypen egal werden.

    Wäre es von Vorteil eine Klasse ObjectManager zu schreiben die alle Objekte enthält?

    Und noch eine Frage zum Schluss^^. Wenn ich z.B. 30 Gegner habe, muss jederein Objekt der Gegnerklasse sein, womit sie sehr Ressourcen fressend werden, gibt es da einen Trick diese alle mit einem Zeiger auf einer Klasse basieret existieren zu lassen?



  • Benutz doch einen eigenen Thread, mit diesem hat dein Problem nichts zu tun.


Anmelden zum Antworten