Program Development Example

1 Problem Statement

Assume that we need to develop a program that implements imaginary computer game called "Disks". The rules of this game are as follows:

  • The board of "Disks" game consists of nine small squares (3 x 3). Each square has a small vertical stick on its middle.
  • Three colour disks (red, yellow, and green) are randomly set onto three sticks.
  • The game assumes the following successive steps (moves). On each step, player can move one of disks from its place to a free neighbour square in each non-diagonal direction.
  • The goal of the game is to place discs on the sticks of the corresponding color.
  • While playing the time is calculated.

The program must implement the following key functions:

  • new game field creation (window cleaning and initial state reproducing)
  • game elements choosing and manipulation using keyboard and mouse
  • counting time and displaying results
  • game field turning.

2 Requirements Setting. Analysis and Design

2.1 Use Case Diagram

The functional requirements for the program are shown on the use case diagram. This diagram should include one actor (user), who can interact with our program in several ways:

  • updating the field of the game, which involves a random arrangement of disks
  • game field settings
  • playing with time counting
  • displaying results.

Figure 2.1 shows the use case diagram.

Figure 2.1 – Use Case Diagram

2.2 Definition of Class Structure

First we define the general structure of the classes. The main class, which is responsible for the logic of the game, will be Scene. This class will contain references to objects of classes derived from the abstract Shape class. The diagram can also display the main() function. The class diagram from a conceptual point of view is shown in Figure 2.2.

Figure 2.2 – Class diagram

Later, the necessary clarifications will be made to the class diagram.

3 Implementation

3.1 Preparing the Programming Environment. Creating a New Project

The standard configuration of the Visual Studio 2019 programming environment allows you to work with GL and GLU libraries. The corresponding header files and library files are already located in the required folders. Unfortunately, the GLUT library needs to be added manually. The current approach to using GLUT is to use the FreeGLUT library, which can be downloaded at https://www.transmissionzero.co.uk/software/freeglut-devel/, then use link in the line with the text "Download freeglut 3.0.0 for MSVC (with PGP signature and PGP key)". Three of the five files that contain this archive should be opened in the appropriate folders:

Files from the archive The folder in which the file should be allocated
freeglut/bin/freeglut.dll ...\Windows\system32\
freeglut/include/GL/ (all files) ...\Program Files (x86)\Windows Kits\10\Include\<LAST_VERSION>\um\gl\
freeglut/lib/freeglut.lib ...\Program Files (x86)\Windows Kits\10\Lib\<LAST_VERSION>\um\x86\

Note: <LAST_VERSION> in the path is a folder whose name consists of numbers and dots; among similar names you should select the folder with the most recent creation date.

If it is impossible to locate the freeglut.dll file in the ...\Windows\system32\ folder, you can copy this file to the project Debug folder each time. Other files can also be located in the project folder next to the source code files, but then in the #include directive in the following examples you need to replace <gl/glut.h> with "glut.h".

The site http://freeglut.sourceforge.net contains a variety of information about the FreeGLUT library.

To execute the program in the Visual Studio environment, you should create a new project (Empty Project) named Disks.

3.2 Creation of Classes

For software projects of large and medium sizes, the general rule is to divide the source code into separate independent parts - logically (classes, namespaces, packages) and physically (separate files). In particular, in our project, in addition to the global namespace with the main() function, we'll create DiskGame namespace for classes and functions specific to the game that is being created, as well as GraphUtils namespace for graphic functions and constants that may be useful in other similar projects. Physically, each essential class should be located in a separate source file. The main() function (main.cpp) and the elements of the GraphUtils namespace (utils.cpp) will be located in separate files.

The common rule is to develop program iteratively, with obligatory testing of intermediate results. Each class should be tested separately. Adding new classes causes changes in previous classes' code. Therefore, previously created classes must be tested again.

We'll start with creation source code from Scene class. To create a new class in Visual Studio programming environment, you can use Project | Add Class... function of the main menu. The only text field called Class name (Scene) must be filled in. After pressing OK button, two new files, Scene.h and Scene.cpp will be automatically created. The Scene.h file will be as follows:

#pragma once
class Scene
{
};

The Scene.cpp file contains the only inclusion of the header file:

#include "Scene.h"

Automatically generated text has several disadvantages:

  • the #pragma once directive is system specific; you cannot recompile code with non-standard directives using non-Microsoft compilers; you should use inclusion guards instead of #pragma once directive
  • there is no way to automatically create a namespace

