资讯动态

EasyX C/C++ 小球动画入门:帧循环、边界反弹与双缓冲防闪烁

发布时间:2026/10/27 22:30:41 来源:尧图企业网站定制
从控制台的黑底白字到屏幕上第一个会动的小球这中间隔着的其实不是会不会写 C/C而是怎么把计算机当成一块可以擦写的画布。我带的几个新人里凡是自己能独立把小球在 EasyX 窗口里画出来、让它匀速跑起来、并且做到不闪的后面学游戏循环、碰撞检测、状态机基本都是一路平推卡在这一步的往往不是语法问题而是几个从来没被讲透的底层概念——坐标系朝哪边、屏幕为什么闪、Sleep 到底在干什么。这篇就是基于 EasyX 库的初级 C/C 编程游戏制作系列里的小球动画第一篇它会带你从零搭好环境、画出一个静态小球、把它改成匀速直线运动、再用双缓冲把闪烁干掉。适合刚学完 C 语言基本语法、能写 for 循环和 if 判断、但还没碰过图形界面的朋友也适合想把课程设计捡起来、给娃做个小演示的上班族。整篇的代码都可以直接抄下来跑跑通了再回头看解释效率更高。1. 为什么第一个练手项目要选会动的小球1.1 EasyX 补上的那块短板学 C/C 最难受的阶段是语法都会了但写出来的东西永远是控制台里那几行字符。你写了个学生成绩管理系统跑起来是一堆菜单数字你写了个猜数字游戏交互体验是请输入 1 到 100 之间的数。这套东西拿来交作业能过但拿来验证自己是不是真的理解了程序运行这件事太弱了。控制台程序的输出是一次性铺满、然后等待输入它天然不适合表达连续变化的东西。EasyX 解决的就是这个断层。它是 Windows 平台上的一个图形库把底层那套 GDI 绘图接口封装成了二十来个函数头文件只有一个graphics.h学会initgraph、cleardevice、solidcircle、Sleep这四个函数你就能让屏幕上出现会动的东西。类比一下命令行像发短信一条一条发、一条一条等回复图形窗口像打视频电话画面是连续刷新的你可以一边说话一边看到对方的表情变化。C/C 游戏制作入门真正开始有做游戏的感觉就是从画面能连续刷新那一刻算起的。1.2 小球是信息量最小、覆盖面最广的载体为什么不是直接做飞机大战、做俄罗斯方块因为那些项目里每一个都能拆出十几个互相纠缠的问题新手一上手就是一团乱麻。而一个圆它需要的信息只有五样圆心的 x 坐标、圆心的 y 坐标、半径、颜色、速度。五样东西却刚好对应了图形编程最核心的五个知识点。小球身上的属性背后对应的核心概念后续会延伸到哪里圆心坐标 (x, y)窗口坐标系、浮点与整数转换摄像机、视口、缩放半径 r图形绘制参数、包围盒思路碰撞检测、物理引擎填充色颜色模型 RGB、绘制状态贴图、精灵、渲染批次速度 (vx, vy)帧循环、时间步长运动学、抛物线、阻尼每帧重画双缓冲、刷新与撕裂渲染管线、垂直同步这张表我一般会直接甩给新人看让他们明白不是你今天学了个玩具而是你学的这五件事在任何规模的游戏项目里都还在。1.3 这一篇能带你走到哪一步先把预期说清楚免得中途觉得怎么还没做出游戏。这一篇的终点是窗口里有一个圆它沿着直线匀速移动碰到窗口边缘能弹回来用鼠标点一下能在点击位置生成一个新的小球全程几乎看不到闪烁。到这里为止你手上还没有游戏但游戏需要的骨架——初始化、循环、更新状态、渲染画面、处理输入、清理——已经全部站起来了。后面的续篇会在这套骨架上加东西重力与抛物线、多个小球之间的弹性碰撞、鼠标拖拽甩球、帧率统计面板、简单的粒子拖尾。每加一样你都会发现它挂在同一个循环里只是更新那一步多算了点东西渲染那一步多画了点东西。这就是游戏循环的迷人之处也是我建议所有人老老实实从小球开始的原因——先把骨架搭对后面加肌肉才不会塌。2. 环境准备别在第一步浪费一晚上2.1 编译器组合怎么选EasyX 是 Windows 平台的东西所以环境基本锁定在 Windows 上。编译器我建议就用主流的那个集成开发环境IDE社区版免费装的时候记得勾上使用 C 的桌面开发这个工作负载里面包含了 C 编译器和 Windows SDK而 EasyX 依赖的正是 Windows SDK 里的 GDI 那一套。不勾这一项后面会出现一堆找不到windows.h的报错跟 EasyX 本身没关系纯粹是 SDK 没装。版本上有个小提醒比较新的 IDE 版本和 EasyX 的适配是最稳的如果你用的是四五年前的老版本可能会出现库文件版本对不上、链接阶段报错的情况。真遇到了别急着怀疑代码先确认两边的版本年代是不是差得太多。另外项目类型要选空项目或者控制台应用程序不要选 Windows 桌面应用程序模板那个模板会自带一堆窗口消息代码跟 EasyX 自己的窗口初始化打架。2.2 安装完之后先做一次目录体检EasyX 的安装包基本是一键式的它会自动扫描你机器上装了哪些版本的 IDE然后让你勾选要安装到哪几个。安装完成后它做的事情其实非常朴素把graphics.h、easyx.h两个头文件复制到 IDE 的 include 目录把easyx.lib、easyxw.lib复制到 lib 目录。我一般会让人装完之后手动去看一眼这两个目录确认文件真的在那里。这个习惯看着多余但能省下后面大量代码明明一模一样为什么别人能跑我不能的困惑。库文件的名字里带w的那个是宽字符Unicode版本不带的是多字节版本这个区别后面会咬你一口先记住。提示如果你只是想快速验证环境不需要建工程文件直接在 IDE 里新建一个单文件.cpp把下面几行贴进去按 F5能弹出一个黑窗口就说明链路通了。2.3 三行代码的最小验证环境装完第一件事不是写动画是确认能不能开窗。这是最小验证代码#include graphics.h #include conio.h int main() { initgraph(800, 600); // 创建一个 800x600 的绘图窗口 setbkcolor(RGB(24, 26, 32)); // 设置背景色为深灰蓝 cleardevice(); // 用背景色清一次屏 _getch(); // 等待任意键防止窗口一闪而过 closegraph(); // 关闭绘图窗口释放资源 return 0; }这里有几个细节值得单独说。initgraph(800, 600)的两个参数是窗口的宽和高单位是像素第三个参数可以省略省略等价于 0表示创建一个普通的绘图窗口、不显示控制台。如果你调试时想同时看到printf的输出可以传EW_SHOWCONSOLE这个标志位控制台窗口会跟图形窗口一起出现很适合排查变量到底算对没算对。cleardevice()这一行很多人会漏。setbkcolor只是修改了当前的背景色这个状态它不会自动重绘屏幕必须调用一次清屏函数背景色才会真正铺上去。不带这一行你看到的是一片纯黑然后误以为自己设的颜色没生效。_getch()来自conio.h作用是卡住程序等按键否则initgraph开完窗紧接着就closegraph你的眼睛只能捕捉到一道残影。2.4 两个一上手就会踩的配置坑第一个坑是字符集。老版本的 EasyX 只支持多字节字符集代码里outtextxy(10, 10, 你好)这样直接传窄字符串是没问题的。新版虽然支持宽字符了但很多人装完之后发现输出中文是乱码或者编译直接报无法从 const char* 转换为 LPCTSTR。解决办法有两个一是在项目属性里把字符集改成使用多字节字符集二是老老实实用_T(你好)宏把字符串包起来。我倾向于第一种简单粗暴反正初学阶段也不太会用到宽字符特有的功能。第二个坑是链接错误。典型的报错长这样LNK2019: 无法解析的外部符号 _initgraph。这条报错的含义非常明确——编译器找到了函数声明所以编译过了但链接器没找到函数实现。也就是说头文件找得到lib 文件没找到。原因通常是安装时选错了 IDE 版本或者你后来升级了 IDE 但没重新装库。这时候别去改代码去查 lib 目录里到底有没有easyx.lib路径对不对。3. 坐标系与绘图基础先把球画出来3.1 屏幕坐标系的 y 轴是朝下的这是所有从数学课转过来的人第一个不适应的地方。你脑子里想的坐标系是笛卡尔坐标系原点在左下角y 轴往上。而屏幕坐标系的原点在左上角x 轴往右y 轴往下。也就是说y 100这个点在y 300这个点的上方。这个差异会持续影响你后面所有的计算。比如你想让球往下掉那速度 vy 就得是正数你想让球往上飞vy 就得是负数。写抛物线的时候重力加速度 g 是正数因为它在把物体往 y 增大的方向拽。刚接触时容易写反写反了的表现是球往天上跑改个符号就好了不用怀疑人生。窗口的客户区大小就是你initgraph传进去的宽高但要注意窗口还有标题栏和边框那部分不属于客户区绘图的坐标原点是客户区的左上角不是整个窗口的左上角。所以你画在 (0, 0) 位置的图形会紧贴客户区左上角出现位置是对的。用getwidth()和getheight()可以拿到当前客户区的宽高这在后面写边界判定的时候比硬编码数字靠谱得多。3.2 画圆的三个函数选错了会白边EasyX 里跟圆相关的函数有三个效果差别很微妙新手经常选错然后盯着屏幕纳闷为什么我的球有一圈奇怪的颜色。函数是否填充是否描边典型用途circle(x, y, r)否是空心圆、调试包围圈、轨迹框fillcircle(x, y, r)是是需要描边的球、带轮廓的气泡solidcircle(x, y, r)是否纯色小球、粒子、光点circle只画轮廓线线条颜色由setlinecolor决定内部是透明的能看到背景。fillcircle会先用填充色涂满内部再用线条色描一圈边。solidcircle只填充、不描边看起来最干净。做小球动画我建议直接上solidcircle因为描边会让球在快速移动时产生一圈拖影感而且球多了之后每一圈描边都是额外的绘制开销。还有一个参数是线宽用setlinestyle(PS_SOLID, 3)之类的方式设置。线宽在画空心圆的时候很有用比如做能量圈效果但画实心球就别折腾了。3.3 颜色的两种写法EasyX 里颜色是个 32 位的整数用COLORREF类型表示。写法有两种。一种是RGB(r, g, b)宏三个参数分别是红绿蓝分量取值 0 到 255这个写法直观改起来方便我基本都用这个。另一种是十六进制字面量比如0xFF6B6B格式是0xBBGGRR注意是蓝绿红的顺序跟网页上常见的#RRGGBB正好反过来很容易写错。写错了不会报错只会得到一朵诡异颜色的花。颜色的当前状态这件事特别值得强调。setfillcolor、setlinecolor、settextcolor这些函数改的是全局的绘图状态不是某个图形的属性改完之后所有后续的绘制都会用新颜色。所以如果你想画一个红球和一个蓝球必须画完红的调一次颜色再画蓝的不能一次性把所有颜色都绑到图形上。这个设计在初学时会觉得别扭但它其实是图形 API 的通用做法理解成画笔和颜料盘就好——你换了笔下一笔才变色。3.4 静态小球完整代码#include graphics.h #include conio.h int main() { initgraph(800, 600); setbkcolor(RGB(24, 26, 32)); cleardevice(); // 球的基本参数 int x 400; // 圆心 x窗口水平中线 int y 300; // 圆心 y窗口垂直中线 int r 40; // 半径 40 像素 // 先画一个外圈光晕可选纯装饰 setfillcolor(RGB(70, 40, 50)); solidcircle(x, y, r 10); // 再画球本体 setfillcolor(RGB(255, 107, 107)); solidcircle(x, y, r); // 画一个高光点让它看起来像球而不是圆 setfillcolor(RGB(255, 190, 190)); solidcircle(x - r / 3, y - r / 3, r / 4); _getch(); closegraph(); return 0; }这段代码里我加了两层纯装饰的东西外圈光晕和高光点。它们对逻辑毫无帮助但对观感的提升非常大——同样是画圆加个高光之后人眼会立刻把它识别成球体而不是圆形色块。这是个便宜的小技巧后面你改成运动版的时候也可以保留视觉效果会好一个档次。注意高光点的坐标是用x - r/3和y - r/3算出来的也就是往左上偏移。因为 y 轴朝下所以左上才是视觉上的左上。这个位置反了的话光看起来就像从地底下打上来的很别扭。4. 从静止到运动动画循环的本质4.1 动画就是擦掉重画电影是每秒 24 张静止画面连起来骗过眼睛的屏幕上的动画是同一回事每一帧都把整个画面清空把物体挪一点点新位置重画一遍连续几十帧下来人眼就看成平滑运动了。这个清空—重画的循环就是游戏循环最原始的样子。对应到代码上就是一个while死循环循环体里先cleardevice()清屏再solidcircle()画球然后让坐标加上一点增量最后Sleep()等一下。四步缺一不可。漏掉清屏球会拖出一条长长的轨迹漏掉坐标更新球永远不动漏掉 SleepCPU 一个风味核心会跑满 100%风扇立刻起飞。我用橡皮擦这个类比跟人解释过一遍接受度很高cleardevice()就是拿橡皮把整块白板擦干净solidcircle()是在白板上画一个新球Sleep()是画完之后停一下手等下一帧再擦。三件事一秒做六十遍你就看到球在动了。4.2 速度要用每秒多少像素不要用每帧多少像素这是本篇最值钱的一个建议。新手最常见的写法是x 5;意思是每帧往右挪 5 个像素。跑起来确实能动没问题但你有没有想过这一秒到底跑了多少像素如果我把Sleep(16)改成Sleep(8)速度立刻翻倍因为帧数翻倍了。速度变成了跟帧率绑死的东西帧率一抖球速就抖。正确做法是把速度定义成物理意义上的量每秒多少像素。然后用每一帧经过的时间去乘它。位移 速度像素/秒 × 时间秒一帧按 60 帧每秒算那就是 1/60 秒也就是 0.0167 秒。速度如果是 220 像素每秒那每帧位移就是220 × 0.0167 ≈ 3.67像素。这样即使以后帧率变了只要你把当前帧的实际耗时算准位移就还是准的。这里有个绕不开的问题位移算出来是小数但屏幕坐标是整数像素。solidcircle的参数是int所以必须强制转换。有个细节一定要记住——位置变量本身必须是浮点数float或double只在绘制的那一刻转成 int。如果你偷懒把 x 定义成 int每帧3.67会被截断成3实际速度就变成了 180 像素每秒误差超过 18%而且这个误差会累积越跑越偏。float x 100.0f; const float vx 220.0f; // 像素/秒 const float dt 1.0f / 60.0f; // 每帧 1/60 秒 x vx * dt; // 累加用浮点 solidcircle((int)x, y, r); // 绘制时再转 int4.3 Sleep 的时间到底该给多少Sleep(16)是新手最常写的值对应大约 62 帧每秒接近显示器的刷新节奏看起来很舒服。但我得提醒一句Sleep的精度没那么好它只能保证至少睡这么久实际可能是 16 毫秒也可能是 18、20 毫秒具体取决于系统的调度粒度。所以你不能靠我 Sleep 了 16 毫秒所以我的帧率就是 62.5来算物理量那是自欺欺人。真要做精确点得用时间戳。EasyX 环境下最简单的是GetTickCount()返回系统启动以来的毫秒数高精度一点的用QueryPerformanceCounter。思路是每帧开头记一个时间帧尾再记一个两者之差就是这一帧的真实耗时用它来做位移计算帧率波动就完全不影响物理表现了。写法帧率稳定性实现难度适用场景Sleep(16)固定值一般受系统调度影响极低初学练手、演示Sleep(16) 定时器校正较好中需要稳定节奏的小项目时间戳差值驱动好中有物理模拟的项目不过话说回来第一版先别上时间戳那样代码会变长把注意力从动画循环这个核心上拽走。就用Sleep(16)配固定dt等你把球跑起来、把闪烁消掉再回来改成时间戳版本那时候你已经有能力理解改动带来的差别了。4.4 第一版会动的球#include graphics.h #include conio.h int main() { const int W 800, H 600; initgraph(W, H); setbkcolor(RGB(24, 26, 32)); float x 100.0f; float y H / 2.0f; const int r 40; const float vx 220.0f; const float dt 1.0f / 60.0f; while (true) { cleardevice(); setfillcolor(RGB(255, 107, 107)); solidcircle((int)x, (int)y, r); setfillcolor(RGB(255, 190, 190)); solidcircle((int)x - r / 3, (int)y - r / 3, r / 4); x vx * dt; if (x W r) x -r; // 出右边界后从左边重新进来 Sleep(16); } closegraph(); return 0; }这段代码跑起来球会从左边匀速滑到右边出去之后再从左侧冒出来往复循环。第一次看到自己写的代码在屏幕上动起来成就感是实打实的我到现在还记得那种感觉。不过你几乎一定会发现一个问题球在动但画面有轻微的抖动边缘还有点毛刺感。原因就在下一节——你现在是直接在窗口上画中间没有任何缓冲。5. 闪烁的成因与双缓冲改造5.1 闪屏究竟是怎么产生的先回忆一下你的每一帧做了两件事先清屏整块画布被涂成背景色再画球。这两个动作不是原子的中间有极短的时间间隔。如果你的程序恰好在清完屏、还没画球的那个瞬间被系统要求刷新窗口屏幕上就会出现一帧只有背景、没有球的画面。一帧两帧你可能看不出来但每秒六十帧里如果有几帧是这种情况人眼就会感知为闪。尤其是球颜色亮、背景色深的时候对比强烈闪烁特别明显。这个现象的本质是你把中间状态暴露给了屏幕。还有一个加重因素GDI 的绘制操作是一个函数一个函数地发往显卡的solidcircle画的时候是从图形内部某一点开始往外铺的如果你的球足够大你能看到它从中心扩散开的过程。球小的时候看不出来球大了就很明显。5.2 批量绘制的三个函数EasyX 提供了解决这个问题的机制叫批量绘制三个函数配套使用函数作用调用时机BeginBatchDraw()开启批量模式之后的绘制先落在内存里循环开始前调一次FlushBatchDraw()把内存里攒好的画面一次性贴到窗口每帧末尾调一次EndBatchDraw()结束批量模式并做一次刷新程序退出前调一次原理说穿了很简单给你一块看不见的后备画布你所有的清屏和绘制都往这块画布上做做完了再整体拷贝到屏幕上。因为拷贝是一次性完成的屏幕上永远只会出现画完了的完整画面不可能出现只擦干净了还没画球的中间态。这就是双缓冲。类比一下你写字的时候先在一张草稿纸上写完再整张纸拍在桌上而不是直接对着桌子一笔一笔划让人看到你写到一半的样子。5.3 三行改造闪烁直接消失改造前后对比代码上真的只多三行BeginBatchDraw(); // 循环外开启 while (true) { cleardevice(); setfillcolor(RGB(255, 107, 107)); solidcircle((int)x, (int)y, r); x vx * dt; FlushBatchDraw(); // 循环内每帧末尾提交画面 Sleep(16); } EndBatchDraw(); // 循环外收尾有个坑我必须提前说FlushBatchDraw()的位置非常讲究它一定要在这一帧所有绘制都完成之后、下一帧清屏之前调用。如果你把它写在cleardevice()前面那等于把上一帧的内容又刷了一遍画面会变成卡顿的慢动作。如果你漏掉它屏幕上干脆什么都没有因为所有东西都攒在内存画布里没提交。这两种情况我都见过排查的时候先看这一行。再一个容易忽略的点beginBatchDraw之后如果你在循环里用了getmessage之类的阻塞式输入画面会卡住不刷新。这种时候得改成非阻塞的输入方式或者手动调一次FlushBatchDraw。这个坑在加交互的时候会撞上先记着。6. 交互与边界让球真正活起来6.1 输入为什么不能再用阻塞式函数前面几版代码里我一直用_getch()卡住程序。现在进了主循环这招就废了——_getch()会死等键盘球立刻停在原地动画彻底卡死。这就是典型的阻塞式输入和游戏循环八字不合。图形程序里的输入处理有两种风格。一种是消息驱动系统把鼠标键盘事件排成队列你挨个取出来处理另一种是状态轮询每帧主动去问现在鼠标在哪个位置、左键是不是按着的。EasyX 两套都给了ExMessage结构体加peekmessage是消息驱动的写法GetMouseMsg是轮询的写法。我推荐用peekmessage因为它有个很实用的参数是否把消息从队列里移除。peekmessage(msg, EM_MOUSE, true)是取走消息false是只看不取。语义上清晰也方便你做按住鼠标拖拽这类持续状态。下面这段是标准骨架ExMessage msg; while (true) { while (peekmessage(msg, EM_MOUSE)) { if (msg.message WM_LBUTTONDOWN) { // msg.x 和 msg.y 就是鼠标点击时的客户区坐标 } } // 更新 渲染 }注意while (peekmessage(...))是个内层循环它的意思是把队列里所有积压的鼠标消息一次处理干净不是只处理一个。用if的写法也行但积压的消息要好几帧才消化完手感会发黏。按键退出可以用conio.h里的_kbhit()配合_getch()这也是非阻塞的_kbhit()只问有没有按键、不等待正好适合放进循环。6.2 碰壁反弹判边界和速度取反球要能弹逻辑就两条判断球有没有碰到墙碰到就把对应方向的速度反过来。判边界的时候有个细节很容易写错能不能用x W不能。因为 x 是圆心坐标球是有半径的圆心碰到右边界的时候球已经有一半出界了。正确的判定是x r W此时整个球刚好贴住右边界。左边是x - r 0上边是y - r 0下边是y r H。还有一件比判定本身更重要的事反弹之后要把球的位置拨回边界内。如果你只把速度取反、不管位置球在某个极端情况下比如速度很大、一帧跨了很远可能已经陷进墙里了下一帧判定还是碰到了于是速度再反一次球就在墙里来回抽搐永远出不来。标准写法是这样的const int W 800, H 600; const int r 40; float x 200, y 200; float vx 260, vy 190; void Update(float dt) { x vx * dt; y vy * dt; if (x - r 0) { x r; vx -vx; } if (x r W) { x W - r; vx -vx; } if (y - r 0) { y r; vy -vy; } if (y r H) { y H - r; vy -vy; } }四个if每个里面都干了两件事把位置拉回合法范围、把速度取反。前者是纠偏后者是改变运动方向。这四行写对了球就会一直在窗口里蹦跶。你会发现球的运动轨迹还挺有规律的因为两个方向的速度是独立的水平方向来回和垂直方向来回叠加起来画出的是一条规则的折线偶尔还能看到球沿着对角线方向走。提示vx和vy别设成整数比比如 200 和 200那样球会一直走 45 度角弹来弹去永远同一条路径看着特别死板。设成 260 和 190 这种没什么公因数的数轨迹就乱得多更耐看。6.3 点击生成新球从单个到多个的结构变化单个球的变量好办直接三个 float。但点击生成新球意味着球的数量不固定这就必须换数据结构了。最合适的容器是std::vector因为球的数量可变且不需要随机访问特定下标。定义一个小结构体把球的所有属性打包struct Ball { float x, y; float vx, vy; int r; COLORREF color; };主循环里维护一个std::vectorBall balls;点击时balls.push_back(...)塞一个新的进去。更新和渲染各写一个循环遍历它。while (true) { ExMessage msg; while (peekmessage(msg, EM_MOUSE)) { if (msg.message WM_LBUTTONDOWN balls.size() 60) { Ball b; b.x (float)msg.x; b.y (float)msg.y; b.vx (float)(rand() % 401 - 200); // -200 ~ 200 b.vy (float)(rand() % 401 - 200); b.r rand() % 12 8; // 8 ~ 19 b.color RGB(140 rand() % 115, 140 rand() % 115, 140 rand() % 115); balls.push_back(b); } } for (auto b : balls) UpdateBall(b, dt); // 内含边界反弹 cleardevice(); for (const auto b : balls) { setfillcolor(b.color); solidcircle((int)b.x, (int)b.y, b.r); } FlushBatchDraw(); Sleep(16); }这里加了个balls.size() 60的上限不是随手写的。GDI 画实心圆的开销不小每个圆都要走一遍路径光栅化球的数量到了两三百个之后你会明显看到帧率往下掉。先限在 60 以内体验是流畅的等后面学了更高效的批量绘制或者贴图方案再把上限放开。球之间互相穿过、不做碰撞这一版是故意的。多球碰撞检测涉及两两距离计算和速度交换复杂度一下子上去了留到续篇讲。现在先让它们自由穿行视觉效果已经足够热闹了。7. 常见问题排查实录7.1 一张表覆盖八成报错新手卡壳的场景其实高度重复。下面这张表是我这些年被问得最多的按现象—原因—解法整理遇到问题先对着找一遍能省掉大量瞎猜的时间。现象最可能的原因解决办法报错LNK2019: _initgraphlib 路径不对或 IDE 版本与库不匹配检查 lib 目录是否有easyx.lib重装对应版本initgraph后窗口一闪而过缺少等待输入的语句在closegraph前加_getch()设置了背景色但画面还是黑的没调cleardevice()setbkcolor后跟一次清屏球拖出一条长尾巴循环里漏了cleardevice()每帧开头清屏加双缓冲后屏幕一片空白漏了FlushBatchDraw()每帧末尾提交一次球卡在墙里抖动不停反弹后没把位置拨回边界内速度取反的同时修正坐标画面卡顿、鼠标点不动用了阻塞式输入换peekmessage或_kbhit中文输出是乱码字符集不匹配项目属性改多字节或字符串包_T()CPU 占用飙到 100%循环里没Sleep帧末加Sleep(16)球的速度和想象中不一样位置变量用了 int小数被截断位置改 float绘制时再转 int7.2 三个只有真跑过才知道的细节第一个细节是开局第一帧会闪一下。initgraph刚创建窗口时客户区里是未初始化的内容你的第一次cleardevice()会把它们覆盖掉但这一帧的观感就是白闪一下。解决方法是把cleardevice()和flushBatchDraw()在进循环之前先手动执行一次让第一帧就是一个干净画面。第二个细节是窗口被拖动或最小化后画面会乱掉。因为窗口重绘的时候不会自动重跑你的绘制逻辑缓冲区里的内容也不再对应屏幕了。EasyX 的批量绘制模式在窗口恢复时偶尔会出现内容残留。如果你的程序需要长时间挂着可以在循环里加一个判断检测窗口是否被挡住/最小化必要时重绘一次。初级项目一般碰不到但知道有这么回事以后不至于莫名其妙。第三个细节是rand()没播种。上面生成随机速度的代码如果你不调srand((unsigned)time(NULL))那每次运行程序球的初始速度序列都是一模一样的。看着像个小问题但调试的时候这个特性反而是好事——行为可复现方便定位。等你确认逻辑没问题了再把播种加上让它每次都不一样。注意srand只需要在程序开头调一次别放进循环里。放循环里的话同一毫秒内多次调用会拿到同一个种子随机数反而变得不随机了。这是个流传很广的经典错误。7.3 性能上的经验判断最后说一下性能的直觉建立这对以后判断该不该优化很有帮助。在 EasyX 这套 GDI 方案下几个量级大致是这样的清屏 800×600 一个全屏填充开销很小随便跑画单个实心圆几十微秒级别六十个球也毫无压力但如果你开始画大量带描边的圆或者每帧画上千个图形帧率就会肉眼可见地掉。判断方法很简单用GetTickCount()把每帧的耗时打出来跑一遍看看实际数字。我一般会让新人做个实验把球的数量从 10 加到 50、100、200记录每档的帧时间画个曲线自己看一眼拐点在哪。这个实验花不了二十分钟但会让你对这套方案的能力边界建立起真实的手感比看任何文章都管用。我个人在实际带人做这个小项目的体会是大多数人卡住的地方从来不是不会写循环而是没有建立起对帧这个概念的身体记忆。你会写while和你脑子里能同时想象出每一帧屏幕被擦掉一次、球被挪动 3.67 个像素、然后画面被整体拍上去这件事是两个层次。后者只能靠反复改参数、看效果、猜原因、验证这个循环来建立。所以我一般不建议新手直接抄一份完整代码跑通就完事而是按这个顺序自己走一遍先让球静止再让它动再让它不闪再让它碰墙最后让它能被鼠标点出来。每加一样修改不超过十行你就有机会看清楚是哪一行在起作用。这个顺序看着笨但它是我见过唯一能让帧这个概念真正长在脑子里的方法。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