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

Sterowanie obiektem o wielu interfejsach

Fiszki IT
Fiszki IT
+1 głos
226 wizyt
pytanie zadane 15 września 2018 w C i C++ przez Piotr Batko Stary wyjadacz (13,170 p.)
edycja 16 września 2018 przez Piotr Batko

Proszę o radę przy projektowaniu gierki. SzucznaInteligencja potrzebuje kilka interfejsów do sterowania Potworem. Potwór może być Chodzący (posiada metody idźWLewo, idźWPrawo), Skaczący (posiada metodę podskocz), itd. Tych typów może być więcej i mogą być one mieszane. Np. może być Potwór który tylko skacze, lub taki który zarówno skacze jak i chodzi.

Teraz załóżmy, że pewna SzucznaInteligencja potrzebuje interfejsów A i B do poprawnego sterowania Potworem. Ten konkretny Potwór -- szkielet -- implementuje interfejsy A, B i C, także SztucznaInteligencja powinna dać radę nim sterować. Pytanie jakie się teraz nasuwa, to w jaki sposób przekazywać Potwora do SztucznejInteligencji?


Niemożliwe jest wywołanie:

sztucznaInteligencja.steruj(szkielet);

Bo jaki powinien być wtedy typ argumentu tej funkcji?


Można by napisać, że funkcja steruj przyjmuje obiekty nowego typu AB, dziedziczącego po wspomnianych A oraz B. Jednak nadal szkieleta nie można by tam przekazać, trzeba by w nim jeszcze ten nowy interfejs AB zaimplementować, a żeby uzyskać porządnie się zachowującą klasę o interfejsach A, B, C trzeba by zaimplementować wszystkie permutacje tych interfejsów AB, AC, BC, ABC -- raczej słabe rozwiązanie.


Można by umożliwić SztucznejInteligencji przekazywanie tych interfejsów osobno:

void SztucznaInteligencja::steruj(
  Chodzacy & chodzacy,
  Skaczacy & skaczacy,
  JeszczeJakis & jeszczeJakis
);

Jednak to prowadzi do nieeleganckiego wywołania:

sztucznaInteligencja.steruj(
  szkielet,
  szkielet,
  szkielet
);

I nie zapobiega przekazaniu interfejsów różnych obiektów np. do dwóch pierwszych szkieleta1, a do trzeciego szkieleta2.

Offtop: Pytanie dałem do kategorii C++, bo nie znalazłem kategorii "Architektura", "OOP" itp.

3 odpowiedzi

+1 głos
odpowiedź 15 września 2018 przez j23 Mędrzec (164,140 p.)
wybrane 16 września 2018 przez Piotr Batko
 
Najlepsza

Możesz zrobić np. tak:

class sztucznaInteligencja
{
public:
    template <typename T> 
    void steruj(T & obj)
    {
        Walker* wlk = dynamic_cast<Walker*>(&obj);
        if(wlk)
        {
            // do some shit...
        }
        
        
        Jumper* jmp = dynamic_cast<Jumper*>(&obj);
        if(jmp)
        {
            // do some shit...
        }
        
        ...
    }
    
    ...
};

Nie jest to rozwiązanie idealne, ale przy takiej hierarchii dziedziczenia rozwiązuje problem.

 

Od C++17 możesz pozbyć się dynamic_castów:

if constexpr (std::is_base_of_v<Walker, T>)
{
            Walker& wlk = obj;

            // do some shit...
}

if constexpr (std::is_base_of_v<Jumper, T>)
{
            Jumper& jmp = obj;

            // do some shit...
}
        

 

PS. to mieszanie polskich i angielskich nazw jest koszmarne.

komentarz 16 września 2018 przez Piotr Batko Stary wyjadacz (13,170 p.)

Przy podejściu z przekazywaniem interfejsów osobno:

WalkerJumper::control(Walker & w, Jumper & j);

Był taki problem, że przy wywołaniu trzeba było pisać:

ai.control(skeleton, skeleton);

To było nieeleganckie i umożliwiało omyłkowe podstawienie dwóch różnych obiektów do jednej funkcji. Czytając Twoją odpowiedź nasunął mi się pewien pomysł :) Można by przecież zrobić tak:

class WalkerJumperAi
{
public:
  template <typename T>
  inline void control(
    T & slave,
    typename std::enable_if<
         std::is_base_of<Walker, T>::value
      && std::is_base_of<Jumper, T>::value,
      T
    >::type * = nullptr)
  {
    control(slave, slave);
  }
private:
  // To ta brzydka funkcja z dwoma interfejsami
  void control(Walker & w, Jumper & j);
};

To pozwoli wywoływać funkcję prosto:

Skeleton skeleton;
WalkerJumperAi ai;
ai.control(skeleton);

A pod spodem, jeżeli będzie on dziedziczył po niezbędnych typach, zostanie wywołana odpowiednia funkcja. Ewentualna pomyłka przy podaniu innych argumentów (obecnie praktycznie niewykonalna, bo mamy jeden argument slave, który podajemy przecież zawsze) i nieeleganckie wywołanie zostało ograniczone do jedynego wystąpienia.

1
komentarz 16 września 2018 przez j23 Mędrzec (164,140 p.)

To inline jest zbędne w tym przypadku, bo metody zdefiniowane wewnątrz definicji klasy są domyślnie inline'owane.

 

Możesz zrobić też tak:

template<typename T,         
        typename = std::enable_if_t<std::is_base_of<Walker, T>::value && std::is_base_of<Jumper, T>::value, T>>
void control(T & slave)
{
        control(slave, slave);
}

wtedy metoda ma tylko jeden parametr.

+2 głosów
odpowiedź 15 września 2018 przez mokrowski VIP (145,420 p.)
edycja 15 września 2018 przez mokrowski

Ale dlaczego brniesz z myśleniem "AI powinno wykonać detekcję kombinacji typów". Nie powinno bo poprzez implementację konkretnej metody sterowania, przyjmuje obiekt o złożonych zdolnościach. Te złożone zdolności stają się charakterystyką obiektu. AI zakłada przecież w swoich algorytmach możliwe reakcje i zdolności danego obiektu z pewnego zbioru zachowań.

Złóż więc te atomowe zachowania w odpowiednie zbiory i wywołaj control dla danego zbioru zachowań.

Tu masz przykład:

#include <iostream>
#include <string>

// Atomic ability
struct Walker {
    virtual void walk() = 0;
    virtual void goLeft() = 0;
    virtual void goRight() = 0;
    virtual ~Walker() {}
};

struct Jumper {
    virtual void jump() = 0;
    virtual void longJump() = 0;
    virtual ~Jumper() {}
};

struct Flayer {
    virtual void fly() = 0;
    virtual void fastFly() = 0;
    virtual ~Flayer() {}
};

// Generic Monster class
struct Monster {
    virtual void attack() = 0;
    virtual ~Monster() {}
};

// Possible ability combination
struct WalkingFlayer: Walker, Flayer, Monster {
};

struct WalkingJumper: Walker, Jumper, Monster {
};

// Specific Monster classes
struct Zombie: WalkingJumper {
    // From Walker
    void walk() override {
        std::cout << "Zombie! Walk, walk, walk...\n";
    }
    void goLeft() override {
        std::cout << "Zombie! I'm turning left.\n";
    }
    void goRight() override {
        std::cout << "Zombie! I'm turning right.\n";
    }
    // From Jumper
    void jump() override {
        std::cout << "Zombie! Jump.\n";
    }
    void longJump() override {
        std::cout << "Zombie! Looong jump.\n";
    }
    // From Monster
    void attack() override {
        std::cout << "Zombie! ATTACK!!!\n";
    }
};

struct HellBat: WalkingFlayer {
    // From Walker
    void walk() override {
        std::cout << "HellBat! Walk... \n";
    }
    void goLeft() override {
        std::cout << "HellBat! To left..\n";
    }
    void goRight() override {
        std::cout << "HellBat! To right...\n";
    }
    // From Flayer
    void fly() override {
        std::cout << "HellBat! Flyyy...\n";
    }
    void fastFly() override {
        std::cout << "HellBat! Fast flyyy .. zip... \n";
    }
    // From Monster
    void attack() override {
        std::cout << "HellBat! ATTACK!!!\n";
    }
};