It is also gud idea to add to each file comment line with file name.

Unfortunately, there are no appropriate options in Class Wizard. You can only correct these drawbacks manually, after each new class generation. In our case, Scene.h file should be corrected as follows:

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

    class Scene
    {
    };

}

#endif

We'll add on_paint() function that must be called automatically, each time program require repainting of our scene:

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

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

}

#endif

The first implementation of on_paint() function includes only window cleanup. For this purpose, inclusion of glut.h header file is necessary. The Scene.cpp file can be modified as follows:

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

namespace DiskGame
{

    void Scene::on_paint(void)
    {
        // specify cyan color for cleaning:
        glClearColor(0, 0.5, 0.5, 0);  
        // clear buffers using specifig color:
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
        // show blank window:
        glutSwapBuffers();
    }

}

To test our class, we need extra source file (e.g. main.cpp) with main() function. To create a new source file, on the Project menu, click Add New Item..., then select a file type - C++ File. You must also type a file name (main) in the Name box. After you click OK, and the empty file opens.

A code within main() function should initialize GLUT, set window size and mode (RGBA color mode). The first version of our main.cpp file will be as follows:

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

using DiskGame::Scene;

Scene *scene; // a pointer to the Scene class

void on_paint()
{
    scene->on_paint(); // call the corresponding function of the class Scene
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);       // initialize GLUT
    scene = new Scene();         // create a "scene" object
    glutInitWindowSize(800, 600);// set the size of the window
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH  
        | GLUT_DOUBLE);          // initialize display modes
    glutCreateWindow("Disks");   // create a window
    glutDisplayFunc(on_paint);   // registering display function
    glutMainLoop();              // starting main loop of event processing
    delete scene;                // remove "scene" object
    return(0);
}

Now we can test our OpenGL application. After starting the program, an empty window with the title "Disks" and a blue background should appear.

Now we can start creating classes that describe the elements of the game. In accordance with the class diagram, we create a Shape class that will be base for other shapes. To the class description, we add fields that represent the attributes shown in the class diagram. Accordingly, we should create access functions. The Shape class will be abstract because it will provide the pure virtual function draw(). It is also necessary to add a constructor with parameters and virtual destructor (the polymorphic classes destructors should be virtual). The source code of the header file will be as follows:

// Shape.h
#ifndef Shape_h
#define Shape_h
 
namespace DiskGame
{
    // Class to represent the abstract shape
    class Shape
    {
    private:
        float xCenter, yCenter, zCenter;          // coordinates of the center
        float xSize, ySize, zSize;                // sizes
        float *diffColor, *ambiColor, *specColor; // colors
    public:
        Shape(float xCenter, float yCenter, float zCenter,
              float xSize, float ySize, float zSize,
              float *diffColor, float *ambiColor, float *specColor);
        virtual ~Shape() { } 
        // Access functions:
        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 zSize) { this->xSize = xSize; }
        void   setYSize(float zSize) { 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; // this function must be overridden in derived classes
    };
 
}
#endif

The Scene.cpp file also should be corrected:

// Shape.cpp
#include "Shape.h"
 
namespace DiskGame
{
 
    // Constructor:
    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);
    }

    // Access functions:

    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;
    }

}

The constants (variables) and functions that are not included in any class can be used to display the shapes of game elements. It is advisable to create a module that consists of the utils.h header file and the utils.cpp implementation file. In this module, in particular, you can define arrays for individual colors. Useful functions that will also be located in the utils module include drawing a parallelepiped, displaying text, and receiving random placement of array elements. Functions should be placed in a separate namespace. The header file will look like this:

// utils.h
#ifndef utils_h
#define utils_h

namespace GraphUtils
{
    // Declaration of arrays that define colors:
    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;

    // Random shuffle of one-dimensional array of integers
    void shuffle(int *a, int size);

    // Drawing a parallelepiped
    void parallelepiped(float length, float width, float height);

    // Display a text string in the specified font at the specified position
    void drawString(void *font, const char* text, float x, float y);
}

#endif

The function of random "shuffling" an array may be as follows:

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

In the previous code, the random number generator is initialized by calling the srand() function. The value of the current time is sent as a parameter. We can get this value using the time(0) function. The call to random_shuffle() is a call to the so-called algorithm, which is part of the Standard Template Library (STL). To work with an array, we pass a pointer to the initial element, as well as a pointer to the memory address after the last element.

