пятница, 8 ноября 2019 г.

Обзор программного движка игры "Творец лабиринтов"

Если Вы читаете данную статью, то Вас заинтересовал движок игры - виртуальный конструктор "Творец лабиринтов" (саму игру  и её исходный код Вы можете скачать прямо с гугл-диска). Движок написан на достаточно популярном языке программирования C++ в среде Visual C++.  Графический движок реализован на базе платформы OpenGL.

OpenGL (Open Graphics Library) — спецификация, определяющая независимый от языка программирования платформонезависимый программный интерфейс для написания приложений, использующих двумерную и трёхмерную компьютерную графику. Включает более 300 функций для рисования сложных трёхмерных сцен из простых примитивов.
Итак вооружайтесь Visual C++ открывайте проект программы (файл "dvigok.dsp") и вперед постигать самую увлекательную науку, позволяющую почувствовать себя творцом - программирование!

В этой статье я разобью программный код на секции и опишу каждую из них.


Чтобы создать свое виртуальное пространство мы прежде всего должны проинициализировать графику с помощью OpenGL и описать работу с устройствами ввода-вывода информации (Этому будет посвящена отдельная статья, обучающая С++ и OpenGL)...

Открываем основной файл программы - это "proj0000.cpp". Первое с чего начинается любая программа на С++ - это подключение библиотек, описывающих классы используемых нами в программе функций:
 
#include <windows.h>        // Заголовочный файл для Windows
#include <gl\gl.h>            // Заголовочный файл для OpenGL32 библиотеки
#include <gl\glu.h>            // Заголовочный файл для GLu32 библиотеки
#include <math.h>
#include <commctrl.h>
#include <gl\glaux.h>        // Заголовочный файл для GLaux библиотеки
#include "Top.h"

#include <windowsx.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
#include <io.h>
#include <stdio.h>
#include <stdarg.h>  // заголовочный файл для манипуляций с переменными аргументами

#include "glModelUt.h"
Используются стандартные библиотеки Visual C++ кроме "Top.h" и "glModelUt.h" . Первая из них написана нами и содержит С++ код некоторых 3d объектов, которые, для примера, не реализованы в виде подгружаемых 3d моделей. То есть Вы можете на их примере понять как можно не прибегая к использованию сложных сторонних программ типа 3dsMAX  создавать программно простые модели используя только команды OpenGL.

Но об этом позже... Благодаря библиотеке стороннего разработчика "glModelUt.h" возможно подгружать 3d модели формата "*.ASE" из файлов, созданных в 3d редакторах (её.рассматривать мы не будем).

Следующим этапом как и в любой программе с++ происходит объявление переменных:
GLuint list_num;
GLuint list1_num;
GLuint list2_num;
GLuint list3_num;

int b;
int fg;
int zem=1;
int lab=1;
int speed=0;

int l;
int q=1;
bool   gp;                              // G Нажата? ( Новое )
GLuint filter;                          // Используемый фильтр для текстур
GLuint fogMode[]= { GL_EXP, GL_EXP2, GL_LINEAR }; // Хранит три типа тумана
GLuint fogfilter;                    // Тип используемого тумана
GLfloat fogColor[4]= {0.5f, 0.5f, 0.5f, 1.0f}; // Цвет тумана

int vihod=0;
bool  blend; // Смешивание НЕТ/ДА? (НОВОЕ)
bool  bp;    // B Нажата? ( Новое )

int labirint[12][12];
static HGLRC hRC;        // Постоянный контекст рендеринга
static HDC hDC;            // Приватный контекст устройства GDI

GLfloat    xrot;            // Вращение X
GLfloat    yrot;            // Y
GLfloat    zrot;            // Z

int Width, Height;                                       // Ширина и высота экрана в пикселах, необходима  для процедуры управления
float VertAngle, HorizAngle;                            // Вертикальный и горизонтальный угол обзора
float CameraX,CameraY,CameraZ;                        // Текущее положение точки наблюдателя

float pX=-k+1,pY=8*k,zX=0,zY=7*k;
int I=0,J=7;
BOOL    keys[256];        // Массив для процедуры обработки клавиатуры