// AI for Monster controls.
struct AI {
    void control(WalkingJumper& monster) {
        std::cout << "Walking Jumper control.\n";
        std::cout << std::string(30, '=') << '\n';
        // Simple fake strategy...
        monster.walk(); monster.jump(); monster.attack(), monster.walk();
        monster.goLeft(); monster.walk(); monster.goRight(); monster.jump();
        std::cout << std::string(30, '=') << '\n';
    }
    void control(WalkingFlayer& monster) {
        std::cout << "Walking Flayer control.\n";
        std::cout << std::string(30, '=') << '\n';
        // Simple fake strategy...
        monster.walk(); monster.attack(); monster.walk(); monster.fly();
        monster.fastFly(); monster.attack(); monster.goLeft(); monster.fly();
        monster.goRight();
        std::cout << std::string(30, '=') << '\n';
    }
};

int main() {
    // Simple naive test
    AI ai;
    Zombie zombie1;
    Zombie zombie2;
    HellBat hb1;
    HellBat hb2;
    HellBat hb3;
    ai.control(zombie1);
    ai.control(hb1);
    ai.control(zombie2);
    ai.control(hb3);
    ai.control(hb2);
}

A jak Ci nie będą pasowały virtual'e, to możesz do implementacji użyć CRTP.

komentarz 16 września 2018 przez Piotr Batko Stary wyjadacz (13,170 p.)
edycja 16 września 2018 przez Piotr Batko

Ale dlaczego brniesz z myśleniem "AI powinno wykonać detekcję kombinacji typów". Nie powinno bo poprzez implementację konkretnej metody sterowania, przyjmuje obiekt o złożonych zdolnościach.

No przecież to rozwiązanie które pokazałeś wykonuje detekcję typów. Czyli konkretna metoda sterowania zostanie wywołana, gdy typ obiektu będzie złożony z: Walker, Flyer, Monster [WalkingFlyer]. Mnie się to bardzo podoba, że na etapie kompilacji zostanę poinformowany, gdy spróbuję sterować szkieletem jak nietoperzem, bo pomyliłem typy. Chciałbym jednak uniknąć tej klasy WalkingFlayer sklejające interfejsy. Dlaczego? Wyjaśniam niżej.


Powiedzmy zauważam, że zarówno WalkingFlayer, jak i WalkingJumper mają interfejsy Monster oraz Walker. Mając już takie dwa interfejsy mógłbym napisać algorytm, który śledzi gracza i atakuje go gdy się do niego zbliży. Zrobiłbym to żeby uniknąć duplikacji kodu, który w identyczny sposób śledziłby i atakował przeciwnika zarówno w WalkingFlayerze (powiedzmy, że chwilowo zamoczył skrzydła i nie lata), jak i WalkingJumperze. No i teraz pytanie: jak to zrobić? Można oczywiście tak:

Teraz do tego FollowerKiller (goni aż zabije) wrzucałbym WalkerMonster. Mógłbym zatem wrzucić zarówno WalkerJumperMonster, jak i WalkerFlyerMonster. To podejście jednak rozwala się, gdybym chciał umożliwić teraz podobny zabieg z dwoma innymi interfejsami, z których jeden należałby już do WalkerMonstera. Np. robię WalkerJumper, żeby napisać zachowanie: idź w prawo 2 m, podskocz, idź w lewo 2 m, podskocz. Diagram klas (pominąłem na nim WalkerFlyerMonster bo już by się zrobił niezły bajzel):

Wydaje mi się, że to jest za dużo zachodu. Chcę nową kombinację interfejsów -- robię nową klasę która je skleja.


Przy podejściu z przekazywaniem interfejsów osobno:

WalkerJumper::control(Walker & w, Jumper & j);

Był jedynie taki problem, że przy wywołaniu trzeba było pisać:

ai.control(skeleton, skeleton);

To było nieeleganckie i umożliwiało omyłkowe podstawienie dwóch różnych obiektów do jednej funkcji. Czytając odpowiedź j23 nasunął mi się pewien pomysł :) Można by przecież zrobić tak:

class WalkerJumperAi
{
public:
  template <typename T>
  inline void control(
    T & slave,
    typename std::enable_if<
         std::is_base_of<Walker, T>::value
      && std::is_base_of<Jumper, T>::value,
      T
    >::type * = nullptr)
  {
    control(slave, slave);
  }
private:
  // To ta brzydka funkcja z dwoma interfejsami
  void control(Walker & w, Jumper & j);
};

To pozwoli wywoływać funkcję prosto:

Skeleton skeleton;
WalkerJumperAi ai;
ai.control(skeleton);

A pod spodem, jeżeli będzie on dziedziczył po niezbędnych typach, zostanie wywołana odpowiednia funkcja. Ewentualna pomyłka przy podaniu innych argumentów (obecnie praktycznie niewykonalna, bo mamy jeden argument slave, który podajemy przecież zawsze) i nieeleganckie wywołanie zostało ograniczone do jedynego wystąpienia.


Osobiście uważam, że to rozwiązanie jest najlepszym z podanych. Nie wprowadza mętliku z dodatkowymi klasami tylko sklejającymi interfejsy, omija problem diamentu. Jeżeli nikt nie przedstawi lepszego rozwiązania, bądź nie pokaże, że to rozwiązanie wcale nie jest takie fajne jak mi się wydaje, oznaczę odpowiedź j23 jako najlepszą (bo dzięki tej odpowiedzi wpadłem na ten pomysł), a jako komentarz do niej dodam opisane tutaj rozwiązanie.

komentarz 16 września 2018 przez mokrowski VIP (145,420 p.)

Przemyśl to co pokazałeś:

1. Stracisz za chwilę kontrolę nad kombinacjami typów które chcesz sprawdzać bo kod meta-szablonowy przykryje Ci sens implementacji.

2. Starasz się "wypchnąć" działanie detekcji możliwie do statycznego etapu kompilacji. Co nie jest złe (samo w sobie). Jednak podając przykład z zamoczonymi skrzydłami, sugerujesz że problem jest "typu runtime". A tam jest sensowne rozwiązanie w postaci Abstrakcyjnej Fabryki (tnące aspekty dziedziczeń i rodzin obiektów). Poza tym ... po co? Jeśli wystarczy na etapie budowania struktury określić jakie ogólne maksymalne właściwości obiekt posiada zaś na etapie wykonania zwracać informację że teraz obiekt tego nie może zrobić bo.. ma mokre skrzydła A to jest odpowiedzialność AI a nie struktury typów by wykrywała takie fakty (a tym bardziej je obsługiwała). To jest problem runtime.

No przecież to rozwiązanie które pokazałeś wykonuje detekcję typów.

Oczywiście że tak! W tym jednak przypadku typ jest zbiorem zachowań bo to interfejs :)

3. Sklejanie interfejsów właśnie podkreśla fakt łączenia różnych atomowych zdolności. Podział tych zdolności wynika z tego aby ... składać ten złożony interfejs który jest także kategorią działań podejmowanych przez AI :) Bo przecież tego chcesz?

4. W żadnym wypadku nie sugeruję łączenia WalkerMonster i WalkerJumper poprzez dziedziczenie z nich. Z interfejsów atomowych składasz jedynie kombinacje których potrzebujesz by nie było diamentu dziedziczenia. Ma być jedna płaska hierarchia. Jeśli zaczniesz implementować polimorficzne control(...) z argumentem "interfejsu atomowego", będziesz miał niejednoznaczność. Poza tym... powiedz... ile będziesz miał takich kombinacji? Czy na tyle by budować takie rozwiązanie które proponujesz? Ile jest przypadków ogólnych a ile szczególnych?

Jeśli nie pasuje Ci duża ilość virtuali (bo mi by nie pasowała zapewne ze względu na wydajność ... tu spekuluję), zrób interfejsy przez składanie z CRTP. IMHO to rozwiązanie byłoby bardzo czyste.

