Новости

Фотки
Надписи на партах
Под гармошку
Обои для рабочего стола

Downloads
Компьютерные магазины
Фуфло

ЕЯС
НКС
ООП
СЦОД

Гостевая

Я ВКонтакте

Объектно-ориентированное программирование

Урок №2. Игра арканоид.

Задача:

Наверное все знают, что это за игра такая "Арканоид". Внизу экрана вправо/влево двигается платформа, управляемая пользователем. Эта платформа отбивает мяч, летающий по экрану и сбивающий разные блоки. Вот такую программу мы и будем писать.

Решение:

В общем так. На экране будут изображаться следующие объекты: платформа, которая будет управляться нажатиями клавиш, шар, который будет постоянно летать, и блоки, которые надо сбить этим шаром. Создаём соответствующие классы:

class Platforma {
};

class Shar {
};

class Block {
};

Все экземпляры этих классов будут изображаться на экране. - А это уже кое-что общее. Поэтому имеет смысл обобщить эти классы, сделав для них одного родителя.

class Object {
};

class Platforma: public Object {
};

class Shar: public Object {
};

class Block: public Object {
};

Теперь подумаем, какие поля класс должен иметь и что этот класс должен уметь. (даже в рифму сказал)

Ну, наверное, и платформа, и шар, и блок должны знать координаты своего местоположения на экране (x1, y1, x2, y2), свой цвет (color), должны уметь нарисовать (show()) и стереть себя (hide()). Ну и конечно же должен быть конструктор, инициализирующий член-данные этого класса.

class Object {

protected:

  int x1, y1, x2, y2;

  COLORS color;

public:

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_);

  void show();

  void hide();
};

Теперь поговорим о реализации. Конструктор - ладно, там всё ясно. А вот функции show() и hide() имеют кое-что общее. Они обе прорисовывают объект. Только функция show() делает это цветом color, а функция hide() - цветом фона. Поэтому, во-первых стоит завести функцию draw(color_), которая в зависимости от параметра color_ будет рисовать объект тем или иным цветом, чтобы потом вызывать эту функцию из функций show() и hide(). А во-вторых, неплохо бы добавить в класс поле bkColor, для того чтобы каждый экземпляр класса знал цвет фона. Так как цвет фона - единый для всех объектов, поле bkColor желательно сделать статическим. Напоминаю, что статический член класса - это член, который в программе всегда один, сколько бы там этих экземпляров этого класса не создавали. Более того, он будет один даже в том случае, если ни одного экземпляра не создано.

class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  void draw (COLORS color_) const;

public:

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }
};

// Ещё один прикол: статический член класса
// должен быть проинициализирован до начала выполнения программы.
COLORS Object::bkColor= BLUE;

Тело функции draw() будет различно для всех наследников. Ну, надеюсь, это понятно, что платформа рисуется по своему, а шар - по своему. Поэтому тело функции draw(color_) мы записывать не будем. Потому что "зачем?", всё равно в классах-наследниках эта функция будет переопределяться.

Ну а для того, чтобы в классах-наследниках повторно не описывать функции show() и hide(), мы функцию draw сделаем виртуальной. И тогда при вызове скажем функции show() из неё будет вызываться функция draw(color_), именно та, которая описана в классе-наследнике, а не которая в классе Object. (Кстати, это и есть так называемый полиморфизм)

А чтобы эту функцию draw(color_) вообще не описывать, мы присвоим этой функции значение 0, и таким образом сделаем эту функцию абстрактной.

class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  virtual void draw (COLORS color_) const= 0;

public:

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }
};

Если класс имеет абстрактную функцию (хотя бы одну), этот класс тоже становится абстрактным. А как, полагаю, вам уже известно, экземпляр абстрактного класса не может быть создан.

Ну это было так... к слову.

Да, кстати, надо бы добавить в класс Object функцию для перемещения объекта. Сами понимаете, должны же платформа и шар как-то двигаться. Пусть этим перемещением занимается функция move(dx,dy), которая будет смещать положение объекта на dx пикселей вправо и на dy пикселей вниз.

class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  virtual void draw (COLORS color_) const {}

public:

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }

  void move (int dx, int dy) {
    if (x1+dx>=&& y1+dy>=&& x2+dx<=getmaxx() && y2+dy<=getmaxy()) {
      hide();
      x1= x1+dx;
      y1= y1+dy;
      x2= x2+dx;
      y2= y2+dy;
      show();
    }
  }
};

