Приклад розробки програми

1 Постановка задачі

Припустимо, що нам необхідно розробити програму, яка реалізує уявну комп'ютерну гру під назвою "Диски". Правила цієї гри такі:

  • На полі гри "Диски" розташовані дев'ять стрижнів. (3 х 3). В нижній частині стрижні пофарбовані в червоний, жовтий і зелений колір.
  • Три кольорові диски (червоний, жовтий і зелений) випадково розташовані на полі.
  • Гра складається з послідовних кроків. На кожному кроці гравець може перемістити один з дисків з місця на вільний сусідній стрижень в кожному недіагональному напрямку.
  • Мета гри полягає в розміщенні дисків на стрижнях відповідного кольору.
  • Під час гри здійснюється підрахунок часу.

Програма повинна реалізовувати такі основні функції:

  • підготовка нового поля гри
  • реалізація гри з можливістю маніпуляції за допомогою маніпулятора "миша"
  • підрахунок часу та виведення результатів
  • можливість обертання поля гри.

2 Визначення вимог. Аналіз і проектування

2.1 Діаграма варіантів використання

Функціональні вимоги до програми наведені на діаграмі варіантів використання. Ця діаграма повинна включати в себе одну дійову особу (Користувач), яка може взаємодіяти з нашою програмою декількома способами:

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

На рис. 2.1 наведена діаграма варіантів використання.

Рисунок 2.1 – Діаграма варіантів використання

2.2 Визначення структури класів

Спочатку слід визначити загальну структуру класів. Основним класом, який відповідає за логіку гри, буде Scene (сцена). Цей клас міститиме посилання на об'єкти класів, похідних від абстрактного класу Shape. На діаграмі також умовно можна відобразити функцію main(). Діаграма класів з концептуальної точки зору наведена на рис. 2.2.

Рисунок 2.2 – Діаграма класів з концептуальної точки зору

Пізніше до діаграми класів будуть внесені необхідні уточнення.

3 Реалізація

3.1 Підготовка середовища програмування. Створення нового проекту

Стандартна конфігурація середовища програмування Visual Studio 2019 передбачає можливість роботи з бібліотеками GL та GLU. Відповідні заголовні файли та файли бібліотек вже розташовані в необхідних теках. Але на жаль бібліотеку GLUT треба додавати вручну. Сучасний підхід до застосування GLUT – це використання бібліотеки FreeGLUT, яку для використання у Visual Studio можна завантажити за адресою https://www.transmissionzero.co.uk/software/freeglut-devel/, далі скориставшись посиланням у рядку "Download freeglut 3.0.0 for MSVC (with PGP signature and PGP key)". Три з п'яти файлів, які містить цей архів, слід розкрити у відповідні теки:

Файли з архіву Тека, у яку слід розкрити файл
freeglut/bin/freeglut.dll ...\Windows\system32\
freeglut/include/GL/ (усі файли) ...\Program Files (x86)\Windows Kits\10\Include\<ОСТАННЯ_ВЕРСІЯ>\um\gl\
freeglut/lib/freeglut.lib ...\Program Files (x86)\Windows Kits\10\Lib\<ОСТАННЯ_ВЕРСІЯ>\um\x86\

Примітка: <ОСТАННЯ_ВЕРСІЯ> у шляху – це тека, ім'я якої складається з цифр і крапок; серед схожих назв слід вибрати теку з найпізнішою датою створення.

У випадку неможливості розташування файлу freeglut.dll в теці ...\Windows\system32\ цей файл можна кожного разу копіювати в теку Debug проекту. Так само інші файли можна розташувати в теці проекту біля файлів сирцевого коду, але тоді в директиві #include у наведених нижче прикладах слід замінити <gl/glut.h> на "glut.h".

Сайт http://freeglut.sourceforge.net містить різноманітну інформацію про бібліотеку FreeGLUT.

Для виконання програмної частини проєкту в середовищі Visual Studio треба створити новий проект (Empty Project) з ім'ям Disks.

3.2 Створення класів

Для програмних проектів великих і середніх розмірів загальним правилом є розділення сирцевого коду на окремі незалежні частини – логічно (класи, простори імен, пакети) і фізично (окремі файли). Зокрема, в нашому проекті окрім глобального простору імен з функцією main() можна виділити простори імен DiskGame для класів і функцій, специфічних для гри, що створюється, а також GraphUtils для графічних функцій і констант, які можуть придатися в інших подібних проєктах. Фізично кожен істотний клас слід розташувати в окремому сирцевому файлі. В окремих файлах будуть розташовані функція main() (main.cpp), а також елементи простору імен GraphUtils (utils.cpp).

До розробки програми слід підходити ітеративно, з обов'язковим тестуванням проміжних результатів. Кожен клас повинен бути перевірений окремо. Додавання нових класів може спричинити зміни в функціональності попередніх класів. Таким чином, раніше створені класи повинні бути перевірені ще раз.

Ми почнемо з створення сирцевого коду класу Scene. Щоб створити новий клас в середовищі програмування Visual Studio, можна скористатися функцією Project | Add Class... головного меню. У вікні Add Class в текстовому полі Class name уводимо ім'я класу (Scene). Після натискання на кнопку OK до проекту додаються нові файли Scene.h і Scene.cpp. Файл Scene.h матиме такий вміст:

#pragma once
class Scene
{
};

Файл Scene.cpp містить лише підключення заголовного файлу:

#include "Scene.h"

Автоматично згенерований текст має деякі недоліки:

  • директива #pragma once є специфічною для реалізації компілятора C++ у середовищі Visual Studio; цей код не можна перекомпілювати з використанням інших компіляторів; замість #pragma once слід використовувати стражі включення;
  • немає можливості автоматичного створення простору імен.

