Klasa Runner i spółka — wersja rozwojowa

Ciąg dalszy wykorzystania mechanizmów obiektowych w przykładzie wykorzystującym klasę Runner. Program prezentuje dziedziczenie, wykorzystanie pól chronionych, ponowną definicję funkcji funkcji składowych w klasie pochodnej.

Prosty program prezentujący klasę Runner dziedziczącą po klasie Point. Program zawiera również klasę PlayBoard, reprezentującą planszę gry, oraz klasę Game. Klasa Game jest klasą nadrzędną, jest właścicielem obiektu klasy Runner oraz obiektu klasy PlayBoard. Na planszy znaki '#’ reprezentują ściany, znaki '$’ prezentują gotówkę, którą może zbierać obiekt klasy Runnner przemieszczając się po planszy.

Program stanowi uzupełnienie wykładu z programowania w językach C/C++. Koncepcja klasy Runner została omówiona we wprowadzeniu do programowania obiektowego (pdf).

Zachęcam do przeanalizowania mechaniki tej prostej „gierki”, i jej rozwoju. Np. poprzez wprowadzenie innych elementów, które może zbierać gracz, elementy mogą też zabierać gotówkę (np. salon gier), można też się pokusić o wprowadzenie elementów ruchomych (duszki „dobre” i „złe”).

Do pobrania wersja źródłowa pliku (main03.cpp).

Poprzednie wersje: main02.cpp, main01.cpp.

Uwaga! Implementacja tylko dla Windows, obsługa konsoli realizowana za pośrednictwem WinApi. Najlepiej kompilować używając MinGW (środowiska Code::Blocka, DevC++) lub kompilatorów w środowisku C++ Builder. Proszę unikać środowisk VisualC++, CLion, QT, będą stwarzać problemy.

// Runner, wersja 03

#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <conio.h>

#if defined(__MINGW32__) || defined(_MSC_VER)
#include <windows.h>
#endif

// Kody klawiszy sterujących kursorem i klawiszy specjalnych
enum KEY_CODES
{
#ifdef __BORLANDC__
	KEY_BLANK = 0x0000,
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	KEY_BLANK = 0x00e0,
#endif
	KEY_UP = 0x4800,
	KEY_DOWN = 0x5000,
	KEY_LEFT = 0x4b00,
	KEY_RIGHT = 0x4d00,
	KEY_ESC = 0x001b
};

// Prototypy funkcji obsługi konsoli
int getKey();
void clearScreen();
void writeCharXY(int x, int y, char c);
void writeStrXY(int x, int y, char s[]);
void writeIntXY(int x, int y, int i);
void writeDoubleXY(int x, int y, double d);
void cursorOff();
void cursorOn();

// Rozmiary ekranu konsoli, nie uwzględniają ostatniego wiersza
// ten jest wierszem statusu i zawiera informacje o stanie gry
const int NUM_OF_COLS = 80;
const int NUM_OF_ROWS = 24;

// Klasa reprezentacji informacji o położeniu punktu
// w przestrzeni 2D, bez żadnych ograniczeń w sensie
// wartości współrzędnych
class Point
{
  public:
    Point(int startX = 0, int startY = 0);
      void setX(int newX);
      void setY(int newY);
      int  getX();
      int  getY();
  protected:
      int  x, y;
};

// Definicje funkcji składowych poza deklaracją klasy
Point::Point(int startX, int startY) : x(startX), y(startY)
{
}

void Point::setX(int newX)
{
  x = newX;
}

void Point::setY(int newY)
{
  y = newY;
}

int Point::getX()
{
  return x;
}

int Point::getY()
{
  return y;
}

// Klasa Runner, dziedziczenie pozycji ekranowej po klasie Point.
// Obiekt klasy Runner będzie zatem posiadał pola X, y oraz dostęp
// do nieprywatnych funkcji składowych. Uwaga -- funkcje ustawiania
// wartości x i y w klasie Point nie kontrolują wartości wstawianych
// do tych pól. W klasie runner dbamy o kontrole, nie pozwalamy ustawić
// wartości nie mieszczących się na ekranie konsoli. Dlatego w tej
// implementacji klasy Runner funkcje skłądowe setX oraz setY zostały
// zdefiniowane ponownie
class Runner : public Point
{
  public:
     Runner(int startX = 1, int startY = 1, char startShape = '*');
     void setShape(char newShape);
     char getShape();

    // Ponowna definicja funkcji ustawiania pozycji
    void setX(int newX);
    void setY(int newY);

    void show();
    void hide();