Вимательный программист заметит, что в функцию move(dx,dy) добавлен условный оператор if. Условие содержит выражение, проверяющее останется ли объект в пределах границ экрана при таком перемещении. И если "да", то перемещение объекта разрешается.

Таким нехитрым образом мы решили предоставить объекту самому следить, за нахождением себя внутри видимой области.

Далее у нас идёт класс Platforma. Сразу переопределяем функцию вырисовывания draw(color_).

class Platforma: public Object {

protected:

  void draw (COLORS color_) const {
    setcolor (color_);
    rectangle (x1, y1, x2, y2);
  }
};

Далее. Поскольку платформа должна уметь двигаться вправо/влево, добавим поле dx, которое будет хранить значение шага, означающее на сколько пикселей переместится платформа за один ход. И ещё добавим 2 функции: move_left() и move_right(), которые, судя по смысловому названию, будут перемещать платформу влево и вправо, соответственно. Из тела этих функций мы будем вызывать уже описанную в родительском классе функцию move(dx,dy). И то правда, зачем нам надо было бы писать новый код для перемещениия влево и вправо. Уж лучше пусть уже готовая функция работает.

По пути, в тело конструктора добавим вызов функции show(), а в деструктор, который мы сейчас создадим, добавим вызов функции hide(). Это мы сделали для того, чтобы платформа рисовалась сразу при создании, а при удалении - стиралась с экрана.

class Platforma: public Object {

protected:

  int dx;

  void draw (COLORS color_) const {
    setcolor (color_);
    rectangle (x1, y1, x2, y2);
  }

public:

  Platforma (int x1, int y1, int x2, int y2, int dx_, COLORS color):
    Object (x1, y1, x2, y2, color), dx(dx_) {
    show();
  }

  ~Platforma() { hide(); }

  void move_left() { move (-dx, 0); }

  void move_right() { move (dx, 0); }
};

Класс платформа написан - давайте сразу проверим его функционирование. Для тестирования применим тот же способ, что использовался в предудыщем уроке. То есть, инициализируем графический режим, создадим экземпляр класса Platforma и обеспечим бесконечный цикл с обработкой нажатий клавиш. Функция main() будет выглядеть примерно так:

void main () {

  int graphDriver= DETECT, graphMode;
  initgraph (&graphDriver, &graphMode, "");
  if (graphresult() != grOk) {
    cout << "Graphics error.\n";
    return;
  }

  Platforma * platforma=
    new Platforma (getmaxx()/2-40, getmaxy()-10, getmaxx()/2+40, getmaxy(), 15, WHITE);

  int quit= 0;

  do {

    switch (getch()) {

      case 27:

        quit= 1;
        break;

      case 'A':
      case 'a':
      case 'Ф':
      case 'ф':

        platforma->move_left();
        break;

      case 'D':
      case 'd':
      case 'В':
      case 'в':

        platforma->move_right();
        break;
    }

  } while (!quit && shar->getLife());

  delete platforma;

  closegraph();
}

Что же мы видим: платформа по нажатию на клавиши 'A' и 'D' действительно двигается вправо и влево, но при этом старое изображение платформы в процессе движения закрашивается синим цветом, хотя вся остальная область экрана - чёрная.

Всё ясно, мы забыли после инициализации графического режима закрасить экран синим цветом (цветом фона). Ликвидируем этот недочёт. Добавим сразу после инициализации графического режима следующий код:

setfillstyle (SOLID_FILL, BLUE);
bar (0, 0, getmaxx(), getmaxy());

Но как же тогда быть, если мы вдруг решим поменять цвет фона, путём изменения инициализации статического поля bkColor (в классе Object)? Значит в классе Object надо написать функцию (getBkColor()), которая будет возвращать этот цвет.

class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  virtual void draw (COLORS color_) const {}

public:

  static COLORS getBkColor() { return bkColor; }

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }

  void move (int dx, int dy) {
    if (x1+dx>=&& y1+dy>=&& x2+dx<=getmaxx() && y2+dy<=getmaxy()) {
      hide();
      x1= x1+dx;
      y1= y1+dy;
      x2= x2+dx;
      y2= y2+dy;
      show();
    }
  }
};

