• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Lista przeciwników z użyciem polimorfizmu - jak w odpowiedni sposób dodawać obiekty?

Object Storage Arubacloud
0 głosów
219 wizyt
pytanie zadane 22 lipca 2016 w C i C++ przez Mumin Nowicjusz (120 p.)
edycja 22 lipca 2016 przez Mumin

Witam! Od jakiegoś czasu tworzę grę top-down shooter i tworząc menedżer przeciwników, mający nimi zarządzać napotkałem się na problem.
Najpierw pokażę kod, a potem przejdę do sedna sprawy.

#pragma once
#include "Bullet.h"
#include "Collider.h"
#include "Enemy.h"
#include "TextureManager.h"

class EnemyManager
{
private:
    Collider* colliderPointer;
    Character* targetOfEnemies;
    TextureManager* textureManagerPointer;
    std::list<Enemy> enemies;
    std::list<std::list<Enemy>::iterator> enemiesToRemove;
public:
    EnemyManager() {};
    ~EnemyManager() {};
    void addEnemies(std::fstream& enemiesFile);
    void removeEnemy(std::list<Enemy>::iterator& it);
    void removeEnemiesToRemove();
    void updateEnemies(float delta);
    void drawEnemies(sf::RenderTarget& target);
    void setTargetOfEnemies(Character* _target);
    void setColliderPointer(Collider& _colliderPointer);
    void setTextureManagerPointer(TextureManager& _textureManagerPointer);
    void fireWhenTargetInRange();
};
void EnemyManager::addEnemies(std::fstream& enemiesFile)
{
    while(!enemiesFile.eof())
    {
        Enemy enemy;
        std::string name;
        enemiesFile >> name;
        enemy.setName(name);
        int health_points;
        enemiesFile >> health_points;
        enemy.setHealthPoints(health_points);
        std::string textureName;
        enemiesFile >> textureName;
        sf::Vector2f position;
        enemiesFile >> position.x;
        enemiesFile >> position.y;
        enemy.setPosition(position);
        sf::Vector2f velocity;
        enemiesFile >> velocity.x;
        enemiesFile >> velocity.y;
        enemy.setVelocity(velocity);
        std::string bulletTextureName;
        enemiesFile >> bulletTextureName;
        enemies.push_back(enemy);
        enemies.back().setTexture(textureManagerPointer->getReferenceToTexture(textureName));
        enemies.back().shootingWeapon.setBulletTexture(textureManagerPointer->getReferenceToTexture(bulletTextureName));
        enemies.back().chooseTarget(targetOfEnemies);
        colliderPointer->addCharacter(&enemies.back());
    }
}

Wszystko działa poprawnie. Jednakże problem pojawia się, kiedy zechcę użyć polimorfizmu, żeby tworzyć przeciwników różnych typów. Wiadomo, lista, która przechowuje teraz przeciwników jako takich, musiałaby wtedy przechowywać wskaźniki do obiektów ich reprezentujących. Wykorzystanie powyższej funkcji nie zadziałałoby, bo po opuszczeniu jej, obiekt na który wskazuje wskaźnik nie istniałby już. Pierwsze rozwiązanie, które zauważyłem i od razu wyrzuciłem do kosza, to stworzenie osobnych list na każdy typ wroga - ale to się przeczy z samą ideą polimorfizmu. Poczytałem coś o fabryce abstrakcyjnej, ale z tego co rozumiem, to przy dodawaniu kolejnej klasy dziedziczącej po Enemy, będę musiał modyfikować klasę bazową fabryki, jak i dodać kolejną pochodną. Przeszukiwanie angielskich for w poszukiwaniu rozwiązania też nie przyniosło żadnych skutków, być może źle formułowałem zapytania do wyszukiwarki. Macie jakieś pomysły? :)

komentarz 22 lipca 2016 przez obl Maniak (51,280 p.)

Chwileczkę, chwileczkę. Rozumiem, że uważasz, że jeżeli zrobisz listę:

std::vector<*enemy> tEnemy;

I będziesz w niej przechowywał wskaźniki do obiektów klasy enemy to to będzie polimorfizm? Poza tym Criss dobrze gada, powinieneś zaalokować pamięć za pomocą operatora new.