Також доцільно додати ім'я файлу до тексту у вигляді коментаря.

На жаль, майстер класів не надає відповідних варіантів. Можна виправити ці недоліки вручну, після кожної нової генерації класу. У нашому випадку файл Scene.h повинен бути виправлений таким чином:

// Scene.h
#ifndef Scene_h
#define Scene_h
 
namespace DiskGame
{

    class Scene
    {
    };

}

#endif

До опису класу додаємо функцію on_paint(), яка викликатиметься автоматично – кожного разу, коли програма потребує перемалювання сцени:

// Scene.h
#ifndef Scene_h
#define Scene_h 
 
namespace DiskGame
{

    class Scene
    {
    public:
        void on_paint();
    };

}

#endif

Перша реалізація функції on_paint() включає в себе тільки очищення вікна. Для цього необхідно включення заголовного файлу glut.h. Файл Scene.cpp може бути змінений таким чином:

// Scene.cpp
#include <windows.h> 
#include <gl/glut.h>
#include "Scene.h"

namespace DiskGame
{

    void Scene::on_paint(void)
    {
        // визначаємо блакитний колір для очищення:
        glClearColor(0, 0.5, 0.5, 0);  
        // очищуємо буфери:
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
        // показуємо очищене вікно:
        glutSwapBuffers();
    }

}

Щоб здійснити тестування нашого класу, ми повинні додати сирцевий файл (наприклад, main.cpp) з функцією main(). Щоб створити новий сирцевий файл, в меню Project слід вибрати Add New Item..., далі (в середній частині вікна) – C++ File. Необхідно ввести ім'я файлу (main) у рядку введення Name. Після натискання кнопки OK відкривається порожній файл.

Код у функції main() повинен ініціалізувати GLUT, встановити розмір вікна і режим кольору RGBA. Перша версія нашого файлу main.cpp буде виглядати так:

// main.cpp
#include <gl/glut.h>
#include "Scene.h"

using DiskGame::Scene;

Scene *scene; // вказівник на клас Scene

void on_paint()
{
    scene->on_paint(); // викликаємо відповідну функцію класу Scene
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);       // ініціалізуємо GLUT
    scene = new Scene();         // створюємо об'єкт "сцена"
    glutInitWindowSize(800, 600);// встановлюємо розміри вікна
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH  
        | GLUT_DOUBLE);          // ініціалізуємо режими відображення
    glutCreateWindow("Disks");   // створюємо вікно
    glutDisplayFunc(on_paint);   // реєструємо функцію відображення
    glutMainLoop();              // стартуємо основний цикл обробки подій
    delete scene;                // видаляємо об'єкт "сцена"
    return(0);
}

Тепер ми можемо здійснити тестування нашого застосунку OpenGL. Після запуску програми повинно з'явитися порожнє вікно з заголовком "Disks" і ціановим фоном.

Тепер можна переходити до створення класів, які описуватимуть елементи гри. Відповідно до діаграми класів створюємо клас Shape, який буде базовим для інших фігур. До опису класу слід додати поля, які репрезентують атрибути, наведені в діаграмі класів. Відповідно слід створити функції доступу. Клас Shape буде абстрактним, оскільки він надаватиме суто віртуальну функцію draw(). Необхідно також додати конструктор з параметрами, а також віртуальний деструктор (у поліморфних класах деструктор повинен бути віртуальним). Сирцевий код заголовного файлу буде таким:

// Shape.h
#ifndef Shape_h
#define Shape_h
 
namespace DiskGame
{
    // Клас для представлення абстрактної фігури
    class Shape
    {
    private:
        float xCenter, yCenter, zCenter;          // координати центру
        float xSize, ySize, zSize;                // розміри
        float *diffColor, *ambiColor, *specColor; // кольори
    public:
        Shape(float xCenter, float yCenter, float zCenter,
              float xSize, float ySize, float zSize,
              float *diffColor, float *ambiColor, float *specColor);
        virtual ~Shape() { } 
        // Функції доступу:
        float  getXCenter() const { return xCenter; }
        float  getYCenter() const { return yCenter; }
        float  getZCenter() const { return zCenter; }
        void   setXCenter(float xCenter) { this->xCenter = xCenter; }
        void   setYCenter(float yCenter) { this->yCenter = yCenter; }
        void   setZCenter(float zCenter) { this->zCenter = zCenter; }
        void   setCoords(float xCenter, float yCenter, float zCenter);
        float  getXSize() const { return xSize; }
        float  getYSize() const { return ySize; }
        float  getZSize() const { return zSize; }
        void   setXSize(float xSize) { this->xSize = xSize; }
        void   setYSize(float ySize) { this->ySize  = ySize;  }
        void   setZSize(float zSize) { this->zSize = zSize; }
        void   setSize(float xSize, float ySize, float zSize);
        float* getDiffColor() const { return diffColor; }
        float* getAmbiColor() const { return ambiColor; }
        float* getSpecColor() const { return specColor; }
        void   setDiffColor(float* diffColor) { this->diffColor = diffColor; }
        void   setAmbiColor(float* ambiColor) { this->ambiColor = ambiColor; }
        void   setSpecColor(float* specColor) { this->specColor = specColor; }
        void   setColors(float* diffColor, float* ambiColor, float* specColor);
        virtual void draw() = 0; // ця функція повинна бути перекрита у похідних класах
    };
 
}
#endif

Відповідно генеруємо та змінюємо файл реалізації:

// Shape.cpp
#include "Shape.h"
 
