Переписываем примеры простых игр из туторалов на OpenGL и WebGL

Переписываем примеры простых игр из туторалов на шейдерный OpenGL

На сайте NoobTuts есть короткая статья Make small Games. В ней говориться, что нужно тренироваться на простых играх. Скопировал на форум, на всякий случай:

Make small Games

В этой теме мы берём туториалы по устаревшему OpenGL версии 1, либо по другим API или движкам и переписываем на актуальный шейдерный OpenGL. Я буду переписывать на TypeScript и WebGL 1.0, потому что это кросс-платформа и запускается в один клик. Возможно буду ещё переписывать на C#/OpenTK, но не в этом году, и даже, наверное, не в следующем. У меня в приоритете TypeScript, WebGL, браузерные интерактивные 3D веб приложения с анимациями, графики на WebGL, небольшие браузерные игры с мультиплеером. TypeScript близок к C#. Есть interface, дженерики. Автором языка TypeScript является Андерс Хейлсберг. Он так же является автором таких языков как: Turbo Pascal, Delphi и C#. Если вам нравится данный челлендж и вы чувствуете, что он будет для вас полезен, то выкладывайте переписанные примеры на своём любимом языке программирования. Главное, чтобы использовался шейдерный OpenGL.

Для удобства навигации, в данном первом сообщении будет содержание со ссылками на соответствующие сообщения. Может вестись переписка и при этом будет сохранён быстрый доступ к нужному коду.

Содержание:

Змейка на C++, SDL2 и OpenGL3.3 из видео туториала от FamTrinli: Создание игр на C++: Змейка

Этот пример я переписал ровно два года назад с устаревшего OpenGL на актуальный шейдерный. Проект создавался в VS2015. Нём все библиотеки подключены. По идее вы можете открыть его в новой VS, например, в VS 2019 и он должен запуститься: Snake_FamTrinliToOpenGL33.zip

main.cpp

#include <glad/glad.h>
#include <SDL.h>
 
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/transform.hpp>
 
#include <vector>
#include <string>
#include <iostream>
 
enum class GameState { PLAY, EXIT };
 
class Color
{
public:
    GLfloat r, g, b;
    Color(GLfloat r, GLfloat g, GLfloat b) :
        r(r), g(g), b(b)
    {
    }
};
 
class Point
{
public:
    GLfloat x, y;
    Point(GLfloat x, GLfloat y) :
        x(x), y(y)
    {
    }
};
 
void initVertexBuffers();
GLuint createShaderProgram();
 
void drawSquare(const glm::vec3 &pos, const Color &color, GLfloat size = 1.0f, GLfloat angle = 0.0f);
 
GameState g_gameState = GameState::PLAY;
SDL_Window *g_window;
GLuint g_vao;
GLuint g_program;
 
float g_frameTime;
float g_maxFPS = 3.0f;
float g_fps;
 
const int N = 30;
const int M = 20;
 
const int SCALE = 25;
 
const int W = SCALE * N;
const int H = SCALE * M;
 
int dir, num = 4;
 
struct
{
    int x, y;
} s[100];
 
class Fruits
{
public:
    int x, y;
 
    void createNew()
    {
        x = rand() % N;
        y = rand() % M;
    }
 
    void drawFruit()
    {
        drawSquare(glm::vec3(x * SCALE, y * SCALE, 0.0f), Color(0.0f, 1.0f, 0.0f), SCALE);
    }
} m[10];
 
const char *vertexShaderSource =
"#version 130 core\n"
 
"in vec2 a_position;"
 
"uniform mat4 u_mvp;"
 
"void main()"
"{"
"    gl_Position= u_mvp * vec4(a_position, 0.0, 1.0);"
"}";
 
const char *fragmentShaderSource =
"#version 130 core\n"
"precision mediump float;"
 
"in vec3 v_Color;"
"out vec4 fragColor;"
 
"uniform vec3 u_color;"
 