komentarz 22 lipca 2016 przez Mumin Nowicjusz (120 p.)
Jeżeli chodzi o polimorfizm, to takie przynajmniej odnoszę wrażenie, ponieważ będę mógł dodawać do tej listy obiekty dziedziczące po klasie Enemy, czyli używać polimorfizmu. Popraw mnie, jeśli się mylę :)

2 odpowiedzi

+2 głosów
odpowiedź 22 lipca 2016 przez criss Mędrzec (172,590 p.)
Jak sam zauważyłeś, póki co obiekt który tworzysz w funkcji przestanie istnieć po wyjściu z niej. Więc chcesz, żeby tak się nie działo. Dlaczego w takim razie nie skorzystasz z operatora new i nie zaalokujesz obiektu dynamicznie? Wtedy ty decydujesz kiedy on zniknie z pamięci. I wtedy wykorzystujesz wspomnianą przez ciebie liste wskaźników. Przy okazji wykorzystaj std::unique_ptr czy shared_ptr.
komentarz 22 lipca 2016 przez Mumin Nowicjusz (120 p.)

Właśnie tak myślałem, żeby użyć operatora new. Wreszcie wiem, do czego może mi się przydać, zawsze był dla mnie zagadką. :) Rozumiem, że mam to zrobić w ten sposób:

enemies.push_back(new Enemy());

Ale tutaj pojawia się pytanie, ponieważ używam konstruktora klasy bazowej. Kiedy będę chciał dodać obiekty klasy pochodnej, to to już nie zadziała. Można zawsze zrobić osobną funkcję dla każdego obiektu, chociaż nie wiem, czy to nie mija się z celem. No cóż, będę się zastanawiał, jeżeli macie jeszcze jakieś pomysły, to chętnie je poznam : )

komentarz 22 lipca 2016 przez obl Maniak (51,280 p.)
Pokaż jakie elementy przyjmuje kontener enemies i jak wygląda twoja klasa Enemy no i po czym ona dziedziczy, bo bez tego to ja za wiele ci nie powiem.
komentarz 22 lipca 2016 przez Mumin Nowicjusz (120 p.)
class Enemy : public Character
{
    friend class Collider;
    friend class EnemyManager;
protected:

    std::string name;
    int health_points;

    sf::Vector2f position;
    sf::Vector2f velocity;
    sf::Vector2f movement;

    Character* target;

    sf::Texture texture_sheet;
    sf::Sprite  character_sprite;

    float coll_left, coll_right, coll_top, coll_bottom;
    bool exist;

    enum STATE {STAND=0, WALK=1, RUN=2};
    STATE state;

    ShootingWeapon shootingWeapon;

    void setName(std::string _name);
    void setHealthPoints(int _health_points);
    void setVelocity(sf::Vector2f _velocity);
public:
    Enemy();
    Enemy(std::string _name, int _health_points, sf::Texture _texture, sf::Vector2f _position, sf::Vector2f _velocity);
    ~Enemy() {};
    void chooseTarget(Character* _target);
    bool isTargetInRange();
    virtual bool isExisting();
    virtual bool isCollidingWithTile(Tile tile);
    virtual bool isCollidingWithBullet(Bullet* bullet);
    virtual void serveCollision();
    virtual void serveCollisionWithBullet();
    void fireOnTarget();
    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
    void update(float delta);
    void setMovement(sf::Vector2f _movement, float delta);
    void setTexture(sf::Texture& texture);
    void setPosition(const sf::Vector2f position);
    sf::Vector2f getPosition();
    void move(float offsetX, float offsetY);
    void updateFiredBullets(float delta);
    void drawFiredBullets(sf::RenderTarget& target);
    void setCollisionBounds();
};

Oto klasa Enemy (IMHO trochę za duża, ale nad tym popracuję potem :P) Kontener enemies ma przechowywać obiekty klasy Enemy, oraz obiekty klas dziedziczących po niej, które póki co nie istnieją (zarówno klasy dziedziczące, jak i ich obiekty). Mają się one różnić sposobem ataku itp. (przesłonięta funkcja).