The usage of random_shuffle() shuffling algorithm requires inclusion of the algorithm header file. In order to initialize the random number generator, we include stdlib.h header file, to get the current time we include time.h header file. The implementation file will look like this:

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

namespace GraphUtils
{
    // Definition of colors:
    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 };

    // Random shuffle of one-dimensional array of integers
    void shuffle(int *a, int size)
    {
        // Initialize the random values generator by current time:
        srand((unsigned)time(0));
        std::random_shuffle(a, a + size); // algorithm of the Standard template library
    }

    // Drawing a parallelepiped
    void parallelepiped(float length, float width, float height)
    {
        glBegin(GL_QUAD_STRIP);
        //edge 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);

        //edge 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);

        //edge 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);

        //edge 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);
        //edge 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);

        //edge 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();
    }

    // Display a text string in the specified font at the specified position
    void drawString(void *font, const char* text, float x, float y)
    {
        if (!text) // null pointer
        {
            return;
        }
        // Establishing text position:
        glRasterPos2f(x, y);
        while (*text)
        {
            // The string is displayed characterwise:
            glutBitmapCharacter(font, *text);
            text++;
        }
    }
}

Now it is possible to create classes derived from Shape. Each class is represented by the header file and the implementation file. It is impractical to redefine destructors, since a virtual destructor is declared in the base class and all automatically created destructors of derived classes will also be virtual. The Board class (game field)

// Board.h
#ifndef Board_h
#define Board_h

#include "shape.h"

namespace DiskGame
{
    // The class that is responsible for drawing a board (a game field)
    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

Implementation file:

#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);
        // Write current matrix to stack
        // (saving the contents of the current matrix for future use):
        glPushMatrix();
        glTranslatef(getXCenter(), getYCenter(), getZCenter());
        GraphUtils::parallelepiped(getXSize(), getYSize(), getZSize());
        // Restore the current matrix from the stack:
        glPopMatrix();
    }

}

The Stick class:

// Stick.h
#ifndef Stick_h
#define Stick_h

#include "shape.h"

namespace DiskGame
{
    // The class that is responsible for drawing a stick
    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

Implementation file:

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

namespace DiskGame
{
    void Stick::draw()
    {
        // Determining the properties of the material:
        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);
        // Write current matrix to stack
        // (saving the contents of the current matrix for future use):
        glPushMatrix();
        glTranslatef(getXCenter(), getYCenter() + getYSize() / 2, getZCenter());
        // The cylinder should be located vertically:
        glRotatef(90, 1, 0, 0);
        GLUquadricObj* quadricObj = gluNewQuadric();
        gluCylinder(quadricObj, getXSize() / 2, getXSize() / 2, getYSize(), 20, 2);
        // The disk should be drawn with the outer face up:
        glRotatef(180, 1, 0, 0);
        gluDisk(quadricObj, 0, getXSize() / 2, 20, 20);
        gluDeleteQuadric(quadricObj);
        // Restore the current matrix from the stack:
        glPopMatrix();
    }

}
            

The Disk class:

// Disk.h
#ifndef Disk_h
#define Disk_h

#include "shape.h"

namespace DiskGame
{
    // The class that is responsible for drawing a disk
    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

Implementation file:

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

namespace DiskGame
{

    void Disk::draw()
    {
        // Determining the properties of the material:
        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);
        // Write current matrix to stack
        // (saving the contents of the current matrix for future use):
        glPushMatrix();
        glTranslatef(getXCenter(), getYCenter() + getYSize() / 2, getZCenter());
        // The cylinder should be located vertically:
        glRotatef(90, 1, 0, 0);
        GLUquadricObj* quadricObj = gluNewQuadric();
        gluCylinder(quadricObj, getXSize() / 2, getXSize() / 2, getYSize(), 20, 2);
        // The disk should be drawn with the outer face up:
        glRotatef(180, 1, 0, 0);
        // Draw the disk from the top:
        gluDisk(quadricObj, innerRadius, getXSize() / 2, 20, 20);
        // Draw the disk from the bottom:
        glTranslatef(0, 0, -getYSize());
        gluDisk(quadricObj, innerRadius, getXSize() / 2, 20, 20);
        gluDeleteQuadric(quadricObj);
        // Restore the current matrix from the stack:
        glPopMatrix();
    }

}