"void main()"
"{"
"    fragColor = vec4(u_color, 1.0);"
"}";
 
void fatalError(std::string errorString)
{
    std::cout << errorString << std::endl;
    std::cout << "Enter any key to quit..." << std::endl;
    int tmp;
    std::cin >> tmp;
    SDL_Quit();
    exit(1);
}
 
void processInput()
{
    SDL_Event event;
 
    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {
            case SDL_QUIT:
                g_gameState = GameState::EXIT;
                break;
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym)
                {
                    case SDLK_w:
                        dir = 0;
                        break;
                    case SDLK_a:
                        dir = 1;
                        break;
                    case SDLK_s:
                        dir = 2;
                        break;
                    case SDLK_d:
                        dir = 3;
                        break;
                }
                break;
        }
    }
}
 
void setGeneralInfo(const glm::vec3 &pos, const Color &color, const glm::vec3 &scale, GLfloat angle)
{
    // Rotate
    GLfloat radian = glm::radians(angle);
    glm::mat4 xformRotateMatrix = glm::rotate(radian, glm::vec3(0.0f, 0.0f, 1.0f));
 
    // Translate
    glm::mat4 xformTranslateMatrix = glm::translate(glm::mat4(), pos);
 
    // Scale
    glm::mat4 xformScaleMatrix = glm::scale(glm::vec3((scale.x, scale.y, scale.z)));
 
    // Set Matrixes
    glm::mat4 projMatrix = glm::ortho(0.0f, (float)W, 0.0f, (float)H, 100.0f, -100.0f);
    glm::mat4 viewMatrix = glm::lookAt(
        glm::vec3(0.0f, 0.0f, 3.0f),    // Camera position
        glm::vec3(0.0f, 0.0f, 0.0f),    // Looks at the origin
        glm::vec3(0.0f, 1.0f, 0.0f));   // Head is up
    glm::mat4 modelMatrix = xformTranslateMatrix * xformRotateMatrix * xformScaleMatrix;
    //glm::mat4 modelMatrix = glm::scale(xformTranslateMatrix, glm::vec3(50.0, 50.0f, 50.0f));
 
    // Set MVP
    glm::mat4 mvp = projMatrix * viewMatrix * modelMatrix;
    GLint u_mvp = glGetUniformLocation(g_program, "u_mvp");
    glUniformMatrix4fv(u_mvp, 1, false, &mvp[0][0]);
 
    // Set Color
    GLint u_color = glGetUniformLocation(g_program, "u_color");
    glUniform3f(u_color, color.r, color.g, color.b);
}
 
