C/C++图形编程入门:基于EGE库实现交互式弹球模拟器

1. 项目概述:从控制台到图形窗口的跨越

很多C/C++的初学者,都是从DEV-C++这个经典的IDE开始的。我们习惯了在黑色的控制台窗口里,用printf打印一行行字符,看着光标闪烁,感受着程序最原始的交互。但时间久了,难免会想:能不能让程序“画”出点东西来?比如一个会动的球,一个简单的游戏界面,或者一个数据可视化图表?这时候,ege.h库就成了我们手中那把打开图形世界大门的钥匙。

ege.h(Easy Graphics Engine,简易图形库)是一个专门为C/C++语言设计的图形库,它的设计初衷就是让图形编程变得简单。你不需要去啃动辄上千页的Windows GDI或者DirectX文档,也不需要去配置复杂的OpenGL环境。在DEV-C++里,你只需要包含一个头文件,链接一个库,就能立刻开始你的绘图之旅。它封装了底层的图形操作,提供了诸如画线、画圆、填充颜色、显示图片、处理鼠标键盘消息等一系列直观的函数。对于教学、小型图形演示、算法可视化乃至2D小游戏开发来说,ege.h都是一个极佳的选择。

这个“综合实践(一)”,目的就是带你跳出零散函数练习的范畴,真正用ege.h库去完成一个有点模样的、综合性的小项目。我们不会只画一个静止的圆,而是要让它动起来,要能响应用户的操作,要有一个清晰的程序结构。这更像是一次从“知道函数怎么用”到“知道项目怎么搭”的升级。无论你是想为枯燥的算法课作业增加一点可视化的趣味,还是想亲手打造一个属于自己的小游戏原型,这次实践都会给你一个扎实的起点。

2. 环境准备与项目初始化

2.1 DEV-C++中配置ege.h库

在开始写代码之前,确保你的开发环境已经就绪。虽然标题提到了DEV-C++ 5.11+,但配置方法对于较新的TDM-GCC版本也是通用的。

首先,你需要获取ege.h库文件。通常,它包含以下几个核心部分:

  1. ege.h头文件。
  2. libgraphics.alibgraphics64.a库文件(对应32位/64位编译)。
  3. 可能还有一些帮助文档和示例。

配置步骤:

  1. 放置头文件:将ege.h文件复制到你的DEV-C++安装目录下的MinGW64\x86_64-w64-mingw32\include文件夹中(这是64位GCC的默认路径。如果是32位,路径可能类似MinGW\include)。这样编译器就能直接找到它。
  2. 放置库文件:将libgraphics64.a(推荐使用64位版本)复制到MinGW64\x86_64-w64-mingw32\lib文件夹下。
  3. 创建项目:打开DEV-C++,新建一个项目。关键的一步是在“文件”->“新建”->“项目”中,选择“Empty Project”,并勾选“C++项目”。给项目起个名字,比如EGE_Practice
  4. 项目链接设置:这是最重要的一步。在项目管理器中,右键点击你的项目名称,选择“项目属性”。在弹出的窗口中,切换到“参数”选项卡。在“链接器”框里,你需要添加对libgraphics64.a库的引用。输入-lgraphics64。注意,这里不需要写文件扩展名.a-l前缀就是告诉链接器去链接名为graphics64的库。

注意:很多新手在这一步出错,是因为路径不对或者链接器参数写错。确保库文件的位数(32/64)与你的编译器位数匹配。如果链接时报告“undefined reference to ...”错误,大概率就是库没有正确链接。

配置完成后,你可以创建一个简单的测试文件来验证。

#include <graphics.h> // 注意,在DEV-C++配置好后,通常直接包含<graphics.h>,它会自动指向ege.h #include <conio.h> int main() { initgraph(640, 480); // 初始化一个640x480的图形窗口 circle(320, 240, 100); // 在窗口中心画一个半径为100的圆 getch(); // 等待按键 closegraph(); // 关闭图形窗口 return 0; }

如果运行后能弹出一个画着白圈的窗口,那么恭喜你,环境配置成功。

2.2 项目结构设计与基础框架

在开始综合实践前,我们先摒弃“所有代码都堆在main函数里”的习惯。一个好的结构能让代码更易读、易维护、易扩展。对于图形程序,尤其是涉及动画和交互的,我们通常采用一种“初始化-主循环-收尾”的经典框架。