GLuint  texture[25];   // Память для пяти наших текстур
GLuint  loop;         // Общая переменная цикла

Далее описываются используемые функции. Первая из них - это функция чтения из файла "Map.hryx" матрицы лабиринта в двумерный массив и сохранение параметров игры в соответствующие переменные:
void karta(void)
{
    FILE *fModel;
    char szString[128];
    fModel=fopen("Map.hryx","rb");
    int i=0;
    for(i=0;i<12;i++)
    {
        int tp;
        FindStr("MAP",fModel);
        sscanf(szString,"MAP %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d",&(labirint[i][0]),&(labirint[i][1]),&(labirint[i][2]),&(labirint[i][3]),&(labirint[i][4]),&(labirint[i][5]),&(labirint[i][6]),&(labirint[i][7]),&(labirint[i][8]),&(labirint[i][9]),&(labirint[i][10]),&(labirint[i][11]));
    }

FindStr("FOG",fModel);               //cчитать из файла состояние тумана
sscanf(szString,"FOG %d",&fg);

FindStr("FOGFILTER",fModel);               //cчитать из файла тип тумана
sscanf(szString,"FOGFILTER %d",&fogfilter);

FindStr("ZEM",fModel);               //cчитать из файла тип текстуры земли
sscanf(szString,"ZEM %d",&zem);

FindStr("LAB",fModel);               //cчитать из файла тип текстуры лабиринта
sscanf(szString,"LAB %d",&lab);

FindStr("SPEED",fModel);               //cчитать из файла скорость игры
sscanf(szString,"SPEED %d",&speed);

    fclose(fModel);
}

 Дополняя эту функцию Вы можете сделать доступными к редактированию в файле "Map.hryx" игры любую переменную, какую пожелаете!

Далее идут две функции: 1) позволяет открыть и прочитать файлы с картинками (с расширением " *.BMP"); 2) используя первую функцию загружает картинки из файлов, конвертирует их в текстуры и сохраняет их в массив типа: texture[номер текстуры]. 
AUX_RGBImageRec *LoadBMP(char *Filename)                // Loads A Bitmap Image
{
        FILE *File=NULL;                                // File Handle
        if (!Filename)                                  // Make Sure A Filename Was Given
        { return NULL;                            // If Not Return NULL
        }
        File=fopen(Filename,"r");                       // Check To See If The File Exists
        if (File)                                       // Does The File Exist?
        {       fclose(File);                           // Close The Handle
                return auxDIBImageLoad(Filename);       // Load The Bitmap And Return A Pointer
        }
        return NULL;                                    // If Load Failed Return NULL
}