Эту функцию (getBkColor()) мы сделали статической, потому что она работает только со статическими членами класса. Просто так удобнее. Статические функции можно вызывать, даже не создавая экземпляра класса.

Ну а место, которое идёт сразу после инициализации графики, мы переделаем вот так:

setfillstyle (SOLID_FILL, Object::getBkColor());
bar (0, 0, getmaxx(), getmaxy());

Следующая наша задача - шар.

Переопределяем функцию draw(color_) так, чтобы она в окне (x1,y1,x2,y2) рисовала круг. Но для этого область (x1,y1,x2,y2) должны быть квадратной.

Что ж... Пусть об этом позаботится конструктор. Его мы сделаем так, чтобы он не допускал возможности создания неквадратной области.

class Shar: public Object {

protected:

  void draw (COLORS color_) const {
    setcolor (color_);
    circle ((x1+x2)/2, (y1+y2)/2, (x2-x1)/2);
  }

public:

  Shar (int cx, int cy, int r, COLORS color):
    Object (cx-r, cy-r, cx+r, cy+r, color) {}
};

Итак, шар может перемещаться в любом направлении по прямой траектории. То есть для перемешения нужно знать горизонтальный и вертикальный шаги. В связи с этим, создадим поля dx и dy, и функцию для перемещения шара - move().

По пути добавим в конструктор инициализацию полей dx и dy и вызов функции show(), и ещё сделаем деструктор с вызовом функции hide().

class Shar: public Object {

protected:

  int dx, dy;

  void draw (COLORS color_) const {
    setcolor (color_);
    circle ((x1+x2)/2, (y1+y2)/2, (x2-x1)/2);
  }

public:

  Shar (int cx, int cy, int r, int dx_, int dy_, COLORS color):
    Object (cx-r, cy-r, cx+r, cy+r, color), dx(dx_), dy(dy_) {
    show();
  }

  ~Shar() { hide(); }

  void move();
};

Функцию перемещения move() запрограммируем так, чтобы шар автоматически отбивался от боковых стенок и потолка. Если шар достиг нижней границы - то пусть он не двигается. (ну это типа произойдёт, если игрок не поймал шарик)

void move() {
  if (x1+dx < 0  ||  x2+dx > getmaxx())
    dx= -dx;

  if (y1+dy < 0)
    dy= -dy;

  if (y2+dy <= getmaxy())
    Object::move (dx, dy);
}

Обратите внимание на код: первое условие проверяет - достигнет ли шар одной из боковых стенок, второе условие - достигнет ли шар потолка, третье - не выйдет ли шар на следующем ходу за нижнюю границу экрана.

Пришло время протестировать класс Shar. Функцию main изменим до такого вида:

void main () {

  int graphDriver= DETECT, graphMode;
  initgraph (&graphDriver, &graphMode, "");
  if (graphresult() != grOk) {
    cout << "Graphics error.\n";
    return;
  }

  setfillstyle (SOLID_FILL, Object::getBkColor());
  bar (0, 0, getmaxx(), getmaxy());

  Platforma * platforma=
    new Platforma (getmaxx()/2-40, getmaxy()-10, getmaxx()/2+40, getmaxy(), 15, WHITE);

  Shar * shar= new Shar (10, 10, 10, 3, 3, LIGHTCYAN);

  int quit= 0;

  do {

    shar->move();

    delay (25);

    if (kbhit())

      switch (getch()) {

        case 27:

          quit= 1;
          break;

        case 'A':
        case 'a':
        case 'Ф':
        case 'ф':

          platforma->move_left();
          break;

        case 'D':
        case 'd':
        case 'В':
        case 'в':

          platforma->move_right();
          break;
      }

  } while (!quit && shar->getLife());

  delete shar;
  delete platforma;

  closegraph();
}

Прошу заметить, что блок обработки нажатий клавиш (switch) заключён под условие (if (kbhit())). Это нужно для того, чтобы функция считывания кода нажатой клавиши (getch()) не вызывалась, если ни одна клавиша не была нажата. Таким образом программа не будет останавливаться и ждать нажатия клавиши. То есть цикл (do..while) будет постоянно выполняться.

В этот цикл (do..while) мы включили вызов shar->move(), чтобы при каждой итерации шар перемещался на один шаг. А чтобы это не происходило слишком быстро, мы добавили функцию задержки delay(25).