namespace DiskGame
{
 
    // Конструктор:
    Shape::Shape(float xCenter, float yCenter, float zCenter,
                 float xSize, float ySize, float zSize,
                 float *diffColor, float *ambiColor, float *specColor)
    {
        setCoords(xCenter, yCenter, zCenter);
        setSize(xSize, ySize, zSize);
        setColors(diffColor, ambiColor, specColor);
    }

    // Функції доступу:

    void Shape::setCoords(float xCenter, float yCenter, float zCenter)
    {
        this->xCenter = xCenter;
        this->yCenter = yCenter;
        this->zCenter = zCenter;
    }

    void Shape::setSize(float xSize, float ySize, float zSize)
    {
        this->xSize = xSize;
        this->ySize = ySize;
        this->zSize = zSize;
    }

    void Shape::setColors(float* diffColor, float* ambiColor, float* specColor)
    {
        this->diffColor = diffColor;
        this->ambiColor = ambiColor;
        this->specColor = specColor;
    }

}

Для відображення фігур та розташування елементів гри можуть придатися константи (змінні) та функції, які не входять у жоден клас. Доцільно створити модуль, який складається із заголовного файлу utils.h і файлу реалізації utils.cpp. У цьому модулі, зокрема, можна визначити масиви для окремих кольорів. До корисних функцій, які також будуть розміщені в модулі utils, можна віднести малювання паралелепіпеда, відображення тексту, а також отримання випадкового розміщення елементів масиву. Функції доцільно розмістити в окремому просторі імен. Заголовний файл матиме такий вигляд:

// utils.h
#ifndef utils_h
#define utils_h

namespace GraphUtils
{
    // Попередній опис масивів, які визначають кольори:
    extern float diffWhite[];
    extern float ambiWhite[];
    extern float specWhite[];

    extern float diffBlue[];
    extern float ambiBlue[];
    extern float specBlue[];

    extern float diffGray[];
    extern float ambiGray[];
    extern float specGray[];

    extern float diffRed[];
    extern float ambiRed[];
    extern float specRed[];

    extern float diffYellow[];
    extern float ambiYellow[];
    extern float specYellow[];

    extern float diffGreen[];
    extern float ambiGreen[];
    extern float specGreen[];

    extern float diffOrange[];
    extern float ambiOrange[];
    extern float specOrange[];

    extern float diffLightBlue[];
    extern float ambiLightBlue[];
    extern float specLightBlue[];

    extern float diffViolet[];
    extern float ambiViolet[];
    extern float specViolet[];

    const float shininess = 64; // блиск

    // Випадкове "тасування" одновимірного масиву цілих чисел
    void shuffle(int *a, int size);

    // Малювання паралелепіпеда
    void parallelepiped(float length, float width, float height);

    // Відображення рядка тексту вказаним шрифтом у вказаній позиції
    void drawString(void *font, const char* text, float x, float y);
}

#endif

Функція випадкового "тасування" масиву може бути такою:

void shuffle(int *a, int size)
{
    srand((unsigned)time(0));
    std::random_shuffle(a, a + size);
}    

У попередньому коді спочатку здійснюється ініціалізація генератора випадкових чисел за допомогою виклику функції srand(). Передаємо значення поточного часу як параметр. Це значення ми можемо отримати за допомогою функції time(0). Виклик random_shuffle() – це виклик так званого алгоритму, який є частиною стандартної бібліотеки шаблонів (STL). Це універсальна функція, яка дозволяє працювати з будь-якими послідовностями. Для роботи з масивом слід передати вказівник на початковий елемент, а також вказівник на адресу пам'яті після останнього елемента.

Для роботи алгоритму тасування random_shuffle() слід підключити заголовний файл algorithm, для того, щоб ініціалізувати генератор випадкових чисел, підключаємо stdlib.h, для отримання поточного часу – time.h. Файл реалізації матиме такий вигляд:

// utils.cpp
#include <algorithm>
#include <time.h>
#include <stdlib.h>
#include <gl/glut.h>
#include "utils.h"

namespace GraphUtils
{
    // Визначення кольорів:
    float diffWhite[] = { 1.0f, 1.0f, 1.0f };
    float ambiWhite[] = { 0.8f, 0.8f, 0.8f };
    float specWhite[] = { 1.0f, 1.0f, 1.0f };

    float diffBlue[] = { 0.0f, 0.0f, 0.6f };
    float ambiBlue[] = { 0.1f, 0.1f, 0.2f };
    float specBlue[] = { 0.2f, 0.2f, 0.8f };

    float diffGray[] = { 0.6f, 0.6f, 0.6f };
    float ambiGray[] = { 0.2f, 0.2f, 0.2f };
    float specGray[] = { 0.8f, 0.8f, 0.8f };

    float diffRed[] = { 0.6f, 0.0f, 0.0f };
    float ambiRed[] = { 0.2f, 0.1f, 0.1f };
    float specRed[] = { 0.8f, 0.2f, 0.2f };

    float diffYellow[] = { 0.9f, 0.9f, 0 };
    float ambiYellow[] = { 0.2f, 0.2f, 0.1f };
    float specYellow[] = { 1.0f, 1.0f, 0.2f };

    float diffGreen[] = { 0, 0.5f, 0 };
    float ambiGreen[] = { 0.1f, 0.2f, 0.1f };
    float specGreen[] = { 0.2f, 0.7f, 0.2f };

    float diffOrange[] = { 0.9f, 0.2f, 0 };
    float ambiOrange[] = { 0.2f, 0.2f, 0.2f };
    float specOrange[] = { 0.8f, 0.8f, 0.8f };