int LoadGLTextures()                // Загрузка картинки и конвертирование в текстуру
{
  int Status=FALSE;                 // Индикатор состояния
  AUX_RGBImageRec *TextureImage[25]; // Создать место для текстуры
  memset(TextureImage,0,sizeof(void *)*25); // Установить указатель в NULL
  if ((TextureImage[0]=LoadBMP("textures//0.bmp")) &&   // Текстура эмблемы
      (TextureImage[1]=LoadBMP("textures//1.bmp")) &&  // Первая маска
      (TextureImage[2]=LoadBMP("textures//SKY_0000.bmp")) && // Первое изображение
      (TextureImage[3]=LoadBMP("textures//SKY_0001.bmp")) && // Вторая маска
      (TextureImage[4]=LoadBMP("textures//SKY_0002.bmp")) &&
      (TextureImage[5]=LoadBMP("textures//SKY_0003.bmp")) &&
      (TextureImage[6]=LoadBMP("textures//SKY_0004.bmp")) &&
      (TextureImage[7]=LoadBMP("textures//SKY_0005.bmp")) &&
      (TextureImage[8]=LoadBMP("textures//8.bmp")) &&
      (TextureImage[9]=LoadBMP("textures//9.bmp")) &&
      (TextureImage[10]=LoadBMP("textures//10.bmp")) &&
      (TextureImage[11]=LoadBMP("textures//11.bmp")) &&
      (TextureImage[12]=LoadBMP("textures//menu.bmp")) &&
      (TextureImage[13]=LoadBMP("textures//ogon//flame1.bmp")) &&
      (TextureImage[14]=LoadBMP("textures//ogon//flame2.bmp")) &&
      (TextureImage[15]=LoadBMP("textures//ogon//flame3.bmp")) &&
      (TextureImage[16]=LoadBMP("textures//ogon//flame4.bmp")) &&
      (TextureImage[17]=LoadBMP("textures//ogon//flame5.bmp")) &&
      (TextureImage[18]=LoadBMP("textures//ogon//flame6.bmp")) &&
      (TextureImage[19]=LoadBMP("textures//ogon//flame7.bmp")) &&
      (TextureImage[20]=LoadBMP("textures//ogon//flame8.bmp")) &&
      (TextureImage[21]=LoadBMP("textures//Font.bmp")) &&
      (TextureImage[22]=LoadBMP("textures//Mask1.bmp")) &&
      (TextureImage[23]=LoadBMP("textures//Untitled.bmp")) &&
      (TextureImage[24]=LoadBMP("textures//Image1.bmp"))) 
  {
    Status=TRUE;                    // Задать статус в TRUE
    glGenTextures(25, &texture[0]);  // Создать 25 текстур
    for (loop=0; loop<25; loop++)    // Цикл по всем текстурам
    {
      glBindTexture(GL_TEXTURE_2D, texture[loop]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY,
        0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);
    }
  }
  for (loop=0; loop<25; loop++)      // Цикл по всем пяти текстурам
  {
    if (TextureImage[loop])         // Если текстура существуют
    {
      if (TextureImage[loop]->data) // Если изображение текстуры существует
      {
        free(TextureImage[loop]->data); // Освободить память изображения
      }
      free(TextureImage[loop]);     // Освободить структуру изображения
    }
  }
  return Status;                    // Возвращаем статус
}

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

Далее идет функция инициализации окна OpenGL, где устанавливаются его основные свойства и параметры:
GLvoid InitGL(GLsizei Width, GLsizei Height)    // Вызвать после создания окна GL
{
    LoadGLTextures();
    glLoadModel("model//sample.ase",&list_num);
    glLoadModel("model//sample1.ase",&list1_num);
    glLoadModel("model//sample2.ase",&list2_num);
    glLoadModel("model//hryx.ase",&list3_num);
    //высота
    VertAngle = 90.0;
    HorizAngle = 0.0;

    glColor4f(1.0f,1.0f,1.0f,0.5f);   // Полная яркость, 50% альфа (НОВОЕ)
    glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Функция смешивания для непрозрачности,
                                  

  // Изменить для S режим генерации текстур на "сферическое наложение" ( Новое )
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
  // Изменить для T режим генерации текстур на "сферическое наложение" ( Новое )
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);


    BuildFont();  // Создаем шрифт
    glEnable(GL_TEXTURE_2D);    // Разрешение наложение текстуры
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
                            // Очистка экрана в черный цвет
    glClearDepth(1.0);        // Разрешить очистку буфера глубины
    glDepthFunc(GL_LESS);    // Тип теста глубины
    glEnable(GL_DEPTH_TEST);// разрешить тест глубины
    glShadeModel(GL_SMOOTH);// запрретить /разрешить/ плавное цветовое сглаживание
    glMatrixMode(GL_PROJECTION);// Выбор матрицы проекции
    glLoadIdentity();        // Сброс матрицы проекции
    gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
                            // Вычислить соотношение геометрических размеров для окна
    glMatrixMode(GL_MODELVIEW);// Выбор матрицы просмотра модели
}