Итак, при тестировании произошло следующее: шар начал двигаться из левого верхнего угла по диагонали, а когда он дошёл до низу, он остановился и больше не двигался. При этом было не важно, находилась платформа в этот момент под шаром или нет.

Вывод таков: программа должна каким-нибудь образом узнать, что случилось с шаром; двигается ли он, наткнулся ли он на платформу, или же он вообще ушёл вниз.

Для того, чтоб определить, достиг ли шар нижней координаты, мы добавим в класс Shar в раздел protected поле life. Изначально оно будет равно 1 (в конструктор добавим инициализацию life). Это будет означать, что шар "жив".

Shar (int cx, int cy, int r, int dx_, int dy_, COLORS color):
  Object (cx-r, cy-r, cx+r, cy+r, color), dx(dx_), dy(dy_), life(1) {
  show();
}

А поскольку функция move() отвечает за движение шара, то она должна знать "достиг ли шар низа". И если достиг, то life меняем на 0.

void Shar::move() {

  if (life) {

    if (x1+dx < 0  ||  x2+dx > getmaxx())
      dx= -dx;

    if (y1+dy < 0)
      dy= -dy;

    if (y2+dy > getmaxy())
      life= 0;
    else
      Object::move (dx, dy);
  }
}

Да, и ещё: программа должна как-то знать о состоянии life. А поскольку поле life - защищенное, мы добавим в раздел public функцию getLife, возвращающую значение life.

int getLife() const { return life; }

Для определения столкновения нужно каким-то образом определять, пересекаются ли области объектов. Для этого можно сделать какую-нибудь функцию, которая будет определять факт пересечения. Но для разнообразия мы сделаем это с помощью перегрузки оператора. Перегрузим оператор && сразу для родительского класса - Object.

class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  virtual void draw (COLORS color_) const {}

public:

  static COLORS getBkColor() { return bkColor; }

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }

  void move (int dx, int dy) {
    if (x1+dx>=&& y1+dy>=&& x2+dx<=getmaxx() && y2+dy<=getmaxy()) {
      hide();
      x1= x1+dx;
      y1= y1+dy;
      x2= x2+dx;
      y2= y2+dy;
      show();
    }
  }

  int operator && (Object &o) const {
    return
      (x1<=o.x1 && o.x1<=x2 && y1<=o.y1 && o.y1<=y2) ||
      (x1<=o.x2 && o.x2<=x2 && y1<=o.y1 && o.y1<=y2) ||
      (x1<=o.x1 && o.x1<=x2 && y1<=o.y2 && o.y2<=y2) ||
      (x1<=o.x2 && o.x2<=x2 && y1<=o.y2 && o.y2<=y2) ||
      (o.x1<=x1 && x1<=o.x2 && o.y1<=y1 && y1<=o.y2) ||
      (o.x1<=x2 && x2<=o.x2 && o.y1<=y1 && y1<=o.y2) ||
      (o.x1<=x1 && x1<=o.x2 && o.y1<=y2 && y2<=o.y2) ||
      (o.x1<=x2 && x2<=o.x2 && o.y1<=y2 && y2<=o.y2);
  }
};

Теперь при такой записи:

Platforma platforma (300, 200, 400, 220, 15, WHITE);
Shar shar (10, 10, 10, 3, 3, LIGHTCYAN);
...
int test= platforma && shar;

переменная test будет равна 1, если platforma и shar пересекаются, или равна 0, если они не имеют общей области.

Теперь в цикле каждый раз перед перемещением шара (shar->move()) мы можем проверять пересёкся шар с платформой или нет. И если всё-таки пересёкся, то удар шара о платформу должен быть отражён. То есть направление вертикального шага у шара должно поменяться на противоположное (dy присвоить -dy). А так как мы не имеем прямого доступа к защищённому полю dy, это сделает созданная нами public-функция, с названием, например, reflect().

class Shar: public Object {

protected:

  int dx, dy, life;

  void draw (COLORS color_) const {
    setcolor (color_);
    circle ((x1+x2)/2, (y1+y2)/2, (x2-x1)/2);
  }

public:

  Shar (int cx, int cy, int r, int dx_, int dy_, COLORS color):
    Object (cx-r, cy-r, cx+r, cy+r, color), dx(dx_), dy(dy_), life(1) {
    show();
  }

  ~Shar() { hide(); }

  void move();

  int getLife() const { return life; }