void drawSquare(const glm::vec3 &pos, const Color &color, GLfloat size /*= 1.0f*/, GLfloat angle /*= 0.0f*/)
{
    setGeneralInfo(pos, color, glm::vec3(size, size, size), angle);
 
    // Draw
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
 
void drawLine(Point p1, Point p2, const Color &color)
{
    Point center(p1.x + (p2.x - p1.x) / 2.0f, p1.y + (p2.y - p1.y) / 2.0f);
 
    GLfloat a = p2.y - p1.y;
    GLfloat b = p2.x - p1.x;
    GLfloat tan = a / b;
    GLfloat angle = glm::degrees(glm::atan(tan));
    GLfloat length = glm::length(glm::vec2(b, a));
    
    setGeneralInfo(
        glm::vec3(center.x, center.y, 0.0f),
        color,
        glm::vec3(1.0f, 1.0f, length),
        angle);
    glDrawArrays(GL_LINES, 4, 2);
}
 
void drawField()
{
    Color color(0.0f, 1.0f, 0.0f);
 
    for (size_t i = 0; i < W; i += SCALE)
    {
        drawLine(Point(i, 0.0f), Point(i, H), color);
    }
 
    for (size_t j = 0; j < H; j += SCALE)
    {
        drawLine(Point(0.0f, j), Point(W, j), color);
    }
}
 
void drawSnake()
{
    for (size_t i = 0; i < num; i++)
    {
        drawSquare(glm::vec3(s[i].x * SCALE, s[i].y * SCALE, 0.0f), Color(0.0f, 0.0f, 1.0f), SCALE);
    }
}
 
void display()
{
    // Clear a screen
    glClear(GL_COLOR_BUFFER_BIT);
 
    // Set a viewport on whole screen
    glViewport(0, 0, W, H);
 
    for (size_t i = 0; i < 10; i++)
    {
        m[i].drawFruit();
    }
 
    drawField();
 
    drawSnake();
 
    SDL_GL_SwapWindow(g_window);
}
 
void calculateFPS()
{
    static const int NUM_SAMPLES = 10;
    static float frameTimes[NUM_SAMPLES];
    static int currentFrame = 0;
 
    static float prevTicks = SDL_GetTicks();
    float currentTicks;
    currentTicks = SDL_GetTicks();
 
    g_frameTime = currentTicks - prevTicks;
    frameTimes[currentFrame % NUM_SAMPLES] = g_frameTime;
 
    prevTicks = currentTicks;
 
    int count;
    currentFrame++;
 
    if (currentFrame < NUM_SAMPLES)
    {
        count = currentFrame;
    }
    else
    {
        count = NUM_SAMPLES;
    }
 
    float frameTimeAverage = 0;
    for (int i = 0; i < count; i++)
    {
        frameTimeAverage += frameTimes[i];
    }
    frameTimeAverage /= count;
 
    if (frameTimeAverage > 0)
    {
        g_fps = 1000.0f / frameTimeAverage;
    }
    else
    {
        g_fps = 60.0f;
    }
}
 
void tick()
{
    for (size_t i = num; i > 0; --i)
    {
        s[i].x = s[i - 1].x;
        s[i].y = s[i - 1].y;
    }
 
    if (dir == 0)
    {
        s[0].y += 1;
    }
    if (dir == 1)
    {
        s[0].x -= 1;
    }
    if (dir == 2)
    {
        s[0].y -= 1;
    }
    if (dir == 3)
    {
        s[0].x += 1;
    }
 
    for (size_t i = 0; i < 10; i++)
    {
        if ((s[0].x == m[i].x) && (s[0].y == m[i].y))
        {
            num++;
            m[i].createNew();
        }
    }
 
    if (s[0].y > M)
    {
        dir = 2;
    }
 
    if (s[0].x < 0)
    {
        dir = 3;
    }
 
    if (s[0].y < 0)
    {
        dir = 0;
    }
 
    if (s[0].x > N)
    {
        dir = 1;
    }
 
    for (size_t i = 1; i < num; i++)
    {
        if (s[0].x == s[i].x && s[0].y == s[i].y)
        {
            num = i;
        }
    }
}
 
void gameLoop()
{
    while (g_gameState != GameState::EXIT)
    {
        processInput();
        float startTicks = SDL_GetTicks();
 
        tick();
        
        display();
 
        calculateFPS();
        // Print only once every 10 frames
        static int frameCounter = 0;
        frameCounter++;
        if (frameCounter == 10)
        {
            std::cout << g_fps << std::endl;
            frameCounter = 0;
        }
 
        float frameTicks = SDL_GetTicks() - startTicks;
        // Limit the FPS to the max FPS
        if (1000.0f / g_maxFPS > frameTicks)
        {
            SDL_Delay(1000.0f / g_maxFPS - frameTicks);
        }
    }
 
    SDL_DestroyWindow(g_window);
 
    SDL_Quit();
}
 
int main(int argc, char **argv)
{
    SDL_Init(SDL_INIT_EVERYTHING);
 
    g_window = SDL_CreateWindow(
        "Snake FamTrinli To OpenGL 3.0",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        W, H,
        SDL_WINDOW_OPENGL);
 
    if (g_window == nullptr)
    {
        fatalError("SDL Window could not be created");
    }
 
    SDL_GLContext glContext = SDL_GL_CreateContext(g_window);
    if (glContext == nullptr)
    {
        fatalError("SDL_GL context could not be created");
    }
 
    if (!gladLoadGL())
    {
        fatalError("Could not initialize glad!");
    }
 
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
 
    g_program = createShaderProgram();
    initVertexBuffers();
 
    for (size_t i = 0; i < 10; i++)
    {
        m[i].createNew();
    }
 
    s[0].x = 10;
    s[0].y = 0;
    for (size_t i = 1; i < num; i++)
    {
        s[i].x = s[0].x;
        s[i].y = s[i - 1].y - 1;
    }
 
    gameLoop();
    return 0;
}
 
GLuint createShader(const char *shaderSource, int shaderType)
{
    GLuint shader = glCreateShader(shaderType);
    glShaderSource(shader, 1, &shaderSource, NULL);
    glCompileShader(shader);
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint maxLength = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
        std::vector<GLchar> errorLog(maxLength);
        glGetShaderInfoLog(shader, maxLength, &maxLength, &errorLog[0]);
        glDeleteShader(shader); // Don't leak the shader.
        std::printf("%s\n", &(errorLog[0]));
        fatalError("Shader failed to compile");
    }
    return shader;
}
 