Powiedzmy zauważam, że zarówno WalkingFlayer, jak i WalkingJumper mają interfejsy Monster oraz Walker. Mając już takie dwa interfejsy mógłbym napisać algorytm, który śledzi gracza i atakuje go gdy się do niego zbliży. Zrobiłbym to żeby uniknąć duplikacji kodu, który w identyczny sposób śledziłby i atakował przeciwnika zarówno w WalkingFlayerze (powiedzmy, że chwilowo zamoczył skrzydła i nie lata), jak i WalkingJumperze....

O... a zwykłej metody nie wystarczy wtedy by "uniknąć duplikacji kodu"? :) W zamian chcesz zbudować "metawykrywacz wszystkich możliwych przypadków kombinacji które przyjdą w przyszłości może do głowy" :) (wybacz ale tak to oceniam). Przecież to problem modularyzacji wywołań w logice aplikacji a nie struktury zależności klas w aplikacji.

IMHO, AI bazuje na sterowaniu obiektem o określonych właściwościach. I to jest problem tego przypadku. Inne algorytmy będą dla "chodzącego latacza (choć może byś w danym momencie kulawy i nie może wystartować... chyba że ze skały... )", inne dla "pełzacza latającego (choć teraz nie poleci bo skrzydła mu spalili a odrosną w następnej turze)". Wyjątki i przypadki przydarzające się w wyniku rozgrywki, obsłużysz zwykłą logiką a nie meta lub strukturą zależności.

Jeśli rozwiązanie które proponujesz Ci się podoba, nie będę forsował swojego :) Dość że zauważam różnicę w energii potrzebnej na utrzymanie kodu i ... powoli występujący paraliż analityczny :) (z rysowania kwadratów i strzałek niewiele wynika a działający kod daje już możliwość sprawdzenia koncepcji). Sprawdź więc to co proponujesz i tyle :)

 

komentarz 16 września 2018 przez Piotr Batko Stary wyjadacz (13,170 p.)

1. Stracisz za chwilę kontrolę nad kombinacjami typów które chcesz sprawdzać bo kod meta-szablonowy przykryje Ci sens implementacji.

Jedna funkcja sprawdzająca, czy argument posiada odpowiednie klasy bazowe, dla każdej klasy korzystającej z wielu interfejsów tego argumentu. Jedna funkcja w klasie miałaby w czymś tak mocno przeszkadzać?

(...) podając przykład z zamoczonymi skrzydłami, sugerujesz że problem jest "typu runtime".

Jest sobie klasa FlyerWalkerAi. Potrafi ona obsługiwać ptaka gdy ma skrzydła sprawne, ale jak siądzie na ziemi, to też może nim sterować (przecież ma do dyspozycji interfejs Walker). Inteligencja chciała sobie ptakiem lecieć w kierunku bohatera, ale on strzela w ptaka jakimś atakiem blokującym latanie. Inteligencja może dostać o tym powiadomienie i przyjąć plan B; zacząć sterować wykorzystując pozostałe, dostępne jeszcze funkcje.

To nie tak, że typ ptaka się zmienił i on już nie jest Flyerem. Po prostu ten typ czasami nie może latać. Nie przewiduję tu zmiany typu podczas wykonania aplikacji.

4. W żadnym wypadku nie sugeruję łączenia WalkerMonster i WalkerJumper poprzez dziedziczenie z nich. Z interfejsów atomowych składasz jedynie kombinacje których potrzebujesz by nie było diamentu dziedziczenia. Ma być jedna płaska hierarchia.

Załóżmy taką sytuację: Mam funkcję pomocniczą wykorzystującą interfejsy A i B, oraz klasę dziedziczącą po A, B i C. W jaki sposób wywołać funkcję pomocniczą na obiekcie takiej klasy? Innymi słowy: jak przekazać HellBata z Twojego pierwszego listingu do funkcji:

patrolBetweenTwoPoints(
  /* Need Walker and Flyer interfaces here */
);

Do tego sprowadza się problem tego wątku. Podany przykład sztucznej inteligencji o którym rozmawiamy, to faktycznie coś, co chciałbym teraz dobrze napisać. Chciałbym jednak również wyciągnąć tego przypadku lekcję na przyszłość.