    float diffLightBlue[] = { 0, 0.6f, 0.9f };
    float ambiLightBlue[] = { 0.2f, 0.2f, 0.2f };
    float specLightBlue[] = { 0.8f, 0.8f, 0.8f };

    float diffViolet[] = { 0.5f, 0, 0.5f };
    float ambiViolet[] = { 0.2f, 0.2f, 0.2f };
    float specViolet[] = { 0.8f, 0.8f, 0.8f };

    // Випадкове "тасування" одновимірного масиву цілих чисел
    void shuffle(int *a, int size)
    {
        // Ініціалізація генератору випадкових значень поточним часом:
        srand((unsigned)time(0));
        std::random_shuffle(a, a + size); // алгоритм стандартної бібліотеки шаблонів
    }

    // Малювання паралелепіпеда
    void parallelepiped(float length, float width, float height)
    {
        glBegin(GL_QUAD_STRIP);
        //грань 1 || YZ, x<0
        glNormal3f(-1.0f, 0.0f, 0.0f);
        glVertex3f(-length / 2, -width / 2, -height / 2);
        glVertex3f(-length / 2, -width / 2, height / 2);
        glVertex3f(-length / 2, width / 2, -height / 2);
        glVertex3f(-length / 2, width / 2, height / 2);

        //грань 2 || ZX, y>0
        glNormal3f(0.0f, 1.0f, 0.0f);
        glVertex3f(length / 2, width / 2, -height / 2);
        glVertex3f(length / 2, width / 2, height / 2);

        //грань 3 || YZ, x>0
        glNormal3f(1.0f, 0.0f, 0.0f);
        glVertex3f(length / 2, -width / 2, -height / 2);
        glVertex3f(length / 2, -width / 2, height / 2);

        //грань 4 || ZX y<0
        glNormal3f(0.0f, -1.0f, 0.0f);
        glVertex3f(-length / 2, -width / 2, -height / 2);
        glVertex3f(-length / 2, -width / 2, height / 2);
        glEnd();

        glBegin(GL_QUADS);
        //грань 5 || YX, z>0
        glNormal3f(0.0f, 0.0f, 1.0f);
        glVertex3f(-length / 2, -width / 2, height / 2);
        glVertex3f(-length / 2, width / 2, height / 2);
        glVertex3f(length / 2, width / 2, height / 2);
        glVertex3f(length / 2, -width / 2, height / 2);

        //грань 6  || YX, z<0
        glNormal3f(0.0f, 0.0f, -1.0f);
        glVertex3f(-length / 2, -width / 2, -height / 2);
        glVertex3f(-length / 2, width / 2, -height / 2);
        glVertex3f(length / 2, width / 2, -height / 2);
        glVertex3f(length / 2, -width / 2, -height / 2);
        glEnd();
    }

    // Відображення рядка тексту вказаним шрифтом у вказаній позиції
    void drawString(void *font, const char* text, float x, float y)
    {
        if (!text) // нульовий указівник
        {
            return;
        }
        // Встановлення позиції тексту:
        glRasterPos2f(x, y);
        while (*text)
        {
            // Рядок виводиться посимвольно:
            glutBitmapCharacter(font, *text);
            text++;
        }
    }
}

Тепер можна створювати класи, похідні від Shape. Кожний клас представлено заголовним файлом і файлом реалізації. Деструктори перевизначати недоцільно, оскільки в базовому класі оголошено віртуальний деструктор і всі автоматично створені деструктори похідних класів будуть також віртуальними. Клас Board (поле гри):

// Board.h
#ifndef Board_h
#define Board_h

#include "shape.h"

namespace DiskGame
{
    // Клас, який відповідає за малювання дошки (поля гри)
    class Board : public Shape
    {
    public:
        Board(float xCenter, float yCenter, float zCenter,
            float xSize, float ySize, float zSize,
            float *diffColor, float *ambiColor, float *specColor)
            : Shape(xCenter, yCenter, zCenter,
                xSize, ySize, zSize,
                specColor, diffColor, ambiColor) { }
        virtual void draw();
    };

}
#endif

Файл реалізації:

#include <gl/glut.h>
#include "Board.h"
#include "utils.h"

namespace DiskGame
{

    void Board::draw()
    {
        glMaterialfv(GL_FRONT, GL_AMBIENT, getAmbiColor());
        glMaterialfv(GL_FRONT, GL_DIFFUSE, getDiffColor());
        glMaterialfv(GL_FRONT, GL_SPECULAR, getSpecColor());
        glMaterialf(GL_FRONT, GL_SHININESS, GraphUtils::shininess);
        // Запис поточної матриці в стек
        // (збереження вмісту поточної матриці для подальшого використання):
        glPushMatrix();
        glTranslatef(getXCenter(), getYCenter(), getZCenter());
        GraphUtils::parallelepiped(getXSize(), getYSize(), getZSize());
        // Відновлення поточної матриці зі стека:
        glPopMatrix();
    }

}

Клас Stick (стрижень):

// Stick.h
#ifndef Stick_h
#define Stick_h

#include "shape.h"

namespace DiskGame
{
    // Клас, який відповідає за малювання стрижня
    class Stick : public Shape
    {
    public:
        Stick(float xCenter, float yCenter, float zCenter,
            float xSize, float ySize, float zSize,
            float *diffColor, float *ambiColor, float *specColor)
            : Shape(xCenter, yCenter, zCenter,
                xSize, ySize, zSize,
                specColor, diffColor, ambiColor) { }
        virtual void draw();
    };
}
#endif

Файл реалізації:

#include <gl/glut.h>
#include "Stick.h"
#include "utils.h"

namespace DiskGame
{
    void Stick::draw()
    {
        // Визначення властивостей матеріалу:
        glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, getAmbiColor());
        glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, getDiffColor());
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, getSpecColor());
        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, GraphUtils::shininess);
        // Запис поточної матриці в стек
        // (збереження вмісту поточної матриці для подальшого використання):
        glPushMatrix();
        glTranslatef(getXCenter(), getYCenter() + getYSize() / 2, getZCenter());
        // Циліндр повинен бути розташований у вертикальному напрямку:
        glRotatef(90, 1, 0, 0);
        GLUquadricObj* quadricObj = gluNewQuadric();
        gluCylinder(quadricObj, getXSize() / 2, getXSize() / 2, getYSize(), 20, 2);
        // Диск повинен бути намальований зовнішньою гранню догори:
        glRotatef(180, 1, 0, 0);
        gluDisk(quadricObj, 0, getXSize() / 2, 20, 20);
        gluDeleteQuadric(quadricObj);
        // Відновлення поточної матриці зі стека:
        glPopMatrix();
    }

}

Клас Disk (диск):

// Disk.h
#ifndef Disk_h
#define Disk_h

#include "shape.h"

namespace DiskGame
{
    // Клас, який відповідає за малювання диска
    class Disk : public Shape
    {
    private:
        float innerRadius;
    public:
        Disk(float xCenter, float yCenter, float zCenter,
            float xSize, float ySize, float zSize,
            float *diffColor, float *ambiColor, float *specColor,
            float innerRadius)
            : Shape(xCenter, yCenter, zCenter, xSize, ySize, zSize,
                specColor, diffColor, ambiColor) {
            this->innerRadius = innerRadius;
        }
        float getInnerRadius() const { return innerRadius; }
        void setInnerRadius(float innerRadius) { this->innerRadius = innerRadius; }
        virtual void draw();
    };
}
#endif

Файл реалізації:

#include <gl/glut.h>
#include "Disk.h"
#include "utils.h"

namespace DiskGame
{

    void Disk::draw()
    {
        // Визначення властивостей матеріалу:
        glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, getAmbiColor());
        glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, getDiffColor());
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, getSpecColor());
        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, GraphUtils::shininess);
        // Запис поточної матриці в стек
        // (збереження вмісту поточної матриці для подальшого використання):
        glPushMatrix();
        glTranslatef(getXCenter(), getYCenter() + getYSize() / 2, getZCenter());
        // Циліндр повинен бути розташований у вертикальному напрямку:
        glRotatef(90, 1, 0, 0);
        GLUquadricObj* quadricObj = gluNewQuadric();
        gluCylinder(quadricObj, getXSize() / 2, getXSize() / 2, getYSize(), 20, 2);
        // Диск повинен бути намальований зовнішньою гранню догори:
        glRotatef(180, 1, 0, 0);
        // Малюємо диск зверху:
        gluDisk(quadricObj, innerRadius, getXSize() / 2, 20, 20);
        // Малюємо диск знизу:
        glTranslatef(0, 0, -getYSize());
        gluDisk(quadricObj, innerRadius, getXSize() / 2, 20, 20);
        gluDeleteQuadric(quadricObj);
        // Відновлення поточної матриці зі стека:
        glPopMatrix();
    }

}

Завдяки поліморфізму можна вказівники на різні фігури розташувати в одному масиві та потім у циклі здійснювати перемалювання. Такий масив повинен бути "гнучким" – дозволяти додавання вказівників під час роботи програми. Такі можливості надає клас std::vector стандартної бібліотеки шаблонів C++.

Основний клас проєкту – Scene. Цей клас який представляє геометрію сцени, контролює правила гри, розташування елементів, а також реалізує обробку подій GLUT. Його слід істотно розширити, додавши необхідні дані (зокрема, вектор фігур) і функції-елементи.

Відповідний заголовний файл матиме такий вигляд:

// Scene.h
#ifndef Scene_h
#define Scene_h

#include "Shape.h"
#include "Disk.h"
#include <vector>

namespace DiskGame
{

    const int M = 3, N = 3; // кількість рядків та колонок поля

                            // Основний клас гри, який представляє геометрію сцени,
                            // контролює правила гри, розташування елементів,
                            // а також реалізує обробку подій GLUT
    class Scene
    {
        std::vector<Shape*> shapes; // "гнучкий" масив указівників на елементи гри
        int button;           // кнопка миші (-1 - не натиснута, 0 - ліва, 2 - права)
        float angleX, angleY; // поточний кут повороту сцени 
        float mouseX, mouseY; // поточні координати
        float width, height;  // Розміри вікна
        float distZ;          // відстань по осі Z до сцени
        bool finish;          // ознака того, що гру завершено
        Disk *disks[N];       // масив указівників на диски
        float xStep, zStep;   // відстань між окремими стрижнями
        int time;             // поточний час у секуднах
        int fields[M][N];     // масив, у якому відображається розміщення дисків:
                              // 0 - диску немає, 
                              // 1, 2, 3 - червоний, жовтий та зелений диски відповідно
        int xFrom, zFrom;     // індекси стрижня, з якого починається пересування
        int xTo, zTo;         // індекси стрижня, яким закінчується пересування
    public:
        Scene(float xStep, float zStep);
        ~Scene();
        void on_paint();
        void on_size(int width, int height);
        void on_mouse(int button, int state, int x, int y);
        void on_motion(int x, int y);
        void on_special(int key, int x, int y);
        void on_timer(int value);
    private:
        void initialize();
        void allocateDisks();
        bool moveDisk(int xFrom, int zFrom, int xTo, int zTo);
        void upDisk(int x, int z);
        void downAllDisks();
        bool findNearest(int x, int z, int& x1, int& z1);
        void resetArr();
        float allocX(int i);
        float allocZ(int i);
    };

}