1
komentarz 22 lipca 2016 przez obl Maniak (51,280 p.)

Źle to robisz, załóżmy, że masz klasę humanEnemy, która dziedziczy po Enemy to obiekt tej klasy dodajesz do kontenera tak:

enemies.push_back(new humanEnemy( i tutaj lista argumentów konstruktora));

A tak w ogóle to destruktor zrób virtualny i klasa Enemy powinna być klasą abstrakcyjną. Zobacz, jak ja użyłem polimorfizmu tutaj.

komentarz 22 lipca 2016 przez Mumin Nowicjusz (120 p.)
edycja 22 lipca 2016 przez Mumin
Klasa Enemy nie była póki co abstrakcyjna, bo testowałem EnemyManager, a nie dodawałem jeszcze tych klas dziedziczących. Chociaż tutaj mogłem cię zmylić, bo napisałem, że lista ma zawierać obiekty klasy Enemy, a tak nie ma być. Mój błąd.. ;)  No, ale dobra, w mojej głowie narodził się pewien pomysł! Po prostu w pliku tekstowym dodam odpowiedni "argument", a potem switch'a, który w zależności od tego "argumentu", użyje odpowiedniego konstruktora, żeby dodać obiekt do listy. Chociaż chciałem uniknąć mega długiego switch'a. Dzięki wielkie! :D Aha, tak chcę się jeszcze upewnić. Czy wirtualny destruktor jest po to, żeby wymusić przesłonięcie go w klasie pochodnej, bo ten z bazowej sprząta tylko po bazowej? :)
1
komentarz 22 lipca 2016 przez criss Mędrzec (172,590 p.)
Wirtualny destruktor jest po to, żeby na pewno został wykonany właściwy destruktor. Jeśli pod wskaźnikiem klasy bazowej masz obiekt klasy pochodnej, to przy delete wykonałby się destruktor klasy bazowej. A tego nie chcemy. Dlatego informujesz o tym kompilator poprzez wirtualny destruktor.

A co do tego switcha, rozwiń bo nie rozumiem co chcesz zrobić, ale wielki switch nie brzmi dobrze.
komentarz 23 lipca 2016 przez Mumin Nowicjusz (120 p.)

Cała sprawa wygląda w ten sposób, że przeciwnicy i ich statystyki są zapisani w pliku tekstowym, którego zawartość wygląda w ten sposób:

0 Kelpie 150 Kelpie 300 700 120 120 Bullet
0 Kelpie 150 Kelpie 700 400 120 120 Bullet

Każda linijka to inny przeciwnik. Wiem, że wygląda to niczym czarna magia (nie wiadomo co jest czym), ale jest to raczej przygotowane nie pod ręczne wpisywanie to pliku, a pod późniejszy edytor map, który automatycznie będzie uzupełniał ten plik, a EnemyManager na jego podstawie doda przeciwników. To 0 na samym początku każdej linijki to właśnie ten typ. Zmodyfikowałem funkcję z pierwszego posta, wkleję tylko część dodaną, bo reszta to zamiana kropek na strzałki :P

void EnemyManager::addEnemies(std::fstream& enemiesFile)
{
    while(!enemiesFile.eof())
    {

        int type;
        enemiesFile>>type;
        switch(type)
        {
        case 0:
           enemies.push_back(new humanEnemy());
            break;
        }
/* (...) */
       }
}

