资讯动态

EasyX图形库实战:从C语言入门到开发小游戏

发布时间:2026/10/24 1:07:12 来源:尧图企业网站定制
很多学完C语言的朋友都会问我同一个问题语法都认识指针也勉强能看懂但一说到写个游戏就完全无从下手。控制台里黑底白字做个贪吃蛇都得靠ASCII字符一个个拼更别提什么画面、动画、音效了。想走正规路子去啃OpenGL或者DirectX又被一堆概念劝退。如果你也卡在这个阶段EasyX图形库基本是你最快能体验到程序画出窗口的路径——它是一个面向C/C初学者的轻量级图形库不需要了解Windows底层消息机制只要包含一个头文件就能用一组类似古早TC风格的函数在窗口里画点、画线、画圆、贴图、响应键盘鼠标。这篇文章不打算讲太深就是以我的实际使用经验带你从环境配置一路写到一个小游戏中间把坐标系、双缓冲、事件循环这些基础概念一次性说清楚。1. 为什么是 EasyX先想清楚它解决什么问题1.1 学完 C 语言之后为什么还是做不出游戏标准C语言的库函数里压根没有图形这个概念。printf能输出字符但字符不等于像素scanf能读键盘但读不到方向键的按下状态。你想让一个方块在屏幕上移动首先得有一个能画方块的窗口这个窗口从哪来Windows平台下你可以直接调用Win32 API创建窗口但CreateWindow、RegisterClass、消息循环这一套对初学者来说过于劝退——很多人还没看到第一个窗口就先被WNDCLASS结构体里的成员绕晕了。EasyX走的是另一条路。它把这些底层细节全部封装成一个个简单函数比如initgraph创建窗口circle画圆rectangle画矩形getch暂停。这种函数的命名方式很直白几乎看一眼就知道干什么用。它的目标用户非常明确刚学完C/C语法、想快速做出可视化效果、又不打算现在就深入图形学底层的初学者。1.2 与其他图形方案放一起比较我不止一次被问到为什么不直接学SDL或者raylib。先把话说清楚当然可以直接学那些但每个人的阶段不一样。下面这张表是我根据自己的实际体验整理的对比可以参考一下方案学习曲线跨平台安装复杂度适合场景EasyX很平缓仅Windows低一键安装C语言课设、小游戏Demo、快速可视化Win32 API陡峭仅Windows低系统自带想深入了解Windows桌面开发SDL2中等跨平台中需配置库目录想做正式一点的2D游戏OpenGL/DirectX很陡峭视API而定高3D渲染、图形学、游戏引擎对大多数刚学完指针和结构体的人来说EasyX最大的优势是安装即用函数即懂。你不用先学窗口创建、设备上下文、消息处理三行代码就能弹出一个可以画图的白窗口。等你在EasyX里把游戏逻辑、坐标计算、碰撞检测这些真正的编程基本功练熟了再跳去学其他图形库会发现很多概念都是相通的只是换了一套接口而已。1.3 这款库适合哪些人不适合哪些人说白了EasyX就是个教学工具不是商业游戏引擎。它适合这几类人一是刚学完C语言、需要做课程设计的学生二是在校想参加小比赛但还没接触过游戏框架的同学三是任何想快速验证一个图形算法想法、又不想搭建复杂环境的开发者。它不适合的人也很明确想做3D游戏、想搞商业级渲染、想跨平台发布游戏的人。这些需求超出了EasyX的设计范围强行用它只会被局限住。我见过有些人把OpenGL教材扔一边非要用EasyX去模拟3D效果最后代码写得又长又乱完全没有必要。选工具一定要看场景EasyX的场景就是轻量、快速、教学、Windows下的小项目。2. 环境准备装库、配 IDE以及最容易卡住的细节2.1 下载安装的正确姿势EasyX的安装包是自动识别编译器的。你下载安装包之后双击运行它会检测电脑上已经装好的Visual Studio版本然后把对应的库文件自动释放到VS的include和lib目录里。这个过程对多数人来说很顺利但有两点需要注意。第一安装之前把所有Visual Studio实例全部关闭否则库文件可能写入失败而且不会给你明显的错误提示。第二安装之后重新打开VS让IDE重新扫描一次目录。如果装完库才发现VS是开着的装完也最好重启一下VS不然新建项目时有概率找不到头文件。装完怎么验证最简单的办法是新建个项目写一行#include graphics.h然后编译。只要能通过编译说明库的位置没问题。如果这里就报错多半是安装版本和编译器版本不匹配或者VS还停留在安装前的状态。2.2 新建项目和字符集问题VS里新建项目选空项目就行别选那些带预置代码的模板空项目干净。建好之后先做一件事右键项目→属性→配置属性→常规→字符集改成使用多字节字符集。这一步很关键。为什么EasyX很多老函数默认接受的是单字节字符串而VS新项目默认是Unicode字符集。如果不改你写中文文字输出的时候会满屏乱码或者直接编译报错。很多新手一上来就卡在这还以为是库坏了其实是字符集的问题。第二步在项目属性→C/C→预处理器→预处理器定义里加上_CRT_SECURE_NO_WARNINGS否则用一些传统函数时会出现安全警告虽然不影响运行但看着心烦。2.3 第一个图形窗口三行代码背后的意义来写第一个程序代码非常短#include graphics.h #include conio.h int main() { initgraph(640, 480); // 创建一个 640x480 的窗口 circle(320, 240, 100); // 画一个圆心在 (320,240)、半径 100 的圆 _getch(); // 等待按键后继续 closegraph(); // 关闭图形窗口 return 0; }这里面initgraph是初始化图形环境参数是窗口的宽和高。执行到这一行屏幕上会弹出一个640像素宽、480像素高的白底窗口。紧接着circle在窗口中央画个圆。_getch()的作用是让程序暂停等用户按任意键再继续——如果没有这一行程序一秒内跑完窗口刚出现就关闭你啥也看不见。closegraph负责回收图形资源释放窗口。这个流程其实就是所有EasyX程序的基本骨架初始化窗口→画图形→等待/交互→关闭窗口。后面不管游戏写到多复杂最外层一定是这个结构。2.4 新手最常见的三个报错我帮人排查过太多环境问题了下面三个出现频率最高。报错一无法打开包含文件“graphics.h”。这说明库文件没有正确安装到VS的include目录。检查一下安装包是否和VS版本匹配或者手动确认VS安装目录下的include文件夹里有没有graphics.h这个文件。报错二error LNK2019: 无法解析的外部符号。头文件找到了但链接库没找到多半是安装包只释放了头文件、没释放lib文件。重新安装一次或者确认lib目录下有没有libEasyX.a或EasyX相关库文件。报错三编译能通过但运行时窗口里文字全是乱码。这就是字符集问题按上面说的改成多字节字符集即可。环境这东西配置正确了一次之后就一直顺了所以值得花十分钟耐心搞定。3. 绘图的底层逻辑坐标系、颜色与画笔3.1 屏幕坐标系和数学坐标系的差别很多人第一次用EasyX画图会犯一个坐标反了的错。数学课上坐标原点在左下角x轴向右y轴向上但屏幕坐标系的原点在左上角x轴向右y轴向下。换句话说y坐标越大点在屏幕上越靠下。这一点在画矩形时特别明显。想画一个左上角在(50,50)、宽200、高100的矩形调用rectangle(50, 50, 250, 150)——明确左上角坐标和右下角坐标的对应关系比想着起点终点更能避免出错。做动画的时候想让小球往下落应该增加y坐标而不是减少。如果你按数学坐标系的直觉写小球会往反方向跑排查半天还以为是物理引擎的错其实只是坐标系搞反了。// 绘制一个从左上角 (100,80) 到右下角 (300, 200) 的矩形 rectangle(100, 80, 300, 200); // 等价于宽 200高 1203.2 颜色系统怎么调出一个喜欢的色调EasyX里颜色用四个字节的整数表示格式是0xRRGGBB即红色、绿色、蓝色各占一个字节每个分量取值0到255。比如0xFF0000是纯红0x00FF00是纯绿0x0000FF是纯蓝。想要黄色红色加绿色0xFFFF00。想调个随机色用RGB(rand() % 256, rand() % 256, rand() % 256)动态生成。库本身也预定义了一些常用颜色宏WHITE、BLACK、RED、GREEN、BLUE、YELLOW等等初学阶段直接用宏就行。真正需要自定义颜色的时候再手写RGB这样代码读起来也清楚。绘图时要注意区分三个概念背景色、线条色、填充色。背景色是窗口底色用setbkcolor设置改完还要cleardevice刷新才会整屏变白或变其他色线条色是画轮廓线的颜色用setlinecolor填充色决定图形内部是什么颜色用setfillcolor。setbkcolor(RGB(30, 30, 30)); // 深灰背景 cleardevice(); // 用背景色清空画布 setlinecolor(WHITE); // 线条白色 setfillcolor(RED); // 填充红色 fillcircle(320, 240, 80); // 画一个红色填充、白色边框的圆3.3 常用绘图函数一览图形函数是EasyX的核心先把最常用的几个记住应付绝大多数项目没问题。函数作用关键参数circle(x, y, r)画实心/空心圆圆心坐标、半径fillcircle(x, y, r)画填充圆同上rectangle(x1, y1, x2, y2)画矩形左上角、右下角坐标fillrectangle(...)填充矩形同上line(x1, y1, x2, y2)画线段两个端点ellipse(x1, y1, x2, y2)画椭圆外接矩形左上角和右下角polygon(points, n)画多边形POINT数组、顶点数量线型也能改。setlinestyle(PS_SOLID, 2)可以设置实线、虚线、点划线以及线条宽度。填充样式由setfillstyle控制比如setfillstyle(BS_SOLID)是纯色填充BS_HATCHED是斜线填充。这些在画一些特殊界面、按钮状态的时候挺有用但初学阶段保持默认就够了。3.4 为什么画面会被擦掉重绘机制EasyX里的绘图是一次性的。你画个圆程序继续执行圆不会自动消失但如果你调用cleardevice()清屏或者窗口被其他程序遮挡后再移开画面可能被系统擦掉需要重新绘制。这其实是所有图形程序的通病。对初学者来说理解画面需要反复重绘是进入动画编程的第一步。每一帧都做三件事清空画布、更新物体位置、重新绘制所有物体。这个三步循环做出来的效果就是动态画面。后面第4章会详细讲这个循环怎么搭。4. 动画与交互帧循环、双缓冲和消息响应4.1 游戏循环的通用骨架所有实时游戏不管用什么库、什么语言核心都是一个死循环。每个循环周期处理输入、更新状态、绘制画面俗称帧循环。EasyX里写起来非常直观while (true) { // 1. 处理输入 // 例如检测方向键改变人物坐标 // 2. 更新游戏状态 // 例如小球位置 速度 * 时间 // 3. 绘制画面 cleardevice(); // ... 绘制所有物体 Sleep(16); // 让帧率接近 60 FPS }这里的Sleep(16)很多人会漏掉。如果不暂停这个循环会疯狂空转CPU占用100%而且画面快得根本看不清。16毫秒对应大约每秒60帧是人眼感觉比较流畅的速度。你可以在10到30之间调数值越小越流畅但越消耗CPU。4.2 双缓冲解决闪烁问题的核心手段新手做动画最容易遇到的一个问题画面疯狂闪烁就像老式电影胶片在跳动。原因很简单——你每一帧都在清屏然后重画而窗口是实时显示在屏幕上的。用户看到的是擦掉旧画面和画上新画面两件事交替发生中间露出的背景就是闪烁。解决办法叫双缓冲原理是先在一块看不见的画布上把这一帧所有内容画完然后一次性整块贴到屏幕上。用户看到的永远是完整的帧闪烁自然消失。EasyX封装好了三个函数BeginBatchDraw(); // 开始批量绘制启用双缓冲 while (true) { cleardevice(); // 移动物体 // 绘制所有内容 FlushBatchDraw(); // 把后台画布一次性刷到屏幕 Sleep(16); } EndBatchDraw(); // 结束批量绘制注意绘制代码全部写在BeginBatchDraw()和EndBatchDraw()之间每一帧结尾调用FlushBatchDraw()刷屏。这个组合是EasyX做动画的标配几乎每个小游戏都得用上。4.3 键盘方向键的读取技巧处理键盘输入EasyX有几个函数。简单场景用_kbhit()判断有没有按键再用_getch()读取具体按键值。但方向键比较特殊——方向键的键码是两字节的第一次_getch()会返回0xE0或者0x00第二次才返回具体方向码。很多新手只读一次得到的数字完全看不懂在哪。if (_kbhit()) { int ch _getch(); if (ch 0xE0) // 说明是方向键 { ch _getch(); // 再读一次 switch (ch) { case 72: // 上 break; case 80: // 下 break; case 75: // 左 break; case 77: // 右 break; } } }方向键这么处理普通字母键就直接拿第一次读取的值判断就行。如果你的游戏只要方向键我建议把它封装成一个类似getDir()的函数返回上下左右四个方向枚举主循环里调用起来会清爽很多。4.4 鼠标从点击到拖拽鼠标交互比键盘简单。EasyX里用MOUSEMSG结构体存鼠标信息通过两个函数配合MouseHit()判断有没有鼠标消息GetMouseMsg()取出最近一次鼠标消息。while (true) { if (MouseHit()) { MOUSEMSG msg GetMouseMsg(); if (msg.uMsg WM_LBUTTONDOWN) // 左键按下 { // msg.x, msg.y 就是鼠标点击位置的坐标 circle(msg.x, msg.y, 30); } if (msg.uMsg WM_MOUSEMOVE) // 鼠标移动 { // 可以用来实现拖拽 } } FlushBatchDraw(); Sleep(16); }这种按消息处理的方式很像真正的Windows编程但EasyX帮你把底层收发包装干净了直接用msg.x、msg.y拿到坐标非常适合做鼠标控制的小游戏、绘图板、简易编辑器。5. 资源加载图片、文字、音乐的常规操作5.1 图片loadimage 与 putimage 的配合图片是让游戏长得像游戏的关键。EasyX里要先创建一个IMAGE对象用loadimage把图片文件读进来再用putimage把它贴到指定位置。最简单的用法是这样IMAGE img; loadimage(img, Lhero.png); // 从文件加载 putimage(100, 100, img); // 贴到 (100,100)这个加载过程看似简单坑主要在路径上。你的程序运行的当前目录不一定是源代码所在目录VS里默认当前目录可能是项目文件所在的目录也可能是可执行文件所在的目录。最稳妥的做法把图片放在和源文件同一个目录然后在项目属性→调试→工作目录里设置成源文件目录或者干脆在loadimage里写完整路径。loadimage还支持缩放加载在图片路径后面再加一个宽高参数例如loadimage(img, Lhero.png, 64, 64)直接把原图缩放到64x64再加载省得运行时反复计算。这个功能做角色贴图时很实用。5.2 文字乱码问题的根源与解法文字输出函数是outtextxy(x, y, 字符串)但它默认接收的是宽字符也就是wchar_t*。写普通字符串会编译报错必须加L前缀settextstyle(24, 0, _T(宋体)); // 字体大小24名称宋体 outtextxy(10, 10, L你好EasyX);如果之前没改字符集这里很容易出现乱码或者编译报错。我的建议很简单项目属性里把字符集设成多字节然后所有字符串常量都用_T()宏包一下比如_T(你好)。在多种字符集切换时_T()会自动适配这样迁到哪个项目都不容易出问题。设置字体用settextstyle(高, 宽, 字体名)第二个参数为0表示宽度自适应。做标题、分数显示、对话文字这个函数基本够用了。5.3 音乐一条命令控制 MP3EasyX本身不提供声音系统但可以调用Windows的API。用mciSendString函数一句话就能播放MP3#include mmsystem.h #pragma comment(lib, winmm.lib) mciSendString(_T(open bgm.mp3 alias music), NULL, 0, NULL); mciSendString(_T(play music repeat), NULL, 0, NULL); mciSendString(_T(close music), NULL, 0, NULL); // 停止并释放这里的alias music相当于给这个播放任务起了个别名后面所有操作都通过这个名字控制。play music repeat里的repeat表示循环播放适合做背景音乐。做音效就把它改成不循环的文件在特定事件触发时调用播放语句。注意mciSendString属于Windows多媒体扩展的一部分必须在Windows环境下运行。这也再次印证了EasyX的定位——它就是一个Windows专属的教学环境。5.4 资源加载的性能习惯新手最容易写出的低效代码是在游戏循环里重复加载图片。loadimage和mciSendString这些操作都是磁盘IO级别的很慢。一帧调用一次画面直接卡成幻灯片。正确做法是程序启动时把需要的图片全部加载到IMAGE数组或结构体里游戏循环中只使用putimage贴图。IMAGE heroFrames[4]; // 启动时一次加载全部帧 loadimage(heroFrames[0], Lhero1.png); // ... // 游戏中只贴图 putimage(x, y, heroFrames[frameIndex]);这跟游戏引擎里资源预加载的概念一样。越早养成这个习惯越好项目大了之后性能差距会非常明显。6. 把前面的东西串起来一个打砖块小游戏6.1 数据设计先想清楚要记录什么前面的知识点单独看都很简单但真正要做游戏难点在于怎么把这些东西组合起来。我拿最经典的打砖块举例带你完整过一遍思路。游戏里有四个核心要素挡板、小球、砖块、得分。数据设计上挡板用一个x坐标加宽度表示球用x坐标、y坐标、横向速度vx、纵向速度vy表示砖块用一个二维数组表示1代表存在、0代表已经打掉。// 挡板 int paddleX 300; int paddleWidth 80; // 小球 float ballX 320, ballY 400; float vx 3, vy -3; const int ballR 8; // 砖块地图 int bricks[5][10] {0}; // 初始化时全部填 1把数据先设计清楚后面主循环里的代码逻辑就顺了。6.2 游戏主循环的实现主循环还是老套路处理输入、更新状态、绘制画面。这里重点说更新状态里的碰撞检测。碰撞逻辑看起来复杂拆开其实就是几个判断球碰到左右边界vx -vx球碰到上边界vy -vy球碰到挡板球心的x在挡板范围里并且球的y加半径接近挡板y坐标弹回去同时让vx根据碰撞位置适当变化球碰到砖块把砖块所在行列找出来标记为0反向vy加分碰撞检测写起来需要一点细心但不难。挡板位置由鼠标控制——读取鼠标x坐标让挡板x跟着走。这套主循环写出来大约五六十行已经是能玩的水平了。6.3 完整代码可以直接跑的最小版本下面给一个最小可运行版本用鼠标控制挡板小球撞砖块消除球落地则游戏结束。完整代码大约110行可以直接复制到VS里编译运行#include graphics.h #include conio.h #include stdlib.h #define BRICK_COLS 10 #define BRICK_ROWS 5 int main() { initgraph(640, 480); // 砖块 int bricks[BRICK_ROWS][BRICK_COLS]; for (int i 0; i BRICK_ROWS; i) for (int j 0; j BRICK_COLS; j) bricks[i][j] 1; // 挡板、球 int paddleX, paddleWidth 90, paddleY 450; float ballX 320, ballY 400, vx 3, vy -3; const int r 8; int score 0; BeginBatchDraw(); while (true) { // 鼠标控制挡板 if (MouseHit()) { MOUSEMSG m GetMouseMsg(); paddleX m.x - paddleWidth / 2; } // 更新球位置 ballX vx; ballY vy; // 左右墙反弹 if (ballX - r 0) { ballX r; vx -vx; } if (ballX r 640) { ballX 640 - r; vx -vx; } // 上墙反弹 if (ballY - r 0) { ballY r; vy -vy; } // 挡板碰撞 if (ballY r paddleY ballX paddleX ballX paddleX paddleWidth) { ballY paddleY - r; vy -vy; } // 砖块碰撞 for (int i 0; i BRICK_ROWS; i) { for (int j 0; j BRICK_COLS; j) { if (bricks[i][j] 1) { int bx j * 64; int by i * 24; if (ballX bx ballX bx 64 ballY by ballY by 24) { bricks[i][j] 0; vy -vy; vx 0.5f; // 每次撞砖稍微提速 score 10; break; } } } } // 球落地游戏结束 if (ballY 480) { outtextxy(200, 200, LGame Over); outtextxy(200, 240, L按任意键退出); FlushBatchDraw(); _getch(); break; } // 绘制画面 cleardevice(); // 画砖块 setfillcolor(RGB(100, 200, 250)); for (int i 0; i BRICK_ROWS; i) { for (int j 0; j BRICK_COLS; j) { if (bricks[i][j]) fillrectangle(j * 64 2, i * 24 2, j * 64 62, i * 24 22); } } // 画挡板 setfillcolor(WHITE); fillrectangle(paddleX, paddleY, paddleX paddleWidth, paddleY 12); // 画球 setfillcolor(YELLOW); fillcircle(ballX, ballY, r); // 显示分数 TCHAR s[64]; _stprintf_s(s, _T(Score: %d), score); outtextxy(10, 10, s); FlushBatchDraw(); Sleep(16); } EndBatchDraw(); closegraph(); return 0; }这个版本我故意留了几个可以调节的地方Sleep(16)控制难度、vx 0.5f控制加速、挡板宽度控制打击手感。你自己改动这些参数马上能感受到游戏节奏变化这种实时反馈正是用EasyX做学习项目的乐趣。6.4 做完之后怎么扩展一个能跑的最小版本只是起点。我会建议按下面的顺序逐步加功能每加一个就相当于练习一个新知识点加结算界面和重新开始的按键逻辑砖块在不同行设置不同颜色和分数加球和挡板之间的角度控制——撞到挡板左端vx为负右端vx为正加关卡系统通关后重置砖块并提速加音效撞砖播放一声短音。每一步都不难但合起来就是一个功能完整的小游戏了。7. 常见的坑与调试建议运行不了、闪屏、乱码7.1 窗口一闪而过这是新手最容易碰到的第一个问题程序运行了窗口闪了一下就消失什么都来不及看。原因是程序执行完main函数就直接退出了窗口自然跟着关闭。解决方法和第一节一样——在closegraph()之前加_getch()让程序停下来等你按一个键再退出。如果你已经写了_getch()还是闪退那可能是代码运行过程中崩了比如访问了非法内存、坐标越界。建议先用最简单的三行代码跑通再逐层加东西。7.2 中文显示乱码乱码的本质是编码不一致。VS源文件通常保存成带BOM的UTF-8而EasyX的文字输出在旧版多字节环境下按GBK解释中文字符就显示成乱码。我在第2章说的两个动作能解决大部分问题项目属性里把字符集改成使用多字节字符集所有字符串常量用_T()包起来。如果还是乱码检查一下源文件保存格式把源文件另存为UTF-8或者GB2312编码再试试。7.3 程序卡死或黑屏黑屏不动的常见原因有三个。一是忘了在循环里Sleep循环体空转导致系统响应不过来二是_getch()在循环里被调用程序每次都在等按键画面自然不动三是逻辑更新有死循环比如某个循环条件永远不满足程序卡死。排查方法是找到问题最小化写一个只更新坐标、不绘制任何图形的循环如果都跑不起来说明问题在逻辑更新而不是绘制。把绘制代码一行行加回来哪个地方开始出问题病灶就在哪。7.4 调试技巧从坐标打印到逐步断点EasyX项目运行时没有控制台输出你想看变量的值怎么办最简单粗暴的办法是把关键变量画在窗口上。用_stprintf_s把数字转成字符串然后outtextxy画出来我在打砖块例子里就这么干过。比如把ballX、ballY、vx都画到窗口一角球飞着飞着突然穿墙你看数值很快能定位是哪一步计算出错。更规范的做法是VS的断点调试。在可疑代码行左侧点击设置断点按F10单步执行观察变量的变化。这一点没得商量——所有编程老手都靠这个吃饭越早熟练掌握越好。另一个小技巧是做游戏时不要完全黑屏调试给背景用深灰而不是纯黑给不同物体配不同颜色这样即使没有文字输出光看画面颜色你也能判断程序执行到哪一步了。EasyX作为一个学习工具最大的价值不是让你做出能上架发售的商业游戏而是在一个极低的门槛下把写代码→看到效果这个循环跑通。我带过好几个从控制台编程刚过渡过来的同学第一周写出会动的弹球第二周就开始自己加功能。那股劲头一旦被点燃后面学什么框架都有底气。这篇文章把绘图、动画、交互、资源加载这些基础都过了一遍代码量都不大建议你逐段敲一遍然后把里面的参数改着玩。等哪一天你能不看参考写出一个完整的打砖块EasyX这块就算真正入门了。

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

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

免费获取报价 →
↑