简介:这是一份C++课程设计《弹弹堂》的PDF文档,面向正在完成游戏开发类课设的高校学生,系统讲解如何用C++与FunCode引擎实现炮台瞄准、力度控制与抛物线弹道的休闲射击游戏。文档从游戏界面、角色分工入手,覆盖目标精灵初始化、随机位置生成、碰撞检测、爆炸动画、事件处理与游戏重置等关键环节,并给出创建工程、导入TanTanTang场景、设置精灵属性与状态管理、处理键盘输入的具体实验步骤,便于按部就班复现项目。内容预览显示文档包含完整实验指南、关键代码片段和易错点说明,可服务于实验报告撰写、期末答辩准备以及图形化编程能力的巩固。资源为单个PDF文件,压缩包约1.25MB,已有64人学习下载,适合具备C++基础、希望从实际项目中提升数学运算与游戏开发思路的读者。
1. 弹弹堂课程设计:不是写游戏,是写一套弹道系统
C++课程设计拿到《弹弹堂》这个题目时,多数人第一反应是“我要写一个游戏”,但实际评分老师要的不是玩法,而是你能否把一个物理问题拆成可计算的 C++ 代码。弹弹堂看起来是回合制对战,本质上是一套抛物线弹道系统:角度、力度、重力、风速,四个参数决定落点,之后才是画面和回合逻辑。这篇笔记就把我做完这个课设后的完整套路拆给你看,从物理公式到 EasyX 界面,再到避坑和文档包装,适合 C++ 刚入门但要交课程设计、以及想选游戏类课设但不知道从哪下手的同学。跟着这个路径做,至少能拿良,把弹坑和 AI 加上去就能冲优。
2. 弹道模型先行:角度、力度、重力与抛物线要怎么落地
2.1 从抛体运动公式到发射参数:先定坐标系
弹弹堂这类弹射游戏,最底层的模型就是抛体运动。你从炮口发射一枚炮弹,炮口有一个初速度,方向由角度决定,大小由力度决定。之后炮弹只受重力影响,如果设计了风场,再叠加一个水平方向的风速。很多初学者一开始会去研究空气阻力、旋转、碰撞回弹,但这些在课程设计阶段都不必做,甚至做了反而会让弹道变得难调,因为阻力系数、反弹系数都是要试出来的,稍有不慎就翻车。
先看基础公式。在平面直角坐标系中,设发射角度为 angle(相对水平方向),初速度为 v0,那么水平速度分量 vx = v0 * cos(angle),垂直速度分量 vy = v0 * sin(angle)。不考虑阻力时,水平方向匀速,垂直方向匀加速,t 时刻的位置为 x = vx * t,y = vy * t - 0.5 * g * t * t。这个公式本身很简单,但放进游戏里会遇到一个坐标系问题:屏幕的 y 轴方向是向下的,左上角是 (0, 0),右下角坐标变大。如果你照抄数学公式,把 y 正方向当成向上,那炮弹会直接飞出屏幕顶部。
所以第一步不是写代码,而是约定坐标。我一般把窗口左上角作为原点,x 向右增加,y 向下增加。在这个约定下,重力加速度 g 是一个正数,单位是像素每平方秒;炮口朝上发射时,vy 是负数,因为炮弹会往 y 减小方向运动;炮口朝下时,vy 为正。这个约定决定了之后所有代码的符号,建议一开始就写清楚。
有了坐标系,就可以把弹道更新写成一个独立的函数,这样后面无论画轨迹还是做伤害计算,都能直接调用它。
struct Bullet { double x, y; // 当前坐标,屏幕坐标,y 向下为正 double vx, vy; // 当前速度分量,像素/秒 double g; // 重力加速度,像素/秒^2 double wind; // 风速,像素/秒^2,正数表示向左吹 }; void updateBullet(Bullet& b, double dt) { b.vx += b.wind * dt; b.vy += b.g * dt; b.x += b.vx * dt; b.y += b.vy * dt; }这个 updateBullet 是整份代码里最核心的函数。每次游戏帧循环调用它,传入固定 dt,炮弹位置就会沿抛物线前进。注意 wind 我把它当作一个加速度而不是恒定速度,这和真实物理中风给物体一个作用力是一样的。如果想让风力更简单,也可以直接把 vx 加上一个常数,但那样风向感会不自然,因为空气阻力最终会阻止速度无限扩大。上面代码里 wind 是加速度,单位是像素/秒^2,具体数值需要根据窗口大小去调。
2.2 风速和空气阻力的取舍:课程设计该做到哪一步
课程设计的评分点通常集中在代码结构、算法实现和整体完成度,而不是物理仿真精度。所以风力和阻力要做到什么程度,是一个需要权衡的事。
先说阻力。真实炮弹飞行会受到空气阻力,阻力与速度成正比或与速度平方成正比。加上阻力后,弹道就不是标准抛物线,而是更接近实际弹道,落点会变近。这个效果看起来很专业,但代价是:你需要引入一个阻力系数 k,它的值只能靠试。k 太小没感觉,k 太大炮弹根本飞不远,而且不同初速度下表现还不一致,调试起来非常花时间。我的建议是课程设计阶段不要做空气阻力,把重力、风速、角度、力度四个变量做好就足够支撑这个题目了。
风速则建议做,因为它能让游戏有变化性。最简单的做法是给每回合随机一个风速值,然后作用于水平方向。注意风速的方向要清晰体现出来,比如用箭头显示。风速对炮弹的作用是持续且恒定的,它就相当于水平方向的额外加速度,在 updateBullet 里已经加上了。如果你想要更直观,可以让风速在每帧小幅波动,但这会增加不确定性,调试弹道时容易让人怀疑人生,建议固定值。
为什么强调取舍?因为课设最怕的是功能堆了很多,但每个都不稳定。评委演示的时候,你按一个角度和力度发射,落点如果明显偏离预期,很难解释清楚。把模型做简单、做稳定,然后用大量测试数据说明“输入角度 45 度、力度 60,落点稳定在 xxx”,比塞一堆花哨物理效果更有说服力。
2.3 把公式变成可测试的C++函数:最小可运行代码
仅仅有 updateBullet 还不够,你需要一个发射入口:输入角度和力度,初始化一个 Bullet,然后模拟运动直到落地。这里有一个细节,角度从鼠标位置来,力度也可能从鼠标拖拽距离来,但底层函数应该只用角度和力度两个参数,这样逻辑和界面分离,后面做 AI 或者单元测试都很方便。
下面给一个完整的发射模拟函数,它接受角度和力度,返回落点的 x 坐标:
#include <cmath> struct Bullet { double x, y; double vx, vy; double g; double wind; }; void updateBullet(Bullet& b, double dt) { b.vx += b.wind * dt; b.vy += b.g * dt; b.x += b.vx * dt; b.y += b.vy * dt; } // 返回落点的 x 坐标;groundY 表示地面的 y 坐标,低于它就认为落地 double simulateShot(double startX, double startY, double angleDeg, double power, double g, double wind, double dt, double groundY) { double angle = angleDeg * 3.14159265358979 / 180.0; Bullet b; b.x = startX; b.y = startY; b.vx = power * cos(angle); b.vy = -power * sin(angle); // 炮口朝上,屏幕坐标 y 向下,所以取负 b.g = g; b.wind = wind; while (b.y < groundY) { updateBullet(b, dt); // 防止 dt 过大穿过地面,做一次线性插值修正 if (b.y >= groundY) { double ratio = (groundY - (b.y - b.vy * dt)) / (b.vy * dt); b.x -= b.vx * dt * (1.0 - ratio); b.y = groundY; break; } } return b.x; }这里有几个参数说明。angleDeg 是用户可感知的度数,内部通过 cos/sin 换算成速度方向。power 是初速度大小,单位可定义为像素/秒,数值范围需要根据窗口大小调整。比如一个 800x600 的窗口,重力如果设为 200 像素/秒^2,power 设个 300 左右能打出不错的距离;如果 power 是 1000,那炮弹直接飞出屏幕。这个参数很玄学,建议做成全局常量,然后多试几组数据。dt 是模拟步长,建议固定为 1.0 / 60.0,也就是一帧的时间;不要在帧循环里用真实帧间隔,否则性能波动会导致弹道不一致。
落地判断我做了防穿处理。模拟步长再小,也可能出现上一帧在地面上方、下一帧穿过地面线的情况。这时候用插值把落点修正到地面上,既避免了炮弹钻地,又让落点更精确。这个细节在课程设计答辩时非常加分,因为老师会故意问“你的模拟会不会穿过地面”。有了 simulateShot,你就可以先写一个控制台测试程序,把角度从 30 度到 60 度、力度从 50 到 200 逐一模拟,打印落点坐标,检查落点是否随力度增加而变远、随角度先远后近。这一步建议至少花半小时做数据测试,不要急着画界面。弹道不准,后面所有东西都是空中楼阁。
注意:这里的 power 和 g 是像素级单位,换机器后一定要重新标定。你可以在程序启动时打印一组已知角度力度的落点,快速验证参数是否合理。
3. 用图形界面把发射和落点画出来:EasyX方案
3.1 为什么课程设计首选EasyX而不是Qt或SDL
在 C++ 课程设计里选图形库,EasyX 是国内最常见的方案,尤其是 Visual Studio 环境下。理由是它直接封装了 Windows GDI,安装是一个 exe,MFC 和 Win32 API 的知识都不需要,画圆、画线、显示图片都是函数调用。相比 Qt 需要额外配置 CMake 和项目模板,SDL 需要处理窗口事件回调,EasyX 的学习曲线最平缓。
当然,EasyX 只支持 Windows,且基于 GDI,效率不算高,但弹弹堂这种 2D 游戏完全够用。如果你在 Linux 环境做课设,可以用 SDL2 或者直接控制台输出字符炮弹,但控制台版很难做出“弹弹堂”的观感。我见过有人用 Qt 做,最后被布局和信号槽整得没时间调弹道,所以不建议课设走 Qt,除非你本来就会。
环境配置上,VS 里 include <graphics.h> 之前,需要先下载 EasyX 安装包,运行后选择安装到当前 VS 版本。装完之后,新建一个空项目,把代码放进去即可。这里有一个小坑:EasyX 的头文件是老式 <graphics.h>,如果你同时装了其他图形库,可能冲突。建议在项目里只保留一份图形库路径配置。
3.2 绘制地图、炮台和弹丸:常用图形API
EasyX 的绘图概念很简单:所有绘制都作用在默认窗口上,用 setfillcolor 设置填充色,用 fillrectangle 画矩形,用 solidcircle 画实心圆。弹弹堂的地图可以做成格子系统,也可以直接用矩形色块拼出地形。我推荐格子系统,因为碰撞检测和弹坑实现都极其方便,只需维护一个二维布尔数组。
下面是地图初始化和绘制的最小代码:
#include <graphics.h> #include <conio.h> const int MAP_W = 80; const int MAP_H = 60; const int CELL = 10; bool solid[MAP_H][MAP_W]; // true 表示有地形 void initMap() { // 生成一个简单的地表:从高度 20 到 50 填充 for (int y = 20; y < MAP_H; y++) { for (int x = 0; x < MAP_W; x++) { solid[y][x] = true; } } } void drawMap() { for (int y = 0; y < MAP_H; y++) { for (int x = 0; x < MAP_W; x++) { if (solid[y][x]) { setfillcolor(RGB(139, 90, 43)); // 泥土色 fillrectangle(x * CELL, y * CELL, x * CELL + CELL, y * CELL + CELL); } } } } void drawPlayer(int x, int y) { // 玩家炮台:一个半圆底座加一根炮管 setfillcolor(RGB(70, 70, 70)); solidrectangle(x - 15, y - 10, x + 15, y + 10); setfillcolor(RGB(200, 200, 200)); solidcircle(x, y, 10); }逻辑说明:initMap 把 20 行以下全部填成地形,形成一块平地。drawMap 遍历整个格子数组,只渲染 solid 为 true 的格子,这样后续做地形破坏只需要把对应格子置为 false。drawPlayer 画的是炮台底座和圆形炮口,这里的 x, y 是炮台底部中心点,实际项目中你可以把炮塔朝向和角度用旋转代替,但课设阶段画个静态炮台也够。
要点是渲染和逻辑分离。drawMap 只负责绘制,不修改 solid;所有对地形的修改都放在逻辑函数里。这样当弹坑出现时,你只需要在逻辑层改数组,下一次帧循环绘制时会自动显现。
3.3 鼠标瞄准与角度力度换算:交互的关键
弹弹堂的瞄准靠鼠标拖拽:按下左键后拖动,松开时根据拖动方向决定角度和力度。常见做法是记录按下点坐标和松开点坐标,角度由两点连线与水平方向的夹角决定,力度由拖拽距离决定,但方向和力度要取反:你从炮口向右拖,炮口应该朝左,类似弹弓。
这个换算代码很短,但非常容易出问题,尤其是浮点运算和屏幕坐标混在一起的时候。我一般先把屏幕坐标换算成“从炮口出发的向量”,再转角度:
// 炮口屏幕坐标 (muzzleX, muzzleY),松开点 (releaseX, releaseY) int dx = releaseX - muzzleX; int dy = releaseY - muzzleY; // 拖拽方向取反,像弹弓一样 double angle = atan2(-dy, -dx) * 180.0 / 3.14159265; if (angle < 0) angle += 360.0; // 力度与拖拽距离成正比 double distance = sqrt(dx * dx + dy * dy); double power = distance * 0.8; // 系数需要根据自己的重力加速度调这里的 angle 是 0 到 360 度,0 度代表正右方,90 度代表正上方。因为屏幕 y 向下,atan2(-dy, -dx) 已经把方向翻转了。power 与拖拽距离成正比,系数 0.8 决定了满拖拽时的初速度,需要配合重力 g 微调,让炮弹能飞到敌人附近而不是全屏乱飞。
绘制预瞄线也是靠这组参数。你可以在每次鼠标移动时调用 simulateShot,把整个轨迹的点算出来,然后画成一系列小圆点或虚线。因为模拟函数是确定的,轨迹线刷新不会卡。这里有个细节:如果你每帧都模拟,不要把 dt 设成和帧率一样的时间间隔,还是用固定 dt。轨迹线用固定 dt 模拟出来是一条稳定曲线,如果 dt 随帧率变化,轨迹线会忽长忽短。
在 EasyX 里接鼠标事件,使用 GetMouseMsg 阻塞获取已经不够灵活,我一般用 peekmessage 配合 ExMessage:
ExMessage msg; while (peekmessage(&msg, EM_MOUSE)) { if (msg.message == WM_LBUTTONDOWN) { pressX = msg.x; pressY = msg.y; } else if (msg.message == WM_LBUTTONUP) { releaseX = msg.x; releaseY = msg.y; // 到这里就拿到了一组发射参数 state = GameState::FLY; } }这样每帧都能处理缓存的鼠标事件,不会因为 getmessage 阻塞导致窗口卡死。EasyX 的 peekmessage 和 Win32 消息循环是兼容的,如果你主循环里有绘图操作,记得在绘制前把鼠标消息处理完。
4. 回合制战斗与伤害结算:把游戏逻辑做成状态机
4.1 回合状态机:移动、发射、结算、换边
弹弹堂是回合制,同一时刻只有一个玩家能操作。如果不把流程理清楚,很容易出现双方同时操作、炮弹穿人等问题。常见做法是用一个状态机,枚举当前处于哪个阶段。
我的实现里有这几个状态:AIM(玩家瞄准)、FLY(炮弹飞行中)、EXPLODE(爆炸与伤害结算)、SWITCH(切换到对方)。每帧只处理当前状态对应的事件,状态切换的时机很关键。
enum class GameState { AIM, FLY, EXPLODE, SWITCH }; GameState state = GameState::AIM; Bullet flyingBullet; bool canFire = true; void updateGame() { switch (state) { case GameState::AIM: // 鼠标输入在消息循环里处理,这里不做 break; case GameState::FLY: updateBullet(flyingBullet, 1.0 / 60.0); if (flyingBullet.y >= groundY) { // 落地,进入爆炸结算 state = GameState::EXPLODE; } break; case GameState::EXPLODE: // 伤害计算和弹坑生成,然后切换回合 state = GameState::SWITCH; break; case GameState::SWITCH: // 切换玩家、更新回合数、重置状态 state = GameState::AIM; break; } }逻辑说明:AIM 状态只等待鼠标松开事件,发射后立刻进入 FLY。FLY 状态每帧调用 updateBullet 推进炮弹,直到炮弹落进地面或碰到地形。EXPLODE 做伤害、弹坑、动画。SWITCH 把控制权交给另一方。这种写法代码清晰,也不容易漏逻辑。
有一个容易踩的坑:炮弹飞行过程中,如果玩家还能发射,就会同时存在两发炮弹。所以发射之后要把 canFire 置为 false,同时清空鼠标拖拽事件,直到 FLY 结束再恢复。另外,炮弹落地后不要立刻切换状态,最好留几帧给爆炸动画,不然画面会非常生硬。我一般会在 EXPLODE 状态维持 30 帧再切 SWITCH,用一个帧计数器控制。
4.2 伤害计算和地形碰撞:弹坑怎么实现
伤害计算的核心是落点距离。弹弹堂的快感来自击中判定:炮弹落点离对方越近,伤害越高。不需要物理引擎,直接比较落点坐标和敌方炮台坐标的直线距离,距离小于某个阈值算直接命中,大于阈值则按衰减曲线算。
我的算法是:
double hitDistance = sqrt((impactX - enemyX) * (impactX - enemyX) + (impactY - enemyY) * (impactY - enemyY)); int maxDamage = 100; int minDamage = 10; int damage = minDamage + (int)((maxDamage - minDamage) * max(0.0, 1.0 - hitDistance / 200.0));minDamage 作为保底伤害,避免距离稍远就完全无伤。maxDamage 和 200 这两个阈值需要根据地图尺寸调整。如果你希望直接命中必死,可以把 minDamage 设成 maxDamage 的 80% 以上。为了效果明显,我建议在爆炸后把伤害数字绘制出来,用 settextcolor 配合 outtextxy 显示在落点上方。
地形碰撞和弹坑是一对逻辑。我这里用格子系统,弹坑就是在爆炸中心半径 range 内,把所有 solid[y][x] 置为 false。这一步用双重循环遍历半径约 30 像素的方格,很快。注意边界判断,防止数组越界。下面是弹坑生成代码,这个代码在所有课设里几乎通用:
void makeCrater(int centerX, int centerY, int range) { int x0 = max(0, (centerX - range) / CELL); int x1 = min(MAP_W - 1, (centerX + range) / CELL); int y0 = max(0, (centerY - range) / CELL); int y1 = min(MAP_H - 1, (centerY + range) / CELL); for (int gy = y0; gy <= y1; gy++) { for (int gx = x0; gx <= x1; gx++) { int pixelX = gx * CELL + CELL / 2; int pixelY = gy * CELL + CELL / 2; double dist = sqrt((pixelX - centerX) * (pixelX - centerX) + (pixelY - centerY) * (pixelY - centerY)); if (dist <= range) { solid[gy][gx] = false; } } } }参数说明:centerX 和 centerY 是爆炸中心像素坐标,range 是爆炸半径像素。先将像素坐标换算到格子坐标,并做边界裁剪,再遍历范围内的格子,检查每个格子中心到爆炸中心的距离。距离小于等于 range 就炸掉。这段代码里用了 max/min 边界处理,如果不做,数组越界是必然的,因为爆炸中心不可能永远离地图边缘超过 range。这个坑我踩过,血泪经验。
如果你不想一格一格判断,也可以在落地时直接填充一个圆形区域,但那样弹坑边缘是锯齿状,不够自然。用中心距离计算能得到一个平滑的圆形弹坑,视觉效果好很多。不过要注意,对面玩家如果站在被炸掉的地形边缘,他的炮台位置可能悬空,这样下一回合他需要移动或掉下去,这属于玩法特性,课程设计里可以允许,但要在报告里说明。
4.3 人机对战AI:简单敌机怎么设计
如果课设要求双人对战,你只需要让两个玩家轮流控制即可。如果要人机对战,AI 其实可以写得很简单:根据敌方位置反推一个角度和力度,然后加一点随机扰动。不需要实时规划弹道,因为那是 AI 算法课程的内容。
一个很稳的 AI 策略是:固定敌我距离,假设当前风速已知,在角度 30 到 80 度之间用二分法找一个能命中敌人的力度,然后每次发射在这个力度上加随机 5% 的偏差,模拟“AI 也会犯错”。这个查找过程可以在 AI 回合时计算,但要在模拟器里跑几十步,别在主线程里做,否则画面会卡。
我建议的做法是:在 AI 回合先花几帧计算,把结果存起来,再用动画播放瞄准线,最后发射。这既给玩家反应时间,也避免了卡顿。计算用的还是 simulateShot 函数,只是把它包装成“给定角度求落点距离”的反向问题。二分法代码如下:
double findPowerForAngle(double targetX, double targetY, double angleDeg, double g, double wind, double startX, double startY) { double low = 50, high = 500; // 全局 groundY 为地面高度,这里直接用于模拟 double groundY = 600; for (int i = 0; i < 20; i++) { double mid = (low + high) / 2; double landingX = simulateShot(startX, startY, angleDeg, mid, g, wind, 1.0 / 60.0, groundY); if (landingX < targetX) low = mid; else high = mid; } return (low + high) / 2; }由于弹道不是单调的,同一落点可能有两个不同角度对应,所以二分法只能找到其中一个解。但没关系,AI 只需要能打中就够。注意这里假设目标在炮口右侧,如果敌人在左侧要把角度换成 100 到 150 度区间。不同位置的二分区间不一样,写死了会翻车。
实际效果想要更像人,可以在计算出的力度上乘一个 0.95 到 1.05 的随机系数,并且让 AI 有一半概率选择高抛角度而不是低平角度。这样玩家会觉得 AI 有策略,而不是每次都同一套路。AI 回合时还可以故意打偏一次,让游戏不至于一边倒,但课设里别做得太明显,否则老师觉得你是故意放水。
5. 弹弹堂课设避坑:编译环境、乱码与弹道不准的排查
5.1 EasyX 装不上或运行报错:环境问题清单
现象:VS 里 include <graphics.h> 报错找不到文件。 原因:EasyX 并没有随 VS 安装,需要单独下载安装包。 解决:到 EasyX 官网下载对应 VS 版本的安装包,运行后选择安装到当前使用的 VS 版本。这一步做完,graphics.h 就能找到。如果还报错,检查项目是不是 x86/x64 平台选错了,EasyX 两者都支持,但安装时要选对。另外,有些同学用了 MinGW 或 Dev-C++,EasyX 官方并不支持,建议换 VS。
还有一个常见报错:编译时大量“无法解析的外部符号 LNK2019”。这个通常是没在项目属性里链接对应的库,EasyX 安装包会自动配置,但如果你把代码拷到别的电脑上,需要重新安装或者手动在项目属性里加附加依赖项。我在答辩前会提前一天在演示机上重新跑一遍环境,免得现场报这个错,直接心态崩了。
5.2 控制台中文乱码与字符集问题
现象:printf 输出中文变成乱码,或者界面上的中文全是问号。 原因:源代码文件的编码和运行时控制台编码不一致,以及 EasyX 绘图默认字符集问题。 解决:在 Visual Studio 中,把源文件保存为 UTF-8 with BOM,并在 main 开头调用 SetConsoleOutputCP(CP_UTF8) 或者 system("chcp 65001")。如果你用 EasyX 的 outtextxy 绘制中文,需要把项目字符集设置为“使用多字节字符集”或统一使用 Unicode。这里我建议全部用宽字符:L"中文" 加 settextstyle 指定字体。中文乱码几乎是每个课设都会遇到的问题,不是你的代码逻辑有问题,而是编码玄学,提前处理能省很多时间。
另外,VS 默认会把源码按本地代码页保存,你的代码里有中文字符串时,换一台电脑可能就乱了。最保险的做法是项目属性里把字符集明确设为 Unicode,然后在所有显示中文的地方使用 _T() 宏或 L 前缀。做课设时坚持一个原则:不在源码里裸写窄字符串中文,统一用宽字符。
5.3 弹道轨迹偏移:精度和时间步长
现象:设同样的角度和力度,两次模拟落点不一样,或者轨迹线抖得厉害。 原因:最可能是用了不固定的 dt。如果你在窗口消息循环里用 GetTickCount 计算帧间隔,再把真实时间间隔传入模拟,那么每一帧的推进量都不同,简化的积分公式会产生明显误差。更隐蔽的是,你在模拟中把 dt 当成了毫秒,但公式里按秒计算,结果整个弹道飞得奇快。 解决:统一用固定 dt = 1.0 / 60.0,模拟每帧推进一次。如果追求更高的精度,就把一帧拆成 4 个子步长,每步 dt/4 调用四次 updateBullet。这样轨迹稳定,调试时也能复现。需要复现弹道时,不要依赖随机数,角度和力度必须作为输入参数打印出来。
我调试弹道时会写一个调试宏,按 F1 就自动打一发当前角度力度的炮弹,同时在控制台输出落点坐标。这样反复调整参数,很快就能把重力、风、力度系数标定好。很多同学直接用鼠标打几十发,打到头晕都找不出规律,就是因为没有可复现的输入。
5.4 随机数不随机:种子设置
现象:每次运行游戏,AI 的偏移都一模一样,风力也完全一样。 原因:使用了 rand() 但没有调用 srand(time(nullptr)) 初始化种子。 解决:在 main 开头调用 srand((unsigned)time(nullptr))。只要设置了种子,每次运行随机序列都不一样。如果你想要可复现的测试场景,可以在程序里留一个开关:固定种子,这样测试弹道时可以拿到一致的随机风。这个开关对答辩演示很有用,因为你可以提前固定一组漂亮的数据,免得现场随机到很不合理的风。
但要注意,srand 只需要调用一次,不要放在每帧里,否则同一秒内的随机序列会重复,看起来还是不够随机。如果你用 C++11 以上,可以直接用 里的 mt19937 和 uniform_real_distribution,比 rand 更稳定。课设里能写出“真正的随机数”这个点,可以在报告里特别说明。
5.5 结构体链表与回调:课设里哪些能加分
现象:老师要求用数据结构,但弹弹堂游戏里没有明显的链表场景。 原因:课程设计评分通常要求体现 C++ 语法的综合运用,比如类、继承、链表、STL。 解决:常见做法是用 STL 的 vector 存储炮弹轨迹点,用 std::list 存储回合记录。如果你想突出“结构体链表”,可以把每次发射的信息(角度、力度、落点、伤害)封装成一个结构体,用 std::queue 存起来,这就有了数据结构的用武之地。回调函数可以用在鼠标回调上,比如 EasyX 的鼠标事件处理,或者把 AI 决策函数作为可配置的函数指针传入。这些点写进报告里,比单纯写游戏逻辑更能让老师觉得你掌握了课程内容。
我一般这样设计:定义一个 ShotRecord 结构体,里面放玩家编号、回合号、角度、力度、风速、落点、伤害,然后塞进 std::vector。这个 vector 既是录像回放的数据源,也是答辩时展示“STL 容器使用”的抓手。稍微再包装一下,你还能实现撤销悔棋功能,但课设里不必做那么深。
5.6 图片资源加载后不显示:路径问题排查
现象:程序里用了 loadimage 加载图片,运行时图片不显示,但也没报错。 原因:工作目录不对。图片相对路径相对于 exe 所在目录,但调试时相对于项目目录(.vcxproj 所在目录),两者经常不一致。 解决:把图片和 exe 放同一目录,或者用绝对路径,或者在代码里用 GetCurrentDirectory 打印当前路径。这个坑在课程设计里很常见,因为资源文件路径是相对路径,从 VS 里按 F5 启动和双击 exe 启动的工作目录不一样。
一个稳妥的做法是:在 main 开头写一行
char buf[512]; GetCurrentDirectoryA(512, buf); printf("当前目录: %s\n", buf);先把当前路径打印出来,再根据打印结果调整资源文件的相对路径。还有个更省事的方法:把所有资源放在一个 resources 文件夹下,代码里拼接绝对路径,比如 "C:\course\resources\bg.jpg",但这样换机器就要改。我最后采用的方式是让程序启动时自动切换工作目录到 exe 所在目录,这样相对路径永远不迷路。这段代码在课程设计答辩演示时特别能救场,因为老师的电脑很卡,可能从资源管理器直接双击 exe 运行,工作目录和代码里假设的不一样。
6. 让课设超出预期:弹坑变形、录像回放与课程设计文档
6.1 弹坑与地形破坏的进阶玩法
格子系统已经能实现弹坑,但视觉上比较硬。可以让弹坑边缘的格子保留一部分泥土,形成上宽下窄的圆坑;也可以在爆炸时用粒子效果延迟几帧生成弹坑,观感更好。这些都不难,核心都是在 makeCrater 里加随机边缘判断,或者把 solid 数组整成 0-255 的强度值。有了强度值后,地形能被多次炸塌,游戏可玩性提升一截。如果你愿意,还可以在地形下方画一根背景色直线表示地下,让被炸掉的部分露出背景,这个细节会让老师觉得你做了“地形破坏”而不只是画了个洞。
6.2 录像回放:保存操作序列
录像回放是个很加分的功能。做法不是录屏幕,而是记录每一回合的操作参数:玩家编号、回合号、角度、力度、风力、落点、伤害。把这些写入文件,回放时按顺序重放模拟,就能还原整局。因为模拟是确定性的,落点必然一致。这个功能也验证了你的弹道代码的确定性,是一个很好的自证。实现时只需要维护一个 ShotRecord 的 vector,在每次发射后 push 一条记录,回放时遍历这个 vector,把对应参数传给 simulateShot 并绘制轨迹即可。
6.3 课程设计报告:从代码到文档
课程设计报告的结构一般包括需求分析、总体设计、模块说明、测试结果和总结。写报告时不要贴大段代码,而是放类图和核心算法流程,尤其要写清楚弹道模型和状态机的设计决策。测试结果部分,把 simulateShot 的落点数据做成表格,比如不同角度力度下的落点坐标,这说明你的程序经过了系统性验证。文档里还要写遇到的坑和解决方法,这部分老师很爱看,因为能体现真实的调试过程。我的习惯是在写代码前就建立好文件夹结构,按“src、doc、data、screenshot”分好,这样报告材料随时都有,不会最后找不到截图。最后提醒一句:报告里的代码和提交的源码一定要一致,不要贴旧版本截图,否则答辩时对不上很尴尬。
最后说一个我的习惯:游戏类课设最怕的不是功能少,而是答辩时临场翻车。我做完弹道模拟后,会先把所有常量写在程序顶部,答辩前调好一组参数并记在纸上,现场演示只按那组参数打,保证弹道漂亮,绝不现场乱调。这套坑踩下来,弹道稳定性和环境配置一定是第一个要解决的,界面反而次要。希望这些经验能帮你把 C++ 课程设计做得从容一点,也希望你能在报告中写出你自己调试出来的数据,祝顺利。
本文还有配套的精品资源,点击获取