Due to polymorphism, it is possible to put pointers to different shapes in a single array, and then repaint all of them in a loop. This array should allow the addition of pointers while running the program. These options provide the class std::vector of the Standard template library.

The main project class is Scene. This class represents the geometry of the scene, controls the rules of the game, the location of the elements, and also implements the processing of GLUT events. It should be substantially expanded by adding necessary data (in particular, the vector of shapes) and member functions.

The corresponding header file will look like this:

// Scene.h
#ifndef Scene_h
#define Scene_h

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

namespace DiskGame
{

    const int M = 3, N = 3; // number of rows and columns of the board

                            // The main class of the game, which represents the geometry of the scene,
                            // controls the rules of the game, the location of the elements,
                            // and also implements the processing of GLUT events
    class Scene
    {
        std::vector<Shape*> shapes; // "flexible" array of pointers to the elements of the game
        int button;           // mouse button (-1 not pressed, 0 left, 2 right)
        float angleX, angleY; // current scene rotation angle 
        float mouseX, mouseY; // current coordinates
        float width, height;  // window sizes
        float distZ;          // Z-axis distance to the scene
        bool finish;          // sign that the game is over
        Disk *disks[N];       // array of pointers to disks
        float xStep, zStep;   // the distance between individual sticks
        int time;             // current time in seconds
        int fields[M][N];     // array that stores disk placement:
                              // 0 - no disk, 
                              // 1, 2, 3 - red, yellow and green discs respectively
        int xFrom, zFrom;     // the indices of the stick from which the movement begins
        int xTo, zTo;         // indexes of the stick on which the movement ends
    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

Implementation file:

// 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;