#endif

Файл реалізації:

// Scene.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <gl/glut.h>
#include <stdio.h>
#include "Scene.h"
#include "utils.h"
#include "Board.h"
#include "Stick.h"

namespace DiskGame
{
    using namespace GraphUtils;

    // Параметри конструктора - відстань між окремими стрижнями:
    Scene::Scene(float xStep, float zStep)
    {
        this->xStep = xStep;
        this->zStep = zStep;

        // Додаємо дошку сірого кольору. 
        // Розміри визначаємо так, щоб поміщалися всі стрижні:
        shapes.push_back(new Board(0.0, 0.0, 0.0, N * xStep, 0.1, M * xStep, diffGray, ambiGray, specGray));
        // Додаємо стрижні (крім останнього ряду):
        for (int i = 0; i < M - 1; i++)
        {
            for (int j = 0; j < N; j++)
            {
                shapes.push_back(new Stick(allocX(j), 0.15, allocZ(i), 0.1, 0.2, 0.1, diffGray, ambiGray, specGray));
            }
        }
        // Додаємо останній ряд стрижнів:
        shapes.push_back(new Stick(allocX(0), 0.15, allocZ(M - 1), 0.1, 0.2, 0.1, diffRed, ambiRed, specRed));
        shapes.push_back(new Stick(allocX(1), 0.15, allocZ(M - 1), 0.1, 0.2, 0.1, diffYellow, ambiYellow, specYellow));
        shapes.push_back(new Stick(allocX(2), 0.15, allocZ(M - 1), 0.1, 0.2, 0.1, diffGreen, ambiGreen, specGreen));
        // Додаємо диски в першому ряду:
        shapes.push_back(disks[0] = new Disk(allocX(0), 0.1, allocZ(0), 0.3, 0.1, 0.3, diffRed, ambiRed, specRed, 0.05));
        shapes.push_back(disks[1] = new Disk(allocX(1), 0.1, allocZ(0), 0.3, 0.1, 0.3, diffYellow, ambiYellow, specYellow, 0.05));
        shapes.push_back(disks[2] = new Disk(allocX(2), 0.1, allocZ(0), 0.3, 0.1, 0.3, diffGreen, ambiGreen, specGreen, 0.05));
        // Здійснюємо ініціалізацію параметрів перед першою грою:
        initialize();
    }

    Scene::~Scene()
    {
        // Видаляємо всі фігури:
        for (int i = 0; i < shapes.size(); i++)
        {
            delete shapes[i];
        }
    }

    // Ініціалізація масиву, в якому відображається розміщення дисків
    void Scene::resetArr()
    {
        // Спочатку всі диски в першому ряду:
        for (int j = 0; j < N; j++)
        {
            fields[0][j] = j + 1;
        }
        // Інші стрижні поки порожні:
        for (int i = 1; i < M; i++)
        {
            for (int j = 0; j < N; j++)
            {
                fields[i][j] = 0;
            }
        }
    }

    // Перерахування індексу масиву fields в координату x
    float Scene::allocX(int i)
    {
        return  xStep * i - (N - 1) * xStep / 2;
    }

    // Перерахування індексу масиву fields в координату z
    float Scene::allocZ(int i)
    {
        return  zStep * i - (M - 1) * zStep / 2;
    }

    // Розташування дисків відповідно до вмісту масиву fields
    void Scene::allocateDisks()
    {
        for (int i = 0; i < M - 1; i++)
        {
            for (int j = 0; j < N; j++)
            {
                if (fields[i][j] > 0)
                {
                    disks[fields[i][j] - 1]->setCoords(allocX(j), 0.1, allocZ(i));
                }
            }
        }
    }

    // Переміщення диску зі вказаної позиції на нову
    bool Scene::moveDisk(int xFrom, int zFrom, int xTo, int zTo)
    {
        // Перевірка можливості переміщення:
        if (xFrom < 0 || zFrom < 0 || xFrom >= N || zFrom >= M || fields[zFrom][xFrom] == 0)
        {
            return false;
        }
        if (xTo < 0 || zTo < 0 || xTo >= N || zTo >= M || fields[zTo][xTo] > 0)
        {
            return false;
        }
        if (xFrom == xTo && zFrom == zTo)
        {
            return false;
        }
        if (xFrom != xTo && zFrom != zTo)
        {
            return false;
        }
        if (xFrom - xTo > 1 || xTo - xFrom > 1 || zFrom - zTo > 1 || zTo - zFrom > 1)
        {
            return false;
        }
        if (disks[fields[zFrom][xFrom] - 1]->getYCenter() < 0.2)
        {
            return false;
        }

        // Переміщення:
        disks[fields[zFrom][xFrom] - 1]->setXCenter(allocX(xTo));
        disks[fields[zFrom][xFrom] - 1]->setZCenter(allocZ(zTo));
        // Внесення змін в дані масиву fields:
        fields[zTo][xTo] = fields[zFrom][xFrom];
        fields[zFrom][xFrom] = 0;
        return true;
    }

    // Підняття догори диску, розташованому на стрижні з відповідними координатами
    void Scene::upDisk(int x, int z)
    {
        if (x < 0 || z < 0 || x >= N || z >= M)
        {
            return;
        }
        if (fields[z][x] > 0)
        {
            disks[fields[z][x] - 1]->setYCenter(0.3);
        }
    }