I teraz w zależności od cyfry, która pojawiła by się w pliku tekstowym (potem zamienię na enum'a), musiałbym użyć innego konstruktora, dodając kolejną pozycję w switch'u. Zakładając, że chcę dodać trochę więcej rodzajów przeciwników, wychodzi nam całkiem duży switch. :(

+1 głos
odpowiedź 23 lipca 2016 przez MetRiko Nałogowiec (37,110 p.)

Spróbuję ci pomóc wypisując w punktach parę rad/wyjaśnień:
Po pierwsze.. zakładamy, że w kodzie ma się znaleźć:
> Klasa EnemyManager
Przechowująca wskaźniki do klasy Enemy oraz wywołująca ich główne metody
> Klasa Enemy (Klasa bazowa)
Zawierająca podstawę przeciwnika tj. zmienne (np. punkty życia), główne metody wirtualne (np. Init(), Live(), Draw())
> Dziedzice klasy Enemy (Klasy dziedziczące)
Określające konkretne zachowanie dla konkretnego przeciwnika oraz posiadające róże dodatkowe zmienne i metody typu: TimeToExplosion, Shoot().
--------------------------
Teraz pora na wyjaśnienie mechanizmów:
class EnemyManager()
> vector<Enemy*> Container; //Zawiera wszystkich przeciwników
> void Live() //Funkcja wykonująca się w logice.. Dla każdego przeciwnika wywołuje jego główną metodę logiki.. coś w stylu: Container[i]->Update();
> void DestroyEnemy() //Podobnie jak Live().. tyle, że w tym wypadku sprawdza czy metoda Enemy.IsDead() zwraca true czy false.. jeżeli zwraca true to z kontenera usuwamy danego delikwenta.. przy odrobinie sprytu można połączyć tą metodę z poprzednią.
> void Draw() //Podobnie jak wcześniej.. metoda ta wywołuje metodę Draw() dla wszystkich przeciwników.. Znajduje się ona jednak w renderze, a nie w logice!
> void AddNewEnemy(Enemy *NewEnemy) //Dodaje nowego przeciwnika do kontenera.. można jej używać np. w ten sposób: EnemyManager.AddNewEnemy(new WaterGolem());
> ~EnemyManager() // "Delete'uje" wszystkie obiekty w kontenerze
class Enemy
> //Jakieś zmienne, które będzie zawierał każdy przeciwnik
> virtual void Live() //Po prostu logika przeciwnika (ta metoda może być pusta)
> virtual void Draw() // -\\- render przeciwnika (ta metoda może być pusta)
> virtual ~Enemy() // Destruktor (w klasie bazowej) MUSI być wirtualny (może być pusty)
> virtual bool IsDead() // Zwraca true jeżeli przeciwnik "nie żyje"/powinien zostać usunięty ze kontenera w menadżerze
class WaterGolem : public Enemy
> //Dodatkowe zmienne potrzebne dla tego konkretnego typu przeciwnika
> void Live() //j.w. jednak tym razem nie może być pusta
> void Draw() // -\\-
> void IsDead() // Jeżeli warunek życia zmienił się (np. dodałeś czas do śmierci przeciwnika) to należy tą metodę tutaj zadeklarować.. jeżeli wyglądała by tak samo jak w klasie bazowej to nie musisz jej tu tworzyć.
--------------------------
Jak to ma działać?
Mając gotową klasę EnemyManager wywołujesz jej główne metody (tj. Live(), Draw()) w odpowiednich miejscach głównej pętli logiki.. teraz gdy będziesz chciał dodać nowego przeciwnika to wystarczy, że zapiszesz np. EnemyManager.AddNewEnemy(new WaterGolem(51,15)) //Gdzie 51,15 to pozycja gdzie ma się pojawić.. operator new zwraca wskaźnik dlatego taki zapis jest w 100% akceptowalny.
Oczywiście do tego wszystkiego należy jeszcze dodać chociażby kontrolę ilość przeciwników (aby nie dodawało więcej niż jakaś określona liczba.. co za dużo to nie zdrowo), albo przesyłanie odpowiednich zmiennych do menadżera by wiedział na czym renderować przeciwników.. czy też funkcję sprawdzającą kolizję z graczem.. ale to już jest zupełnie inny temat. Mam nadzieję, że pomogłem i rozwiałem twoje wątpliwości.

Podobne pytania

0 głosów
2 odpowiedzi 197 wizyt
pytanie zadane 13 sierpnia 2023 w C i C++ przez Janchess Początkujący (480 p.)
+1 głos
1 odpowiedź 151 wizyt
pytanie zadane 5 czerwca 2020 w C i C++ przez kamylmeister Nowicjusz (190 p.)
0 głosów
1 odpowiedź 300 wizyt
pytanie zadane 9 maja 2020 w C i C++ przez chomik1 Nowicjusz (140 p.)

92,631 zapytań

141,491 odpowiedzi

319,862 komentarzy

62,011 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...