Jest zasada ISP, próbuję ją stosować, ale pojawia się problem z przekazaniem obiektu do funkcji, która potrzebuje mieć dwa interfejsy do jego obsługi. Uogólniając Twoje rozwiązanie problemu ze sztuczną inteligencją uzyskujemy: masz rozdzielone interfejsy A, B, C; funkcja potrzebuje do pracy A i B? Scal rozdzielone interfejsy (A, B -> AB) i niech funkcja przyjmuje argument tego scalonego typu AB. No dobra, no to tworzę klasę o tych trzech interfejsach A, B i C, ale skoro jakaś funkcja potrzebuje przyjmować AB, to muszę odziedziczyć interfejsy: AB (naraz, to jest ten scalony) i osobno C.

I wyżej Cię pytam, co dalej z takim podejściem, gdy pojawi się funkcja potrzebująca do pracy interfejsów B oraz C? Nie mogę zrobić BC i dziedziczyć z AB i BC, bo uzyskuję wtedy diament. Mam teraz założyć sobie: "e tam, nigdy mi się tak nie przytrafi"?

1
komentarz 16 września 2018 przez mokrowski VIP (145,420 p.)
Cóż.. brnij. Zobaczysz sam co Ci się przytrafi a co nie. Ja dyplomu z prekognicji nie mam ;)
+1 głos
odpowiedź 15 września 2018 przez Criss Mędrzec (172,660 p.)
Rozwiązanie IMO zależy od tego co ma się dziać w tej funkcji `steruj`. Bo jeśli `steruj` miałaby przyjmować Monster* czy Monster& to i tak musiałbyś sprawdzać typ, żeby móc wołać te metody od chodzenia, skakania itd..

Ogólnie to proponuje wyrzucić wszystkie te interfejsy Skaczący, Chodzący itd., a ich metody wrzucić bezpośrednio do Monster. Nie jako pure virtual, ale zwykłe virtual ale z pustą implementacja. Jeśli bardziej wyspecyfikowana klasa potwora (dziedzicząca po Monster) chce skakać, to implementuje metode do skakania itd. Być może przydadzą ci się też metody canJump(), canWalk() itd...

Jeśli to ci z jakiegoś powodu nie odpowiada, to możesz mieć osobne `steruj` dla każdego rodzaju potwora (jeśli np. jest i skaczący i chodzący to puszczasz go do dwóch `steruj`), ale IMO bardzo brzydkie i odradzam coś takiego. Praktycznie rezygnujesz z korzystania z polimorfizmu.
komentarz 15 września 2018 przez Piotr Batko Stary wyjadacz (13,170 p.)

Proponujesz coś takiego?

class Monster
{
public:
  virtual bool canJump() const { return false; }
  virtual void jump() {}

  virtual bool canWalk() const { return false; }
  virtual void walkLeft() {}
  virtual void walkRight() {}

  virtual bool canAttack() const { return false; }
  virtual void attack() {}

  virtual bool canSwim() const { return false; }
  virtual void swimLeft() {}
  virtual void swimRight() {}

  virtual bool canFly() const { return false; }
  virtual void flyUp() {}
  virtual void flyDown() {}
  virtual void flyLeft() {}
  virtual void flyRight() {}

  virtual bool isLife() const { return false; }
  virtual Health getHealth() const {}

  virtual bool haveResistances() const { return false; }
  virtual Resistances getResistances() const {}

  virtual bool havePosition() const { return false; }
  virtual Position getPosition() const {}

  // Jak coś jeszcze przyjdzie do głowy to tu trzeba to wrzucić
};

Ja to diagnozuję jako Boski Obiekt i łamanie zasady ISP, ale może po prostu inaczej się nie da? :)

komentarz 15 września 2018 przez Criss Mędrzec (172,660 p.)

Ja to diagnozuję jako Boski Obiekt i łamanie zasady ISP

Troche, ale to tylko interfejs, więc  nie jest źle. Ale faktycznie sporo tego narąbane w jednym miejscu, więc może niech te Chodzący, Skaczący (...) zostaną, ale Monster z nich dziedziczy?