GLuint createShaderProgram()
{
    GLuint program = glCreateProgram();
    GLuint vShader = createShader(vertexShaderSource, GL_VERTEX_SHADER);
    GLuint fShader = createShader(fragmentShaderSource, GL_FRAGMENT_SHADER);
 
    glAttachShader(program, vShader);
    glAttachShader(program, fShader);
    glLinkProgram(program);
    glUseProgram(program);
 
    return program;
}
 
void initVertexBuffers()
{
    GLfloat vertices[] = {
        0.0f, 1.0f,    // Square
        0.0f, 0.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
        -0.5f, 0.0f,    // Line
        0.5f, 0.0f
    };
 
    GLuint vbo;
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    int numVertices = sizeof(vertices) / sizeof(vertices[0]);
    glBufferData(GL_ARRAY_BUFFER, numVertices * sizeof(GLfloat), vertices, GL_STATIC_DRAW);
 
    glGenVertexArrays(1, &g_vao);
    glBindVertexArray(g_vao);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
 
    GLint a_position = glGetAttribLocation(g_program, "a_position");
    glVertexAttribPointer(a_position, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(a_position);
}
  • Окно создаётся с помощью библиотеки SDL2
  • Для создания матриц (трансформаций, матрицы перспективы и вида) используется математическая библиотека GLM
  • Для загрузки указателей на функции OpenGL используется библиотека GLAD

Змейка на WebGL 1.0 и TypeScript из туториала NoobTuts: Python Snake Game

Игровая демка и исходники в песочнице: запустить в один клик на любой ОС

Исходники на GitHub

Этот код я переписал ровно год назад. Получается, что раз в год у меня случается обострение. Код близок к оригиналу. Оригинал на устаревшем OpenGL версии 1. Я поменял ортогональный вид на вид с перспективой.

Описание:

  • Музыка и звуки с помощью HTMLAudioElement
  • Графика с помощью WebGL 1.0 и GLSL
  • Трансформации с помощью математической библиотеки glMatrix
  • Язык программирования TypeScript
  • Настроена компиляция из TS в ES5
  • Настроена установка точек останова и пошаговое выполнение в редакторе кода VSCode с использованием Source Maps
  • Настроена загрузка AMD-модулей в браузер с помощью библиотеки RequireJS

Запуск игры локально из исходников, при условии, что установлен NodeJS

  • Скачать архив по ссылкам выше
  • Перейти в папку с игрой из консоли
  • Набрать в консоле команду: npm install
  • Набрать в консоле команду: tsc
  • Запустить index.html по localhost с сервера

Ты не там темы размещаешь. Я для этого раздел выделил: Статьи на [около]программистскую тематику
Переместить туда?

Я подумал, что это на статьи мало похоже, это ближе к обсуждению. Да, можно переместить.

Змейка на WebGL 1.0 и TypeScript из видео туториала от FamTrinli: Создание игр на C++: Змейка

Переписал змейку от FamTrinli на WebGL 1.0 и TypeScript из туториала: Создание игр на C++: Змейка Постарался, как можно ближе к оригиналу - весь код в одном файле. Этот код особенно близок к версии: Змейка на C++, SDL2 и OpenGL3.3

Управление: WASD или стрелки.

Запустить в браузере: https://8observer8.github.io/webgl10-ts/fam-trinlis-snake/
Запустить в песочнице: https://next.plnkr.co/edit/ZrelePNPjqlyj5pp?preview

Скачать исходники: https://github.com/8Observer8/fam-trilnis-snake-in-webgl10-typescript

fam-trinlis-snake-in-webgl10-typescript

Понг на TypeScript и WebGL 1.0 из текстового туториала от NoobTuts: C++ 2D Pong Game

Запустить в браузере Release версию: http://ivan8observer8.herokuapp.com/webgl10-ts/pong2d-from-noobtuts/ Я использую бесплатный хостинг для Node.js/Express, поэтому иногда нужно подождать 10 секунд, чтобы разбудить сервер.

Скачать исходники: https://github.com/8Observer8/pong2d-from-noobtuts_webgl10-typescript
На GitHub есть инструкция, как собрать Release и Debug версии.

pong2d-from-noobtuts_webgl10-typescript

Портировал игру Bitballoon летом 2017 года в 3D на WebGL 1.0 и язык TypeScript. Bitballon - это оказывается название хостинга, а я тогда подумал, что это название игры. 2D вариант это игры написал один иностранный студент, вот ег тема My first HTML5 game. Я тогда переписал игру на WebGL 1.0, GLSL и TypeScript. Нужно было потренироваться. Для математики я использую библиотеку glMatrix, которая реализует линейную алгебру. Этот иностранный студент добавил ссылку на мою 3D версию его 2D игры на свое странице.

Игровые модели я перевёл из 2D спрайтов игры в 3D модели в Blender для практики. Очень интересно переводить 2D игры в 3D. На шейдерном OpenGL очень легко перейти из 2D в 3D - нужно просто заменить ортогональную матрицу на матрицу перспективы при умножении матриц на вектор в вертекстном шейдере. Рекомендую заниматься созданием простых 3D моделей в бесплатном легковесном 2D/3D редакторе Blender, чтобы лучше ориентироваться в компьютерной графике и понимать как загружать модели из форматов .obj и .dae (Collada) из Blender. Я предпочитаю .dae, потому что кроме статическим моделей из Blender можно экспортировать в этот файл матрицы трансформаций для вертексов, а последовательное умножение таких матриц позволяет сложные каскадные анимации, то есть так называемые скелетные анимации. Математику скелетных анимаций я активно изучаю по книге 3D Graphics for Game Programming. Книга подходит не только для игр, а для любых сложных анимаций инженерных механизмов. Ещё изучаю математику скелетных анимаций по примеру на GithHub по видео серией уроков: OpenGL Skeletal Animation Tutorial #1

BitballoonModels1

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

  • программирование в целом
  • выбранный язык программирования
  • шейдерный OpenGL
  • математика компьютерной графики

Данную игрушку я написал в январе 2017 года (вы можете посмотреть дату на GitHub), когда ещё данная тема не была создана. Это порт с C++ и устаревшей нешейдерной версии OpenGL на шейдерный OpenGL и язык TypeScript. У автора оригинала очень интересная и познавательная архитектура проекта. Рекомендую разбирать эту архитектуру классов и переписывать на тот язык, который вы выбрали.

Порт змейки Антона Te с С++ (OpenGL 1.5, FreeGLUT) на TypeScript (WebGL 1.0, glMatrix - библиотека для линейной алгберы)

Оригинальное видео от Антона Те: Writing Snake game in 10 minutes

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