Дальнейший код описывает расчет позиций курсора и соответствeующую ориентацию наблюдателя в пространстве OpenGL, а также описывает изменение местоположения наблюдателя в пространстве используя соответствующие клавиши. Т.е. функция позволяет нам осматриваться и перемещаться по нашему лабиринту:
void HandleControls()
{
    float PI = 3.1415926535;
    static long xCurPosOld, yCurPosOld;

    POINT Point;
    GetCursorPos(&Point);

    float Speed=0.07f;
    VertAngle+= (yCurPosOld-Point.y)*0.1f;
    HorizAngle -= (xCurPosOld-Point.x)*0.1f;

    if(VertAngle< 0.0) VertAngle= 0.0;
    if(VertAngle> 180.0) VertAngle= 180.0;

    if(HorizAngle< 0.0) HorizAngle = 359.0;
    if(HorizAngle> 359.0) HorizAngle = 0.0;

    if( GetAsyncKeyState(81) < 0)      //вверх
    {
    CameraZ-=0.05;
    }

    if( GetAsyncKeyState(90) < 0)       //вниз
    {
    CameraZ+=0.05;
    }


    if( GetAsyncKeyState(87) < 0 || GetAsyncKeyState(VK_LBUTTON) < 0 || GetAsyncKeyState(VK_UP) < 0 )  //VK_UP
    {
        if(CameraX-Speed*sin(HorizAngle/180*PI)<zX)if(CameraX-Speed*sin(HorizAngle/180*PI)>=pX){    CameraX -= Speed*sin(HorizAngle/180*PI);};
        if(CameraY-Speed*cos(HorizAngle/180*PI)<zY)if(CameraY-Speed*cos(HorizAngle/180*PI)>=pY){CameraY -= Speed*cos(HorizAngle/180*PI);};
      
    }

    if( GetAsyncKeyState(83) < 0 || GetAsyncKeyState(VK_RBUTTON) < 0 || GetAsyncKeyState(VK_DOWN) < 0 )  //VK_DOWN
    {
        if(CameraX+Speed*sin(HorizAngle/180*PI)<zX)if(CameraX+Speed*sin(HorizAngle/180*PI)>=pX){    CameraX += Speed*sin(HorizAngle/180*PI);};
        if(CameraY+Speed*cos(HorizAngle/180*PI)<zY)if(CameraY+Speed*cos(HorizAngle/180*PI)>=pY){CameraY += Speed*cos(HorizAngle/180*PI);};
    
    }
    if( GetAsyncKeyState(68) < 0 || GetAsyncKeyState(VK_RIGHT) < 0 )                                        //VK_RIGHT
    {
        if(CameraX+Speed*sin((HorizAngle-90)/180*PI)<zX)if(CameraX+Speed*sin((HorizAngle-90)/180*PI)>=pX){CameraX += Speed*sin((HorizAngle-90)/180*PI);};
        if(CameraY+Speed*cos((HorizAngle-90)/180*PI)<zY)if(CameraY+Speed*cos((HorizAngle-90)/180*PI)>=pY){CameraY += Speed*cos((HorizAngle-90)/180*PI);};
      
    }
    if( GetAsyncKeyState(65) < 0 || GetAsyncKeyState(VK_LEFT) < 0)                                         //VK_LEFT
    {
        if(CameraX+Speed*sin((HorizAngle+90)/180*PI)<zX)if(CameraX+Speed*sin((HorizAngle+90)/180*PI)>=pX){CameraX += Speed*sin((HorizAngle+90)/180*PI);};
        if(CameraY+Speed*cos((HorizAngle+90)/180*PI)<zY)if(CameraY+Speed*cos((HorizAngle+90)/180*PI)>=pY){CameraY += Speed*cos((HorizAngle+90)/180*PI);};
      
    }

    if(Point.x==Width-1)
    {    Point.x = 1;
        SetCursorPos(Point.x,Point.y);
    }
    if(Point.y==Height-1)
    {    Point.y = 1;
        SetCursorPos(Point.x,Point.y);
    }
    if(Point.x==0)
    {    Point.x = Width-2;
        SetCursorPos(Point.x,Point.y);
    }
    if(Point.y==0)
    {    Point.y = Height-2;
        SetCursorPos(Point.x,Point.y);
    }

   xCurPosOld = Point.x;
    yCurPosOld = Point.y;
}

Следующая функция задает размеры нашего лабиринта и производит расчет центров каждой ячейки матрицы, для определения места установки той или иной 3d модели:
void center_cub_labirint()
{
    int i,j;
//расчет центров кубов
for(i=0;i<=11;i++)
for(j=0;j<=11;j++)
{X[i][j]=(i*k+(i+1)*k)/2;
 Y[i][j]=(j*k+(j+1)*k)/2;};

}