  void reflect() { dy= -dy; }
}

Но вот в чём проблема: шар - не должен залезть на платформу! То есть рисунок шара не должен оказаться на рисунке платформы. И это вполне логично. Отсюда следует, что мы должны как-то заранее узнать, где окажется шар на следующем шаге.

Для этого в класс Shar мы добавим функцию getNextMove(), которая будет каким-то образом возвращать координаты прямоугольника. Этот прямоугольник будет представлять собой область экрана, в которой шар оказется при вызове функции move().

А возвращаемую область экрана мы представим в виде экземпляра класса Object:

class Shar: public Object {

protected:

  int dx, dy, life;

  void draw (COLORS color_) const {
    setcolor (color_);
    circle ((x1+x2)/2, (y1+y2)/2, (x2-x1)/2);
  }

public:

  Shar (int cx, int cy, int r, int dx_, int dy_, COLORS color):
    Object (cx-r, cy-r, cx+r, cy+r, color), dx(dx_), dy(dy_), life(1) {
    show();
  }

  ~Shar() { hide(); }

  void move();

  int getLife() const { return life; }

  void reflect() { dy= -dy; }

  Object getNextPosition() const;
};

Из того, что функция должна указывать следующий шаг шара, следует, что условные операторы в функции getNextMove() должны быть такие же, как и в функции move. Однако значения полей dx и dy новая функция изменять не должна, так как эта функция носит чисто информативный характер. Поэтому мы и вводим дополнительные переменные dx_ и dy_.

Object Shar::getNextPosition() const {
  int dx_= dx, dy_= dy;

  if (x1+dx < 0  ||  x2+dx > getmaxx())
    dx_= -dx;

  if (y1+dy < 0)
    dy_= -dy;

  return Object (x1+dx_, y1+dy_, x2+dx_, y2+dy_, color);
}

Та-ак... Компилируем... Опять фигня какая-то.... Не компилируется. :(

Ах, да-а-а... Экземпляр абстрактного класса Object не может быть создан. А абстрактный он потому, что содержит абстрактную функцию:

virtual void draw (COLORS color_) const= 0;

Придётся немного изменить код, чтобы функция перестала быть виртуальной:

virtual void draw (COLORS color_) const {}

Во-о! Теперь компилятор доволен.

Далее, в функцию main() внутрь цикла (do...while) перед вызовом shar->move() мы добавим:

if (*platforma && shar->getNextPosition())
  shar->reflect();

теперь программа будет определять, куда сходит шар и, в случае его удара о платформу, шар изменит движение.

Готовая компилируемая программа будет иметь вид:

#include <conio.h> #include <dos.h> #include <graphics.h> #include <iostream.h> #include <stdlib.h>
class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  virtual void draw (COLORS color_) const {}

public:

  static COLORS getBkColor() { return bkColor; }

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }

  void move (int dx, int dy) {
    if (x1+dx>=&& y1+dy>=&& x2+dx<=getmaxx() && y2+dy<=getmaxy()) {
      hide();
      x1= x1+dx;
      y1= y1+dy;
      x2= x2+dx;
      y2= y2+dy;
      show();
    }
  }

  int operator && (Object &o) const {
    return
      (x1<=o.x1 && o.x1<=x2 && y1<=o.y1 && o.y1<=y2) ||
      (x1<=o.x2 && o.x2<=x2 && y1<=o.y1 && o.y1<=y2) ||
      (x1<=o.x1 && o.x1<=x2 && y1<=o.y2 && o.y2<=y2) ||
      (x1<=o.x2 && o.x2<=x2 && y1<=o.y2 && o.y2<=y2) ||
      (o.x1<=x1 && x1<=o.x2 && o.y1<=y1 && y1<=o.y2) ||
      (o.x1<=x2 && x2<=o.x2 && o.y1<=y1 && y1<=o.y2) ||
      (o.x1<=x1 && x1<=o.x2 && o.y1<=y2 && y2<=o.y2) ||
      (o.x1<=x2 && x2<=o.x2 && o.y1<=y2 && y2<=o.y2);
  }
};

COLORS Object::bkColor= BLUE;


class Platforma: public Object {

protected:

  int dx;

  void draw (COLORS color_) const {
    setcolor (color_);
    rectangle (x1, y1, x2, y2);
  }

public:

  Platforma (int x1, int y1, int x2, int y2, int dx_, COLORS color):
    Object (x1, y1, x2, y2, color), dx(dx_) {
    show();
  }

  ~Platforma() { hide(); }

  void move_left() { move (-dx, 0); }

  void move_right() { move (dx, 0); }
};


class Shar: public Object {

protected:

  int dx, dy, life;

  void draw (COLORS color_) const {
    setcolor (color_);
    circle ((x1+x2)/2, (y1+y2)/2, (x2-x1)/2);
  }

public:

  Shar (int cx, int cy, int r, int dx_, int dy_, COLORS color):
    Object (cx-r, cy-r, cx+r, cy+r, color), dx(dx_), dy(dy_), life(1) {
    show();
  }

  ~Shar() { hide(); }

  void move();

  int getLife() const { return life; }

  Object getNextPosition() const;

  void reflect() { dy= -dy; }
};

void Shar::move() {

  if (life) {

    if (x1+dx < 0  ||  x2+dx > getmaxx())
      dx= -dx;

    if (y1+dy < 0)
      dy= -dy;

    if (y2+dy > getmaxy())
      life= 0;
    else
      Object::move (dx, dy);
  }
}

Object Shar::getNextPosition() const {
  int dx_= dx, dy_= dy;

  if (x1+dx < 0  ||  x2+dx > getmaxx())
    dx_= -dx;

  if (y1+dy < 0)
    dy_= -dy;

  return Object (x1+dx_, y1+dy_, x2+dx_, y2+dy_, color);
}

class Block {
};

void main () {

  int graphDriver= DETECT, graphMode;
  initgraph (&graphDriver, &graphMode, "");
  if (graphresult() != grOk) {
    cout << "Graphics error.\n";
    return;
  }

  setfillstyle (SOLID_FILL, Object::getBkColor());
  bar (0, 0, getmaxx(), getmaxy());

  Platforma * platforma=
    new Platforma (getmaxx()/2-40, getmaxy()-10, getmaxx()/2+40, getmaxy(), 15, WHITE);

  Shar * shar= new Shar (10, 10, 10, 3, 3, LIGHTCYAN);

  int quit= 0;

  do {

    if (*platforma && shar->getNextPosition())
      shar->reflect();

    shar->move();

    delay (25);

    if (kbhit())

      switch (getch()) {

        case 27:

          quit= 1;
          break;

        case 'A':
        case 'a':
        case 'Ф':
        case 'ф':

          platforma->move_left();
          break;

        case 'D':
        case 'd':
        case 'В':
        case 'в':

          platforma->move_right();
          break;
      }

  } while (!quit && shar->getLife());

  delete shar;
  delete platforma;

  closegraph();
}

Можете скопировать этот код и проверить. По экрану будет летать шарик, а платформа, перемещаемая игроком, будет его отбивать. Если игрок не поймает шар, игра закончится.

Теперь пора бы подумать о различных препятствиях для шара.

Оформляем класс Block. Описываем функцию draw(), конструктор и деструктор.

class Block: public Object {

protected:

  void draw (COLORS color_) {
    setcolor (color_);
    rectangle (x1, y1, x2, y2);
  }

public:

  Block (int x1, int y1, int x2, int y2, COLORS color):
    Object (x1, y1, x2, y2, color) {
    show();
  }

  ~Block() { hide(); }
};

С другой стороны, как-то слишком примитивно, рисовать блок всего лишь контуром прямоугольника. Сделаем прямоугольник цветным изнутри. Для этого добавим ещё одно поле - color2. А поскольку в функцию draw() передаётся только один параметр color_, нам придётся изменить её так, чтобы второй цвет (цвет фона блока) выбирался в зависимости от значения параметра color_.

class Block: public Object {

protected:

  COLORS color2;

  void draw (COLORS color_) {
    setcolor (color_);
    rectangle (x1, y1, x2, y2);

    // если цвет color_ является цветом фона
    // значит скорее всего функция вызывается из функции hide()
    if (color_==bkColor)
      // устанавливаем цвет заливки, равный цвету фона
      setfillstyle (SOLID_FILL, bkColor);
    // иначе функция скорее всего вызывается из функции show()
    else
      // устанавливаем цвет заливки, равный цвету color2
      setfillstyle (SOLID_FILL, color2);

    bar (x1+1, y1+1, x2-1, y2-1);
  }

public:

  // добавляем параметр color2_ для инициализации поля color2
  Block (int x1, int y1, int x2, int y2, COLORS color, COLORS color2_):
    Object (x1, y1, x2, y2, color), color2(color2_) {
    show();
  }

  ~Block() { hide(); }
};

Теперь осталось добавить эти блоки в игру и обеспечить их взаимодействие с шаром.

#include <conio.h> #include <dos.h> #include <graphics.h> #include <iostream.h> #include <stdlib.h>
class Object {

protected:

  int x1, y1, x2, y2;

  static COLORS bkColor;
  COLORS color;

  virtual void draw (COLORS color_) const {}

public:

  static COLORS getBkColor() { return bkColor; }

  Object (int x1_, int y1_, int x2_, int y2_, COLORS color_):
    x1(x1_), y1(y1_), x2(x2_), y2(y2_), color(color_) {}

  void show() const { draw (color); }

  void hide() const { draw (bkColor); }

  void move (int dx, int dy) {
    if (x1+dx>=&& y1+dy>=&& x2+dx<=getmaxx() && y2+dy<=getmaxy()) {
      hide();
      x1= x1+dx;
      y1= y1+dy;
      x2= x2+dx;
      y2= y2+dy;
      show();
    }
  }

  int operator && (Object &o) const {
    return
      (x1<=o.x1 && o.x1<=x2 && y1<=o.y1 && o.y1<=y2) ||
      (x1<=o.x2 && o.x2<=x2 && y1<=o.y1 && o.y1<=y2) ||
      (x1<=o.x1 && o.x1<=x2 && y1<=o.y2 && o.y2<=y2) ||
      (x1<=o.x2 && o.x2<=x2 && y1<=o.y2 && o.y2<=y2) ||
      (o.x1<=x1 && x1<=o.x2 && o.y1<=y1 && y1<=o.y2) ||
      (o.x1<=x2 && x2<=o.x2 && o.y1<=y1 && y1<=o.y2) ||
      (o.x1<=x1 && x1<=o.x2 && o.y1<=y2 && y2<=o.y2) ||
      (o.x1<=x2 && x2<=o.x2 && o.y1<=y2 && y2<=o.y2);
  }
};

COLORS Object::bkColor= BLUE;


class Platforma: public Object {

protected:

  int dx;

  void draw (COLORS color_) const {
    setcolor (color_);
    rectangle (x1, y1, x2, y2);
  }

public:

  Platforma (int x1, int y1, int x2, int y2, int dx_, COLORS color):
    Object (x1, y1, x2, y2, color), dx(dx_) {
    show();
  }

  ~Platforma() { hide(); }

  void move_left() { move (-dx, 0); }

  void move_right() { move (dx, 0); }
};


class Shar: public Object {

protected:

  int dx, dy, life;

  void draw (COLORS color_) const {
    setcolor (color_);
    circle ((x1+x2)/2, (y1+y2)/2, (x2-x1)/2);
  }

public:

  Shar (int cx, int cy, int r, int dx_, int dy_, COLORS color):
    Object (cx-r, cy-r, cx+r, cy+r, color), dx(dx_), dy(dy_), life(1) {
    show();
  }

  ~Shar() { hide(); }

  void move();

  int getLife() const { return life; }

  Object getNextPosition() const;

  void reflect() {
    dx= random (21) - 10;
    dy= -dy;
  }
};

void Shar::move() {

  if (life) {

    if (x1+dx < 0  ||  x2+dx > getmaxx())
      dx= -dx;

    if (y1+dy < 0)
      dy= -dy;

    if (y2+dy > getmaxy())
      life= 0;
    else
      Object::move (dx, dy);
  }
}

Object Shar::getNextPosition() const {
  int dx_= dx, dy_= dy;

  if (x1+dx < 0  ||  x2+dx > getmaxx())
    dx_= -dx;

  if (y1+dy < 0)
    dy_= -dy;

  return Object (x1+dx_, y1+dy_, x2+dx_, y2+dy_, color);
}

class Block: public Object {

protected:

  COLORS color2;

  void draw (COLORS color_) const {
    if (color_==bkColor)
      setfillstyle (SOLID_FILL, bkColor);
    else
      setfillstyle (SOLID_FILL, color2);
    bar (x1+1, y1+1, x2-1, y2-1);
    setcolor (color_);
    rectangle (x1, y1, x2, y2);
  }

public:

  Block (int x1, int y1, int x2, int y2, COLORS color, COLORS color2_):
    Object (x1, y1, x2, y2, color), color2(color2_) {
    show();
  }

  ~Block() { hide(); }
};

void main () {

  int graphDriver= DETECT, graphMode;
  initgraph (&graphDriver, &graphMode, "");
  if (graphresult() != grOk) {
    cout << "Graphics error.\n";
    return;
  }

  setfillstyle (SOLID_FILL, Object::getBkColor());
  bar (0, 0, getmaxx(), getmaxy());

  Platforma * platforma=
    new Platforma (getmaxx()/2-40, getmaxy()-10, getmaxx()/2+40, getmaxy(), 15, WHITE);

  Shar * shar= new Shar (10, 10, 10, 3, 3, LIGHTCYAN);

  // объявляем константу kBlock - количество блоков
  const int kBlock= 12;
  // объявляем block - массив блоков
  Block * block[kBlock];
  // создаём экземпляры класса Block и
  // присваиваем указатели на них массиву block
  for (int i=0; i<3; i++)           // двойной вложенный цикл нужен
    for (int j=0; j<4; j++)         // для размещения блоков матрицей 3х4
      // координаты - специально вычисляются
      // цвет границы блока - белый
      // цвет самого блока - случайный
      block[i*4+j]=
        new Block (100+j*100, 100+i*70, 140+j*100, 115+i*70, WHITE, (COLORS) random (8));

  int quit= 0;

  do {

    // запоминаем, куда сходит шар
    Object sharNext= shar->getNextPosition();

    // проверяем, не стукнулся ли шар с блоком
    for (int i=0; i<kBlock; i++)
      // если указатель на блок не равен NULL и шар стукнулся с блоком
      if (block[i] != NULL && (sharNext && *block[i])) {
        // отбиваем шар
        shar->reflect();
        // удаляем блок
        delete block[i];
        // зануляем указатель, чтобы знать, что i-го блока уже нет
        block[i]= NULL;
      }

    if (*platforma && sharNext)
      shar->reflect();

    shar->move();

    delay (25);

    if (kbhit())

      switch (getch()) {

        case 27:

          quit= 1;
          break;

        case 'A':
        case 'a':
        case 'Ф':
        case 'ф':

          platforma->move_left();
          break;

        case 'D':
        case 'd':
        case 'В':
        case 'в':

          platforma->move_right();
          break;
      }

  } while (!quit && shar->getLife());

  // удаляем блоки, которые остались
  for (i=0; i<kBlock; i++)
    if (block[i] != NULL)
      delete block[i];

  delete shar;
  delete platforma;

  closegraph();
}

Вот, собственно, всё. Простейший арканоид сделан. Правда, шар будет двигаться по одной заданной траектории, да и платформу надо подставлять в одни и те же места. Поэтому такая реализация неитересна и требует доработки.

Например:

Перегруженный оператор && можно изменить так, чтобы он возвращал что-то вроде угла, под под которым пересеклись объекты. А затем можно поменять функцию reflect() так, чтобы она отбивала шар под определённым углом, который будет зависеть от угла соприкосновения.

А для начала, функцию reflect() изменим вот так:

void reflect() {
  dx= random (21) - 10;
  dy= -dy;
}

Теперь хоть добавится элемент случайности при отбивании шара от платформы и от блоков.

Далее.

От класса Shar можно сделать несколько наследников, которые будут обладать ещё какими-то функциями.

То же касается класса Block. От него можно несколько наследников сделать. Один наследник, к примеру, будет иметь 3 жизни, и это количество будет уменьшаться с каждым ударом. Другой, например, вообще будет небьющимся. Третий - можено сделать двигающимся.

Всё зависит от вашей фантазии.


Урок №1. Перемещение объекта.

Урок №2. Игра "Арканоид".

Урок №3. Обработка нажатий клавиш.

Урок №4. Системный таймер.

Урок №5. Немного о виртуальных функциях.

^^ наверх ^^

Дизайн: Красиков Виктор, kv630@mail.ru, ICQ - 319227
Местонахождение: Россия, респ. Бурятия, г. Улан-Удэ
Время на сервере: 02.04.23 02:06
Время в Улан-Удэ: 02.04.23 07:06
Время генерации страницы 0.008763 сек.