    // Опускання всіх дисків
    void Scene::downAllDisks()
    {
        for (int i = 0; i < N; i++)
        {
            disks[i]->setYCenter(0.1);
        }
    }

    // Ініціалізація даних (виконується спочатку, а потім з кожним оновленням гри):
    void Scene::initialize()
    {
        resetArr();      // початкове заповнення масиву fields
                         // "Тасування" масиву. Оскільки двовимірний масив у C++ зберігається як 
                         // одновимірний, здійснюємо його приведення до типу одновимірного масиву:
        GraphUtils::shuffle((int *)fields, (M - 1) * N);
        allocateDisks(); // розташування дисків відповідно до масиву fields
                         // Ініціалізація елементів даних:
        distZ = -2;
        angleX = -10;
        angleY = 30;
        time = 0;
        finish = false;
    }

    // Пошук стрижня, найближчого до позиції курсору миші:
    bool Scene::findNearest(int x, int y, int& x1, int& z1)
    {
        int viewport[4];
        int iMin = -1, jMin = -1;
        double mvMatrix[16], projMatrix[16];
        double minDist = 2000;

        for (int i = 0; i < M; i++)
        {
            for (int j = 0; j < N; j++)
            {

                // Світові x, y, z поточного стрижня:
                double wx = allocX(j);
                double wy = 0.1;
                double wz = allocZ(i);

                // Заповнюємо масив viewport поточною областю перегляду:
                glGetIntegerv(GL_VIEWPORT, viewport);

                // Заповнюємо масиви поточними матрицями:
                glGetDoublev(GL_MODELVIEW_MATRIX, mvMatrix);
                glGetDoublev(GL_PROJECTION_MATRIX, projMatrix);

                // Світові x, y, z координати, що обертаються:
                double dx, dy, dz;

                // Отримуємо координати точки, на яку спроектовано поточний стрижень:
                gluProject(wx, wy, wz, mvMatrix, projMatrix, viewport, &dx, &dy, &dz);
                dy = viewport[3] - dy - 1; // dy необхідно перерахувати
                double d = (x - dx) * (x - dx) + (y - dy) * (y - dy); // квадрат відстані
                if (d < minDist) // знайшли ближчий стрижень
                {
                    minDist = d;
                    iMin = i;
                    jMin = j;
                }
            }
        }
        if (minDist < 1000) // знайшли найближчий стрижень
        {
            x1 = jMin;
            z1 = iMin;
            return true;
        }
        else
        {
            return false;
        }
    }

    // Оброблювач події, пов'язаної з перемалюванням вікна
    void Scene::on_paint()
    {
        char text[128]; // Масив символів, 
                        // Заповнення масиву символів відповідно до стану гри:
        if (finish)
        {
            sprintf(text, "Game over. Time: %d sec.   F2 - Restart game   Esc - Exit", time);
        }
        else
        {
            sprintf(text, "F2 - Restart game   Esc - Exit              Time: %d sec.", time);
        }
        // Встановлюємо область перегляду таку, щоб вона вміщувала все вікно:
        glViewport(0, 0, width, height);

        // Ініціалізуємо параметри матеріалів і джерела світла:
        float lightAmbient[] = { 0.0f, 0.0f, 0.0f, 1.0f }; // колір фонового освітлення 
        float lightDiffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f }; // колір дифузного освітлення 
        float lightSpecular[] = { 1.0f, 1.0f, 1.0f, 1.0f };// колір дзеркального відображення
        float lightPosition[] = { 1.0f, 1.0f, 1.0f, 0.0f };// розташування джерела світла

