[C++] Snake - obiektowo


(Manonim93) #1

Cześć,

Chciałbym napisać znaną wszystkim grę “Snake” z użyciem Allegro 5. Mój problem polega jednak na tym, że ni jak nie mogę określić obiektów i relacji między nimi.

Dodam, że język C++ znam w miarę dobrze ale moja wiedza na temat wzorców projektowych i Inżynierii oprogramowania jest słaba.

Jedyne co na razie wymyśliłem to stworzenie następujących klas:

  • POINT - zwykły punkt

  • SNAKE - lista POINTów , oraz dlugosc, metody ruch() i rosnij()

  • GAME - klasa zawierająca obiekt SNAKE, współrzędne jedzenia, stan gry, ostatni ruch, licznik czasu itp.

  • LOGIC - która miała by wskaźnik do klasy GAME i sprawdzałaby ogólnie kolizje, wygrane, przegrane.

  • EVENTS - która również miała by wskaźnik do klasy GAME i odpowiadałaby za wczytywanie klawiszy.

  • SCENE - klasa tworząca okno w Allegro 5, wyświetlająca całą grę


(Frankfurterium) #2

Dawno temu też to pisałem (jak chyba jakaś 1/3 studentów :P). Moja (prostsza) koncepcja była taka:

  1. Planszę najłatwiej zamodelować jako tablicę dwuwymiarową. Każdy punkt może zawierać kawałek węża, ścianę albo jabłuszko.

  2. Skup się na “głowie” węża. Musisz znać jej pozycję i wybrany przez gracza kierunek ruchu.

  3. Przy każdym ruchu usuwasz ostatni punkt ogona i umieszczasz przed głową - tam, gdzie wskazuje kierunek ruchu. Teraz on jest głową

  4. Po zeżarciu jabłuszka dodajesz punkt przed głowę, ale nie usuwasz z ogona.

  5. Naturalnie przy każdym ruchu badasz, czy pole jest puste, stoi na nim jabłko, ściana albo ogon i podejmujesz odpowiednią akcję.


(Manonim93) #3

Pytam raczej o zależności między obiektami, a nie jak zaimplementować ruch czy rośnięcie węża.


(etam) #4

Czy klasy LOGIC i EVENTS to na pewno klasy?

Kwestia sprawdzania kolizji (węża z samym sobą, lub innymi obiektami na planszy), to jest sprawdzanie, czy dany POINT jest równy innemu, bądź znajduje się w jakiejś kolekcji (np. kolejce węża). To by były odpowiednie metody węża wywoływane w głównej pętli programu.

Obsługa klawiszy, to też jest czynność, nie obiekt. W głównej pętli programu wczytujesz klawisze i na tej podstawie modyfikujesz np. kierunek poruszania się węża, lub przerywasz pętlę w celu wyjścia z gry.

Też nie wiem co masz na myśli pod “zależności między obiektami”.


(Frankfurterium) #5

Początkujący programiści czasem bywają nadgorliwi i niekiedy każdą ■■■■■ółkę chcieliby zamodelować w UML-u z OCL-em, wszędzie wciskać wzorce projektowe itd. Ten przypadek jest tak nieskomplikowany, że nie ma co się rozwodzić nad projektem czy dokumentacją. Skoro wiesz, jak to zaimplementować, to tak zrób. Projekt ma ci pomóc w implementacji, a nie odwrotnie.

No i po mojemu upychanie wszystkiego do obiektów tylko wydłuża implementację. Do tego naprawdę wystarczy jedna tablica, wektor, pętla i parę funkcji pomocniczych, ew. klasa/struktura ze współrzędnymi, żeby łatwiej trzymać dane w wektorze.


([alex]) #6

Bardzo uprości całość następująca struktura:

typedef enum { wEmpty, wApple, wSnake, wBorderUp, wBorderDn, wBorderLf, wBorderRt, wBorderUpLf, ... } What;

typedef enum { drUp, drDn, drLf, drRt } Dir;

const struct { int dx,dy; } DIR[]={{0,-1},{0,1},{-1,0},{1,0}};

