Files
3DMAZE_C/3dmaze.cpp
T
2026-06-19 21:51:51 +03:00

441 lines
19 KiB
C++

#define _USE_MATH_DEFINES
#define NOMINMAX
#include <windows.h>
// Решение проблемы с конфликтом min/max в GDI+
#include <algorithm>
using std::min;
using std::max;
#include <gdiplus.h>
#include <cmath>
#include <vector>
#include <map>
#include <random>
#include <chrono>
// Линковка библиотеки GDI+
#pragma comment (lib, "Gdiplus.lib")
// Константы
const int WIDTH = 800;
const int HEIGHT = 600;
const int HALF_HEIGHT = HEIGHT / 2;
const int TILE_SIZE = 64;
const float fov = static_cast<float>(M_PI) / 3.0f;
const int NUM_RAYS = 160;
const int SCALE = WIDTH / NUM_RAYS;
const int MAP_SIZE = 32;
struct Color {
unsigned char b, g, r, a;
Color() : r(0), g(0), b(0), a(255) {}
Color(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b), a(255) {}
};
std::vector<Color> frame_buffer(WIDTH* HEIGHT);
float player_x = 2.0f * TILE_SIZE + 32.0f;
float player_y = 2.0f * TILE_SIZE + 32.0f;
int cell_x = 2;
int cell_y = 2;
float player_angle = 0.0f;
const float player_speed = 100.0f;
const float rotation_speed = 2.0f;
bool demo_mode = true;
bool day_mode = false;
int current_dir_index = 0;
const std::vector<float> DIRECTIONS = { 0.0f, static_cast<float>(M_PI) / 2.0f, static_cast<float>(M_PI), -static_cast<float>(M_PI) / 2.0f };
enum class AIState { MOVE, TURN };
AIState ai_state = AIState::MOVE;
float target_angle = 0.0f;
std::map<std::pair<int, int>, int> world_map;
std::mt19937 rng(std::random_device{}());
float random_float() {
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
return dist(rng);
}
void generate_maze(int size) {
for (int row = 0; row < size; ++row) {
for (int col = 0; col < size; ++col) {
if (col == 0 || col == size - 1 || row == 0 || row == size - 1) {
world_map[{col, row}] = 1;
}
else if (col >= 1 && col <= 3 && row >= 1 && row <= 3) {
world_map[{col, row}] = 0;
}
else {
world_map[{col, row}] = (random_float() < 0.30f) ? 1 : 0;
}
}
}
}
int get_wall_type(int col, int row) {
auto it = world_map.find({ col, row });
if (it != world_map.end()) return it->second;
return 1;
}
std::vector<Color> tex_wall(TILE_SIZE* TILE_SIZE);
std::vector<Color> tex_ceil(TILE_SIZE* TILE_SIZE);
std::vector<Color> tex_floor(TILE_SIZE* TILE_SIZE);
bool load_png_texture(const wchar_t* filename, std::vector<Color>& buffer) {
Gdiplus::Bitmap bitmap(filename);
if (bitmap.GetLastStatus() != Gdiplus::Ok) {
return false;
}
Gdiplus::BitmapData bitmapData;
Gdiplus::Rect rect(0, 0, bitmap.GetWidth(), bitmap.GetHeight());
if (bitmap.LockBits(&rect, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, &bitmapData) == Gdiplus::Ok) {
UINT* pixels = (UINT*)bitmapData.Scan0;
int stride = bitmapData.Stride / 4;
for (int y = 0; y < TILE_SIZE; ++y) {
for (int x = 0; x < TILE_SIZE; ++x) {
int src_x = (x * bitmap.GetWidth()) / TILE_SIZE;
int src_y = (y * bitmap.GetHeight()) / TILE_SIZE;
UINT p = pixels[src_y * stride + src_x];
Color c;
c.a = (p >> 24) & 0xFF;
c.r = (p >> 16) & 0xFF;
c.g = (p >> 8) & 0xFF;
c.b = p & 0xFF;
buffer[y * TILE_SIZE + x] = c;
}
}
bitmap.UnlockBits(&bitmapData);
return true;
}
return false;
}
void create_textures() {
if (!load_png_texture(L"wall.png", tex_wall)) {
std::fill(tex_wall.begin(), tex_wall.end(), Color(125, 45, 30));
for (int y : {0, 16, 32, 48, 63})
for (int x = 0; x < 64; ++x) tex_wall[y * TILE_SIZE + x] = Color(110, 105, 100);
for (int x : {0, 32, 64}) {
if (x < 64) {
for (int y = 0; y < 16; ++y) tex_wall[y * TILE_SIZE + x] = Color(110, 105, 100);
for (int y = 32; y < 48; ++y) tex_wall[y * TILE_SIZE + x] = Color(110, 105, 100);
}
}
for (int x : {16, 48}) {
for (int y = 16; y < 32; ++y) tex_wall[y * TILE_SIZE + x] = Color(110, 105, 100);
for (int y = 48; y < 64; ++y) tex_wall[y * TILE_SIZE + x] = Color(110, 105, 100);
}
}
if (!load_png_texture(L"top.png", tex_ceil)) {
std::fill(tex_ceil.begin(), tex_ceil.end(), Color(75, 75, 75));
}
if (!load_png_texture(L"floor.png", tex_floor)) {
std::fill(tex_floor.begin(), tex_floor.end(), Color(45, 45, 50));
for (int i = 0; i < TILE_SIZE; i += 16) {
for (int y = 0; y < TILE_SIZE; ++y) tex_floor[y * TILE_SIZE + i] = Color(25, 25, 28);
for (int x = 0; x < TILE_SIZE; ++x) tex_floor[i * TILE_SIZE + x] = Color(25, 25, 28);
}
}
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_DESTROY: PostQuitMessage(0); return 0;
case WM_KEYDOWN:
if (wParam == 'Y') { demo_mode = !demo_mode; ai_state = AIState::MOVE; }
if (wParam == 'L') { day_mode = !day_mode; }
if (wParam == VK_ESCAPE) PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
const wchar_t CLASS_NAME[] = L"MazeWindowClass";
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClass(&wc);
RECT rect = { 0, 0, WIDTH, HEIGHT };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX, FALSE);
HWND hwnd = CreateWindowEx(0, CLASS_NAME, L"3DMAZE",
WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, hInstance, NULL);
if (hwnd == NULL) {
Gdiplus::GdiplusShutdown(gdiplusToken);
return 0;
}
ShowWindow(hwnd, nCmdShow);
generate_maze(MAP_SIZE);
create_textures();
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = WIDTH;
bmi.bmiHeader.biHeight = -HEIGHT;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
auto last_time = std::chrono::high_resolution_clock::now();
bool running = true;
while (running) {
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) running = false;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
auto current_time = std::chrono::high_resolution_clock::now();
float dt = std::chrono::duration<float>(current_time - last_time).count();
last_time = current_time;
if (demo_mode) {
if (ai_state == AIState::MOVE) {
float target_x = static_cast<float>(cell_x * TILE_SIZE + 32);
float target_y = static_cast<float>(cell_y * TILE_SIZE + 32);
float move_cos = std::round(std::cos(DIRECTIONS[current_dir_index]));
float move_sin = std::round(std::sin(DIRECTIONS[current_dir_index]));
float dist = std::hypot(target_x - player_x, target_y - player_y);
if (dist > 1.0f) {
float step = player_speed * dt;
if (step > dist) step = dist;
player_x += move_cos * step;
player_y += move_sin * step;
}
else {
player_x = target_x; player_y = target_y;
std::vector<int> available_dirs;
for (size_t i = 0; i < DIRECTIONS.size(); ++i) {
int t_cos = static_cast<int>(std::round(std::cos(DIRECTIONS[i])));
int t_sin = static_cast<int>(std::round(std::sin(DIRECTIONS[i])));
if (get_wall_type(cell_x + t_cos, cell_y + t_sin) == 0) available_dirs.push_back(static_cast<int>(i));
}
int backward_dir_index = (current_dir_index + 2) % 4;
std::vector<int> forward_and_sides;
for (int d : available_dirs) if (d != backward_dir_index) forward_and_sides.push_back(d);
int next_dir = current_dir_index;
if (!forward_and_sides.empty()) {
if (std::find(forward_and_sides.begin(), forward_and_sides.end(), current_dir_index) != forward_and_sides.end() && random_float() < 0.75f) {
next_dir = current_dir_index;
}
else {
std::uniform_int_distribution<int> dist_dir(0, static_cast<int>(forward_and_sides.size() - 1));
next_dir = forward_and_sides[dist_dir(rng)];
}
}
else if (std::find(available_dirs.begin(), available_dirs.end(), backward_dir_index) != available_dirs.end()) {
next_dir = backward_dir_index;
}
if (next_dir != current_dir_index) {
current_dir_index = next_dir; target_angle = DIRECTIONS[current_dir_index]; ai_state = AIState::TURN;
}
else {
cell_x += static_cast<int>(move_cos); cell_y += static_cast<int>(move_sin);
}
}
}
else if (ai_state == AIState::TURN) {
float angle_diff = std::fmod(target_angle - player_angle + static_cast<float>(M_PI), 2.0f * static_cast<float>(M_PI));
if (angle_diff < 0) angle_diff += 2.0f * static_cast<float>(M_PI);
angle_diff -= static_cast<float>(M_PI);
if (std::abs(angle_diff) > 0.01f) {
float step = rotation_speed * dt;
if (step > std::abs(angle_diff)) step = std::abs(angle_diff);
player_angle += (angle_diff > 0 ? 1.0f : -1.0f) * step;
}
else {
player_angle = target_angle;
float move_cos = std::round(std::cos(DIRECTIONS[current_dir_index]));
float move_sin = std::round(std::sin(DIRECTIONS[current_dir_index]));
cell_x += static_cast<int>(move_cos); cell_y += static_cast<int>(move_sin); ai_state = AIState::MOVE;
}
}
}
else {
if (GetAsyncKeyState('A') || GetAsyncKeyState(VK_LEFT)) player_angle -= 3.0f * dt;
if (GetAsyncKeyState('D') || GetAsyncKeyState(VK_RIGHT)) player_angle += 3.0f * dt;
float cos_a = std::cos(player_angle), sin_a = std::sin(player_angle);
float mx = 0, my = 0;
if (GetAsyncKeyState('W') || GetAsyncKeyState(VK_UP)) { mx = player_speed * cos_a * dt; my = player_speed * sin_a * dt; }
else if (GetAsyncKeyState('S') || GetAsyncKeyState(VK_DOWN)) { mx = -player_speed * cos_a * dt; my = -player_speed * sin_a * dt; }
float r = 12.0f;
float sign_x = (mx > 0) ? r : (mx < 0 ? -r : 0);
float sign_y = (my > 0) ? r : (my < 0 ? -r : 0);
if (get_wall_type(static_cast<int>((player_x + mx + sign_x) / TILE_SIZE), static_cast<int>(player_y / TILE_SIZE)) == 0) player_x += mx;
if (get_wall_type(static_cast<int>(player_x / TILE_SIZE), static_cast<int>((player_y + my + sign_y) / TILE_SIZE)) == 0) player_y += my;
cell_x = static_cast<int>(player_x / TILE_SIZE); cell_y = static_cast<int>(player_y / TILE_SIZE);
}
Color bg_color = day_mode ? Color(140, 140, 145) : Color(15, 15, 15);
std::fill(frame_buffer.begin(), frame_buffer.end(), bg_color);
float start_angle = player_angle - fov / 2.0f;
for (int ray = 0; ray < NUM_RAYS; ++ray) {
float angle = start_angle + ray * (fov / NUM_RAYS);
float r_cos = (std::cos(angle) != 0) ? std::cos(angle) : 0.00001f;
float r_sin = (std::sin(angle) != 0) ? std::sin(angle) : 0.00001f;
float x_depth = 9999.0f; int x_texture = 0;
float x_step = TILE_SIZE / r_cos;
float first_x = (r_cos > 0) ? static_cast<float>((static_cast<int>(player_x / TILE_SIZE) + 1) * TILE_SIZE) : static_cast<float>(static_cast<int>(player_x / TILE_SIZE) * TILE_SIZE);
float current_depth_x = (first_x - player_x) / r_cos;
for (int i = 0; i < 30; ++i) {
float tx = player_x + current_depth_x * r_cos;
float ty = player_y + current_depth_x * r_sin;
int col = static_cast<int>((tx + (r_cos > 0 ? 1.0f : -1.0f)) / TILE_SIZE);
int row = static_cast<int>(ty / TILE_SIZE);
if (get_wall_type(col, row) > 0) {
x_depth = current_depth_x;
x_texture = static_cast<int>(std::fmod(ty, static_cast<float>(TILE_SIZE)));
if (x_texture < 0) x_texture += TILE_SIZE;
break;
}
current_depth_x += std::abs(x_step);
}
float y_depth = 9999.0f; int y_texture = 0;
float y_step = TILE_SIZE / r_sin;
float first_y = (r_sin > 0) ? static_cast<float>((static_cast<int>(player_y / TILE_SIZE) + 1) * TILE_SIZE) : static_cast<float>(static_cast<int>(player_y / TILE_SIZE) * TILE_SIZE);
float current_depth_y = (first_y - player_y) / r_sin;
for (int i = 0; i < 30; ++i) {
float tx = player_x + current_depth_y * r_cos;
float ty = player_y + current_depth_y * r_sin;
int col = static_cast<int>(tx / TILE_SIZE);
int row = static_cast<int>((ty + (r_sin > 0 ? 1.0f : -1.0f)) / TILE_SIZE);
if (get_wall_type(col, row) > 0) {
y_depth = current_depth_y;
y_texture = static_cast<int>(std::fmod(tx, static_cast<float>(TILE_SIZE)));
if (y_texture < 0) y_texture += TILE_SIZE;
break;
}
current_depth_y += std::abs(y_step);
}
float depth = 0; int x_tex_coord = 0; bool is_vertical_wall = false;
if (x_depth < y_depth) { depth = x_depth; x_tex_coord = x_texture; is_vertical_wall = true; }
else { depth = y_depth; x_tex_coord = y_texture; is_vertical_wall = false; }
int wall_y_pos = HALF_HEIGHT;
if (depth < 9999.0f) {
float fixed_depth = depth * std::cos(player_angle - angle);
if (fixed_depth < 1.0f) fixed_depth = 1.0f;
int proj_height = static_cast<int>((TILE_SIZE / fixed_depth) * 600.0f);
if (proj_height > HEIGHT * 2) proj_height = HEIGHT * 2;
wall_y_pos = HALF_HEIGHT - proj_height / 2;
int shadow = 0;
if (day_mode) {
shadow = is_vertical_wall ? 15 : 0;
}
else {
int base_shadow = static_cast<int>(fixed_depth * 0.75f);
if (is_vertical_wall) base_shadow += 20;
shadow = std::clamp(base_shadow, 0, 255);
}
int start_screen_y = std::max(0, wall_y_pos);
int end_screen_y = std::min(HEIGHT, wall_y_pos + proj_height);
for (int screen_y = start_screen_y; screen_y < end_screen_y; ++screen_y) {
int tex_y = ((screen_y - wall_y_pos) * TILE_SIZE) / proj_height;
tex_y = std::clamp(tex_y, 0, TILE_SIZE - 1);
Color pixel_color = tex_wall[tex_y * TILE_SIZE + std::clamp(x_tex_coord, 0, TILE_SIZE - 1)];
pixel_color.r = static_cast<unsigned char>(std::max(0, pixel_color.r - shadow));
pixel_color.g = static_cast<unsigned char>(std::max(0, pixel_color.g - shadow));
pixel_color.b = static_cast<unsigned char>(std::max(0, pixel_color.b - shadow));
for (int s = 0; s < SCALE; ++s) {
int pixel_x = ray * SCALE + s;
if (pixel_x < WIDTH) frame_buffer[screen_y * WIDTH + pixel_x] = pixel_color;
}
}
float cos_tilt = std::cos(player_angle - angle);
for (int screen_y = 0; screen_y < wall_y_pos; screen_y += 4) {
if (screen_y >= HALF_HEIGHT) break;
float current_dist = (static_cast<float>(TILE_SIZE * HEIGHT)) / (2.0f * static_cast<float>(HALF_HEIGHT - screen_y) * cos_tilt);
int tex_x = static_cast<int>(player_x + r_cos * current_dist) % TILE_SIZE;
int tex_y_floor = static_cast<int>(player_y + r_sin * current_dist) % TILE_SIZE;
if (tex_x < 0) tex_x += TILE_SIZE;
if (tex_y_floor < 0) tex_y_floor += TILE_SIZE;
int f_shadow = day_mode ? 0 : std::clamp(static_cast<int>(current_dist * 0.75f), 0, 255);
Color c_color = tex_ceil[tex_y_floor * TILE_SIZE + tex_x];
c_color.r = static_cast<unsigned char>(std::max(0, c_color.r - f_shadow));
c_color.g = static_cast<unsigned char>(std::max(0, c_color.g - f_shadow));
c_color.b = static_cast<unsigned char>(std::max(0, c_color.b - f_shadow));
Color fl_color = tex_floor[tex_y_floor * TILE_SIZE + tex_x];
fl_color.r = static_cast<unsigned char>(std::max(0, fl_color.r - f_shadow));
fl_color.g = static_cast<unsigned char>(std::max(0, fl_color.g - f_shadow));
fl_color.b = static_cast<unsigned char>(std::max(0, fl_color.b - f_shadow));
for (int h = 0; h < 4; ++h) {
int curr_y = screen_y + h;
int floor_y = HEIGHT - curr_y - 1;
if (curr_y < wall_y_pos) {
for (int s = 0; s < SCALE; ++s) {
int pixel_x = ray * SCALE + s;
if (pixel_x < WIDTH) {
frame_buffer[curr_y * WIDTH + pixel_x] = c_color;
frame_buffer[floor_y * WIDTH + pixel_x] = fl_color;
}
}
}
}
}
}
}
HDC hdc = GetDC(hwnd);
StretchDIBits(hdc, 0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT, frame_buffer.data(), &bmi, DIB_RGB_COLORS, SRCCOPY);
ReleaseDC(hwnd, hdc);
}
Gdiplus::GdiplusShutdown(gdiplusToken);
return 0;
}