И, наконец, следующая функция объединяет всю графическую часть программы (все функции, связанные с созданием OpenGL сцены) воедино и осуществляет отрисовку нашего виртуального мира кадр за кадром. Одно выполнение этой функции рисует один кадр нашей игры. Учитывая что данная функция выполняется многократно (так как она вызывается в цикле "While" основной "материнской" функции WinMain()), картинка на экране изменяется, отражая наше перемещение, повороты и т.п. - эффект кинопленки:
GLvoid DrawGLScene(GLvoid)
{

   HandleControls();
    // Будем очищать экран, заполняя его цветом тумана. ( Изменено )
//glEnable(GL_FOG);
/*if (keys['B'] && !bp)  */ if (fg == 1)                  
       {glClearColor(0.5f,0.5f,0.5f,1.0f);
              bp=TRUE;                       
              glEnable(GL_FOG);
       }
       /*if (!keys['B'])*/ if (fg == 0)                   // ’B’ отжата?
             {
              bp=FALSE;// тогда bp возвращает ложь
            glDisable(GL_FOG);
                }

                     // Включает туман (GL_FOG)
glFogi(GL_FOG_MODE, fogMode[fogfilter]);// Выбираем тип тумана
glFogfv(GL_FOG_COLOR, fogColor);        // Устанавливаем цвет тумана
glFogf(GL_FOG_DENSITY, 0.15f);          // Насколько густым будет туман
glHint(GL_FOG_HINT, GL_NICEST);      // Вспомогательная установка тумана
glFogf(GL_FOG_START, 10.0f);             // Глубина, с которой начинается туман
glFogf(GL_FOG_END, 11.0f);               // Глубина, где туман заканчивается.

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glRotated(-VertAngle, 1.0, 0.0, 0.0);
    glRotated(HorizAngle, 0.0, 0.0, 1.0);
////////////////////////////////////////////////////////////////////////////////

if(vihod==0){
    if (CameraX<(-X[I][J]-k/2))I=I+1;
    if (CameraX>=(-X[I][J]+k/2))I=I-1;
    if (CameraY<(-Y[I][J]-k/2))J=J+1;
    if (CameraY>=(-Y[I][J]+k/2))J=J-1;

    if(labirint[I+1][J]==1)pX=-X[I][J]-k/2+0.5; else pX=-X[I+1][J];
    if(labirint[I-1][J]==1)zX=-X[I][J]+k/2-0.5; else zX=-X[I-1][J];
    if(labirint[I][J+1]==1)pY=-Y[I][J]-k/2+0.5; else pY=-Y[I][J+1];
    if(labirint[I][J-1]==1)zY=-Y[I][J]+k/2-0.5; else zY=-Y[I][J-1];

    if(labirint[I+1][J]==5)vihod=1;
    if(labirint[I-1][J]==5)vihod=1;
    if(labirint[I][J+1]==5)vihod=1;
    if(labirint[I][J-1]==5)vihod=1;
}
    

    glTranslatef(CameraX,CameraY,CameraZ);
     
////////////////////////////////////////////////////////////////////////////////

GLUquadricObj *quadObj;
 quadObj = gluNewQuadric(); // создаем новый объект
int i,j ;

GLfloat front_color[] = {1,1,1,1};

           glPushMatrix();
glMaterialfv(GL_FRONT , GL_SPECULAR, front_color);
 // земля
Top2(quadObj,texture[zem]);
//прорисовка лабиринта
glEnable(GL_TEXTURE_2D);
glTranslatef(-0.5*k,-0.5*k,0);
for(i=0;i<=11;i++){glTranslatef(k,0,0);
for(j=0;j<=11;j++){glTranslatef(0,k,0);
    if(labirint[i][j]==1)
    {
    glBindTexture(GL_TEXTURE_2D, texture[lab] );
    glCallList(list_num);
    }
    if(labirint[i][j]==2) //зеркальный шар
    {
    glEnable(GL_TEXTURE_GEN_S);    // Включим генерацию координат текстуры для S ( НОВОЕ )
    glEnable(GL_TEXTURE_GEN_T);    // Включим генерацию координат текстуры для T ( НОВОЕ )
    glBindTexture(GL_TEXTURE_2D, texture[1] );
    glCallList(list1_num);
    glDisable(GL_TEXTURE_GEN_S);        // Отключим генерацию текстурных координат ( НОВОЕ )
    glDisable(GL_TEXTURE_GEN_T);        // Отключим генерацию текстурных координат ( НОВОЕ )
    }
    if(labirint[i][j]==3)
    {
    glBindTexture(GL_TEXTURE_2D, texture[11] );
    glCallList(list2_num);
    }
    if(labirint[i][j]==5)
    {
    glBindTexture(GL_TEXTURE_2D, texture[23] );
    glCallList(list3_num);
    }
    if(labirint[i][j]==4) //ловушка
    {
    glEnable(GL_TEXTURE_GEN_S);    // Включим генерацию координат текстуры для S ( НОВОЕ )
    glEnable(GL_TEXTURE_GEN_T);    // Включим генерацию координат текстуры для T ( НОВОЕ )
    glBindTexture(GL_TEXTURE_2D, texture[lab] );
    glCallList(list_num);
    glDisable(GL_TEXTURE_GEN_S);        // Отключим генерацию текстурных координат ( НОВОЕ )
    glDisable(GL_TEXTURE_GEN_T);        // Отключим генерацию текстурных координат ( НОВОЕ )  
          
    /*  
    glEnable(GL_BLEND);       // Разрешение смешивания
    glDisable(GL_DEPTH_TEST);
    glBlendFunc(GL_DST_COLOR,GL_ZERO);
    glBindTexture(GL_TEXTURE_2D, texture[22] );
    glCallList(list_num);
    
    glBlendFunc(GL_ONE, GL_ONE);
    glBindTexture(GL_TEXTURE_2D, texture[24] );
    glCallList(list_num);
    glEnable(GL_DEPTH_TEST);    // Разрешение теста глубины
    glDisable(GL_BLEND);     // Запрещение смешивания */
    }
if(j==11)glTranslatef(0,-12*k,0);
}};

glFlush();
glDisable(GL_TEXTURE_2D);


//glEnable(GL_DEPTH_TEST);//обработать все непрозрачные поверхности


glPopMatrix();
glPushMatrix();


glEnable(GL_BLEND);        // Включаем смешивание
glEnable(GL_TEXTURE_2D);

glColor3f(0.5,1.0,1.0);
glPrint(10,10,"HRYX production",1);
glPrint(10,460,(char *)glGetString(GL_RENDERER),1);
if (vihod==1)glPrint(200,250,"-= EXIT =-",1);
if (vihod==1)glPrint(200,200,". Congratulation!!! press Esk...",1);

glTranslatef(k/2,(7*k+k/2),1);
glRotatef(l,0,0,1);
gluQuadricTexture(quadObj, GL_TRUE);
glBindTexture(GL_TEXTURE_2D,texture[10]);
glColor4f(1,1,1,0.5);  
gluSphere(quadObj, 1, 30, 30); // рисуем сферуw
glPushMatrix();
glRotatef(l*2,0,0,-1);
glColor4f(1,1,1,0.1);
gluSphere(quadObj, 1.5, 30, 30); // рисуем сферу
glPopMatrix();

glDisable(GL_BLEND);        // Выключаем смешивание
  

l=l+2;
gluDeleteQuadric(quadObj);
auxSwapBuffers();
glPopMatrix();


////////////////////////////////////////////////////////////////////////////////
}

Ну вот, собственно и вся структура программы. Развивайте её и модернизируйте. В этой статье я не ставил задачу описать конкретные команды языка С++ и OpenGL. Этому будут посвящены следующие статьи, которые позволят Вам изучить базовые приемы программирования Open GL и разобраться в коде данной игры.

Комментариев нет:

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.

Ностальгия по детству с ПМК или воспитание востребованных детей с Arduino и Raspberry-Pi.

Когда-то, когда мне было 12 лет меня заинтересовал программируемый микрокалькулятор БЗ-21, который был у отца. В журналах "Техника мо...