class Map

  {

   unsigned Size,Width,Grow;

   class Cell

     {

      Cell *next,*prev;

      What what;

      Cell():next(0),prev(0),what(wEmpty) {}

     };

   Cell *Tb,*first,*last;

   Map(unsigned Height,unsigned Width,unsigned SnakeY,unsigned SnakeX):Size(Height*Width),Width(Width),Grow(0),Tb(new Cell[Size]),first(SnakeY*Width+SnakeX),last(first) {}

   ~Map() { delete[] Tb; }

   bool Move(Dir d)

     {

      unsigned pos=first-Tb;

      Cell *tmp=Tb+(pos/Width+DIR[d].dy)*Width+pos%Width+DIR[d].dx;

      if(tmp->what==wApple) Grow+=1+rand()%4;

      else if(tmp->what!=wEmpty) return false;

      tmp->next=first;

      first->prev=tmp;

      first=tmp;

      tmp->what=wSnake;

      if(Grow) --Grow;

      else

         {

          tmp=last->prev;

          tmp->next=0;

          last->prev=0;

          last=tmp;

          last->what=wEmpty;

         }

      return true;

     }

   void Draw(Screen &S) { for(unsigned y=0,i=0;i
  };

I masz absolutnie cały engine, zostało jedynie dodać GUI. Uwaga , pisano na sucho!


(etam) #7

No to żeś przydzwonił [alex]. Ja rozumiem, że klas nie należy wciskać tam gdzie nie trzeba, ale to co zaprezentowałeś jest przegięciem w drugą stronę.


([alex]) #8

etam, naprawdę nie rozumiem o co ci biega. Dwie proste klasy załatwiają właściwie całość. Nie widzę żadnego przegięcia.


(etam) #9

Chodzi mi o to, że czytając metodę Move można sobie włosy z głowy powyrywać. Wskaźnik wskaźnika wskaźnikiem pogania. Bez słowa komentarza ciężko się w tym połapać. http://www.osnews.com/story/19266/WTFs_m

O ile dobrze rozumiem cała plansza to dwuwymiarowa tablica obiektów klasy Cell zapisana w jednej tablicy. Korzystając ze wskaźników first i last oraz next i prev spinasz ze sobą komórki, które zawierają węża. Idea dobra, aczkolwiek wydaje mi się, że implementacja już na sam początek generuje dużo WTFs/m.

Powiedz co o tym myślisz (michal_93 jeżeli chcesz mieć jakąś frajdę z tego programu, to zajrzyj tutaj, ale dopiero jak skończysz swoją wersję):

http://susepaste.org/25849788

Postarałem się zaimplementować ten sam pomysł, ale w sposób obiektowy.


([alex]) #10

etam, to na co ty to przetworzyłeś to właśnie ten zły projekt. Ponieważ u mnie jest właśnie te dwa ■■■ - trzy wiersze przy przesunięciu głowy, oraz cztery wiersze do przesunięcia ogona. U ciebie to się stało trzema dodatkowymi klasami w których prawie każda metoda jest tym ■■■. Oraz to twoje to klasyczny przypadek pchania klas w każdą dziurę tak że już się nie mieści. Nie wieżę że ktoś będzie dłużej analizować 7 wierszy niż współdziałanie tych trzech klas z 12 metodami (w sumie). Może w tym moim projekcie ma sens wydzielić z metody Move trzy metody:

Ceil *NextHeadCeil(Dir d);

void PushHead(Ceil*) // wraz ze zmianą nazwy first na head

void PopTail() // wraz ze zmianą nazwy last na tail

To się nazywa przydzwoniłeś!


(etam) #11

Wersja uproszczona/ulepszona: http://susepaste.org/5404472

Twoja wersja z zaproponowanymi poprawkami: http://susepaste.org/59629030

Teraz są dużo bardziej podobne do siebie.

Na prawdę uważasz, że

Tb+(pos/Width+DIR[d].dy)*Width+pos%Width+DIR[d].dx

jest czytelniejsze od

get_cell(get_point(cell) + dir2vec(d))

i

Cell *tmp = last->prev;

tmp->next = NULL;

last->prev = NULL;

last = tmp;

prostsze od

body.pop_back();

?

Przy okazji polecam obejrzeć http://channel9.msdn.com/Events/GoingNa … pp11-Style (przynajmniej fragment 12:56 - 17:36)


([alex]) #12

Nie doczytałeś mojego postu (lub nie zrozumiałeś).

Uważam że:

Cell *tmp=NextHeadCeil(Dir d);

// Gdzie

Ceil *NextHeadCeil(Dir d) { unsigned pos=head-Tb; return Tb+(pos/Width+DIR[d].dy)*Width+pos%Width+DIR[d].dx; }

jest czytelniejsze od:

get_cell(get_point(cell) + dir2vec(d));

// Gdzie

struct Point { unsigned y, x; };

struct Vector2d { int dy, dx;};

Point operator+(const Point& p, const Vector2d& v) { return {p.y + v.dy, p.x + v.dx}; }

const Vector2d& dir2vec(Dir d) {

        static const Vector2d DIR[] = {{0,-1}, {0,1}, {-1,0}, {1,0}};

        return DIR[static_cast(d)];

}

                Cell& get_cell(const Point& p) { return board.at(p.y*size.width + p.x); }


                Point get_point(const Cell& cell) {

                        unsigned pos = &cell - board.data();

                        board.at(pos);

                        return {pos/size.width, pos%size.width};

                }

Oraz:

PopTail();

jest czytelniejsze od:

body.pop_back();

EDIT: Twoim problemem (i niestety nie tylko twoim) jak widzisz coś niezbyt zrozumiałego natychmiast zaczynasz jeszcze bardziej komplikować, zapominając o:

Więc uprościłem nieco, przy okazji dodając absolutnie minimalistyczny nie przenośny i prymitywny, windowsowy, konsolowy TUI:

#include 

#include 

#include 

#include 

#include 


typedef enum

  {

   wEmpty, wApple, wSnake, 

   wBorderUp, wBorderDn, wBorderLf, wBorderRt, 

   wBorderUpLf, wBorderDnLf, wBorderUpRt, wBorderDnRt

  } What;

typedef enum { drUp, drDn, drLf, drRt } Dir;


class Screen;

class Map

  {

   public:

   struct Cell

     {

      private:

      Cell *prev;

      What what;

      public:

      Cell():prev(0),what(wEmpty) {}

      operator What()const { return what; }

      bool SetWhat(What w) { if(what==wEmpty) what=w; return what==w; }

      bool SetApple() { return SetWhat(wApple); }

      friend class Map;

     };

   private:

   unsigned Size,Width,Grow,Apples;

   Cell *Tb,*head,*tail;

   public:

   Map(unsigned Height,unsigned Width,unsigned SnakeY,unsigned SnakeX):

     Size(Height*Width),Width(Width),Grow(0),Apples(0),

     Tb(new Cell[Size]),head(Tb+SnakeY*Width+SnakeX),tail(head) 

     {

     }

   ~Map() { delete[] Tb; }

   Cell *NextHeadCeil(Dir d) { static int DIR[]={-Width,+Width,-1,+1}; return head+DIR[d]; } //ale można zamienić poniższym

/* {

      if(d==drUp) return head-Width;

      if(d==drDn) return head+Width;

      if(d==drLf) return head-1;

      if(d==drRt) return head+1;

      throw "■■■";

     }*/

   void PushHead(Cell *newhead) { (head=head->prev=newhead)->what=wSnake; }

   void PopTail() { tail->what=wEmpty; tail=tail->prev; }

   bool Move(Dir d)

     {

      Cell *newhead=NextHeadCeil(d);

      if((Tb>head)||(head>=Tb+Size)) return false; // na wypadek gdyby nie było przewidzianych krawędzi

      if(newhead->what==wApple)

        {

         --Apples;

         Grow+=1+std::rand()%4;

        }

      else if(newhead->what!=wEmpty) return false;

      PushHead(newhead);

      if(Grow) --Grow;

      else PopTail();

      return true;

     }

   void RandApples(unsigned n) { while(Apples
   Cell *operator[](unsigned y) { return Tb+y*Width; }

  };

const unsigned MapHeight=24,MapWidth=70;

class Screen

  {

   Map map;

   Dir dir;

   public:

   Screen():map(MapHeight,MapWidth,MapHeight>>1,MapWidth>>1),dir(drLf) {}

   void Draw()

     {      

      for(unsigned y=0;y
     }

   void MakeBorder()

     {

      unsigned h=MapHeight-1,w=MapWidth-1;

      map[0][0].SetWhat(wBorderUpLf);

      map[h][0].SetWhat(wBorderDnLf);

      map[0][w].SetWhat(wBorderUpRt);

      map[h][w].SetWhat(wBorderDnRt);

      for(unsigned y=1;y
        {

         map[y][0].SetWhat(wBorderLf);

         map[y][w].SetWhat(wBorderRt);

        }

      for(unsigned x=1;x
        {

         map[0][x].SetWhat(wBorderUp);

         map[h][x].SetWhat(wBorderDn);

        }

     }

   void Draw(unsigned y,unsigned x,What w)

     {

      if(y&&!x) std::cout<
      static char TB[]=" @*--||++++";

      std::cout<
     }

   void NextFrame(int ticks)

     {

      long whait=clock()+ticks;

      while(whait>clock())

        {

         if((kbhit())&&(getch()==224))

           {

            switch(getch())

              {

               case 72: dir=drUp; break;

               case 80: dir=drDn; break;

               case 77: dir=drRt; break;

               case 75: dir=drLf; break;

              }

           }

        }

     }

   void run(unsigned apples,unsigned ticks)

     {

      HANDLE hStdOut=GetStdHandle(STD_OUTPUT_HANDLE);

      COORD coord={0,0};

      CONSOLE_SCREEN_BUFFER_INFO csbi;

      GetConsoleScreenBufferInfo(hStdOut,&csbi);

      MakeBorder();

      while(map.Move(dir))

        {

         map.RandApples(apples);

         SetConsoleCursorPosition(hStdOut,coord);

         Draw();

         NextFrame(ticks);

        }

     }

  };


int main()

  {

   Screen S;

   S.run(3,6);

   std::cout<
   for(unsigned i=0;i<20;++i)

     {

      static char GameOver[]="Game Over";

      std::cout<<'\r'<
      Sleep(300);

     }

   return 0;

  }

(Withorlo2) #13

A co jeśli Pan Wąż ma więcej niż jeden element i zachce nam się cofnąć o 180 stopni jednym ruchem?

-Game Over :smiley:


([alex]) #14

Jest tylko dwie sensowne reakcje na “próbę cofania się”:

  • Game Over.

  • Ignorowanie polecenia.


(etam) #15

// Chwilę mi to zajęło, bo miałem parę innych spraw na głowie, ale udało się

Ty twierdzisz, że komplikuję. Ja twierdzę, że upraszczam. Klasy takie jak Point czy Vector2d nie są skomplikowane, a pozwalają się precyzyjnie wyrażać. Tak samo metody klasy Board takie jak get_cell i get_point samym interfejsem dają czytającemu pojęcie co jest w środku, a ich zawartość jest prosta jak budowa cepa. Moim zdaniem takie podejście jest czytelniejsze od siedmiu operacji matematycznych w jednej linijce.

Z drugiej strony kwestia klas: moim zdaniem każda rzecz, która w sposób logiczny stanowi pewną odrębną całość powinna mieć własną klasę. U ciebie klasa Map to jest zupa planszowo-wężowo-jabłkowa.

Słowo kluczowe na dziś: Hermetyzacja.

Będzie dłużej, jeżeli w każdej linijce trzeba odkrywać co autor miał na myśli. Jeżeli klasy i ich metody są dobrze zaprojektowane, to nie trzeba nawet do nich zaglądać, żeby wiedzieć co się dzieje.


([alex]) #16

Z tego:

Cell *tmp=NextHeadCeil(Dir d);

// Gdzie

Ceil *NextHeadCeil(Dir d) { unsigned pos=head-Tb; return Tb+(pos/Width+DIR[d].dy)*Width+pos%Width+DIR[d].dx; }

uproszczeniem jest to:

Cell *tmp=NextHeadCeil(Dir d);

// Gdzie

Cell *NextHeadCeil(Dir d) { static int DIR[]={-Width,+Width,-1,+1}; return head+DIR[d]; }

można by nazwać uproszczeniem to:

Cell *tmp=NextHeadCeil(Dir d);

// Gdzie

Ceil *NextHeadCeil(Dir d) 

   {

    unsigned pos=head-Tb;

    unsigned posy=pos/Width+DIR[d].dy,

    unsigned posx=pos%Width+DIR[d].dx;

    unsigned newpos=posy*Width+posx;

    return Tb+newpos; 

   }

ale z całą pewnością to co ty podałeś:

get_cell(get_point(cell) + dir2vec(d));

// Gdzie

struct Point { unsigned y, x; };

struct Vector2d { int dy, dx;};

Point operator+(const Point& p, const Vector2d& v) { return {p.y + v.dy, p.x + v.dx}; }

const Vector2d& dir2vec(Dir d) {

        static const Vector2d DIR[] = {{0,-1}, {0,1}, {-1,0}, {1,0}};

        return DIR[static_cast(d)];

}

                Cell& get_cell(const Point& p) { return board.at(p.y*size.width + p.x); }


                Point get_point(const Cell& cell) {

                        unsigned pos = &cell - board.data();

                        board.at(pos);

                        return {pos/size.width, pos%size.width};

                }

nie da się nazwać uproszczeniem, za chińskiego.

No jasne, ty napisałeś, ty rozumiesz. Poproś jakiegoś początkującego o uporządkowanie powyższych 4-ch fragmentów od najbardziej zrozumiałego do najmniej zrozumiałego.

Bo przekładasz formę nad treścią. Zrób eksperyment o którym mówię wyżej a będziesz miał poprawną interpretacje tego stwierdzenia.

A ty rozumiesz to pojęcie? Uważasz że masz te klasy hermetyczne? U mnie wystarczy dodać dwa wierszy w sekcji private:

Map(const Map&){}

void operator=(const Map&){}

Będzie absolutnie hermetyczne, nie zrobiłem tego bo liczę na rozsądek programisty jeden snake = jedna mapa.

Aby u ciebie doprowadzić do hermetyczności to trzeba duuuużo kodu podopisywać.


(etam) #17

A co te dwa wiersze mają wspólnego z hermetyzacją? Z boost::noncopyable ci się pomyliło.


([alex]) #18

To tobie się myli. Hermetyzacja (w uproszczeniu) to kiedy nie ma szans zepsuć obiektu danej klasy używając udostępnionych metod i składowych. W twoim projekcie - kompletny brak hermetyzacji.