    // Constructor parameters - the distance between individual sticks:
    Scene::Scene(float xStep, float zStep)
    {
        this->xStep = xStep;
        this->zStep = zStep;

        // Add a gray board. 
        // Determine the dimensions so that all the sticks are placed:
        shapes.push_back(new Board(0.0, 0.0, 0.0, N * xStep, 0.1, M * xStep, diffGray, ambiGray, specGray));
        // Add sticks (except the last row):
        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));
            }
        }
        // Add the last row of sticks:
        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));
        // Add disks in the first row:
        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 the parameters before the first play:
        initialize();
    }

    Scene::~Scene()
    {
        // Remove all shapes:
        for (int i = 0; i < shapes.size(); i++)
        {
            delete shapes[i];
        }
    }

    // Initialization of the array, which stores the placement of disks
    void Scene::resetArr()
    {
        // First all discs in the first row:
        for (int j = 0; j < N; j++)
        {
            fields[0][j] = j + 1;
        }
        // Other sticks are empty:
        for (int i = 1; i < M; i++)
        {
            for (int j = 0; j < N; j++)
            {
                fields[i][j] = 0;
            }
        }
    }

    // Recalculation of the fields array index into the x coordinate
    float Scene::allocX(int i)
    {
        return  xStep * i - (N - 1) * xStep / 2;
    }

    //  Recalculation of the fields array index into the z coordinate
    float Scene::allocZ(int i)
    {
        return  zStep * i - (M - 1) * zStep / 2;
    }

    // Disk allocation according to the contents of the fields array
    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));
                }
            }
        }
    }

    // Moving a disk from a specified position to a new one
    bool Scene::moveDisk(int xFrom, int zFrom, int xTo, int zTo)
    {
        // Checking the ability to move:
        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;
        }

        // Moving:
        disks[fields[zFrom][xFrom] - 1]->setXCenter(allocX(xTo));
        disks[fields[zFrom][xFrom] - 1]->setZCenter(allocZ(zTo));
        // Making changes to the fields array:
        fields[zTo][xTo] = fields[zFrom][xFrom];
        fields[zFrom][xFrom] = 0;
        return true;
    }

    // Lifting up a disk located on a stick with corresponding coordinates
    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);
        }
    }

    // Dropping all disks
    void Scene::downAllDisks()
    {
        for (int i = 0; i < N; i++)
        {
            disks[i]->setYCenter(0.1);
        }
    }

    // Data initialization (performed initially, and then with each game update):
    void Scene::initialize()
    {
        resetArr();      // initial fill of the fields array
        // "Shuffle" array. Since a two-dimensional array in C ++ is stored as one-dimensional, 
        // we perform its casting to the type of a one-dimensional array:
        GraphUtils::shuffle((int *)fields, (M - 1) * N);
        allocateDisks(); // location of drives in accordance with the fields array
        // Initializing data elements:
        distZ = -2;
        angleX = -10;
        angleY = 30;
        time = 0;
        finish = false;
    }

    // Find the stick nearest to the mouse cursor position:
    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++)
            {

                // World x, y, z coordinates of the current stick:
                double wx = allocX(j);
                double wy = 0.1;
                double wz = allocZ(i);

                // Fill the viewport array with the current viewport:
                glGetIntegerv(GL_VIEWPORT, viewport);

                // Fill the arrays with current matrices:
                glGetDoublev(GL_MODELVIEW_MATRIX, mvMatrix);
                glGetDoublev(GL_PROJECTION_MATRIX, projMatrix);

                // World x, y, z coordinates that rotate:
                double dx, dy, dz;

                // Get the coordinates of the point on which the current stick is projected:
                gluProject(wx, wy, wz, mvMatrix, projMatrix, viewport, &dx, &dy, &dz);
                dy = viewport[3] - dy - 1; // dy must be recalculated
                double d = (x - dx) * (x - dx) + (y - dy) * (y - dy); // squared distance
                if (d < minDist) // found a closer stick
                {
                    minDist = d;
                    iMin = i;
                    jMin = j;
                }
            }
        }
        if (minDist < 1000) // found the nearest stick
        {
            x1 = jMin;
            z1 = iMin;
            return true;
        }
        else
        {
            return false;
        }
    }

    // Event handler associated with window redraw
    void Scene::on_paint()
    {
        char text[128]; // Array of characters
        // Fill in the array of characters according to the state of the game:
        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);
        }
        // Set the view area so that it contains the entire window:
        glViewport(0, 0, width, height);

        // Initialize light source parameters:
        float lightAmbient[] = { 0.0f, 0.0f, 0.0f, 1.0f }; // ambient light color
        float lightDiffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f }; // diffuse light color
        float lightSpecular[] = { 1.0f, 1.0f, 1.0f, 1.0f };// specular light color
        float lightPosition[] = { 1.0f, 1.0f, 1.0f, 0.0f };// position of light source

        // Set light source parameters:
        glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuse);
        glLightfv(GL_LIGHT0, GL_SPECULAR, lightSpecular);
        glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);

        // specify cyan color for cleaning:
        if (finish)
        {
            glClearColor(0, 0.7, 0.7, 0);
        }
        else
        {
            glClearColor(0, 0.5, 0.5, 0);
        }

        // clear buffers:
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glPushMatrix();
        // Appliy matrix operations to the projection matrix:
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity(); // replace the current matrix with the identity matrix

        // To display text, it's best to use an orthographic projection:
        glOrtho(0, 1, 0, 1, -1, 1);
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glColor3f(1, 1, 0); // yellow text
        drawString(GLUT_BITMAP_TIMES_ROMAN_24, text, 0.01, 0.95);
        glPopMatrix();

        // Appliy matrix operations to the projection matrix:
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();

        // Specify a viewing frustum into the left-side world coordinate system, 
        // 60 - the field of view angle in the y direction,
        // width/height - the field of view in the x direction,
        // 1 and 100 - distances from the viewer to the near and far clipping plane
        gluPerspective(60, width / height, 1, 100);

        // enable modelview matrix mode:
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glTranslatef(0, 0, distZ); // the camera shifts from the origin to distZ, 

        glRotatef(angleX, 0.0f, 1.0f, 0.0f);  // then rotates about the axis Oy
        glRotatef(angleY, 1.0f, 0.0f, 0.0f);  // then rotates about the axis Ox
        glEnable(GL_DEPTH_TEST);  // enable depth buffer (to cut off the invisible parts of the image)
        glEnable(GL_LIGHTING);    // enable light setting mode
        // Add light source # 0 (from eight possible sources), now it shines from the eyes:
        glEnable(GL_LIGHT0);

        // Draw all shapes:
        for (int i = 0; i < shapes.size(); i++)
        {
            shapes[i]->draw();
        }

        // Turn off everything that's turned on:
        glDisable(GL_LIGHT0);
        glDisable(GL_LIGHTING);
        glDisable(GL_DEPTH_TEST);
        glFlush();
        // Show the window:
        glutSwapBuffers(); // swap buffers
    }

    // Event handler associated with changing window size
    void Scene::on_size(int width, int height)
    {
        this->width = width;
        if (height == 0)
            height = 1;
        this->height = height;
    }

    // Event handler associated with the click of a mouse buttons
    void Scene::on_mouse(int button, int state, int x, int y)
    {
        // Save current mouse coordinates:
        mouseX = x;
        mouseY = y;
        if ((state == GLUT_UP)) // button not pressed
        {
            downAllDisks();
            // Checking the end of the game:
            if (fields[M - 1][0] == 1 && fields[M - 1][1] == 2 && fields[M - 1][2] == 3)
            {
                finish = true;
            }
            this->button = -1;  // no button pressed
            return;
        }
        this->button = button;  // store information about the buttons
        if (finish)
        {
            return;
        }
        // Select the disk to move:
        if (button == 0 && findNearest(x, y, xFrom, zFrom))
        {
            upDisk(xFrom, zFrom);
        }
    }

    // Event handler associated with mouse clicked
    void Scene::on_motion(int x, int y)
    {
        switch (button)
        {
        case 0: // left button is the movement of the disk
            if (finish)
                break;
            if (findNearest(x, y, xTo, zTo))
            {
                moveDisk(xFrom, zFrom, xTo, zTo);
                xFrom = xTo;
                zFrom = zTo;
            }
            break;
        case 2: // right button is the rotation of the scene
            angleX += x - mouseX;
            angleY += y - mouseY;
            mouseX = x;
            mouseY = y;
            break;
        }
    }

    // Event handler associated with pressing the function keys and arrows 
    void Scene::on_special(int key, int x, int y)
    {
        switch (key) {
        case GLUT_KEY_UP:   // oncoming
            if (distZ > -1.7)
            {
                break;
            }
            distZ += 0.1;
            break;
        case GLUT_KEY_DOWN: // distance grows
            distZ -= 0.1;
            break;
        case GLUT_KEY_F2:   // new game
            initialize();
            break;
        }
    }

    int tick = 0; // counter whose value changes every 25 ms

    // Timer event handler
    void Scene::on_timer(int value)
    {
        tick++;
        if (tick >= 40) // counted the next second
        {
            if (!finish)// seconds increase if the game is not over
            {
                time++;
            }
            tick = 0;   // reset counter
        }
        on_paint();     // redraw the window
    }

}