komentarz 15 września 2018 przez Piotr Batko Stary wyjadacz (13,170 p.)

Napisz proszę jakie problemy rozwiązuje zaproponowane przez Ciebie podejście.


ISP mówi: "klient nie powinien zależeć od metod których nie używa". Jeżeli piszę sztuczną inteligencję która potrzebuje chodzić w lewo 2 metry, podskoczyć, w prawo 2 metry, podskoczyć i tak w kółko, to nie potrzebuję dostawać funkcji do odpytywania o punkty życia czy latanie.

Gdyby sztuczna inteligencja tylko chodziła, potwór dziedziczący z wielu interfejsów może być po prostu przekazany tak:

sztucznaInteligencja.steruj(static_cast<Chodzacy &>(szkielet));

(Rzutuję powyżej żeby pokazać typ przyjmowany przez funkcję steruj). Jeżeli natomiast ta klasa potrzebuje kilku interfejsów, to wywołanie zaczyna się komplikować. Zerknij jeszcze raz do pytania, tam pokazałem jakby to wyglądało. Może powinienem po prostu zaakceptować to wywołanie z kilkukrotnie podawanym szkieletem? (patrz ostatni listing w pytaniu)

komentarz 15 września 2018 przez Criss Mędrzec (172,660 p.)

Jeżeli piszę sztuczną inteligencję która potrzebuje chodzić w lewo 2 metry, podskoczyć, w prawo 2 metry, podskoczyć i tak w kółko, to nie potrzebuję dostawać funkcji do odpytywania o punkty życia czy latanie.

No dobra, ale to niemożliwe o ile nie masz hard-coded obiektów, albo RTTI. 

Może powinienem po prostu zaakceptować to wywołanie z kilkukrotnie podawanym szkieletem? (patrz ostatni listing w pytaniu)

O ile masz jakąś niewielką liczbę ustalonych rodzajów zachowań, to może to jest dobre wyjście. Albo tak jak ja napisałem na końcu... to w zasadzie ten sam pomysł... Ale czy ta sztuczna inteligencja nie będzie się zachowywać inaczej w zależności jakie kombinacje cech dostanie? Czy skakanie, chodzenie, latanie to zupełnie niezależne moduły takiej SI?

Myślę, że za mało wiem o twoim projekcie i jak te klasy będą używane, dlatego się troche nie rozumiemy.

komentarz 15 września 2018 przez Piotr Batko Stary wyjadacz (13,170 p.)
edycja 15 września 2018 przez Piotr Batko

Nie bardzo rozumiem co to są "obiekty hard-coded". Mógłbym zrobić tak:

class Skeleton :
  public Walker,
  public Jumper
{
  // ...  
}

sztucznaInteligencja.steruj(
  static_cast<Walker &>(skeleton),
  static_cast<Jumper &>(skeleton)
);

(Te rzutowania znowu żeby pokazać przyjmowane typy przez funkcję)


Myślałem o tej sztucznej inteligencji tak: gdy chcę napisać jak patrolować, to wystarczy mi Chodzacy. SiPatrolująca przyjmowała by zatem tylko Chodzacego. Pisząc następnie zachowanie jakiegoś potworka -- np. szkieleta -- potrzebowałbym nim chodzić, skakać i atakować. Przyjmował bym zatem jego trzy interfejsy. Mógłbym jednak wewnątrz, gdybym chciał tym szkieletem partolować, wykorzystać uprzednio napisany moduł do patrolowania.

Podobne pytania

0 głosów
0 odpowiedzi 123 wizyt
0 głosów
5 odpowiedzi 74 wizyt
0 głosów
2 odpowiedzi 84 wizyt
Porady nie od parady
Odznacz odpowiedź zieloną fajką, jeśli uważasz, że jest ona najlepsza ze wszystkich i umożliwiła ci rozwiązanie problemu.Najlepsza odpowiedź

84,721 zapytań

133,526 odpowiedzi

295,919 komentarzy

55,997 pasjonatów

Motyw:

Akcja Pajacyk

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

Oto dwie polecane książki warte uwagi. Pełną listę znajdziesz tutaj.

...