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

Принцип работы

Итак, в чём же принцип работы такого рода движков? Да всё просто: от игрока бросается виртуальные лучи, которые ударясь об стену рисуя прямоугольники на экране. Каждому лучу соотвествует одна полоска проецированной стены на экране.

Но у моей первоначальной версии не хватало одного: поддержки углов, да и область видимости было только 3 квадрата.

void rendering(RectangleShape rects[], int columns[], int tex_nums[]){
         if (!texture.loadFromFile("brick_wall.jpg"))
    {
       // error...
       std::cout<<"brick_wall.jpg not found!"<<std::endl;
    }
     if (!tex_soldier.loadFromFile("soldier.png"))
    {
       // error...
       std::cout<<"soldier.png not found!"<<std::endl;
    }
        for(int i = 0; i < 3; i++){
        //rects[i].setFillColor(sf::Color::Green);
        float h = 480.f;
        float c {h / columns[i]+1};        // c = 5.2
        if(tex_nums[i]==1){
                rects[i].setTexture(&tex_soldier);
        }
        else{
                rects[i].setTexture(&texture);
        }
        rects[i].setSize(sf::Vector2f(210,c));
        rects[i].setPosition(sf::Vector2f(i*210, 240-c/2));
    }
}
void raycasting(int x, int y, Enemy enemy) {
        
    static int tmp[3] = {0, 0, 0};
    static int tex_nums[3] = {0,0,0};
    // Обнуление временного массива перед новым вычислением
    std::fill(tmp, tmp + 3, 0);
    std::fill(tex_nums, tex_nums + 3, 0);

    for (int i = y; i >= 0; i--) {
        // Проверяем на границы
        if (x - 1 >= 0 && map[i][x - 1] == 0) tmp[0]++;
        
        
        if (map[i][x] == 0) tmp[1]++;
        if (x + 1 < 5 && map[i][x + 1] == 0) tmp[2]++;
       
         
        if (map[i][x] == 2 && enemy.getHP()>0){ tex_nums[1] = 1; is_visible = true;}
              
    }
    if(tmp[0] == 0) tmp[0] = 1;
    if(tmp[2] == 0) tmp[2] = 1;
    std::cout << x << " " << y << std::endl;
    std::cout << tmp[0] << " " << tmp[1] << " " << tmp[2] << std::endl;
    rendering(rectangles, tmp, tex_nums);
}
Мой первый и примитивный псевдотрёхмерный движок
Мой первый и примитивный псевдотрёхмерный движок

Поэтому я решил переписать, с JS на свой любимый C++, код о котором говорится в начале этой статьи.

#include <SFML/Graphics.hpp>
#include <cmath>
#include <vector>

struct Player {
    float x = 1;
    float y = 1;
    float angle = 1;
    float fov = M_PI / 4;
} player;

std::vector<std::vector<int>> map = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 0, 1, 0, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 1, 0, 1, 0, 0, 1},
    {1, 0, 1, 0, 0, 1, 0, 0, 1},
    {1, 0, 0, 1, 0, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1}
};

int getMapCell(int y, int x) {
    if (y < 0 || y >= (int)map.size() || x < 0 || x >= (int)map[0].size()) {
        return 1;
    }
    return map[y][x];
}

float castRay(float rayAngle) {
    float x = player.x;
    float y = player.y;
    float dx = cos(rayAngle);
    float dy = sin(rayAngle);

    int i = 0;
    while (getMapCell(static_cast<int>(y), static_cast<int>(x)) == 0) {
        x += dx * 0.1;
        y += dy * 0.1;
        i++;
        if (i > 400) break;
    }

    const float distance = sqrt(pow((x - player.x), 2) + pow((y - player.y), 2));
    const float wallHeight = 300 / distance;

    return wallHeight;
}

std::vector<sf::RectangleShape> wallSlices;

void drawWallSlice(int i, float wallHeight, float sliceWidth) {
    // Создаем новый прямоугольник для каждого среза стены
    sf::RectangleShape slice(sf::Vector2f(sliceWidth, wallHeight));
    slice.setFillColor(sf::Color(180, 0, 180));
    
    // Позиционируем срез по центру экрана по вертикали
    float yPosition = 300 - wallHeight / 2;
    slice.setPosition(i * sliceWidth, yPosition);
    
    wallSlices.push_back(slice);
}

void raycast() {
    // Очищаем предыдущие срезы
    wallSlices.clear();
    
    const int rays = 200;
    const int screenWidth = 800;
    const float sliceWidth = screenWidth / rays;
    const float angleStep = player.fov / rays;

    for (int i = 0; i < rays; i++) {
        const float rayAngle = player.angle - (player.fov / 2) + i * angleStep;
        float wallHeight = castRay(rayAngle);
        drawWallSlice(i, wallHeight, sliceWidth);
    }
}

int main() {
    sf::RenderWindow window(sf::VideoMode(800, 600), "Raycaster");
    
    // Настройка игрока для лучшей видимости
    player.x = 1.5;
    player.y = 1.5;
    player.angle = 0;

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed)
                window.close();
            
            // Добавляем управление для тестирования
            if (event.type == sf::Event::KeyPressed) {
                if (event.key.code == sf::Keyboard::Left) {
                    player.angle -= 0.1;
                }
                if (event.key.code == sf::Keyboard::Right) {
                    player.angle += 0.1;
                }
            }
        }

        // Выполняем рейкастинг
        raycast();
        
        // Очищаем экран
        window.clear(sf::Color::Black);
        
        // Отрисовываем все срезы стен
        for (const auto& slice : wallSlices) {
            window.draw(slice);
        }
        
        window.display();
    }

    return 0;
}

P.S: Чтобы использовать текстуры стены используйте этот код:

    slice.setTexture(&wallTexture);
    int texX = static_cast<int>((i * sliceWidth)) % wallTexture.getSize().x;
    slice.setTextureRect(sf::IntRect(texX, 0, 1, wallTexture.getSize().y));

вместо этого:

  slice.setFillColor(sf::Color(180, 0, 180));
Скриншот моей игры типа Hovertank 3D, где роли врагов будут выступать санитары, а в роли спасаемых тянки
Скриншот моей игры типа Hovertank 3D, где роли врагов будут выступать санитары, а в роли спасаемых тянки

Заодно, за это время, у меня появилась вот такая оригинальная идея для игры,которая использует мемы и аниме в виде спарйтов.

Исходиники первой версии (Я уже не сижу на гитхабе, поэтому не удивлятесь лишней папке magic)

Комментарии (0)