专栏文章
迷宫2
个人记录参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @miorutqb
- 此快照首次捕获于
- 2025/12/03 00:07 3 个月前
- 此快照最后确认于
- 2025/12/03 00:07 3 个月前
CPP
#include <bits/stdc++.h>
#include <graphics.h> // EasyX 图形库
// 额外的 Windows API 和其他功能所需的头文件
#include <windows.h> // 包含 Windows API,如 GetAsyncKeyState, GetCursorPos, SetCursorPos
#include <cmath> // 数学函数,如 cos, sin, fabs
#include <fstream> // 文件输入/输出,用于地图加载
#include <functional> // std::function,用于 generateChunk 中的 lambda 递归
#include <random> // 随机数生成,用于 generateChunk
// 如果需要声音,取消注释以下行并确保链接 winmm.lib
#pragma comment(lib, "winmm.lib") // Visual Studio
#include <mmsystem.h> // For PlaySound
#define ll long long
using namespace std;
// --- 常量和枚举 ---
// EasyX 窗口尺寸
static const int SCREEN_WIDTH = 150;
static const int SCREEN_HEIGHT = 80;
const double PI = acos(-1.0); // 更精确的 PI 值
// 游戏相关常量
constexpr int CHUNK_SIZE = 20; // 每个迷宫块的大小
const double PLAYER_HEIGHT = 0.5; // 玩家离地面的高度 (0 为地面)
const double GRAVITY = 0.008; // 重力加速度 (为 EasyX 调整,帧率更高)
const double JUMP_FORCE = 0.1; // 跳跃的初始向上速度 (为 EasyX 调整)
const double MOVE_SPEED = 0.05; // 玩家移动速度 (为 EasyX 调整)
const double MOUSE_SENSITIVITY = 0.002; // 鼠标相机灵敏度 (为 EasyX 调整)
const double KEY_SENSITIVITY = 0.03; // 键盘相机灵敏度 (为 EasyX 调整)
const double PITCH_SENSITIVITY = 0.03; // 键盘俯仰灵敏度 (为 EasyX 调整)
const double MAX_PITCH_ANGLE = PI / 2.0 - 0.1; // 限制俯仰角度,防止翻转
// 方块类型枚举
enum BlockType {
EMPTY = 0,
WALL = 1,
TREASURE = 2,
EXIT = 3,
MONSTER = 4,
TRAP = 5
};
// 地图分块结构体
struct Chunk {
int map[CHUNK_SIZE][CHUNK_SIZE];
};
using ChunkCoord = pair<int,int>;
// 用于 unordered_map 的 pair 哈希函数
struct pair_hash {
size_t operator()(const ChunkCoord& p) const {
return std::hash<int>()(p.first) ^ (std::hash<int>()(p.second)<<1);
}
};
unordered_map<ChunkCoord,Chunk,pair_hash> chunkPool; // 全局分块池
// 粒子结构体 (为 EasyX 调整颜色类型)
struct Particle {
double x,y,z,vx,vy,vz;
int life;
COLORREF color; // EasyX 使用 COLORREF
};
// 玩家结构体
struct Player {
double x=1.5,y=1.5,z=PLAYER_HEIGHT,angle=0,pitch=0,vz=0; // z, pitch, vz 为新增
bool onGround=true; // 是否在地面上
int health=100, treasures=0; // 生命值和宝藏数量
double cosA,sinA,cosP,sinP; // 角度 (yaw) 和俯仰角 (pitch) 的余弦/正弦
inline void updateTrig(){ cosA=cos(angle); sinA=sin(angle); cosP=cos(pitch); sinP=sin(pitch); }
} player;
static vector<Particle> particles; // 全局粒子列表
// 怪物结构体和列表
struct Monster {
double x, y; // 精确的浮点坐标 (瓷砖中心)
double moveCooldown; // 移动冷却计时器
int id; // 唯一标识符
};
static vector<Monster> monsters; // 全局活跃怪物列表
// 怪物常量
const double MONSTER_MOVE_DELAY = 0.5; // 怪物每 0.5 秒移动一次
const double MONSTER_DETECTION_RANGE = 40.0; // 怪物在 40 单元内检测到玩家
static BlockType selectedBlockType = WALL; // 当前选择的放置方块类型
// 粒子常量
const double PARTICLE_GRAVITY = 0.008; // 调整后的粒子重力
const double PARTICLE_FRICTION = 0.98;
bool showMiniMap=false, mouseMode=false; // 小地图显示和鼠标模式开关
bool backroomsMode=false; // “后室”模式开关
POINT lastMousePos; // 鼠标上次位置,用于鼠标模式
bool gameOver = false; // 游戏结束标志
long long startTime; // 游戏开始时间
int gameEndReason = 0; // 0: 手动退出, 1: 胜利, 2: 失败
// 伤害闪屏变量
static double damageFlashTimer = 0.0; // 剩余闪屏时间 (秒)
const double DAMAGE_FLASH_DURATION = 0.2; // 总闪屏持续时间 (秒)
// 地图模式声明
enum class MapMode { Infinite, Existing };
static MapMode mapMode = MapMode::Infinite; // 默认为无限生成
// 小地图缩放变量
static int minimap_radius = 7; // 默认显示半径为 7 (15x15 瓷砖)
const int MIN_MINIMAP_RADIUS = 3; // 最小小地图半径 (7x7)
const int MAX_MINIMAP_RADIUS = 20; // 最大小地图半径 (41x41)
// 小地图翻转变量
static bool minimap_flipped = false; // false = 正常, true = 水平翻转
// 函数前向声明
int getWorldAt(int x, int y);
void setWorldAt(int x, int y, int value);
// void playSound(const char* filename); // 如果需要声音,取消注释此行
// 怪物移动和挖掘的辅助函数
// 如果怪物成功移动 (或通过挖掘移动) 返回 true
bool tryMoveMonster(Monster& m, int old_mx, int old_my, int new_mx, int new_my) {
// 检查目标方块是否被其他怪物占用
for (const auto& other_m : monsters) {
if (&other_m == &m) continue; // 跳过自己
if (static_cast<int>(other_m.x) == new_mx && static_cast<int>(other_m.y) == new_my) {
return false; // 目标单元格被其他怪物占用,无法移动
}
}
int target_block_type = getWorldAt(new_mx, new_my);
if (target_block_type == WALL || target_block_type == TREASURE || target_block_type == EXIT || target_block_type == TRAP) {
// 是障碍物,怪物将挖掘。
// 挖掘目标周围 3x3 区域
for (int dy_dig = -1; dy_dig <= 1; ++dy_dig) {
for (int dx_dig = -1; dx_dig <= 1; ++dx_dig) {
int dig_x = new_mx + dx_dig;
int dig_y = new_my + dy_dig;
// 只清除非空方块,并且不清除地图上的其他怪物
if (getWorldAt(dig_x, dig_y) != EMPTY && getWorldAt(dig_x, dig_y) != MONSTER) {
setWorldAt(dig_x, dig_y, EMPTY);
}
}
}
// 挖掘后,目标瓷砖现在是空的,怪物可以移动过去
setWorldAt(old_mx, old_my, EMPTY); // 清除怪物旧的地图位置
m.x = new_mx + 0.5; // 更新怪物精确位置到新瓷砖中心
m.y = new_my + 0.5;
setWorldAt(static_cast<int>(m.x),static_cast<int>(m.y), MONSTER); // 设置怪物新的地图位置
// playSound("dig.wav"); // 怪物挖掘音效 (如果启用)
return true;
} else if (target_block_type == EMPTY) {
// 是空地,直接移动
setWorldAt(old_mx, old_my, EMPTY); // 清除怪物旧的地图位置
m.x = new_mx + 0.5; // 更新怪物精确位置
m.y = new_my + 0.5;
setWorldAt(static_cast<int>(m.x),static_cast<int>(m.y), MONSTER); // 设置怪物新的地图位置
return true;
}
return false; // 无法移动到此类型的方块
}
// 获取分块坐标和分块内的局部偏移
inline ChunkCoord getChunkPos(int x, int y) {
int cx = (x >= 0) ? (x/CHUNK_SIZE) : ((x+1-CHUNK_SIZE)/CHUNK_SIZE);
int cy = (y >= 0) ? (y/CHUNK_SIZE) : ((y+1-CHUNK_SIZE)/CHUNK_SIZE);
return ChunkCoord(cx, cy);
}
inline void getLocalPos(int x, int y, int& lx, int& ly) {
lx = x % CHUNK_SIZE; if(lx<0) lx+=CHUNK_SIZE;
ly = y % CHUNK_SIZE; if(ly<0) ly+=CHUNK_SIZE;
}
// 生成迷宫分块
void generateChunk(const ChunkCoord& coord) {
Chunk& chunk = chunkPool[coord];
// 初始化为墙壁
for(int y=0; y<CHUNK_SIZE; ++y)
for(int x=0; x<CHUNK_SIZE; ++x)
chunk.map[y][x] = WALL;
// 随机引擎,以分块坐标为种子,保证一致性生成
uint64_t seed = coord.first * 73856093ull ^ coord.second * 19349663ull;
mt19937 rng(seed);
// DFS 生成迷宫路径
function<void(int,int)> dfs = [&](int x,int y){
chunk.map[y][x] = EMPTY;
// 打乱方向
vector<int> dir = {0,1,2,3};
shuffle(dir.begin(), dir.end(), rng);
static const int dx[] = {1,-1,0,0}, dy[] = {0,0,1,-1};
for(int d:dir) {
int nx = x + dx[d]*2, ny = y + dy[d]*2;
if(nx > 0 && nx < CHUNK_SIZE-1 && ny > 0 && ny < CHUNK_SIZE-1 && chunk.map[ny][nx] == WALL) {
chunk.map[y+dy[d]][x+dx[d]] = EMPTY; // 挖路
dfs(nx, ny);
}
}
};
dfs(1, 1); // 从 (1,1) 开始 DFS
// 放置动态元素 (宝藏, 出口, 怪物, 陷阱)
auto place_element = [&](int type, int chance_inv, int min_dist_border) {
if (rng() % chance_inv == 0) {
int tx = rng() % (CHUNK_SIZE - 2 * min_dist_border) + min_dist_border;
int ty = rng() % (CHUNK_SIZE - 2 * min_dist_border) + min_dist_border;
// 只有在空地上放置,并且怪物避免在玩家出生点附近生成
if (chunk.map[ty][tx] == EMPTY) {
// 大致距离检查,避免怪物在玩家出生点 (0,0 块,1.5, 1.5) 附近生成
int world_x = coord.first * CHUNK_SIZE + tx;
int world_y = coord.second * CHUNK_SIZE + ty;
if (type == MONSTER && sqrt(pow(world_x - player.x, 2) + pow(world_y - player.y, 2)) < MONSTER_DETECTION_RANGE + 5) {
return; // 避免怪物在初始时离玩家太近
}
chunk.map[ty][tx] = type;
// 如果是怪物,添加到全局怪物列表
if (type == MONSTER) {
monsters.push_back({
(double)coord.first * CHUNK_SIZE + tx + 0.5, // 怪物位于瓷砖中心
(double)coord.second * CHUNK_SIZE + ty + 0.5,
MONSTER_MOVE_DELAY, // 初始冷却时间
(int)monsters.size() // 简单 ID
});
}
}
}
};
place_element(TREASURE, 10, 2); // 1/10 几率, 离边界最小 2 单元
place_element(EXIT, 30, 3); // 1/30 几率, 离边界最小 3 单元
place_element(MONSTER, 40, 2); // 1/40 几率, 离边界最小 2 单元
place_element(TRAP, 25, 2); // 1/25 几率, 离边界最小 2 单元
// 确保与相邻分块的连通性
// 左侧: 如果左邻居存在且其右侧有路径,则在此处打开路径
ChunkCoord leftC = {coord.first - 1, coord.second};
if (chunkPool.count(leftC)) {
Chunk& lc = chunkPool[leftC];
for (int y = 3; y < CHUNK_SIZE - 3; y += 2) { // 遍历潜在路径点
if (lc.map[y][CHUNK_SIZE - 2] == EMPTY) { // 如果左侧分块的右边缘路径是开放的
chunk.map[y][0] = EMPTY; // 打开此分块的左边缘
chunk.map[y][1] = EMPTY; // 以及分块内的下一个单元格
}
}
}
// 顶部: 与顶部邻居相同
ChunkCoord upC = {coord.first, coord.second - 1};
if (chunkPool.count(upC)) {
Chunk& uc = chunkPool[upC];
for (int x = 3; x < CHUNK_SIZE - 3; x += 2) {
if (uc.map[CHUNK_SIZE - 2][x] == EMPTY) {
chunk.map[0][x] = EMPTY;
chunk.map[1][x] = EMPTY;
}
}
}
// 打开右侧/底部路径: 这些点将被未来的相邻分块考虑
for (int y = 3; y < CHUNK_SIZE - 3; y += 2) {
if (chunk.map[y][CHUNK_SIZE - 3] == EMPTY) { // 如果内部路径到达右侧倒数第三列
chunk.map[y][CHUNK_SIZE - 2] = EMPTY; // 打开到倒数第二列
}
}
for (int x = 3; x < CHUNK_SIZE - 3; x += 2) {
if (chunk.map[CHUNK_SIZE - 3][x] == EMPTY) { // 如果内部路径到达底部倒数第三行
chunk.map[CHUNK_SIZE - 2][x] = EMPTY; // 打开到倒数第二行
}
}
}
// 地图访问封装
int getWorldAt(int x, int y) {
ChunkCoord c = getChunkPos(x, y);
int lx, ly;
getLocalPos(x, y, lx, ly);
if (chunkPool.count(c)) {
return chunkPool[c].map[ly][lx];
} else {
if (mapMode == MapMode::Infinite) {
// 如果分块不存在且在无限模式下,生成它
generateChunk(c);
return chunkPool[c].map[ly][lx]; // 返回新生成的方块类型
} else { // MapMode::Existing
// 如果在现有地图模式下且分块未加载,则超出定义地图范围。视为墙壁。
return WALL;
}
}
}
// 设置世界方块类型
void setWorldAt(int x, int y, int value) {
ChunkCoord c = getChunkPos(x, y);
int lx, ly;
getLocalPos(x, y, lx, ly);
// 只有在分块池中存在时才修改 (已加载或已生成)
if(chunkPool.count(c)) {
chunkPool[c].map[ly][lx] = value;
}
}
// 从文件加载现有地图
void loadExistingMap(const string& filename) {
ifstream fin(filename);
if (!fin) {
cerr << "错误:无法打开地图文件: " << filename << endl;
mapMode = MapMode::Infinite; // 回退到无限生成模式
cout << "已切换到无限自动生成模式。\n";
return;
}
// 清除之前运行/无限模式的动态元素
chunkPool.clear();
monsters.clear();
int chunkX, chunkY;
int loaded_chunks = 0;
while (fin >> chunkX >> chunkY) {
ChunkCoord coord = {chunkX, chunkY};
Chunk c;
for (int y = 0; y < CHUNK_SIZE; ++y) {
for (int x = 0; x < CHUNK_SIZE; ++x) {
int v_int;
if (!(fin >> v_int)) {
cerr << "错误:地图文件格式不正确,在读取区块 (" << chunkX << "," << chunkY << ") 的块数据时出错。\n";
fin.close();
mapMode = MapMode::Infinite; // 回退
cout << "已切换到无限自动生成模式。\n";
return;
}
BlockType v = static_cast<BlockType>(v_int);
c.map[y][x] = v_int; // 直接存储整数值
// 如果是怪物,添加到动态列表
if (v == MONSTER) {
monsters.push_back({
(double)coord.first * CHUNK_SIZE + x + 0.5, // 怪物位于瓷砖中心
(double)coord.second * CHUNK_SIZE + y + 0.5, // 修复: 将ty改为y
MONSTER_MOVE_DELAY, // 初始冷却时间
(int)monsters.size() // 简单 ID
});
}
}
}
chunkPool[coord] = c;
loaded_chunks++;
}
fin.close();
cout << "已加载 " << loaded_chunks << " 个区块。\n";
// 设置玩家位置到任何已加载分块中的第一个可用空地
bool found_spawn_location = false;
for (const auto& pair : chunkPool) {
ChunkCoord current_chunk_coord = pair.first;
const Chunk& current_chunk = pair.second;
int spawn_x_offset = current_chunk_coord.first * CHUNK_SIZE;
int spawn_y_offset = current_chunk_coord.second * CHUNK_SIZE;
for (int dy = 0; dy < CHUNK_SIZE; ++dy) {
for (int dx = 0; dx < CHUNK_SIZE; ++dx) {
if (current_chunk.map[dy][dx] == EMPTY) {
player.x = spawn_x_offset + dx + 0.5; // 居中于瓷砖
player.y = spawn_y_offset + dy + 0.5;
found_spawn_location = true;
goto end_spawn_search; // 使用 goto 跳出嵌套循环
}
}
}
}
end_spawn_search:; // goto 标签
if (!found_spawn_location) {
cout << "警告:在所有加载的区块中未找到空地作为玩家出生点,玩家可能出生在墙内。\n";
}
if (chunkPool.empty()) {
cout << "警告:没有加载任何区块,请检查地图文件。\n";
mapMode = MapMode::Infinite; // 如果没有加载任何分块,回退
cout << "已切换到无限自动生成模式。\n";
}
}
// 检查按键状态 (EasyX 代码中已有,这里重用)
inline bool keyDown(int v) { return GetAsyncKeyState(v) & 0x8000; }
// 播放音效函数 (已注释掉,因为需要额外的库和音效文件)
// void playSound(const char* filename) {
// PlaySoundA(filename, NULL, SND_FILENAME | SND_ASYNC);
// }
// 游戏结束/胜利时显示消息的函数 (使用 EasyX)
void displayGameEndScreen(const string& message) {
cleardevice(); // 清除 EasyX 窗口
setbkcolor(BLACK); // 黑色背景
settextcolor(WHITE); // 白色文本
settextstyle(30, 0, _T("Consolas")); // 更大的字体
// 计算文本位置以居中显示
// 注意: textwidth 和 textheight 接受 TCHAR* 参数,所以需要转换为 _T() 字符串
int textWidth = textwidth(_T(message.c_str())); // 这里直接用 string::c_str() 转换可能会导致问题,
// 如果 EasyX 使用多字节字符集,string 的内容可能是 UTF-8
// 如果使用宽字节字符集,string 的内容应该转换为 wstring
// 最保险的做法是创建一个 TCHAR 数组
TCHAR t_message[256]; // 假设消息长度不会超过 255
_tcscpy_s(t_message, _countof(t_message), _T(message.c_str())); // 复制到 TCHAR 数组
int textHeight = textheight(t_message); // _T(message.c_str())
outtextxy((SCREEN_WIDTH - textWidth) / 2, (SCREEN_HEIGHT - textHeight) / 2 - 20, t_message); // _T(message.c_str())
string prompt_str = "Press ENTER to exit."; // 这里保持 string,因为这是英文提示
TCHAR t_prompt[64];
_tcscpy_s(t_prompt, _countof(t_prompt), _T(prompt_str.c_str())); // 同样处理
int promptWidth = textwidth(t_prompt);
int promptHeight = textheight(t_prompt);
outtextxy((SCREEN_WIDTH - promptWidth) / 2, (SCREEN_HEIGHT - promptHeight) / 2 + 20, t_prompt);
FlushBatchDraw(); // 确保绘制可见
// 通过控制台等待 ENTER 键 (因为 EasyX 窗口可能在游戏结束后被隐藏)
// 或者可以使用 EasyX 的按键检测循环来等待,但这需要保持 EasyX 窗口的活跃。
// 这里选择重新显示控制台并等待输入。
ShowWindow(GetConsoleWindow(), SW_SHOW); // 重新显示控制台窗口
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 清除任何剩余的输入缓冲区
cin.get();
}
int main() {
// 初始控制台交互 (选择地图模式)
SetConsoleTitle(_T("迷迷世界")); // 设置控制台标题
srand((unsigned)time(NULL)); // 初始化随机数生成器
ios::sync_with_stdio(false); // 关闭 C++ 标准流与 C 标准流的同步
system("cls"); // 清屏以显示菜单
cout << "\n\n";
cout << " --------------------------------------------------\n";
cout << " | 欢迎来到 迷迷世界 |\n";
cout << " | (Maze Runner 3D) |\n";
cout << " --------------------------------------------------\n";
cout << "\n";
cout << " 请选择地图模式:\n";
cout << " 1. 无限自动生成地图 (Infinite procedural map)\n";
cout << " 2. 加载已有地图 (Load existing map from file)\n";
cout << " --------------------------------------------------\n";
cout << " 输入编号并回车:";
int choice_input = 1; // 默认选择
cin >> choice_input;
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 清除行尾的换行符
if (choice_input == 2) {
mapMode = MapMode::Existing;
cout<<"请选择地图:\n";
cout<<"1.空旷的小房间 (map1.txt)\n"; // 假设文件名为 map1.txt
cout<<"2.石墙林立 (map2.txt)\n"; // 假设文件名为 map2.txt
string fname_choice_str;
getline(cin, fname_choice_str);
int fname_choice = 0;
try {
fname_choice = stoi(fname_choice_str);
} catch (const std::invalid_argument& ia) {
cerr << "无效输入, 默认为 1.\n";
fname_choice = 1;
} catch (const std::out_of_range& oor) {
cerr << "输入超出范围, 默认为 1.\n";
fname_choice = 1;
}
string fname_base;
if (fname_choice == 1) {
fname_base = "map1";
} else if (fname_choice == 2) {
fname_base = "map2";
} else {
cout << "无效选择,默认为 'map1.txt'.\n";
fname_base = "map1";
}
loadExistingMap(fname_base + ".txt");
} else {
mapMode = MapMode::Infinite;
cout << " 已选择无限自动生成模式。\n";
}
// 初始化 EasyX 窗口 (在控制台交互后)
initgraph(SCREEN_WIDTH, SCREEN_HEIGHT);
SetWindowText(GetHWnd(), _T("迷迷世界 - Maze Runner 3D")); // 设置 EasyX 窗口标题
// 隐藏控制台窗口,现在 EasyX 窗口是主要的显示界面
ShowWindow(GetConsoleWindow(), SW_HIDE);
// 设置鼠标初始位置,以便鼠标模式开启时,鼠标光标在屏幕中心
RECT r_easyx; GetWindowRect(GetHWnd(),&r_easyx);
lastMousePos={ (r_easyx.left+r_easyx.right)/2, (r_easyx.top+r_easyx.bottom)/2 };
SetCursorPos(lastMousePos.x,lastMousePos.y);
// 使用 EasyX 显示游戏介绍和操作指南
cleardevice(); // 清屏
setbkcolor(BLACK); // 背景色
settextcolor(WHITE); // 文本颜色
settextstyle(20, 0, _T("Consolas")); // 字体样式
// 以下是游戏介绍和操作指南的 EasyX 文本绘制,调整 Y 坐标以适应窗口高度
int current_y = 30; // 初始 Y 坐标
const int line_height = 20; // 每行行高
outtextxy(50, current_y, _T("--------------------------------------------------")); current_y += line_height;
outtextxy(50, current_y, _T("| 欢迎来到 迷迷世界 |")); current_y += line_height;
outtextxy(50, current_y, _T("| (Maze Runner 3D) |")); current_y += line_height;
outtextxy(50, current_y, _T("--------------------------------------------------")); current_y += line_height * 2;
outtextxy(50, current_y, _T("**游戏概述:**")); current_y += line_height;
outtextxy(50, current_y, _T("《迷迷世界3D》是一款独特的、在EasyX中运行的3D第一人称迷宫冒险游戏。")); current_y += line_height;
outtextxy(50, current_y, _T("你将身处一个无限生成、不断变化的像素化迷宫中。你的目标是探索、收集宝藏、")); current_y += line_height;
outtextxy(50, current_y, _T("生存下来并找到出口!")); current_y += line_height * 2;
outtextxy(50, current_y, _T("**核心玩法与特色:**")); current_y += line_height;
outtextxy(50, current_y, _T("- 无限迷宫: 程序生成,每次体验都不同。")); current_y += line_height;
outtextxy(50, current_y, _T("- 动态元素: 收集'$'宝藏,寻找'E'出口。小心'M'怪物和'T'陷阱,它们会扣除生命!")); current_y += line_height;
outtextxy(50, current_y, _T("- 自由互动: 按'X'挖掘任何方块,按'1'放置方块,改变迷宫。")); current_y += line_height;
outtextxy(50, current_y, _T("- 辅助模式: 按'M'切换小地图,按'K'切换独特的“后室”视觉模式。")); current_y += line_height * 2;
outtextxy(50, current_y, _T("**操作指南:**")); current_y += line_height;
outtextxy(50, current_y, _T("- WASD: 移动 (按住SHIFT冲刺)")); current_y += line_height;
outtextxy(50, current_y, _T("- 空格键 (SPACE): 跳跃")); current_y += line_height;
outtextxy(50, current_y, _T("- 左/右箭头键: 水平转向")); current_y += line_height;
outtextxy(50, current_y, _T("- 上/下箭头键: 垂直俯仰")); current_y += line_height;
outtextxy(50, current_y, _T("- ALT 键: 换鼠标视角模式")); current_y += line_height; // 鼠标模式改为"换"以区分
outtextxy(50, current_y, _T("- M 键: 切换小地图显示")); current_y += line_height;
outtextxy(50, current_y, _T("- K 键: 切换“后室”模式")); current_y += line_height;
outtextxy(50, current_y, _T("- X 键: 挖掘前方的任何方块 (除了空地)")); current_y += line_height;
outtextxy(50, current_y, _T("- 1 键: 在前方空地放置当前选定的方块类型")); current_y += line_height;
outtextxy(50, current_y, _T("- [ 键 (左方括号): 循环选择上一个要放置的方块类型")); current_y += line_height;
outtextxy(50, current_y, _T("- ] 键 (右方括号): 循环选择下一个要放置的方块类型")); current_y += line_height;
outtextxy(50, current_y, _T("- ESC 键: 退出游戏")); current_y += line_height;
outtextxy(50, current_y, _T("- + 键: 增大迷你地图尺寸")); current_y += line_height;
outtextxy(50, current_y, _T("- - 键: 减小迷你地图尺寸")); current_y += line_height;
outtextxy(50, current_y, _T("- R 键: 左右翻转迷你地图显示")); current_y += line_height * 2;
outtextxy(50, current_y, _T("--------------------------------------------------")); current_y += line_height;
outtextxy(50, current_y, _T("准备好了吗?按下 ENTER 键开始冒险..."));
FlushBatchDraw(); // 刷新显示
// 等待用户在 EasyX 窗口中按下 Enter 键开始游戏
while(!keyDown(VK_RETURN)) {
Sleep(10); // 短暂延迟,避免 CPU 占用过高
}
cleardevice(); // 清屏
FlushBatchDraw(); // 刷新
startTime = GetTickCount(); // 记录游戏开始时间
BeginBatchDraw(); // 开启批量绘制,避免闪烁
// 按键状态变量 (用于检测按键按下/释放,避免重复触发)
bool altPrev=false, mPrev=false, xPrev=false, kPrev=false, rPrev=false;
bool onePrev=false, spacePrev=false;
bool cyclePrevPrev = false, cycleNextPrev = false;
bool plusPrev = false, minusPrev = false;
long long lastFrameTime = GetTickCount(); // 上一帧的时间
// 游戏主循环
while(!gameOver){
long long currentFrameTime = GetTickCount();
double deltaTime = (currentFrameTime - lastFrameTime) / 1000.0; // 计算帧时间 (秒)
lastFrameTime = currentFrameTime;
// 更新伤害闪屏计时器
damageFlashTimer = max(0.0, damageFlashTimer - deltaTime);
// --- 输入处理 ---
bool alt=keyDown(VK_MENU);
if(alt&&!altPrev) {
mouseMode=!mouseMode;
// 切换到鼠标模式时,重新将鼠标光标定位到窗口中心
if(mouseMode) {
RECT r_easyx_current; GetWindowRect(GetHWnd(),&r_easyx_current);
lastMousePos={ (r_easyx_current.left+r_easyx_current.right)/2, (r_easyx_current.top+r_easyx_current.bottom)/2 };
SetCursorPos(lastMousePos.x,lastMousePos.y);
}
}
altPrev=alt;
if(mouseMode){
POINT p; GetCursorPos(&p);
int dx=p.x-lastMousePos.x;
int dy=p.y-lastMousePos.y; // 用于俯仰角
player.angle+=dx*MOUSE_SENSITIVITY;
player.pitch-=dy*MOUSE_SENSITIVITY; // 负号用于 Y 轴反转
SetCursorPos(lastMousePos.x,lastMousePos.y); // 将鼠标光标重新定位到中心
}
// 水平视角旋转 (偏航)
if(keyDown(VK_LEFT)) player.angle-=KEY_SENSITIVITY;
if(keyDown(VK_RIGHT)) player.angle+=KEY_SENSITIVITY;
// 垂直视角旋转 (俯仰)
if(keyDown(VK_UP)) player.pitch+=PITCH_SENSITIVITY;
if(keyDown(VK_DOWN)) player.pitch-=PITCH_SENSITIVITY;
player.pitch = max(-MAX_PITCH_ANGLE, min(MAX_PITCH_ANGLE, player.pitch)); // 限制俯仰角度
// 更新玩家的三角函数值 (在角度/俯仰角改变后必须更新)
player.updateTrig();
// 移动 (WASD)
double nx=player.x, ny=player.y;
double currentMoveSpeed = MOVE_SPEED;
if (keyDown(VK_SHIFT)) currentMoveSpeed *= 1.5; // 按住 SHIFT 冲刺
if(keyDown('W')){ // 前进
nx+=player.cosA*currentMoveSpeed;
ny+=player.sinA*currentMoveSpeed;
}
if(keyDown('S')){ // 后退
nx-=player.cosA*currentMoveSpeed;
ny-=player.sinA*currentMoveSpeed;
}
if(keyDown('A')){ // 横向左移
nx+=player.sinA*currentMoveSpeed;
ny-=player.cosA*currentMoveSpeed;
}
if(keyDown('D')){ // 横向右移
nx-=player.sinA*currentMoveSpeed;
ny+=player.cosA*currentMoveSpeed;
}
// 简单的水平碰撞检测
// 检查玩家包围盒的四个角
double player_half_width = 0.2; // 玩家的有效半宽用于碰撞
// 仅检查 X 方向的移动是否会导致碰撞
bool can_move_x = (
getWorldAt(static_cast<int>(nx - player_half_width), static_cast<int>(player.y - player_half_width)) != WALL &&
getWorldAt(static_cast<int>(nx + player_half_width), static_cast<int>(player.y - player_half_width)) != WALL &&
getWorldAt(static_cast<int>(nx - player_half_width), static_cast<int>(player.y + player_half_width)) != WALL &&
getWorldAt(static_cast<int>(nx + player_half_width), static_cast<int>(player.y + player_half_width)) != WALL
);
// 仅检查 Y 方向的移动是否会导致碰撞
bool can_move_y = (
getWorldAt(static_cast<int>(player.x - player_half_width), static_cast<int>(ny - player_half_width)) != WALL &&
getWorldAt(static_cast<int>(player.x + player_half_width), static_cast<int>(ny - player_half_width)) != WALL &&
getWorldAt(static_cast<int>(player.x - player_half_width), static_cast<int>(ny + player_half_width)) != WALL &&
getWorldAt(static_cast<int>(player.x + player_half_width), static_cast<int>(ny + player_half_width)) != WALL
);
// 如果没有碰撞,则应用移动
if (can_move_x) player.x = nx;
if (can_move_y) player.y = ny;
// 跳跃
bool space = keyDown(VK_SPACE);
if(space && !spacePrev && player.onGround){
player.vz = JUMP_FORCE;
player.onGround = false;
// playSound("jump.wav");
}
spacePrev = space;
// 应用重力
if(!player.onGround) {
player.vz -= GRAVITY;
player.z += player.vz;
}
// 与地面的碰撞 (z=0)
if(player.z <= PLAYER_HEIGHT){
player.z = PLAYER_HEIGHT; // 将玩家 Z 限制在地面以上
player.vz = 0; // 垂直速度归零
player.onGround = true; // 标记为在地面上
}
// 小地图开关
if(keyDown('M')&&!mPrev) showMiniMap=!showMiniMap;
mPrev=keyDown('M');
// “后室”模式开关
if(keyDown('K')&&!kPrev) backroomsMode=!backroomsMode;
kPrev=keyDown('K');
// 小地图翻转开关 (R 键)
bool r = keyDown('R');
if(r && !rPrev) minimap_flipped = !minimap_flipped;
rPrev = r;
// 循环选择放置方块类型 ([ 和 ] 键)
bool cyclePrev = keyDown(VK_OEM_4); // VK_OEM_4 是 '['
bool cycleNext = keyDown(VK_OEM_6); // VK_OEM_6 是 ']'
const int min_placeable_type = WALL; // BlockType::WALL (1)
const int max_placeable_type = TRAP; // BlockType::TRAP (5)
const int num_placeable_types = max_placeable_type - min_placeable_type + 1; // 总共 5 种可放置类型
if(cyclePrev && !cyclePrevPrev){
selectedBlockType = (BlockType)(((selectedBlockType - min_placeable_type - 1 + num_placeable_types) % num_placeable_types) + min_placeable_type);
// playSound("change_block.wav");
}
if(cycleNext && !cycleNextPrev){
selectedBlockType = (BlockType)(((selectedBlockType - min_placeable_type + 1) % num_placeable_types) + min_placeable_type);
// playSound("change_block.wav");
}
cyclePrevPrev = cyclePrev;
cycleNextPrev = cycleNext;
// 小地图尺寸调整 (+ 和 - 键)
bool plus = keyDown(VK_OEM_PLUS); // 用于 '+' 键
bool minus = keyDown(VK_OEM_MINUS); // 用于 '-' 键
if (plus && !plusPrev) {
minimap_radius = min(MAX_MINIMAP_RADIUS, minimap_radius + 1);
}
if (minus && !minusPrev) {
minimap_radius = max(MIN_MINIMAP_RADIUS, minimap_radius - 1);
}
plusPrev = plus;
minusPrev = minus;
// --- 交互逻辑 ---
// 玩家当前所在的瓷砖
int current_tile_x = static_cast<int>(player.x);
int current_tile_y = static_cast<int>(player.y);
int current_tile_type = getWorldAt(current_tile_x, current_tile_y);
if (current_tile_type == TREASURE) {
player.treasures++;
setWorldAt(current_tile_x, current_tile_y, EMPTY); // 消耗宝藏
// playSound("collect.wav");
} else if (current_tile_type == EXIT) {
gameOver = true; // 游戏结束,玩家胜利
gameEndReason = 1; // 设置胜利原因
} else if (current_tile_type == MONSTER || current_tile_type == TRAP) {
player.health -= 10; // 受到伤害
damageFlashTimer = DAMAGE_FLASH_DURATION; // 触发红色闪屏
if (player.health < 0) player.health = 0; // 生命值最低为 0
setWorldAt(current_tile_x, current_tile_y, EMPTY); // 伤害后移除怪物/陷阱
// playSound("hit.wav");
// 如果是怪物,也从动态列表中移除
if (current_tile_type == MONSTER) {
for (int i = monsters.size() - 1; i >= 0; --i) {
if (static_cast<int>(monsters[i].x) == current_tile_x && static_cast<int>(monsters[i].y) == current_tile_y) {
monsters.erase(monsters.begin() + i);
break;
}
}
}
}
if (player.health <= 0) {
gameOver = true; // 游戏结束,玩家失败
gameEndReason = 2; // 设置失败原因
}
// 计算挖掘/放置的目标瓷砖 (前方 1.5 单元距离)
int target_tile_x = static_cast<int>(player.x + player.cosA * 1.5);
int target_tile_y = static_cast<int>(player.y + player.sinA * 1.5);
// 防止 targeting 玩家所在的瓷砖
if (target_tile_x == current_tile_x && target_tile_y == current_tile_y) {
// 不与玩家所在瓷砖进行挖掘/放置交互
} else {
// 挖掘 (X 键) - 现在可以破坏任何非空方块
if(keyDown('X') && !xPrev){
int block_to_destroy = getWorldAt(target_tile_x, target_tile_y);
if(block_to_destroy != EMPTY){ // 破坏任何非空方块
setWorldAt(target_tile_x, target_tile_y, EMPTY);
// 如果是怪物,也从动态列表中移除
if (block_to_destroy == MONSTER) {
for (int i = monsters.size() - 1; i >= 0; --i) {
if (static_cast<int>(monsters[i].x) == target_tile_x && static_cast<int>(monsters[i].y) == target_tile_y) {
monsters.erase(monsters.begin() + i);
break;
}
}
}
// 添加粒子效果
COLORREF p_color = RGB(128,128,128); // 默认灰色 (墙壁)
switch (block_to_destroy) {
case WALL: p_color = RGB(128,128,128); break;
case TREASURE: p_color = RGB(255,255,0); break;
case EXIT: p_color = RGB(0,255,0); break;
case MONSTER: p_color = RGB(255,0,0); break;
case TRAP: p_color = RGB(139,0,0); break;
}
for(int i=0; i<10; ++i){ // 生成 10 个粒子
double px = target_tile_x + 0.5;
double py = target_tile_y + 0.5;
double pz = player.z; // 粒子在玩家高度附近生成
double angle = (double)rand()/RAND_MAX * PI * 2;
double speed = (double)rand()/RAND_MAX * 0.2 + 0.05;
double vz_p = (double)rand()/RAND_MAX * 0.1 + 0.05;
particles.push_back({
px, py, pz,
cos(angle) * speed, sin(angle) * speed, vz_p,
30, p_color // 粒子生命周期 30 帧
});
}
// playSound("dig.wav");
}
}
// 放置选定的方块类型 (1 键)
if (keyDown('1') && !onePrev) {
if (getWorldAt(target_tile_x, target_tile_y) == EMPTY) { // 只能在空地上放置
// 如果放置怪物,也添加到动态列表中
if (selectedBlockType == MONSTER) {
monsters.push_back({
(double)target_tile_x + 0.5,
(double)target_tile_y + 0.5,
MONSTER_MOVE_DELAY,
(int)monsters.size()
});
}
setWorldAt(target_tile_x, target_tile_y, selectedBlockType);
// playSound("place.wav");
}
}
}
xPrev = keyDown('X');
onePrev = keyDown('1');
// --- 怪物 AI 更新 ---
for (int i = 0; i < monsters.size(); ++i) {
Monster& m = monsters[i];
m.moveCooldown -= deltaTime; // 减少冷却时间
int old_mx = static_cast<int>(m.x); // 怪物当前所在瓷砖
int old_my = static_cast<int>(m.y);
double dx = player.x - m.x;
double dy = player.y - m.y;
double dist_to_player_sq = dx*dx + dy*dy; // 使用平方距离提高效率
// 只有在检测范围内且冷却时间结束时才移动
if (dist_to_player_sq <= MONSTER_DETECTION_RANGE * MONSTER_DETECTION_RANGE && m.moveCooldown <= 0) {
m.moveCooldown = MONSTER_MOVE_DELAY; // 重置冷却时间
int dir_x = 0;
if (dx > 0.1) dir_x = 1; // 阈值避免 dx 恰好为 0 导致的问题
else if (dx < -0.1) dir_x = -1;
int dir_y = 0;
if (dy > 0.1) dir_y = 1;
else if (dy < -0.1) dir_y = -1;
bool moved_this_turn = false;
// 优先移动策略 (斜向 -> X轴 -> Y轴)
if (dir_x != 0 && dir_y != 0) {
moved_this_turn = tryMoveMonster(m, old_mx, old_my, old_mx + dir_x, old_my + dir_y);
}
if (!moved_this_turn && dir_x != 0) {
moved_this_turn = tryMoveMonster(m, old_mx, old_my, old_mx + dir_x, old_my);
}
if (!moved_this_turn && dir_y != 0) {
moved_this_turn = tryMoveMonster(m, old_mx, old_my, old_mx, old_my + dir_y);
}
}
}
// --- 粒子更新 ---
for (int i = particles.size() - 1; i >= 0; --i) {
Particle& p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.z += p.vz;
p.vz -= PARTICLE_GRAVITY; // 应用重力
p.vx *= PARTICLE_FRICTION; // 应用摩擦力
p.vy *= PARTICLE_FRICTION;
p.vz *= PARTICLE_FRICTION;
p.life--;
if (p.life <= 0) {
particles.erase(particles.begin() + i); // 生命周期结束,移除粒子
}
}
// --- 渲染帧 (EasyX) ---
cleardevice(); // 清除整个窗口,绘制新帧
// 根据俯仰角和玩家 Z 高度调整“地平线”
int verticalOffset = int(player.pitch * (SCREEN_HEIGHT / PI / 2.0));
verticalOffset += int((player.z - PLAYER_HEIGHT) * SCREEN_HEIGHT * 0.5); // 根据玩家 Z 高度调整,0.5 因子使效果不那么极端
int halfH_shifted = (SCREEN_HEIGHT>>1) + verticalOffset; // 新的地平线 Y 坐标
// 天空 / 天花板
COLORREF skyBaseColor = backroomsMode ? RGB(255, 230, 100) : RGB(135,206,235); // 后室模式为黄色调,否则为天蓝色
setfillcolor(skyBaseColor);
solidrectangle(0, 0, SCREEN_WIDTH, halfH_shifted);
// 后室模式: 模拟天花板灯光 (简单的矩形)
if(backroomsMode){
int light_grid_x_count = 5;
int light_grid_y_count = 3;
int light_size = 15; // 方形灯的大小
for (int lx = 0; lx < light_grid_x_count; ++lx) {
for (int ly = 0; ly < light_grid_y_count; ++ly) {
int center_x = (SCREEN_WIDTH / light_grid_x_count) * (lx + 0.5);
int center_y = (halfH_shifted / light_grid_y_count) * (ly + 0.5);
setfillcolor(WHITE);
solidrectangle(center_x - light_size/2, center_y - light_size/2,
center_x + light_size/2, center_y + light_size/2);
}
}
}
// 地面
COLORREF groundBaseColor = backroomsMode ? RGB(180, 160, 50) : RGB(110,110,110); // 后室模式为深黄色调,否则为灰色
setfillcolor(groundBaseColor);
solidrectangle(0, halfH_shifted, SCREEN_WIDTH, SCREEN_HEIGHT);
// 射线投射
for(int x=0; x<SCREEN_WIDTH; ++x) {
double cameraX = 2 * x / double(SCREEN_WIDTH) - 1; // 屏幕 x 坐标在 [-1, 1] 范围内的映射
double rayDirX = player.cosA - player.sinA * cameraX; // 射线方向 X 分量
double rayDirY = player.sinA + player.cosA * cameraX; // 射线方向 Y 分量
int mapX = int(player.x), mapY = int(player.y); // 射线当前所在的地图块
double deltaX = fabs(1 / rayDirX), deltaY = fabs(1 / rayDirY); // 射线在 X/Y 方向前进一格所需距离
double sideX, sideY; // 射线到下一个 X/Y 方向边界的距离
int stepX, stepY; // 步进方向 (+1 或 -1)
if (rayDirX < 0) { stepX = -1; sideX = (player.x - mapX) * deltaX; }
else { stepX = 1; sideX = (mapX + 1.0 - player.x) * deltaX; }
if (rayDirY < 0) { stepY = -1; sideY = (player.y - mapY) * deltaY; }
else { stepY = 1; sideY = (mapY + 1.0 - player.y) * deltaY; }
int side; // 击中的是 X 边 (0) 还是 Y 边 (1)
int hitType=WALL; // 默认击中墙壁
// DDA 算法 (数字差分分析)
while(true) {
if(sideX < sideY) { sideX += deltaX; mapX += stepX; side = 0; }
else { sideY += deltaY; mapY += stepY; side = 1; }
hitType = getWorldAt(mapX, mapY); // 获取当前瓷砖类型
if(hitType) break; // 击中墙壁或物体则退出
}
// 计算垂直距离
double perp = side==0 ? (mapX - player.x + (1-stepX)/2.0)/rayDirX
: (mapY - player.y + (1-stepY)/2.0)/rayDirY;
perp = max(perp, 0.1); // 避免除以零或过小的数
// 计算墙壁在屏幕上的顶部和底部 Y 坐标
// 假设墙壁高度为 1 (从 Z=0 到 Z=1)
int wall_top_y = int(halfH_shifted - (1.0 - player.z) * SCREEN_HEIGHT / perp);
int wall_bottom_y = int(halfH_shifted - (0.0 - player.z) * SCREEN_HEIGHT / perp);
int drawStartY = max(0, wall_top_y); // 实际绘制的起始 Y 坐标 (屏幕范围内)
int drawEndY = min(SCREEN_HEIGHT, wall_bottom_y); // 实际绘制的结束 Y 坐标 (屏幕范围内)
// 墙壁着色和颜色
double shade_factor = 1.0 / (1.0 + perp * perp * 0.01); // 距离衰减着色
shade_factor = max(0.0, min(1.0, shade_factor)); // 限制在 0-1 之间
COLORREF col;
int baseR, baseG, baseB;
switch (hitType) {
case WALL:
// 后室模式为黄色调,否则 Y 边 (侧面) 更暗,X 边 (正面) 稍亮
baseR = backroomsMode ? 255 : (side ? 100 : 150);
baseG = backroomsMode ? 230 : (side ? 100 : 150);
baseB = backroomsMode ? 100 : (side ? 100 : 150);
break;
case TREASURE: baseR = 255; baseG = 255; baseB = 0; break; // 黄色
case EXIT: baseR = 0; baseG = 255; baseB = 0; break; // 绿色
case MONSTER: baseR = 255; baseG = 0; baseB = 0; break; // 红色
case TRAP: baseR = 139; baseG = 0; baseB = 0; break; // 暗红色
default: baseR = 0; baseG = 0; baseB = 0; break; // 不应出现非空方块
}
// 应用距离着色
col = RGB(static_cast<int>(baseR * shade_factor),
static_cast<int>(baseG * shade_factor),
static_cast<int>(baseB * shade_factor));
setlinecolor(col); // 设置线条颜色
line(x, drawStartY, x, drawEndY); // 绘制垂直的墙壁线段
}
// 粒子渲染 (在墙壁之后绘制,使其显示在墙壁之上)
for (const auto& p : particles) {
double px_rel = p.x - player.x; // 粒子相对于玩家的 X 坐标
double py_rel = p.y - player.y; // 粒子相对于玩家的 Y 坐标
double pz_rel = p.z - player.z; // 粒子相对于玩家的 Z 坐标
// 根据玩家偏航角旋转相对坐标
double rotX = px_rel * player.cosA + py_rel * player.sinA;
double rotY_depth = py_rel * player.cosA - px_rel * player.sinA; // 深度 (旋转前的 Y 分量)
// 应用俯仰角旋转到垂直分量
double final_pz = pz_rel * player.cosP - rotY_depth * player.sinP; // 旋转后的有效 Z
double final_depth = rotY_depth * player.cosP + pz_rel * player.sinP; // 旋转后的有效深度
if (final_depth > 0.1) { // 粒子在玩家前方
double inv_depth = 1.0 / final_depth;
int screenX = int(SCREEN_WIDTH / 2 + rotX * SCREEN_HEIGHT * inv_depth); // 计算屏幕 X 坐标
int screenY = int(SCREEN_HEIGHT / 2 - final_pz * (SCREEN_HEIGHT/2) * inv_depth); // 计算屏幕 Y 坐标
// 限制屏幕坐标范围并绘制小圆点
if (screenX >= 0 && screenX < SCREEN_WIDTH && screenY >= 0 && screenY < SCREEN_HEIGHT) {
int particle_size = max(1, int(5.0 / final_depth)); // 粒子大小随距离变化
setfillcolor(p.color); // 设置粒子颜色
solidcircle(screenX, screenY, particle_size); // 绘制填充圆
}
}
}
// 怪物渲染 (与粒子渲染类似)
for (const auto& m : monsters) {
double px_rel = m.x - player.x;
double py_rel = m.y - player.y;
double pz_rel = PLAYER_HEIGHT - player.z; // 怪物假定在玩家高度 (离地 0.5)
// 根据玩家偏航角旋转相对坐标
double rotX = px_rel * player.cosA + py_rel * player.sinA;
double rotY_depth = py_rel * player.cosA - px_rel * player.sinA;
// 应用俯仰角旋转
double final_pz = pz_rel * player.cosP - rotY_depth * player.sinP;
double final_depth = rotY_depth * player.cosP + pz_rel * player.sinP;
if (final_depth > 0.1) { // 怪物在玩家前方
double inv_depth = 1.0 / final_depth;
int screenX = int(SCREEN_WIDTH / 2 + rotX * SCREEN_HEIGHT * inv_depth);
int screenY = int(SCREEN_HEIGHT / 2 - final_pz * (SCREEN_HEIGHT/2) * inv_depth);
// 限制屏幕坐标范围
if (screenX >= 0 && screenX < SCREEN_WIDTH && screenY >= 0 && screenY < SCREEN_HEIGHT) {
// 绘制一个简单的红色矩形作为怪物
int monster_width = max(5, int(50.0 / final_depth)); // 怪物大小随距离变化
int monster_height = max(5, int(70.0 / final_depth));
setfillcolor(RGB(255,0,0)); // 红色
solidrectangle(screenX - monster_width/2, screenY - monster_height/2,
screenX + monster_width/2, screenY + monster_height/2);
}
}
}
// HUD (信息显示)
settextstyle(16, 0, _T("Consolas")); // 字体大小
setbkmode(TRANSPARENT); // 文本背景透明
settextcolor(player.health > 50 ? GREEN : RED); // 生命值根据高低显示不同颜色
// 注意: to_string() 返回的是 std::string,需要转换为 TCHAR*。
// 最简单的方法是构建 std::wstring,然后获取其 c_str()。
// 或者使用 _stprintf_s 或 _snwprintf 等函数。
// 考虑到兼容性,这里使用 _T() 和 string::c_str(),但这依赖于项目字符集设置正确。
outtextxy(5, 5, _T(("HP:" + to_string(player.health)).c_str())); // 显示生命值
settextcolor(YELLOW);
outtextxy(SCREEN_WIDTH - 150, 5, _T(("TREASURES:" + to_string(player.treasures)).c_str())); // 显示宝藏数量
long long elapsedTime = (currentFrameTime - startTime) / 1000; // 游戏已进行时间 (秒)
settextcolor(WHITE);
outtextxy(5, 25, _T(("TIME:" + to_string(elapsedTime) + "s").c_str())); // 显示游戏时间
string selected_block_str_base = "PLACE: ";
string selected_block_name;
switch (selectedBlockType) {
case WALL: selected_block_name = "墙壁"; break;
case TREASURE: selected_block_name = "宝藏"; break;
case EXIT: selected_block_name = "出口"; break;
case MONSTER: selected_block_name = "怪物"; break;
case TRAP: selected_block_name = "陷阱"; break;
default: selected_block_name = "未知"; break;
}
string full_selected_block_str = selected_block_str_base + selected_block_name;
settextcolor(CYAN);
outtextxy(SCREEN_WIDTH - 200, 25, _T(full_selected_block_str.c_str())); // 显示当前选择的放置方块类型
// 准星
setlinecolor(WHITE); // 白色
line(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, SCREEN_WIDTH/2 + 5, SCREEN_HEIGHT/2); // 水平线
line(SCREEN_WIDTH/2, SCREEN_HEIGHT/2 - 5, SCREEN_WIDTH/2, SCREEN_HEIGHT/2 + 5); // 垂直线
// 小地图
if(showMiniMap){
int V = 2 * minimap_radius + 1; // 小地图总宽度/高度 (瓷砖数量)
int tile_size = max(2, min(20, (SCREEN_WIDTH / 4) / V)); // 瓷砖大小,限制在合理范围
int minimap_draw_width = V * tile_size;
int minimap_draw_height = V * tile_size;
int sx = SCREEN_WIDTH - minimap_draw_width - 10; // 小地图绘制起始 X (右侧留边 10 像素)
int sy = 50; // 小地图绘制起始 Y (在 HUD 下方)
// 绘制小地图边框
setlinecolor(RGB(150,150,150)); // 修复: 将GREY替换为RGB值
rectangle(sx - 1, sy - 1, sx + minimap_draw_width + 1, sy + minimap_draw_height + 1);
int cx=static_cast<int>(player.x), cy=static_cast<int>(player.y); // 玩家当前所在地图块
for(int dy_idx=0;dy_idx<V;++dy_idx){
for(int dx_idx=0;dx_idx<V;++dx_idx){
int map_dx = dx_idx - minimap_radius; // 相对于玩家的 X 偏移
int map_dy = dy_idx - minimap_radius; // 相对于玩家的 Y 偏移
int wx = cx + map_dx;
int wy = cy + map_dy;
if (minimap_flipped) { // 如果小地图翻转,则水平翻转 X 坐标
wx = cx - map_dx;
}
int v=getWorldAt(wx,wy); // 获取该位置的方块类型
COLORREF col;
switch(v){
case WALL: col=RGB(100,100,100); break; // 墙壁 (深灰色)
case TREASURE: col=RGB(255,255,0); break; // 宝藏 (黄色)
case EXIT: col=RGB(0,255,0); break; // 出口 (绿色)
case MONSTER: col=RGB(255,0,0); break; // 怪物 (红色)
case TRAP: col=RGB(139,0,0); break; // 陷阱 (暗红色)
default: col=backroomsMode ? RGB(255, 230, 100) : RGB(135,206,235); break; // 空地颜色与天空颜色匹配
}
setfillcolor(col); // 设置填充颜色
solidrectangle(sx + dx_idx * tile_size, sy + dy_idx * tile_size,
sx + (dx_idx + 1) * tile_size -1, sy + (dy_idx + 1) * tile_size -1); // 绘制填充矩形
}
}
// 绘制玩家在小地图中心
setfillcolor(RED);
solidcircle(sx + minimap_radius * tile_size + tile_size/2, sy + minimap_radius * tile_size + tile_size/2, tile_size/3);
// 绘制玩家方向箭头
char arrow_char_raw = ' ';
double normalized_angle = fmod(player.angle, 2 * PI); // 归一化角度到 [0, 2*PI)
if (normalized_angle < 0) normalized_angle += 2 * PI;
// 根据角度选择箭头字符 (8 个方向)
if (normalized_angle >= (15 * PI / 8.0) || normalized_angle < (PI / 8.0)) arrow_char_raw = '>';
else if (normalized_angle >= (PI / 8.0) && normalized_angle < (3 * PI / 8.0)) arrow_char_raw = '/'; // 右上
else if (normalized_angle >= (3 * PI / 8.0) && normalized_angle < (5 * PI / 8.0)) arrow_char_raw = '^';
else if (normalized_angle >= (5 * PI / 8.0) && normalized_angle < (7 * PI / 8.0)) arrow_char_raw = '\\'; // 左上
else if (normalized_angle >= (7 * PI / 8.0) && normalized_angle < (9 * PI / 8.0)) arrow_char_raw = '<';
else if (normalized_angle >= (9 * PI / 8.0) && normalized_angle < (11 * PI / 8.0)) arrow_char_raw = '/'; // 左下
else if (normalized_angle >= (11 * PI / 8.0) && normalized_angle < (13 * PI / 8.0)) arrow_char_raw = 'v';
else arrow_char_raw = '\\'; // 右下
settextcolor(RED);
settextstyle(12, 0, _T("Consolas")); // 箭头使用较小的字体
// 箭头字符是ASCII,不需要_T(),但是 outtextxy 期望 TCHAR*。
// 简单处理:将 char 转换为 string,再用 _T()。
string arrow_str(1, arrow_char_raw);
outtextxy(sx + minimap_radius * tile_size + tile_size/2 - textwidth(_T("M"))/2,
sy + minimap_radius * tile_size + tile_size/2 - textheight(_T("M"))/2,
_T(arrow_str.c_str()));
// 恢复 HUD 字体样式
settextstyle(16, 0, _T("Consolas"));
}
// 伤害闪屏效果
if (damageFlashTimer > 0.0) {
// EasyX 直接的 alpha 混合比较复杂,这里使用简单粗暴的方案:
// 直接在整个屏幕上绘制一个实心红色矩形,模拟闪烁效果
setfillcolor(RGB(255, 0, 0)); // 纯红色
solidrectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
}
// 刷新并准备下一帧
FlushBatchDraw();
Sleep(10); // 控制帧率,避免过高
}
// 结束批量绘制并关闭窗口
EndBatchDraw();
closegraph();
// 根据游戏结束原因显示最终消息
if (gameEndReason == 2) { // 玩家失败
displayGameEndScreen("GAME OVER! You ran out of health. Time: " + to_string((GetTickCount() - startTime) / 1000) + "s");
} else if (gameEndReason == 1) { // 玩家胜利
displayGameEndScreen("YOU ESCAPED! Treasures collected: " + to_string(player.treasures) + ". Time: " + to_string((GetTickCount() - startTime) / 1000) + "s");
} else { // 手动退出
displayGameEndScreen("Thanks for playing!");
}
return 0;
}
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...