На хабре было много статей как создать такой движок, но их проблема была в том что они давали только отрывки кода, не объясняя полностью весь процесс, который бы мог бы полезен новичкам или полным нулям. Поэтому я решил импровизировать, но во многом мне помогла эта статья
Принцип работы
Итак, в чём же принцип работы такого рода движков? Да всё просто: от игрока бросается виртуальные лучи, которые ударясь об стену рисуя прямоугольники на экране. Каждому лучу соотвествует одна полоска проецированной стены на экране.
Но у моей первоначальной версии не хватало одного: поддержки углов, да и область видимости было только 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));

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

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

domix32
18.09.2025 18:27Псевдо-3D
А почему псевдо? Что рейкастинг, что реймаршинг работают аналогичным образом везде. Только где-то есть фишки а ля тени/отражения и свободное вращение камеры в любую сторону.
А зачем вы shapes каждый кадр копируете по несколько раз? Дорого гонять память. А вообще рекомендовал бы посмотреть как устроен движок например у javidx9, там есть хороший пример как писать подобный движок. Например, вот вам пример с псевдографикой.

Jijiki
18.09.2025 18:27там платформо зависимый рейкастинг(он реализовал на сколько помню консольный по ресурсам минимальный основанную на консоли винды свою библиотеку), достаточно пускать по гайду вольфа(тоесть по ширине - стены, пол до горизонта самое простое, можно прикрутить кватернионы, убрать искажения от кватерниона ну и прочее), и рисовать линии, это просто чтобы разобраться а далее на сколько хватит запала, ну тоесть можно прикрутить редактор по квадратикам в карту ну и прочее, если это действительно интересно
я бы посоветовал другой "гайд RSt2ycuV8JA" на гитхабе есть пример к чему можно стремиться, но по итогу даже не знаю, по итогу захочется бсп или воксели
на реддите видел тоже пример, там математическая демка, человек всё реализовал внутри лучи света(прозрачное смешивание, свечение частичек), замок почти 3д, я даже не представляю как такое возможно )
stungnthumz
Были написаны не Вами.
pda0
Фатальный недостаток! :-D