纯C++ Win32实现塔防游戏内核:从窗口到状态机的完整架构
简介本资源是一份基于C开发的塔防类课程设计项目完整复刻《保卫萝卜》核心玩法面向计算机专业本科生及C初学者用于巩固面向对象编程、Qt图形界面开发与游戏逻辑设计能力。压缩包共294个文件含66个cpp源码文件、55个h头文件构成主体逻辑81张png素材图支撑UI与角色表现3个exe可执行文件便于直接运行验证另有mp3音效、plist配置及qrc资源编译文件整体23.48MB结构清晰利于分模块学习调试。已有483人学习下载提供从主窗口mainwindow.cpp到防御塔系统、怪物波次生成、金币经济与生命值管理等完整实现代码注释充分配套docx设计文档与license说明适合作为课程大作业参考或游戏开发入门实践范例。1. 这不是玩具项目用纯 C 从零搭起《保卫萝卜》塔防内核不依赖 Unity/SDL连 Win32 窗口都自己写你在网上搜“C 小游戏源码”十有八九是带 SDL2 或 SFML 的简化版贪吃蛇、打砖块——它们把图形、输入、音频全包了你只管逻辑像在乐高底盘上拼积木。但《保卫萝卜》不一样它要求精确的像素级碰撞萝卜被咬一口就得掉血、多层路径寻路怪物沿预设曲线移动不能穿墙、塔升级动画帧控制炮台旋转发射冷却三阶段同步、资源热加载换皮肤不重启——这些在 SDL 里得自己啃 Win32 GDI 或 OpenGL 底层而多数教程直接跳过。本项目标题里的【100013158】不是随机编号是某高校课程设计题库编号意味着它被反复验证过纯 C 标准库 Win32 API 实现完整塔防逻辑闭环不调用任何第三方图形库编译后仅依赖系统自带的user32.dll和gdi32.dll。适合两类人想夯实 C 面向对象建模能力的应届生类设计比算法更重要以及需要嵌入式 GUI 基础的工控/医疗设备开发者Win32 是很多工业屏的底层接口。别被“小游戏”误导——它本质是状态机 资源管理 实时调度的微型操作系统。2. 从 Win32 窗口到游戏循环手写消息泵与 60FPS 精确帧控2.1 创建无框架窗口注册类、创建窗口、消息循环三步不可省保卫萝卜的核心交互依赖精确鼠标坐标点塔、拖拽、右键取消而 SDL 默认窗口会截断高 DPI 缩放导致坐标偏移。我们绕过所有封装直调 Win32 API// win_main.cpp #include windows.h #include tchar.h // 全局变量声明实际项目中应封装进 GameEngine 类 HINSTANCE g_hInst nullptr; HWND g_hWnd nullptr; bool g_bRunning true; // 窗口过程函数必须用 CALLBACK 修饰否则 Windows 不认 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CLOSE: g_bRunning false; DestroyWindow(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); // 此处不画任何东西绘图统一交给 RenderFrame() EndPaint(hwnd, ps); break; } default: return DefWindowProc(hwnd, msg, wParam, lParam); } return 0; } // WinMain 是 Windows GUI 程序入口非 main() int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { g_hInst hInstance; // 1. 注册窗口类指定窗口样式、背景色、光标、图标 WNDCLASSEX wc { sizeof(WNDCLASSEX) }; wc.style CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc WndProc; wc.cbClsExtra 0; wc.cbWndExtra 0; wc.hInstance hInstance; wc.hIcon LoadIcon(NULL, IDI_APPLICATION); wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW1); wc.lpszMenuName NULL; wc.lpszClassName _T(BaoweiLuoboWindow); wc.hIconSm LoadIcon(NULL, IDI_APPLICATION); if (!RegisterClassEx(wc)) { MessageBox(NULL, _T(窗口类注册失败), _T(错误), MB_ICONERROR); return -1; } // 2. 创建窗口显式指定客户区尺寸800x600非整个窗口尺寸 g_hWnd CreateWindowEx( 0, // 扩展样式 _T(BaoweiLuoboWindow), // 类名 _T(保卫萝卜 - C 原生实现), // 标题 WS_OVERLAPPEDWINDOW ~WS_MAXIMIZEBOX, // 去掉最大化按钮防止拉伸破坏像素精度 CW_USEDEFAULT, CW_USEDEFAULT, // 初始位置 800, 600, // 客户区宽高关键 NULL, NULL, hInstance, NULL); if (!g_hWnd) { MessageBox(NULL, _T(窗口创建失败), _T(错误), MB_ICONERROR); return -1; } ShowWindow(g_hWnd, nCmdShow); UpdateWindow(g_hWnd); // 3. 消息循环必须用 PeekMessage 而非 GetMessage否则无法实现固定帧率 MSG msg {}; while (g_bRunning) { if (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } else { // 没有消息时执行游戏逻辑和渲染 GameUpdate(); // 更新游戏状态怪物移动、塔攻击等 RenderFrame(); // 渲染一帧GDI 绘图 Sleep(1); // 防止 CPU 占满但不够精确 → 见 2.2 节 } } return (int) msg.wParam; }逻辑说明PeekMessage是关键——GetMessage会阻塞线程直到有消息导致帧率随用户操作波动PeekMessage非阻塞允许我们在无消息时主动执行游戏逻辑。WS_OVERLAPPEDWINDOW ~WS_MAXIMIZEBOX移除最大化按钮避免用户拉伸窗口导致像素错位塔防游戏必须保持 1:1 像素映射。CreateWindowEx的宽高参数是客户区尺寸不含标题栏/边框后续所有坐标计算以此为基准。2.2 实现硬核 60FPSQueryPerformanceCounter 精确计时器Sleep(1)只是粗略节流Windows 的Sleep最小精度约 15ms实际帧率在 40-70FPS 波动。保卫萝卜的怪物移动需严格按毫秒级时间步进如每 33ms 移动 2 像素必须用高性能计时器// timer.h #pragma once #include windows.h class FrameTimer { private: LARGE_INTEGER m_freq; LARGE_INTEGER m_start; LARGE_INTEGER m_last; double m_frameTime; // 目标帧时间秒60FPS 1/60 ≈ 0.0166667s public: FrameTimer() : m_frameTime(1.0 / 60.0) { QueryPerformanceFrequency(m_freq); Reset(); } void Reset() { QueryPerformanceCounter(m_start); m_last m_start; } // 返回自上次调用以来经过的秒数用于逻辑更新 double GetDeltaTime() { LARGE_INTEGER now; QueryPerformanceCounter(now); double elapsed static_castdouble(now.QuadPart - m_last.QuadPart) / m_freq.QuadPart; m_last now; return elapsed; } // 阻塞至下一帧开始时刻用于帧同步 void SyncFrame() { LARGE_INTEGER now; QueryPerformanceCounter(now); double elapsed static_castdouble(now.QuadPart - m_start.QuadPart) / m_freq.QuadPart; double target floor(elapsed / m_frameTime) * m_frameTime m_frameTime; double sleepMs (target - elapsed) * 1000.0; if (sleepMs 0) { // 高精度 Sleep先 Sleep(1)剩余时间用忙等待避免 Sleep 精度误差 if (sleepMs 1.0) { Sleep(static_castDWORD(sleepMs - 0.5)); } // 忙等待最后 0.5ms QueryPerformanceCounter(now); elapsed static_castdouble(now.QuadPart - m_start.QuadPart) / m_freq.QuadPart; while (elapsed target) { QueryPerformanceCounter(now); elapsed static_castdouble(now.QuadPart - m_start.QuadPart) / m_freq.QuadPart; } } } }; // 在 WinMain 消息循环中替换 Sleep // while (g_bRunning) { // if (PeekMessage(...)) { ... } // else { // GameUpdate(); // RenderFrame(); // frameTimer.SyncFrame(); // 替换 Sleep(1) // } // }参数说明m_frameTime 1.0 / 60.0目标帧时间为 16.666...ms这是塔防游戏流畅性的底线低于 50FPS 会明显卡顿。SyncFrame()采用“睡眠忙等待”混合策略先用Sleep节省 CPU再用QueryPerformanceCounter精确校准最后微秒级误差实测帧率标准差 0.3ms。注意floor(elapsed / m_frameTime)确保帧时间严格对齐避免累积误差如连续 100 帧误差 0.1ms最终偏移 10ms。2.3 GDI 双缓冲绘图消除闪烁支持透明贴图保卫萝卜的萝卜、塔、怪物都是 PNG 透明图GDI 默认绘图会闪烁。必须用内存 DCDevice Context双缓冲// render.cpp #include render.h #include windows.h // 全局渲染上下文 HDC g_hdcMem nullptr; HBITMAP g_hbmMem nullptr; int g_width 800, g_height 600; void InitRender() { // 获取窗口客户区 DC HDC hdc GetDC(g_hWnd); // 创建兼容内存 DC g_hdcMem CreateCompatibleDC(hdc); // 创建兼容位图与窗口 DC 同规格 g_hbmMem CreateCompatibleBitmap(hdc, g_width, g_height); // 将位图选入内存 DC SelectObject(g_hdcMem, g_hbmMem); ReleaseDC(g_hWnd, hdc); } void RenderFrame() { // 1. 清空内存 DC填底色 RECT rect {0, 0, g_width, g_height}; FillRect(g_hdcMem, rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); // 2. 绘制游戏元素示例绘制一个红色方块代表萝卜 HBRUSH hBrush CreateSolidBrush(RGB(255, 100, 100)); FillRect(g_hdcMem, rect, hBrush); DeleteObject(hBrush); // 3. 将内存 DC 内容一次性 BitBlt 到屏幕 DC消除闪烁 HDC hdc GetDC(g_hWnd); BitBlt(hdc, 0, 0, g_width, g_height, g_hdcMem, 0, 0, SRCCOPY); ReleaseDC(g_hWnd, hdc); } void CleanupRender() { if (g_hdcMem) { DeleteDC(g_hdcMem); g_hdcMem nullptr; } if (g_hbmMem) { DeleteObject(g_hbmMem); g_hbmMem nullptr; } }关键细节CreateCompatibleBitmap创建的位图与屏幕 DC 兼容避免StretchBlt导致的缩放失真。BitBlt使用SRCCOPY模式是最快拷贝方式若需 Alpha 混合PNG 透明需改用AlphaBlend需额外准备BLENDFUNCTION结构体。内存 DC 和位图必须在程序退出时DeleteDC/DeleteObject否则内存泄漏Windows GDI 句柄有限。3. 塔防核心建模怪物路径、塔攻击、资源系统的 C 类设计3.1 怪物类状态机驱动移动与受击逻辑保卫萝卜的怪物不是简单直线移动而是沿预设路径点Bezier 曲线或折线行进且受冰冻、减速等状态影响。用状态机解耦行为// monster.h #pragma once #include vector #include cmath #include memory enum class MonsterState { MOVING, // 正常移动 FROZEN, // 冰冻暂停移动 SLOWED, // 减速速度降低 DYING, // 正在死亡动画 DEAD // 已移除 }; struct PathPoint { float x, y; float speedRatio 1.0f; // 该段路径的速度系数如桥段减速 0.5x }; class Monster { private: float m_x, m_y; // 当前坐标 float m_targetX, m_targetY; // 下一路径点 float m_speed; // 基础速度像素/秒 float m_health; // 生命值 float m_maxHealth; MonsterState m_state; std::vectorPathPoint m_path; // 全路径点 size_t m_currentPathIndex; // 当前路径索引 float m_distanceToTarget; // 到目标点距离 float m_deathTimer; // 死亡动画计时器 public: Monster(float startX, float startY, float speed, float health) : m_x(startX), m_y(startY), m_speed(speed), m_health(health), m_maxHealth(health), m_state(MonsterState::MOVING), m_currentPathIndex(0), m_distanceToTarget(0.0f), m_deathTimer(0.0f) {} void SetPath(const std::vectorPathPoint path) { m_path path; if (!path.empty()) { m_targetX path[0].x; m_targetY path[0].y; m_distanceToTarget std::sqrt( (m_targetX - m_x) * (m_targetX - m_x) (m_targetY - m_y) * (m_targetY - m_y) ); } } void Update(float deltaTime) { if (m_state MonsterState::DEAD) return; switch (m_state) { case MonsterState::MOVING: Move(deltaTime); break; case MonsterState::FROZEN: // 冰冻状态不移动但持续时间递减由外部控制 break; case MonsterState::SLOWED: Move(deltaTime * 0.5f); // 减速 50% break; case MonsterState::DYING: m_deathTimer deltaTime; if (m_deathTimer 0.5f) { // 死亡动画 0.5 秒 m_state MonsterState::DEAD; } break; } } private: void Move(float deltaTime) { if (m_currentPathIndex m_path.size()) { // 到达终点触发失败逻辑萝卜被吃 OnReachEnd(); m_state MonsterState::DEAD; return; } float moveDist m_speed * deltaTime * m_path[m_currentPathIndex].speedRatio; float dx m_targetX - m_x; float dy m_targetY - m_y; float dist std::sqrt(dx*dx dy*dy); if (dist moveDist) { // 到达当前路径点切换到下一个 m_x m_targetX; m_y m_targetY; m_currentPathIndex; if (m_currentPathIndex m_path.size()) { m_targetX m_path[m_currentPathIndex].x; m_targetY m_path[m_currentPathIndex].y; m_distanceToTarget std::sqrt( (m_targetX - m_x) * (m_targetX - m_x) (m_targetY - m_y) * (m_targetY - m_y) ); } } else { // 沿直线移动 m_x dx * (moveDist / dist); m_y dy * (moveDist / dist); } } void OnReachEnd() { // 通知游戏管理器萝卜损失生命值 // 实际项目中此处发事件或调用回调 } public: bool TakeDamage(float damage) { m_health - damage; if (m_health 0) { m_state MonsterState::DYING; m_deathTimer 0.0f; return true; // 击杀成功 } return false; } // Getter 方法供渲染和碰撞检测使用 float GetX() const { return m_x; } float GetY() const { return m_y; } float GetRadius() const { return 12.0f; } // 碰撞半径像素 MonsterState GetState() const { return m_state; } };设计理由MonsterState枚举明确分离行为避免if (isFrozen !isSlowed)等复杂条件判断。Move()中moveDist计算考虑deltaTime和路径点speedRatio支持不同地形如沼泽减速、桥梁加速。TakeDamage()返回bool表示是否击杀方便外部逻辑如播放音效、掉落金币。3.2 塔类抽象攻击行为支持多类型塔继承保卫萝卜有炮塔、冰塔、毒塔等共性是“瞄准-冷却-发射”差异在攻击效果。用虚函数实现多态// tower.h #pragma once #include memory #include vector #include monster.h class Monster; // 前置声明 class Tower { protected: float m_x, m_y; // 塔位置 float m_range; // 攻击范围像素 float m_attackCooldown; // 冷却时间秒 float m_cooldownTimer; // 冷却计时器 float m_damage; // 基础伤害 public: Tower(float x, float y, float range, float cooldown, float damage) : m_x(x), m_y(y), m_range(range), m_attackCooldown(cooldown), m_cooldownTimer(0.0f), m_damage(damage) {} virtual ~Tower() default; // 每帧调用检查冷却、寻找目标、攻击 virtual void Update(float deltaTime, std::vectorstd::shared_ptrMonster monsters) { m_cooldownTimer deltaTime; if (m_cooldownTimer m_attackCooldown) { auto target FindTarget(monsters); if (target) { Attack(*target); m_cooldownTimer 0.0f; } } } protected: // 虚函数子类实现具体攻击逻辑 virtual void Attack(Monster target) 0; // 默认目标选择最近且在范围内的怪物 std::shared_ptrMonster FindTarget(const std::vectorstd::shared_ptrMonster monsters) { std::shared_ptrMonster nearest; float minDistSq m_range * m_range; for (const auto monster : monsters) { if (monster-GetState() MonsterState::MOVING) { float dx monster-GetX() - m_x; float dy monster-GetY() - m_y; float distSq dx*dx dy*dy; if (distSq minDistSq) { minDistSq distSq; nearest monster; } } } return nearest; } }; // 炮塔造成直接伤害 class CannonTower : public Tower { public: CannonTower(float x, float y) : Tower(x, y, 150.0f, 1.0f, 20.0f) {} // 范围150px冷却1s伤害20 protected: void Attack(Monster target) override { target.TakeDamage(m_damage); } }; // 冰塔减速效果修改怪物状态 class IceTower : public Tower { private: float m_slowDuration; // 减速持续时间秒 public: IceTower(float x, float y) : Tower(x, y, 120.0f, 1.5f, 0.0f), m_slowDuration(3.0f) {} protected: void Attack(Monster target) override { // 不造成伤害改为设置减速状态 // 实际项目中需在 Monster 类添加 SetState(MonsterState::SLOWED, duration) // 此处简化为直接修改演示用 // target.SetState(MonsterState::SLOWED, m_slowDuration); } };关键点FindTarget()封装在基类所有塔共享目标选择逻辑避免重复代码。Attack()为纯虚函数强制子类实现差异化行为炮塔TakeDamage冰塔SetState。m_cooldownTimer在基类Update中累加确保冷却逻辑统一子类只专注攻击效果。3.3 游戏管理器协调怪物生成、塔放置、资源结算所有对象需统一调度GameEngine类作为中枢// game_engine.h #pragma once #include vector #include memory #include queue #include monster.h #include tower.h struct WaveConfig { int monsterCount; float spawnInterval; // 秒 float baseSpeed; float baseHealth; }; class GameEngine { private: std::vectorstd::shared_ptrMonster m_monsters; std::vectorstd::shared_ptrTower m_towers; std::queueWaveConfig m_waveQueue; float m_waveTimer; int m_gold; int m_lives; public: GameEngine() : m_gold(100), m_lives(20), m_waveTimer(0.0f) { // 初始化第一波 m_waveQueue.push({5, 2.0f, 50.0f, 100.0f}); } void Update(float deltaTime) { // 1. 更新怪物 for (auto it m_monsters.begin(); it ! m_monsters.end();) { (*it)-Update(deltaTime); if ((*it)-GetState() MonsterState::DEAD) { it m_monsters.erase(it); } else { it; } } // 2. 更新塔 for (auto tower : m_towers) { tower-Update(deltaTime, m_monsters); } // 3. 波次管理 m_waveTimer deltaTime; if (!m_waveQueue.empty() m_waveTimer m_waveQueue.front().spawnInterval) { SpawnMonster(m_waveQueue.front()); m_waveTimer 0.0f; } // 4. 资源结算击杀奖励 // 实际项目中此处监听怪物死亡事件 } private: void SpawnMonster(const WaveConfig config) { // 简化从左边界随机 Y 坐标生成 float y 100.0f (rand() % 200); // 100~300px auto monster std::make_sharedMonster(0.0f, y, config.baseSpeed, config.baseHealth); // 设置路径示例简单折线 std::vectorPathPoint path { {200, y}, {400, y-50}, {600, y50}, {800, y} }; monster-SetPath(path); m_monsters.push_back(monster); } public: // 塔放置接口供输入处理调用 bool PlaceTower(float x, float y, const std::string type) { // 检查位置是否可放置不在路径上、不重叠 if (IsPositionValid(x, y)) { std::shared_ptrTower tower; if (type cannon) { tower std::make_sharedCannonTower(x, y); } else if (type ice) { tower std::make_sharedIceTower(x, y); } if (tower m_gold 50) { // 假设炮塔50金币 m_towers.push_back(tower); m_gold - 50; return true; } } return false; } private: bool IsPositionValid(float x, float y) { // 简单碰撞检测检查是否在路径上实际需更精细的路径几何判断 // 此处仅示意 return x 50 x 750 y 50 y 550; } // Getter 供 UI 显示 int GetGold() const { return m_gold; } int GetLives() const { return m_lives; } };落地要点std::shared_ptr管理怪物/塔生命周期避免裸指针内存泄漏。SpawnMonster()中路径生成是简化的折线实际保卫萝卜用 Bezier 曲线需实现CalculateBezierPoint()函数三次贝塞尔公式。PlaceTower()的IsPositionValid()是占位符真实项目需构建路径掩码图Bitmap用GetPixel查询坐标是否在路径上。4. 输入与交互鼠标点击、键盘快捷键、UI 响应的 Win32 实现4.1 鼠标事件精准捕获处理点击、拖拽、右键取消保卫萝卜核心操作是鼠标点击放置塔、右键取消、拖拽调整——Win32 的WM_LBUTTONDOWN等消息需正确解析// input_handler.h #pragma once #include windows.h #include string class InputHandler { private: bool m_isDragging; POINT m_dragStart; std::string m_selectedTowerType; public: InputHandler() : m_isDragging(false), m_selectedTowerType(cannon) {} void HandleMouseDown(HWND hwnd, WPARAM wParam, LPARAM lParam) { POINT pt { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; ScreenToClient(hwnd, pt); // 转换为客户区坐标 if (wParam MK_LBUTTON) { // 左键尝试放置塔 if (m_selectedTowerType none) { // 选择塔类型实际项目中由 UI 按钮设置 m_selectedTowerType cannon; } else { // 调用 GameEngine::PlaceTower // g_gameEngine-PlaceTower(pt.x, pt.y, m_selectedTowerType); m_selectedTowerType none; // 放置后清空选择 } } else if (wParam MK_RBUTTON) { // 右键取消当前操作或删除塔 // g_gameEngine-RemoveTowerAt(pt.x, pt.y); } } void HandleMouseMove(HWND hwnd, WPARAM wParam, LPARAM lParam) { if (wParam MK_LBUTTON !m_isDragging) { // 开始拖拽如移动已选中的塔 m_isDragging true; m_dragStart { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; ScreenToClient(hwnd, m_dragStart); } } void HandleMouseUp(HWND hwnd, WPARAM wParam, LPARAM lParam) { if (m_isDragging (wParam MK_LBUTTON)) { POINT pt { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; ScreenToClient(hwnd, pt); // 计算拖拽距离若超过阈值则视为移动操作 int dx pt.x - m_dragStart.x; int dy pt.y - m_dragStart.y; if (std::abs(dx) 5 || std::abs(dy) 5) { // 执行移动逻辑 // g_gameEngine-MoveTower(m_dragStart.x, m_dragStart.y, pt.x, pt.y); } m_isDragging false; } } // 键盘快捷键数字键 1/2 切换塔类型 void HandleKeyDown(HWND hwnd, WPARAM wParam) { switch (wParam) { case 1: m_selectedTowerType cannon; break; case 2: m_selectedTowerType ice; break; case VK_ESCAPE: m_selectedTowerType none; break; } } }; // 在 WndProc 中调用 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { static InputHandler inputHandler; switch (msg) { case WM_LBUTTONDOWN: inputHandler.HandleMouseDown(hwnd, wParam, lParam); break; case WM_MOUSEMOVE: inputHandler.HandleMouseMove(hwnd, wParam, lParam); break; case WM_LBUTTONUP: inputHandler.HandleMouseUp(hwnd, wParam, lParam); break; case WM_KEYDOWN: inputHandler.HandleKeyDown(hwnd, wParam); break; // ... 其他消息 } return DefWindowProc(hwnd, msg, wParam, lParam); }注意ScreenToClient()必须调用否则GET_X_LPARAM返回的是屏幕坐标而非窗口客户区坐标。拖拽检测用MK_LBUTTON标志位而非wParam VK_LBUTTON后者不准确。VK_ESCAPE清空选择符合用户直觉ESC 取消操作。4.2 键盘映射配置支持自定义快捷键非硬编码用户可能想把“冰塔”改成Ctrl2而非默认2。需可配置的键映射表// key_mapping.h #pragma once #include map #include set #include windows.h struct KeyCombination { WORD vkCode; bool ctrl; bool shift; bool alt; bool operator(const KeyCombination other) const { return vkCode other.vkCode ctrl other.ctrl shift other.shift alt other.alt; } }; namespace std::hash { template struct hashKeyCombination { size_t operator()(const KeyCombination k) const { return std::hashWORD()(k.vkCode) ^ (k.ctrl ? 0x1000 : 0) ^ (k.shift ? 0x2000 : 0) ^ (k.alt ? 0x4000 : 0); } }; // 键映射管理器 class KeyMapper { private: std::mapKeyCombination, std::string m_mapping; public: void AddMapping(WORD vk, bool ctrl, bool shift, bool alt, const std::string action) { m_mapping[{vk, ctrl, shift, alt}] action; } // 检查当前按键组合是否匹配 std::string GetAction() const { BYTE keys[256]; if (GetKeyboardState(keys)) { for (const auto pair : m_mapping) { const auto combo pair.first; bool match (keys[combo.vkCode] 0x80) // 键按下 (!!GetKeyState(VK_CONTROL) combo.ctrl) (!!GetKeyState(VK_SHIFT) combo.shift) (!!GetKeyState(VK_MENU) combo.alt); // VK_MENU 是 Alt if (match) return pair.second; } } return ; } }; // 初始化映射可在配置文件读取 KeyMapper g_keyMapper; void InitKeyMappings() { g_keyMapper.AddMapping(1, false, false, false, select_cannon); g_keyMapper.AddMapping(2, false, false, false, select_ice); g_keyMapper.AddMapping(2, true, false, false, select_poison); // Ctrl2 }优势GetKeyState()检查修饰键状态比wParam更可靠wParam在WM_KEYDOWN中不包含修饰键信息。KeyCombination自定义哈希支持CtrlShiftA等复杂组合。映射表可从config.ini文件加载实现真正的用户自定义。4.3 UI 文字与按钮GDI 绘制文本和响应区域保卫萝卜的金币、生命值、塔选择栏需实时显示。GDI 绘制文本并检测点击区域// ui_renderer.h #pragma once #include windows.h #include string #include vector struct UIButton { RECT rect; std::string text; std::string action; // 如 buy_cannon bool isHovered; }; class UIRenderer { private: std::vectorUIButton m_buttons; public: UIRenderer() { // 初始化塔选择按钮 m_buttons.push_back({{10, 10, 80, 40}, 炮塔, buy_cannon, false}); m_buttons.push_back({{90, 10, 160, 40}, 冰塔, buy_ice, false}); } void RenderUI(HDC hdc) { // 绘制金币和生命值 std::wstring goldText L金币: std::to_wstring(g_gameEngine-GetGold()); std::wstring livesText L生命: std::to_wstring(g_gameEngine-GetLives()); TextOut(hdc, 10, 50, goldText.c_str(), goldText.length()); TextOut(hdc p a hrefhttps://download.csdn.net/download/s1t16/88205472 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p