    void moveUp();
    void moveDown();
    void moveLeft();
    void moveRight();

  private:
    char shape; // Znak określający wygląd elementu
    // Pomocnicze funkcje weryfikacji pozycji ekranowej
    bool isXOnScreen(int x);
    bool isYOnScreen(int y);
};

// Definicje funkcji składowych poza deklaracją klasy
Runner::Runner(int startX, int startY, char startShape)
{
  // Wartości x, y i shape mogłyby być ustawione na liście inicjalizacyjnej.
  // Ale wartości parametrów konstruktora mogły być nieprawidłowe, dlatego
  // są ustawione w ciele konstruktora z wykorzystaniem odpowiednich funkcji
  // ustawiających
  setX(startX);
  setY(startY);
  setShape(startShape);
}

void Runner::setShape(char newShape)
{
  shape = (newShape > 32 && newShape <= 127)
          ? newShape : '*';
}

void Runner::setX(int newX)
{
  // Wykorzystujemy możliwość bezpośredniego dostępu
  // do pola chronionego x klasy Point
  x = isXOnScreen(newX) ? newX : 1;

  // Alternatywnie, gdyby pole x było prywatne:
  // Point::setX(isXOnScreen(newX) ? newX : 1);
  // No, można napisać też jak w przedszkolu:
  // if(isXOnScreen(newX))
  //   Point::setX(newX);
  // else
  //   Point::setX(1);
}

void Runner::setY(int newY)
{
  // Wykorzystujemy możliwość bezpośredniego dostępu
  // do pola chronionego y klasy Point
  y = isYOnScreen(newY) ? newY : 1;
}

void Runner::show()
{
  writeCharXY(x, y, shape);
}

void Runner::hide()
{
  writeCharXY(x, y, ' ');
}

char Runner::getShape()
{
  return shape;
}

void Runner::moveUp()
{
  hide();
  if(y > 1)
    --y;
  show();
}

void Runner::moveDown()
{
  hide();
  if(y < NUM_OF_ROWS)
    ++y;
  show();
}

void Runner::moveLeft()
{
  hide();
  if(x > 1)
    --x;
  show();
}

void Runner::moveRight()
{
  hide();
  if(x < NUM_OF_COLS)
    ++x;
  show();
}

bool Runner::isXOnScreen(int newX)
{
  return (newX > 0 && newX <= NUM_OF_COLS);
}

bool Runner::isYOnScreen(int newY)
{
  return (newY > 0 && newY <= NUM_OF_ROWS);
}

// Klasa reprezentująca planszę gry. Funkcje publiczne pozwalają na testowanie
// czy na zadanej pozycji planszy są ściany labiryntu (znak #) oraz czy na
// zadanej pozycji jest gotówka (znak $)
class PlayBoard
{
  public:
    PlayBoard();
    bool isWallOnXY(Point p);
    bool isCashOnXY(Point p);
    void clearCashOnXY(Point p);
    void show();
  private:
    char playBoardMap[NUM_OF_ROWS][NUM_OF_COLS+1];
};

// Definicje funkcji składowych poza deklaracją klasy
PlayBoard::PlayBoard()
{
  // Drobna sztuczka: ze względu na problemy związane z inicjalizacją tablicowych
  // pól klasy (do wersji C++11), pole playBoardMap będące tablicą dwuwymiarową
  // jest inicjowane w konstruktorze zawartością statycznej tablicy initialMap.
  // Tę ostatnią można dowolnie edytować ustalając zawartość planszy.
  static char initialMap[NUM_OF_ROWS][NUM_OF_COLS+1] =
  {
      // Liczby poniżej ułatwiają orientacje w tablicy, uwaga wymiary muszą się zgadzać
      //01234567890123456789012345678901234567890123456789012345678901234567890123456789
      "################################################################################",
      "#                                                     $    #    $    #    $ $  #",
      "#         #                                           #    #    #    #    # $  #",
      "#         #                                           #    $    #    $    # $  #",
      "#         ####################  ################  ##############################",
      "#         #       # $  $ #                                                     #",
      "#         #       #      #                                                     #",
      "# #########  $    #### #########################################################",
      "#  $ #            #      #          #              #$$$$                       #",
      "#  $ #                   #          #       #      ##########################  #",
      "# ##### ##################          #       #      #                           #",
      "#    $#                             #       #      ############# ###############",
      "#     #                                     #                                  #",
      "#  ###########  ######################################################## #######",
      "#                                  # $                                      #$ #",
      "######################## #################### ############################# #  #",
      "#                                                                              #",
      "#                                                   ############################",
      "#                                                   #            #$$$$$$$$$$$$$#",
      "# #########################                         #  ########  #             #",
      "#                     #                                #      #  #             #",
      "#                     ##################################      #  #             #",
      "#                                 $$$#                                         #",
      "################################################################################"
    };
    // Kopiowane kolejnych wierszy z mapy statycznej do playBoardMap
    for(int i = 0; i < 24; ++i)
      strcpy(playBoardMap[i], initialMap[i]);
}