        // Встановлюємо параметри джерела світла:
        glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuse);
        glLightfv(GL_LIGHT0, GL_SPECULAR, lightSpecular);
        glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);

        // Визначаємо блакитний колір для очищення:
        if (finish)
        {
            glClearColor(0, 0.7, 0.7, 0);
        }
        else
        {
            glClearColor(0, 0.5, 0.5, 0);
        }

        // Очищуємо буфери:
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glPushMatrix();
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();

        // Для відображення тексту, краще використовувати ортографічну проекцію:
        glOrtho(0, 1, 0, 1, -1, 1);
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glColor3f(1, 1, 0); // жовтий текст
        drawString(GLUT_BITMAP_TIMES_ROMAN_24, text, 0.01, 0.95);
        glPopMatrix();

        // Включаємо режим роботи з матрицею проекцій:
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();

        // Задаємо усічений конус видимості в лівосторонній системі координат, 
        // 60 - кут видимості в градусах по осі у,
        // width/height - кут видимості уздовж осі x,
        // 1 и 100 - відстань від спостерігача до площин відсікання по глибині:
        gluPerspective(60, width / height, 1, 100);

        // Включаємо режим роботи з видовою матрицею:
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glTranslatef(0, 0, distZ);	// камера з початку координат зсувається на distZ, 

        glRotatef(angleX, 0.0f, 1.0f, 0.0f);  // потім обертається по осі Oy
        glRotatef(angleY, 1.0f, 0.0f, 0.0f);  // потім обертається по осі Ox
        glEnable(GL_DEPTH_TEST);	// включаємо буфер глибини (для відсікання невидимих частин зображення)

                                    // Включаємо режим для установки освітлення:
        glEnable(GL_LIGHTING);

        // Додаємо джерело світла № 0 (їх може бути до 8), зараз воно світить з "очей":
        glEnable(GL_LIGHT0);

        // Малюємо усі фігури:
        for (int i = 0; i < shapes.size(); i++)
        {
            shapes[i]->draw();
        }

        // Вимикаємо все, що включили:
        glDisable(GL_LIGHT0);
        glDisable(GL_LIGHTING);
        glDisable(GL_DEPTH_TEST);
        glFlush();
        // показуємо вікно:
        glutSwapBuffers(); // перемикання буферів
    }

    // Оброблювач події, пов'язаної зі зміною розмірів вікна 
    void Scene::on_size(int width, int height)
    {
        this->width = width;
        if (height == 0)
            height = 1;
        this->height = height;
    }

    // Оброблювач подій, пов'язаних з натисканням кнопок миші
    void Scene::on_mouse(int button, int state, int x, int y)
    {
        // Зберігаємо поточні координати миші:
        mouseX = x;
        mouseY = y;
        if ((state == GLUT_UP)) // кнопка віджата
        {
            downAllDisks();
            // Перевірка закінчення гри:
            if (fields[M - 1][0] == 1 && fields[M - 1][1] == 2 && fields[M - 1][2] == 3)
            {
                finish = true;
            }
            this->button = -1;  // ніяка кнопка не натиснута
            return;
        }
        this->button = button;  // зберігаємо інформацію про кнопки
        if (finish)
        {
            return;
        }
        // Вибираємо диск для пересування:
        if (button == 0 && findNearest(x, y, xFrom, zFrom))
        {
            upDisk(xFrom, zFrom);
        }
    }

    // Оброблювач подій, пов'язаних з пересуванням миші з натисненою кнопкою
    void Scene::on_motion(int x, int y)
    {
        switch (button)
        {
        case 0: // ліва кнопка - пересування диску
            if (finish)
                break;
            if (findNearest(x, y, xTo, zTo))
            {
                moveDisk(xFrom, zFrom, xTo, zTo);
                xFrom = xTo;
                zFrom = zTo;
            }
            break;
        case 2: // права кнопка - обертання сцени
            angleX += x - mouseX;
            angleY += y - mouseY;
            mouseX = x;
            mouseY = y;
            break;
        }
    }

    // Оброблювач подій, пов'язаних з натисненням функціональних клавіш і стрілок 
    void Scene::on_special(int key, int x, int y)
    {
        switch (key) {
        case GLUT_KEY_UP:   // наближення
            if (distZ > -1.7)
            {
                break;
            }
            distZ += 0.1;
            break;
        case GLUT_KEY_DOWN: // віддалення
            distZ -= 0.1;
            break;
        case GLUT_KEY_F2:   // нова гра
            initialize();
            break;
        }
    }

    int tick = 0; // лічильник, значення якого змінюється кожні 25 мс

                  // Оброблювач події від таймера
    void Scene::on_timer(int value)
    {
        tick++;
        if (tick >= 40) // нарахували наступну секунду
        {
            if (!finish)// секунди нарощуються, якщо гру не закінчено
            {
                time++;
            }
            tick = 0;   // скинули лічильник
        }
        on_paint();     // здійснюємо перемалювання вікна
    }

}

Примітка: директиву препроцесора #define _CRT_SECURE_NO_WARNINGS додано для того, щоб компілятор адекватно реагував на функцію sprintf().

Відповідні зміни слід здійснити в файлі main.cpp:

// main.cpp
#include <gl/glut.h>
#include "Scene.h"

using DiskGame::Scene;

Scene *scene; // вказівник на клас Scene

void on_paint()
{
    // Викликаємо відповідну функцію класу Scene:
    scene->on_paint();
}

void on_size(int width, int height)
{
    // Викликаємо відповідну функцію класу Scene:
    scene->on_size(width, height);
}

void on_mouse(int button, int state, int x, int y)
{
    // Викликаємо відповідну функцію класу Scene:
    scene->on_mouse(button, state, x, y);
}

void on_motion(int x, int y)
{
    // Викликаємо відповідну функцію класу Scene:
    scene->on_motion(x, y);
}

void on_special(int key, int x, int y)
{
    // Викликаємо відповідну функцію класу Scene:
    scene->on_special(key, x, y);
}

void on_keyboard(unsigned char key, int x, int y)
{
    // Обробка подій від клавіатури:
    if (key == 27)
        exit(0);
}

void on_timer(int value)
{
    // Обробка події від таймера
    scene->on_timer(value);
    glutTimerFunc(25, on_timer, 0); // зa 25 мс викличеться ця функція
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);         // ініціалізуємо GLUT
    scene = new Scene(0.4, 0.4);   // створюємо об'єкт "сцена"
    glutInitWindowSize(800, 600);  // встановлюємо розміри вікна
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);// ініціалізуємо режими відображення
    glutCreateWindow("Disks");     // створюємо вікно
    glutDisplayFunc(on_paint);     // реєструємо функцію відображення
    glutReshapeFunc(on_size);      // реєструємо функцію обробки зміни розмірів вікна
    glutMotionFunc(on_motion);     // реєструємо функцію, яка відповідає за переміщення миші з натиснутою кнопкою
    glutMouseFunc(on_mouse);       // реєструємо функцію, яка відповідає за натискання на кнопку миші
    glutKeyboardFunc(on_keyboard); // реєструємо функцію, яка відповідає за натискання клавіш
    glutSpecialFunc(on_special);   // реєструємо функцію, яка відповідає за натискання спеціальних клавіш
    glutTimerFunc(25, on_timer, 0);// кожні 25 мс викликається ця функція
    glutMainLoop();                // стартуємо основний цикл обробки подій
    delete scene;                  // видаляємо об'єкт "сцена"
    return(0);
}

Тепер програму можна завантажити на виконання і перевірити її роботу.

 

up