Note: the #define _CRT_SECURE_NO_WARNINGS preprocessor directive is added to force the compiler to respond adequately to the sprintf() function.

Corresponding changes should be made in the main.cpp file:

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

using DiskGame::Scene;

Scene *scene; // a pointer to the Scene class

void on_paint()
{
    // Call the appropriate function of Scene class:
    scene->on_paint();
}

void on_size(int width, int height)
{
    // Call the appropriate function of Scene class:
    scene->on_size(width, height);
}

void on_mouse(int button, int state, int x, int y)
{
    // Call the appropriate function of Scene class:
    scene->on_mouse(button, state, x, y);
}

void on_motion(int x, int y)
{
    // Call the appropriate function of Scene class:
    scene->on_motion(x, y);
}

void on_special(int key, int x, int y)
{
    // Call the appropriate function of Scene class:
    scene->on_special(key, x, y);
}

void on_keyboard(unsigned char key, int x, int y)
{
    // Handling events from the keyboard:
    if (key == 27)
        exit(0);
}

void on_timer(int value)
{
    // Processing the event from the timer
    scene->on_timer(value);
    glutTimerFunc(25, on_timer, 0); // after 25 ms, this function will be called
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);         // initialize GLUT
    scene = new Scene(0.4, 0.4);   // create a "scene" object
    glutInitWindowSize(800, 600);  // set the size of the window
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE); // initialize display modes
    glutCreateWindow("Disks");     // create a window
    glutDisplayFunc(on_paint);     // register display function
    glutReshapeFunc(on_size);      // register the function of processing the resizing of the window
    glutMotionFunc(on_motion);     // register the function that is responsible for moving the mouse with the pressed button
    glutMouseFunc(on_mouse);       // register the function that is responsible for clicking on the mouse button
    glutKeyboardFunc(on_keyboard); // register the function that is responsible for keystrokes
    glutSpecialFunc(on_special);   // register the function that is responsible for pressing the special keys
    glutTimerFunc(25, on_timer, 0);// every 25 ms this function is called
    glutMainLoop();                // starting main loop of event processing
    delete scene;                  // remove "scene" object
    return(0);
}

Now the program can be started to check its work.

Previous Next

up