void PlayBoard::show()
{
  for(int i = 0; i < 24; ++i)
      writeStrXY(1, i + 1, playBoardMap[i]);
}

bool PlayBoard::isWallOnXY(Point p)
{
  // Pozycja ekranowa liczona jest od 1, indeksy w tablicy od 0
  return playBoardMap[p.getY() - 1][p.getX() - 1] == '#';
}

bool PlayBoard::isCashOnXY(Point p)
{
  // Pozycja ekranowa liczona jest od 1, indeksy w tablicy od 0
  return playBoardMap[p.getY() - 1][p.getX() - 1] == '$';
}

void PlayBoard::clearCashOnXY(Point p)
{
  // Pozycja ekranowa liczona jest od 1, indeksy w tablicy od 0
   playBoardMap[p.getY() - 1][p.getX() - 1] = ' ';
}

// Klasa zarządzania grą. Połączona z klasą PlayBoard i Runner związkiem
// kompozycji (całość-część). Klasa realizuje w funkcji run() "aktywne"
// przepytywanie klawiatury. Jeżeli żaden klawisz nie został naciśnięty
// wywoływana jest procedura backgroundProcess(). W tej wersji realizuje
// ona wyświetlanie zegara i aktualizacjê pozycji runner'a oraz kwoty
// zebranej gotówki. Docelowo w tej procedurze można zrealizować animowanie
// ruchomych obiektów gry.
// Każde naciśnięcie klawisza skutkuje jego obsługą (funkcja processKey(int))
// i powrotem do aktywnego przepytywania klawiatury.
class Game
{
  public:
	Game();
	~Game();
	void run();
  private:
	Runner runner;
	PlayBoard playBoard;
	int cash;
	void backgroundProcess();
	void processKey(int key);
	void updateGameStatus();
	void displayClock();
};

// Definicje funkcji składowych poza deklaracją klasy

// Uwaga! lista inicjalizacyjna przeznaczona jest (oprócz aktywowania
// konstruktorów klas bazowych) do inicjowana pól obiektów danej klasy.
// Pole cash (typ prosty int) jest inicjowane wartością zero. Pola runner
// i playBoard są obiektami. Obiekty inicjuje się poprzez aktywowanie ich
// konstruktorów. Umieszczenie na liście inicjalizacyjnej pole będącego
// obiektem powoduje aktywowanie odpowiedniego konstruktora dla obiektu.
// Jeżeli obiekt nie wystąpi na liście inicjalizacyjnej, kompilator
// automatycznie uaktywni dla niego konstruktor domyślny przed wejściem
// do ciała konstruktora klasy nadrzędnej. Ważne -- kolejność aktywowania
// konstruktorów dla pól obiektowych jest zgodna z ich występowaniem
// w deklaracji klasy a nie w kolejności umieszczania tych pól na liście
// inicjalizacyjnej
Game::Game() : runner(40, 12, '*'), playBoard(), cash(0)
{
  clearScreen();
  cursorOff();
}

Game::~Game()
{
  clearScreen();
  cursorOn();
}

void Game::updateGameStatus()
{
  char info[128];
  // Przygotowanie łańcucha znaków z informacjami i wyświetlenie
  sprintf(info, "X=%-2d Y=%-2d", runner.getX(), runner.getY());
  writeStrXY(1, 25, info);
  // Przygotowanie łańcucha znaków z informacjami i wyświetlenie
  sprintf(info, "Gotowka=%-4d", cash);
  writeStrXY(20, 25, info);
}