// 1. 引入必要的头文件 #include <graphics.h> #include <cmath> // 用于数学计算,如sin, cos #include <ctime> // 用于获取时间,控制动画 // 2. 定义全局常量与变量 const int WINDOW_WIDTH = 800; const int WINDOW_HEIGHT = 600; const int BALL_RADIUS = 30; // 定义一个结构体来表示小球 struct Ball { int x, y; // 圆心坐标 int vx, vy; // 速度向量 (vx, vy) color_t color; // 颜色 }; // 3. 函数声明 void initBall(Ball& ball); void updateBall(Ball& ball); void drawBall(const Ball& ball); void processInput(Ball& ball); // 4. 主函数框架 int main() { // 4.1 初始化图形模式 initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); // 设置背景色(深灰色) setbkcolor(DARKGRAY); cleardevice(); // 用背景色清屏 // 4.2 初始化游戏对象(如小球) Ball myBall; initBall(myBall); // 4.3 主循环 while (!kbhit()) { // 当没有按键按下时循环 // 清屏(或用其他方式更新画面) cleardevice(); // 处理输入(如鼠标、键盘) processInput(myBall); // 更新逻辑(如位置、状态) updateBall(myBall); // 绘制图形 drawBall(myBall); // 延时,控制帧率 delay_ms(16); // 大约60帧/秒 (1000ms/60 ≈ 16ms) } // 4.4 收尾工作 closegraph(); return 0; } // 5. 函数定义(具体实现在后面章节展开) void initBall(Ball& ball) { ball.x = WINDOW_WIDTH / 2; ball.y = WINDOW_HEIGHT / 2; ball.vx = 5; ball.vy = 3; ball.color = RED; } // updateBall, drawBall, processInput 函数暂时留空...

这个框架清晰地划分了程序的各个阶段。initgraphclosegraph是图形程序的“门”。主循环是程序的心脏,它不断执行“处理输入-更新逻辑-渲染输出”这三个步骤,这是实时图形应用的核心模式,也被称为“游戏循环”。delay_ms用于控制循环速度,避免CPU占用率100%并维持稳定的动画帧率。

3. 核心绘图功能与动画原理实现

3.1 图形绘制与颜色填充

ege.h提供了丰富的绘图函数,理解它们的用法是基础。我们结合小球项目来实践。

基本图形绘制:

  • circle(x, y, radius):画圆。(x, y)是圆心坐标。
  • line(x1, y1, x2, y2):画直线。
  • rectangle(x1, y1, x2, y2):画矩形。(x1, y1)是左上角,(x2, y2)是右下角。
  • ellipse(x, y, stangle, endangle, xradius, yradius):画椭圆弧。

颜色设置:

  • setcolor(color):设置当前绘图颜色(影响线条、边框)。
  • setfillcolor(color):设置当前填充颜色。
  • setbkcolor(color):设置背景颜色。 颜色可以用预定义常量如RED,GREEN,BLUE,也可以用EGERGB(r, g, b)EGEARGB(a, r, g, b)来自定义。

填充函数:

  • fillcircle(x, y, radius):填充圆。
  • fillrectangle(x1, y1, x2, y2):填充矩形。
  • floodfill(x, y, border_color):种子填充。

现在,我们来完善drawBall函数:

void drawBall(const Ball& ball) { // 设置填充颜色为小球颜色 setfillcolor(ball.color); // 设置边框颜色为白色 setcolor(WHITE); // 绘制一个填充的圆,代表小球 fillcircle(ball.x, ball.y, BALL_RADIUS); // 为了更美观,可以在小球中心画一个高光点,模拟简单光照 setfillcolor(EGERGB(255, 255, 200)); // 浅黄色作为高光 solidcircle(ball.x - BALL_RADIUS/3, ball.y - BALL_RADIUS/3, BALL_RADIUS/6); }

这里用了两个技巧:一是用fillcircle画实心球体,二是用solidcircle(无边框的填充圆)画了一个小圆模拟高光,让小球看起来更有立体感。这只是最简单的美化,更复杂的效果可以用渐变填充或贴图实现。

3.2 动画与运动逻辑实现

动画的本质就是连续快速地显示一系列静态画面,每一帧中物体的位置稍有变化。在我们的主循环中,updateBall函数就是负责计算每一帧小球新位置的。

基础运动:匀速直线运动。位置 = 原位置 + 速度 × 时间。由于我们主循环的每次迭代代表一帧,可以近似认为时间间隔是固定的(由delay_ms控制),所以更新公式简化为:x = x + vx;y = y + vy;

边界碰撞检测:当小球碰到窗口边缘时,应该反弹。反弹的物理原理是速度方向反转(假设完全弹性碰撞)。

void updateBall(Ball& ball) { // 1. 根据速度更新位置 ball.x += ball.vx; ball.y += ball.vy; // 2. 检测与左右边界的碰撞 if (ball.x - BALL_RADIUS <= 0 || ball.x + BALL_RADIUS >= WINDOW_WIDTH) { ball.vx = -ball.vx; // 水平速度反向 // 修正位置,防止小球卡在边界外 if (ball.x - BALL_RADIUS <= 0) ball.x = BALL_RADIUS; if (ball.x + BALL_RADIUS >= WINDOW_WIDTH) ball.x = WINDOW_WIDTH - BALL_RADIUS; } // 3. 检测与上下边界的碰撞 if (ball.y - BALL_RADIUS <= 0 || ball.y + BALL_RADIUS >= WINDOW_HEIGHT) { ball.vy = -ball.vy; // 垂直速度反向 // 修正位置 if (ball.y - BALL_RADIUS <= 0) ball.y = BALL_RADIUS; if (ball.y + BALL_RADIUS >= WINDOW_HEIGHT) ball.y = WINDOW_HEIGHT - BALL_RADIUS; } }

实操心得:碰撞检测后立刻进行位置修正是个重要技巧。因为计算机是离散计算的,某一帧小球可能已经“嵌入”边界几个像素。如果不修正,下一次更新时它可能因为仍然在边界“内部”而无法触发碰撞检测,导致视觉上的抖动或穿墙。这种修正保证了物体的“可碰撞体积”始终在合法范围内。

更复杂的运动:你可以引入加速度模拟重力,让vy每帧增加一个小的重力常量。也可以模拟摩擦力,让vxvy每帧乘以一个略小于1的系数(如0.99),这样速度会逐渐衰减。

4. 用户交互与程序控制

4.1 键盘与鼠标事件处理

一个没有交互的程序是枯燥的。ege.h提供了便捷的函数来获取输入状态。

键盘输入:

  • kbhit():检查当前是否有按键按下,常用于控制主循环。
  • getch():获取一个按键的字符码。它会等待输入。
  • GetAsyncKeyState(int vKey):这是一个更强大的函数,可以查询某个特定键的实时状态(是否被按下)。它非常适合用于游戏中的连续控制。参数vKey是虚拟键码,例如VK_LEFT代表左箭头键。

让我们用键盘来控制小球的速度方向:

void processInput(Ball& ball) { // 使用GetAsyncKeyState检测方向键 if (GetAsyncKeyState(VK_LEFT) & 0x8000) { ball.vx = -5; // 按左键,向左运动 } else if (GetAsyncKeyState(VK_RIGHT) & 0x8000) { ball.vx = 5; // 按右键,向右运动 } else { // 如果左右键都没按,水平速度可以保持不变或归零,这里选择归零 // ball.vx = 0; } if (GetAsyncKeyState(VK_UP) & 0x8000) { ball.vy = -5; // 按上键,向上运动 } else if (GetAsyncKeyState(VK_DOWN) & 0x8000) { ball.vy = 5; // 按下键,向下运动 } else { // ball.vy = 0; } // 空格键重置小球位置和速度 if (GetAsyncKeyState(VK_SPACE) & 0x8000) { ball.x = WINDOW_WIDTH / 2; ball.y = WINDOW_HEIGHT / 2; ball.vx = 3; ball.vy = 3; } }

GetAsyncKeyState(VK_LEFT) & 0x8000这个判断条件的意思是:检查返回值的高位是否为1(即0x8000),如果为1则表示该键当前正被按下。这比getch()更适合需要持续响应的场景。

鼠标输入:

  • MouseHit():类似kbhit(),检查是否有鼠标消息。
  • GetMouseMsg():获取一个鼠标消息结构体MOUSEMSG,里面包含了坐标、按键状态等信息。

我们可以让小球跟随鼠标点击的位置移动:

void processInput(Ball& ball) { // ... 键盘处理代码同上 ... // 鼠标处理 MOUSEMSG mmsg; if (MouseHit()) { // 如果有鼠标消息 mmsg = GetMouseMsg(); if (mmsg.uMsg == WM_LBUTTONDOWN) { // 如果消息是左键按下 // 将小球瞬间移动到鼠标点击的位置 ball.x = mmsg.x; ball.y = mmsg.y; // 并给一个随机的速度 ball.vx = rand() % 11 - 5; // 范围[-5, 5] ball.vy = rand() % 11 - 5; } // 还可以处理WM_MOUSEMOVE(鼠标移动)、WM_RBUTTONDOWN(右键按下)等 } }

4.2 界面元素与状态显示

一个完整的程序通常需要显示一些信息,比如分数、速度、状态提示等。ege.h使用outtextxy函数在指定坐标输出字符串。

// 在drawBall函数中或主循环的绘制阶段添加 void drawUI(const Ball& ball) { char info[256]; // 缓冲区 // 设置文字颜色和背景模式(透明背景) setcolor(LIGHTGREEN); setbkmode(TRANSPARENT); // 关键!设置文字背景透明,否则会有色块 // 显示小球坐标和速度 sprintf(info, "Position: (%d, %d)", ball.x, ball.y); outtextxy(10, 10, info); sprintf(info, "Velocity: (%d, %d)", ball.vx, ball.vy); outtextxy(10, 30, info); // 显示操作提示 setcolor(YELLOW); outtextxy(10, WINDOW_HEIGHT - 60, "Control: Arrow Keys to move, SPACE to reset"); outtextxy(10, WINDOW_HEIGHT - 40, "Mouse: Left Click to teleport ball"); outtextxy(10, WINDOW_HEIGHT - 20, "Press any key to exit"); }

然后在主循环的绘制部分调用drawUI(myBall)setbkmode(TRANSPARENT)非常重要,它让文字背景透明,不会遮盖后面的图形。

5. 性能优化与高级技巧

5.1 双缓冲与画面撕裂

如果你运行上面的程序,可能会发现小球在快速运动时,画面有闪烁感。这是因为我们在主循环中直接调用cleardevice()清屏,然后立即绘制,屏幕在清空和绘制的瞬间会被用户看到,造成闪烁。

双缓冲技术是解决这个问题的标准方案。原理是:我们不在屏幕上直接绘图,而是先在一个内存中的“后台缓冲区”画好一整帧图像,然后一次性把这个缓冲区的内容“翻”到屏幕上(这个操作很快,肉眼几乎不可见)。ege.h已经内置了双缓冲支持,只需要两个函数:

  • setrendermode(RENDER_MANUAL):设置为手动渲染模式。在此模式下,所有绘图操作都发生在后台缓冲区,不会立即显示。
  • delay_ms()Sleep():这两个函数在内部会自动执行缓冲区交换。也就是说,当你调用delay_ms(16)延时的时候,ege已经帮你把画好的那一帧显示到屏幕上了。

所以,优化非常简单,只需要在initgraph之后加上一行:

initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); setrendermode(RENDER_MANUAL); // 开启手动渲染模式,启用双缓冲

这样,主循环中的闪烁问题就基本解决了。cleardevice()清空的是后台缓冲区,fillcircle等绘图函数也是画在后台缓冲区,直到delay_ms时才会统一显示。

5.2 图像加载与精灵动画

绘制几何图形是基础,但很多项目需要图片。ege.h使用PIMAGE类型来处理图像。

// 全局变量 PIMAGE img_background = NULL; PIMAGE img_ball_sprite = NULL; // 在初始化阶段加载图片 void loadImages() { img_background = newimage(); // 从文件加载背景图,支持bmp, jpg, png, gif等格式 getimage(img_background, "background.jpg"); // 确保图片文件在项目目录下 img_ball_sprite = newimage(); getimage(img_ball_sprite, "ball_sprite.png"); } // 在绘制阶段使用图片 void drawWithImages(const Ball& ball) { // 1. 绘制背景图(铺满窗口) putimage(0, 0, img_background); // 2. 用图片代替圆形绘制小球 // 假设ball_sprite.png是60x60的球体图片,我们要让球的中心对准图片中心 int draw_x = ball.x - getwidth(img_ball_sprite) / 2; int draw_y = ball.y - getheight(img_ball_sprite) / 2; putimage(draw_x, draw_y, img_ball_sprite); } // 程序退出前释放图像内存 void cleanupImages() { delimage(img_background); delimage(img_ball_sprite); }

在主函数中,initgraph之后调用loadImages(),主循环中用drawWithImages替换drawBallclosegraph之前调用cleanupImages()

精灵动画:如果ball_sprite.png是一张包含多个动画帧的雪碧图(Sprite Sheet),你可以通过putimage的另一个重载版本来只绘制其中一部分,通过切换绘制的源矩形区域来实现动画效果。这需要你记录当前动画帧的索引和计时。

5.3 定时器与帧率控制

我们之前用delay_ms(16)来粗略控制帧率(~60FPS)。但delay是阻塞的,而且不精确。更专业的做法是计算每一帧实际消耗的时间(delta time),并用它来更新物体位置,这样能保证在不同性能的电脑上,物体的运动速度是相同的,而不是帧率越高运动越快。

#include <chrono> // 需要C++11支持,DEV-C++需在编译选项添加 -std=c++11 // 使用高精度时钟 auto last_time = std::chrono::high_resolution_clock::now(); while (!kbhit()) { auto current_time = std::chrono::high_resolution_clock::now(); // 计算上一帧到这一帧的时间差(秒) float delta_time = std::chrono::duration<float>(current_time - last_time).count(); last_time = current_time; // 清屏、处理输入... // 使用delta_time来更新位置,这样速度单位就是 像素/秒 ball.x += static_cast<int>(ball.vx * delta_time); ball.y += static_cast<int>(ball.vy * delta_time); // 绘制... // 不使用固定的delay,而是根据目标帧率计算需要等待的时间 // 或者不等待,完全依赖循环速度(垂直同步) }

对于初学者项目,简单的delay_ms足以应付。但理解delta_time的概念对于未来进行更复杂的游戏或模拟开发至关重要。

6. 综合实践:构建一个交互式弹球模拟器

现在,让我们把前面所有的知识点整合起来,构建一个功能更丰富的“交互式弹球模拟器”。这个模拟器将包含多个小球、简单的物理碰撞(球与球之间)、可交互的挡板,以及更美观的UI。

6.1 多对象管理与碰撞检测

我们需要管理多个Ball对象。使用std::vector是方便的选择。

#include <vector> std::vector<Ball> balls; void initBalls(int count) { balls.clear(); for (int i = 0; i < count; ++i) { Ball b; b.x = rand() % (WINDOW_WIDTH - 2*BALL_RADIUS) + BALL_RADIUS; b.y = rand() % (WINDOW_HEIGHT - 2*BALL_RADIUS) + BALL_RADIUS; b.vx = rand() % 11 - 5; // -5 到 5 b.vy = rand() % 11 - 5; b.color = EGERGB(rand()%256, rand()%256, rand()%256); // 随机颜色 balls.push_back(b); } }

更新和绘制时,遍历balls向量即可。

球与球的碰撞检测是难点。简单的方法是两两检测,计算圆心距离是否小于半径之和。

void checkBallCollision(std::vector<Ball>& balls) { for (size_t i = 0; i < balls.size(); ++i) { for (size_t j = i + 1; j < balls.size(); ++j) { Ball& a = balls[i]; Ball& b = balls[j]; int dx = a.x - b.x; int dy = a.y - b.y; int distance_sq = dx*dx + dy*dy; // 距离的平方 int min_distance = BALL_RADIUS * 2; // 最小距离(直径) int min_distance_sq = min_distance * min_distance; if (distance_sq < min_distance_sq) { // 发生碰撞!这里实现一个简化的弹性碰撞响应 // 最简单的处理:交换速度(假设质量相同) std::swap(a.vx, b.vx); std::swap(a.vy, b.vy); // 强制分离,防止粘在一起 float distance = sqrt((float)distance_sq); float overlap = (min_distance - distance) / 2.0f; float dx_unit = dx / distance; float dy_unit = dy / distance; a.x += (int)(overlap * dx_unit); a.y += (int)(overlap * dy_unit); b.x -= (int)(overlap * dx_unit); b.y -= (int)(overlap * dy_unit); } } } }

updateBall(现在应该是updateBalls)函数中,先更新所有球的位置(与边界碰撞),然后调用checkBallCollision处理球间碰撞。

6.2 添加可控制挡板

挡板可以用一个矩形来表示,其位置由鼠标的Y坐标控制。

struct Paddle { int x, y; // 通常x固定,y随鼠标变化 int width, height; color_t color; } paddle; void initPaddle() { paddle.x = 50; // 固定在左侧 paddle.width = 20; paddle.height = 100; paddle.color = BLUE; } void updatePaddle() { // 获取鼠标Y坐标,并让挡板中心跟随鼠标 MOUSEMSG mmsg; if (MouseHit()) { mmsg = GetMouseMsg(); if (mmsg.uMsg == WM_MOUSEMOVE) { paddle.y = mmsg.y - paddle.height / 2; // 限制挡板不超出窗口上下边界 if (paddle.y < 0) paddle.y = 0; if (paddle.y + paddle.height > WINDOW_HEIGHT) paddle.y = WINDOW_HEIGHT - paddle.height; } } } void drawPaddle() { setfillcolor(paddle.color); setcolor(WHITE); fillrectangle(paddle.x, paddle.y, paddle.x + paddle.width, paddle.y + paddle.height); }

同时,你需要在球的位置更新逻辑中,加入球与挡板的碰撞检测(判断球是否与矩形区域相交),并做出反弹响应。

6.3 完整程序流程与效果增强

将以上所有模块组合,你的主函数结构将更加清晰:

int main() { // 初始化 initgraph(...); setrendermode(RENDER_MANUAL); loadImages(); initBalls(10); // 10个小球 initPaddle(); initUI(); // 初始化UI字体等 // 主循环 while (!kbhit()) { // 计算deltaTime (可选) // 处理输入 processInput(); // 处理键盘、鼠标,更新paddle // 更新逻辑 updateBalls(); // 更新所有球的位置(含边界、球间、球与挡板碰撞) updatePaddle(); // 渲染 cleardevice(); drawBackground(); drawBalls(); drawPaddle(); drawUI(); // 绘制分数、提示等信息 // 延时并交换缓冲区 delay_fps(60); // ege.h提供的函数,更方便地控制帧率 } // 清理 cleanupImages(); closegraph(); return 0; }

你还可以增加更多效果,比如碰撞时播放音效(ege.hmciSendString相关封装)、记录得分(球撞到挡板加分,漏球减分)、增加不同类型的球或障碍物。

7. 常见问题与调试技巧实录

7.1 编译与链接错误排查

  • “undefined reference to `initgraph’…”:这是最常见的错误,表示链接器找不到ege库的实现。请严格按照2.1节检查:
    1. 库文件libgraphics64.a是否放到了正确的lib目录。
    2. 项目属性->参数->链接器,是否添加了-lgraphics64
    3. 如果是32位项目,确保使用libgraphics.a-lgraphics
  • “graphics.h: No such file or directory”:头文件找不到。检查ege.h是否在include目录,或者尝试在代码中使用#include <ege.h>并确保头文件路径正确。
  • 程序一闪而过:图形窗口打开后立即关闭。确保在主循环结束前有等待机制,比如getch()while (!kbhit())循环。同时检查initgraph是否成功。

7.2 运行时问题与图形异常

  • 画面闪烁严重:没有启用双缓冲。确保在initgraph后立即调用setrendermode(RENDER_MANUAL)
  • 动画卡顿、不流畅
    • 检查主循环中是否有非常耗时的操作(如每帧都加载图片)。
    • 尝试减少delay_ms的值,或使用delay_fps(60)
    • 如果小球数量很多,碰撞检测(O(n²)复杂度)可能成为瓶颈。可以考虑空间划分优化,如网格法。
  • 文字有黑色背景块:忘记调用setbkmode(TRANSPARENT)。在输出文字前设置此模式。
  • 图片显示为黑色或错误
    • 检查图片路径是否正确。建议使用相对路径,并将图片放在与.exe相同的目录。
    • 检查图片格式是否被支持。getimage对某些PNG的透明通道支持可能有问题,可尝试转换为BMP或调整PNG格式。
    • 确保在initgraph之后才加载图片。

7.3 逻辑与交互调试技巧

  • 使用控制台输出:在关键逻辑处(如碰撞发生时)使用printf向DEV-C++的控制台输出变量值。虽然开了图形窗口,但控制台依然存在。这是最直接的调试方法。
  • 绘制调试信息:直接在图形窗口上绘制辅助线、点或文字来可视化状态。例如,在碰撞检测时,画出两个球的圆心连线;在鼠标点击处画一个十字。
    // 在draw函数中 setcolor(YELLOW); line(ball1.x, ball1.y, ball2.x, ball2.y); // 画出球心连线 char dist[50]; sprintf(dist, "d^2=%d", dx*dx+dy*dy); outtextxy((ball1.x+ball2.x)/2, (ball1.y+ball2.y)/2, dist); // 显示距离平方
  • 简化问题:当多球碰撞逻辑复杂时,先回归到两个球的场景,确保基础碰撞正确,再加入第三个球。同样,先让物体匀速运动,再增加重力或摩擦力。
  • 帧步进调试:在主循环内,可以设置一个布尔变量pause,用某个按键(如‘P’键)来控制。当暂停时,按一次‘N’键(Next)只执行一次循环更新。这样你可以一帧一帧地观察程序状态的变化。