void Game::displayClock()
{
  char info[128];

  time_t t = time(NULL); // Pobranie aktualnego czasu
  struct tm *ptm = localtime(&t); // "Rozpakowanie" informacji o czasie

  // Przygotowanie łańcucha znaków z informacjami o czasie i wyświetlenie
  sprintf( info, "%02d:%02d:%02d", ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
  writeStrXY(72, 25, info);
}

void Game::backgroundProcess()
{
  updateGameStatus();
  displayClock();
}

void Game::processKey(int key)
{
  // Zapamietujemy aktualną pozycję runner'a
  Point newPos = runner;
  switch(key)
  {
      case KEY_UP:    // Przewidywana nowa pozycja
                      newPos.setY(runner.getY()-1);
                      // Czy można się przemieścić na tę pozycję?
                      if(!playBoard.isWallOnXY(newPos))
                        runner.moveUp();
		              break;
	  case KEY_DOWN:  newPos.setY(runner.getY()+1);
	                  if(!playBoard.isWallOnXY(newPos))
                        runner.moveDown();
		              break;
	  case KEY_LEFT:  newPos.setX(runner.getX()-1);
                      if(!playBoard.isWallOnXY(newPos))
	                    runner.moveLeft();
		              break;
	  case KEY_RIGHT: newPos.setX(runner.getX()+1);
	                  if(!playBoard.isWallOnXY(newPos))
	                    runner.moveRight();
		              break;
  }//switch
  // Zebranie gotówki z nowej pozycji, o ile na niej jest
  if(playBoard.isCashOnXY(runner))
  {
     cash += 100;
     playBoard.clearCashOnXY(runner);
  }
}

void Game::run()
{
  int key = 0;

  playBoard.show();
  runner.show();

  // Aktywne przepytywanie klawiatury. Rezultatem funkcji _kbhit()
  // jest true jeżeli w buforze klawiatury oczekuje nieodczytany znak
  do
  {
    if(!_kbhit())
      backgroundProcess();
    else
      processKey(key = getKey());
  }
  while(key != KEY_ESC);
}

int main()
{
  Game game;
  game.run();
  return EXIT_SUCCESS;
}

// Funkcje obsługi konsoli, sprawa techniczna, w sensie merytorycznym nieistotna
int getKey()
{
	int key = _getch();
	return (key == KEY_BLANK) ? _getch() << 8 : key;
}

void clearScreen()
{
#ifdef __BORLANDC__
	clrscr();
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	COORD leftTop = { 0, 0 };
	CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
	int numOfCells = 80 * 25;
	DWORD writtenItems;
	HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
	if (GetConsoleScreenBufferInfo(consoleHandle, &consoleInfo))
		numOfCells = consoleInfo.dwSize.X * consoleInfo.dwSize.Y;

	FillConsoleOutputAttribute(consoleHandle, 0xf,
		numOfCells, leftTop, &writtenItems);
	FillConsoleOutputCharacter(consoleHandle, ' ',
		numOfCells, leftTop, &writtenItems);
#else
#error "Nieobslugiwana platforma"
#endif
}

void writeCharXY(int x, int y, char c) {
#ifdef __BORLANDC__
	gotoxy(x, y);
	putch(c);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	COORD cursorPos;
	DWORD written;

	cursorPos.X = x - 1;
	cursorPos.Y = y - 1;

	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cursorPos);
	WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), &c, 1, &written, 0);
#else
#error "Nieobslugiwana platforma"
#endif
}

void writeStrXY(int x, int y, char s[])
{
#ifdef __BORLANDC__
	gotoxy(x, y);
	cputs(s);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	COORD cursorPos;
	DWORD written;

	cursorPos.X = x - 1;
	cursorPos.Y = y - 1;

	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cursorPos);
	WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), s, strlen(s), &written, 0);
#else
#error "Nieobslugiwana platforma"
#endif
}

void writeIntXY(int x, int y, int i)
{
	char s[80];
#if defined(_MSC_VER)
	sprintf_s(s, 80, "%d", i);
#else
	sprintf(s, "%d", i);
#endif
	writeStrXY(x, y, s);
}

void writeDoubleXY(int x, int y, double d)
{
	char s[80];
#if defined(_MSC_VER)
	sprintf_s(s, 80, "%g", d);
#else
	sprintf(s, "%g", d);
#endif
	writeStrXY(x, y, s);
}

void cursorOff()
{
#ifdef __BORLANDC__
	_setcursortype(_NOCURSOR);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	CONSOLE_CURSOR_INFO cursorInfo;
	HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleCursorInfo(consoleHandle, &cursorInfo);
	cursorInfo.bVisible = false;
	SetConsoleCursorInfo(consoleHandle, &cursorInfo);
#else
#error "Nieobslugiwana platforma"
#endif
}

void cursorOn()
{
#ifdef __BORLANDC__
	_setcursortype(_NORMALCURSOR);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	CONSOLE_CURSOR_INFO cursorInfo;
	HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleCursorInfo(consoleHandle, &cursorInfo);
	cursorInfo.bVisible = true;
	SetConsoleCursorInfo(consoleHandle, &cursorInfo);
#else
#error "Nieobslugiwana platforma"
#endif
}