ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++图形编程入门:用EasyX从零实现贪吃蛇游戏

C++图形编程入门:用EasyX从零实现贪吃蛇游戏 写这个项目的起因很简单我见过太多人学C学到指针、类就放弃了理由是“看不见摸不着”不知道学这些东西到底能干嘛。图形编程库EasyX恰好能解决这个痛点——它能让你用熟悉的C语法很快画出一个窗口、一个圆、一个方块然后把游戏一点点做出来。贪吃蛇又是所有小游戏里最适合练手的规则谁都知道但要做对“蛇身体跟着头走”“吃到食物变长”“碰到墙和身体就结束”这些核心逻辑其实需要你把程序结构、数据结构、循环、键盘输入、碰撞检测这些C基本功全部串起来。这篇文章我准备按真正的开发流程来写从需求拆解到环境搭建从数据结构到完整代码最后把常见的坑全列出来。目标是让一个只会C基础语法的新手也能照着手写出一个能跑、能玩、不会闪屏的贪吃蛇。如果你已经有一定基础直接跳到第4章看代码也行但建议把第1章和第3章读完——游戏逻辑想清楚了代码怎么写都不会乱。1. 项目拆解先想清楚贪吃蛇到底在做什么1.1 从需求到代码核心游戏逻辑就这三件事很多人上手就写代码写到哪里算哪里结果越写越乱最后连自己都被绕晕。我习惯先问一个问题贪吃蛇的游戏循环到底在干什么剥离掉画面和声效它其实只有三件事反复进行读取玩家的输入按了哪个方向键根据输入和当前状态更新游戏数据蛇头往哪走、有没有吃到食物、角色有没有死亡把更新后的数据画到窗口上画蛇、画食物、画分数这三件事对应到代码里就是三个函数handleInput()、updateGame()、renderGame()外面套一个while循环。这个“输入→更新→渲染”的循环模型是所有实时游戏的地基。别小看这个模型它和你在终端里写的那些“从上到下跑一遍就结束”的控制台程序有本质区别游戏程序必须在每一帧都重新计算状态并且一直运行到玩家退出为止。理清了这个核心循环接下来的功能清单就很好定了。我第一次带人做这个项目会把功能分成“必须做”和“能做但先不急”两批。优先级功能说明必须做蛇的移动按方向键改变蛇头方向身体随之移动必须做吃到食物增长蛇头碰到食物长度加一重新生成食物必须做碰撞检测撞墙或撞到自己游戏结束必须做画面绘制窗口里能看到蛇、食物、分数建议做暂停/重新开始按空格暂停按R重新开局进阶做速度递增每吃一个食物移动间隔缩短一点1.2 技术选型为什么选择EasyX做图形应用C的常见选择有Windows控制台、EasyX、SDL2、SFML、Qt等。其中Qt偏重SDL2和SFML虽然功能强但光是把开发和链接环境配好就能劝退不少新手。EasyX的优势在于它是一个面向C初学者的Windows图形库接口像一个极简画布你只需要一条initgraph(400, 400)就能弹出一个窗口配合rectangle、fillrectangle、outtextxy这些直白得不能再直白的函数就能完成绘图。更关键的是EasyX不需要你去额外学习复杂的窗口消息机制。它把Windows底层API封装起来让你专注于游戏逻辑本身。这一点对学习“游戏逻辑”特别重要——如果你的时间都花在理解Win32窗口类的回调函数上你根本不会有精力去体会贪吃蛇的移动算法哪一步写错了。所以我的结论很简单新手用EasyX图形库越简单越好等把游戏逻辑吃透再迁移到SDL2或者SFML都很轻松。1.3 功能清单与代码分工写代码前先在纸上把函数名列出来。我的习惯是initGame()初始化蛇的初始位置、方向、食物、分数generateFood()在随机位置生成食物且不能生成在蛇身上handleInput()检测方向键、空格、R键updateGame()移动蛇身体、检查碰撞、处理吃食物renderGame()用EasyX把整个画面画出来如果你只想写一个文件那这个几个函数从上到下放好主函数里调用。后续如果想练习工程化再把这些逻辑拆到Snake.h、Game.h这样的类里。先单文件跑通比什么都重要。2. 开发环境搭建从零搞定EasyX2.1 IDE选择Visual Studio最适合VSCode和Dev-C怎么配我强烈建议用 Visual Studio Community原因只有一个EasyX官方安装包对VS版本的适配做得最好安装完直接就能用你基本不需要手动处理配置。如果你坚持用 VSCode那需要自己搞定MinGW-w64、tasks.json、launch.json还要在编译命令里手动链接EasyX的库。EasyX官网其实提供了针对MinGW的安装方式但步骤比VS繁琐不少很多人卡在这。VSCode配置C/C环境本身不难网上教程很多但如果你是为了玩EasyX我建议先放弃VSCode别在环境上内耗。Dev-C的情况类似。它自带的编译器是老版的MinGWEasyX新版本对它的兼容不算好我踩过坑安装完提示找不到graphics.h手动配了半天才跑起来。结论是新手用VS老手随意。安装步骤很简单去Visual Studio官网下载Community版本勾选“使用C的桌面开发”工作负载。去EasyX官网下载最新版安装包运行。安装程序会自动识别VS版本勾选后直接安装。装好之后新建一个空C项目默认就会支持EasyX。2.2 验证环境跑通第一个EasyX窗口不要一上来就写贪吃蛇先写一个最简单的窗口测试。新建一个源文件粘贴下面代码#include graphics.h #include conio.h int main() { initgraph(400, 400); // 创建 400x400 的窗口 setfillcolor(RED); // 设置填充颜色为红色 fillcircle(200, 200, 100); // 在窗口中心画一个半径为100的红色圆 _getch(); // 等待键盘输入防止窗口闪退 closegraph(); // 关闭窗口 return 0; }按F5编译运行如果你看到窗口里出现一个红色圆形按任意键窗口关闭那么环境就算通了。这一步能过滤掉大部分配置问题。我见过很多同学代码逻辑没问题结果卡在graphics.h找不到上排查方向大多集中在安装不完整、VS版本不对、项目被建成了控制台应用但没包含EasyX头文件目录这几个点。2.3 关于字符集和中文乱码EasyX早期的文本接口使用ASCII后来支持了宽字符。如果你在代码里写outtextxy(10, 10, 分数: 0)在某些VS配置下会报“无法从const char*转换为LPCTSTR”之类的错误。解决方式有两种要么在项目属性里把“字符集”改成“使用多字节字符集”要么在字符串前面加_T()宏比如_T(分数: 0)。我一般建议直接使用英文或拼音显示在窗口里避免这个麻烦后续代码里我都统一用_T宏这样不管项目用哪种字符集都能编译。3. 核心数据结构与算法设计3.1 数据表示坐标结构体、方向枚举、蛇身容器游戏里最重要的数据就是“蛇本身”。蛇由多个等大的小方块组成每个小方块的位置用两个坐标表示。所以我先定义一个坐标结构体struct Point { int x; int y; Point(int xx 0, int yy 0) : x(xx), y(yy) {} };注意我在构造时给了默认值这样Point pt;可以编译通过不报语法错误。这只是个小细节但新手经常栽在结构体构造函数上。蛇身则用std::vectorPoint来保存。为什么不用链表很多教材里贪吃蛇喜欢用链表讲但链表在增加尾部节点和删除尾部节点时确实方便却让代码变得更复杂。而vector支持头尾操作虽然insert(begin())是线性复杂度但蛇最多几百节这个开销完全可以忽略。对新学者来说vector随机访问方便调试时下标很容易看还能直接配合for循环遍历。方向用枚举enum Direction { UP, DOWN, LEFT, RIGHT };然后把游戏相关的常量放一块常量含义WIDTH 20游戏区域横向格子数HEIGHT 20游戏区域纵向格子数BLOCK 30每个格子的像素尺寸WIN_WIDTH 600窗口宽度 WIDTH * BLOCKWIN_HEIGHT 600窗口高度 HEIGHT * BLOCK如果你的窗口是600x600游戏区域被分成20x20的格子蛇每次移动一格一格正好30像素。这套坐标系统的设计是整个游戏渲染的基础逻辑上用格子坐标渲染时乘以BLOCK变成像素坐标。两者一定不能混。3.2 蛇的移动头插尾删的细节蛇移动的逻辑是所有新手最容易写错的地方。很多人的第一反应是我把每一节身体沿着方向往前挪一格。这个思路不是不行但要写一堆循环还容易把每一节的前后关系搞乱。正确的做法是“头插尾删”先根据移动方向算出新的蛇头坐标把这个新蛇头插到vector开头然后看有没有吃到食物如果吃到了蛇尾不要删除长度就加一了。如果没吃到把vector末尾的尾巴删掉长度不变。这个逻辑非常像排队队伍最前面的人往前走一步最后面那个人离开如果中间有人补进队伍吃到食物队尾就不离开。我给学生讲这个例子的时候几乎所有人都能立刻听懂。还有两个必须注意的点第一不能允许蛇直接掉头。比如当前方向是RIGHT玩家按LEFT蛇头会一头扎进自己身体。所以要加判断if (GetAsyncKeyState(VK_UP) dir ! DOWN) dir UP; if (GetAsyncKeyState(VK_DOWN) dir ! UP) dir DOWN; if (GetAsyncKeyState(VK_LEFT) dir ! RIGHT) dir LEFT; if (GetAsyncKeyState(VK_RIGHT) dir ! LEFT) dir RIGHT;第二头插的时候要暂时不检查蛇尾那一节。因为蛇尾即将被删除就算新头撞到旧尾巴其实不会死。但新手简化处理成“碰到自己就死”问题也不大只是体验上略有瑕疵。我下面给的代码也做了简化虽然有个别边界case但对学习和练习足够了。3.3 食物生成与随机数陷阱食物就是游戏区里的一个坐标用一个Point保存。每次生成的时候要在 [0, WIDTH-1] 和 [0, HEIGHT-1] 范围内随机选一个格子。这里会用到rand()但随机数有两个坑如果只用rand()每次运行游戏食物位置是一样的因为默认种子相同。所以要在main里加上srand((unsigned)time(NULL))用当前时间做随机种子。生成时还要保证食物不落在蛇身上。标准做法是一个do-while循环void generateFood() { bool valid; do { valid true; food.x rand() % WIDTH; food.y rand() % HEIGHT; for (size_t i 0; i snake.size(); i) { if (snake[i].x food.x snake[i].y food.y) { valid false; break; } } } while (!valid); }如果蛇已经占满了整个游戏区这个循环会死循环。实际游戏中不太可能出现这种情况但如果要做严谨可以加一行判断当snake.size() WIDTH * HEIGHT时直接判定玩家胜利并结束循环。这在后面代码里我给了一个简单处理。3.4 碰撞检测的边界条件碰撞检测要检查两类情况第一撞墙。因为蛇的坐标是格子坐标只要新头的x或y跑到 [0, WIDTH-1] 或 [0, HEIGHT-1] 之外就说明撞墙了。第二撞自己。遍历蛇身所有节点看有没有和新头坐标重叠。这里注意如果蛇身特别长新头正好撞在即将被删除的尾巴上其实不会死但我上面说了简化处理。吃食物的检测更简单只要新头坐标和food坐标完全相同就算吃到。吃到后不删尾、加分、重新生成食物、适当提高游戏速度。这里“适当提高速度”的实现方式是直接把休眠时间speed减几毫秒实战效果非常直接if (speed 60) speed - 3;别把速度降得太低否则游戏快到人的反应速度完全跟不上。新手做这个项目时可以先固定速度跑通之后再加入“吃一个减一点”的逻辑。4. 完整代码实现贪吃蛇从0到14.1 代码结构说明我给出的代码偏向“能跑、好懂、容易改”。所有代码可以放在同一个.cpp文件里用VS建一个空C项目直接粘贴运行。函数顺序是全局声明 →generateFood→initGame→handleInput→updateGame→renderGame→main。这样先保证被调用的函数在前面已经声明过避免编译报“未定义标识符”之类的错。为了避免窗口闪屏我用了EasyX的双缓冲绘图BeginBatchDraw()在游戏循环前调用每一帧画完调用FlushBatchDraw()把缓冲内容一次性显示到屏幕上循环结束后EndBatchDraw()。这个技巧后面专门讲。4.2 新手直接可用的完整源码下面的代码是完整版包含方向控制、吃食物、碰撞检测、分数显示、暂停、重新开始、速度递增。#include graphics.h #include conio.h #include vector #include ctime #include cstdlib using namespace std; const int WIDTH 20; // 游戏区域宽格数 const int HEIGHT 20; // 游戏区域高格数 const int BLOCK 30; // 每格像素宽度 const int WIN_WIDTH WIDTH * BLOCK; const int WIN_HEIGHT HEIGHT * BLOCK; enum Direction { UP, DOWN, LEFT, RIGHT }; struct Point { int x; int y; Point(int xx 0, int yy 0) : x(xx), y(yy) {} }; vectorPoint snake; // 蛇身节点坐标 Direction dir RIGHT; // 当前方向 Point food; // 食物坐标 int score 0; // 得分 int speed 120; // 每次移动间隔毫秒 bool isPaused false; // 是否暂停 bool isGameOver false; // 是否结束 void generateFood() { bool valid; do { valid true; food.x rand() % WIDTH; food.y rand() % HEIGHT; for (size_t i 0; i snake.size(); i) { if (snake[i].x food.x snake[i].y food.y) { valid false; break; } } } while (!valid); } void initGame() { snake.clear(); // 初始蛇长度为3向右移动蛇头在坐标 (5, 10) snake.push_back(Point(5, 10)); snake.push_back(Point(4, 10)); snake.push_back(Point(3, 10)); dir RIGHT; score 0; speed 120; isPaused false; isGameOver false; generateFood(); } void handleInput() { if (GetAsyncKeyState(VK_UP) 0x8000 dir ! DOWN) dir UP; if (GetAsyncKeyState(VK_DOWN) 0x8000 dir ! UP) dir DOWN; if (GetAsyncKeyState(VK_LEFT) 0x8000 dir ! RIGHT) dir LEFT; if (GetAsyncKeyState(VK_RIGHT) 0x8000 dir ! LEFT) dir RIGHT; // 按空格暂停/继续 if (GetAsyncKeyState(VK_SPACE) 0x8000) { isPaused !isPaused; Sleep(200); // 防止一次按键被重复触发 } // 按 R 重新开始 if (GetAsyncKeyState(R) 0x8000) { initGame(); Sleep(200); } } void updateGame() { if (isGameOver || isPaused) return; // 计算新蛇头坐标 Point newHead snake[0]; if (dir UP) newHead.y--; else if (dir DOWN) newHead.y; else if (dir LEFT) newHead.x--; else if (dir RIGHT) newHead.x; // 撞墙检测 if (newHead.x 0 || newHead.x WIDTH || newHead.y 0 || newHead.y HEIGHT) { isGameOver true; return; } // 撞自己检测简化最后一个节点即将移除不单独忽略 for (size_t i 0; i snake.size(); i) { if (snake[i].x newHead.x snake[i].y newHead.y) { isGameOver true; return; } } snake.insert(snake.begin(), newHead); // 吃到食物 if (newHead.x food.x newHead.y food.y) { score; if (speed 60) speed - 3; generateFood(); } else { snake.pop_back(); } } void drawBlock(int x, int y, COLORREF color) { setfillcolor(color); solidrectangle(x * BLOCK 1, y * BLOCK 1, (x 1) * BLOCK - 1, (y 1) * BLOCK - 1); } void renderGame() { setbkcolor(RGB(235, 235, 220)); cleardevice(); // 画网格浅灰虚线一样的效果 setlinecolor(RGB(200, 200, 185)); for (int i 0; i WIDTH; i) { line(i * BLOCK, 0, i * BLOCK, WIN_HEIGHT); } for (int i 0; i HEIGHT; i) { line(0, i * BLOCK, WIN_WIDTH, i * BLOCK); } // 画食物红色方块 drawBlock(food.x, food.y, RED); // 画蛇头用亮绿色身体用深绿色 for (size_t i 0; i snake.size(); i) { if (i 0) drawBlock(snake[i].x, snake[i].y, RGB(0, 200, 0)); else drawBlock(snake[i].x, snake[i].y, RGB(0, 140, 0)); } // 显示分数 settextcolor(RGB(80, 80, 80)); settextstyle(24, 0, _T(Consolas)); TCHAR info[32]; _stprintf_s(info, _T(Score: %d), score); outtextxy(10, 10, info); // 暂停提示 if (isPaused) { settextcolor(RGB(0, 0, 0)); settextstyle(36, 0, _T(SimHei)); outtextxy(WIN_WIDTH / 2 - 60, WIN_HEIGHT / 2 - 20, _T(Paused)); } // 游戏结束提示 if (isGameOver) { settextcolor(RGB(255, 0, 0)); settextstyle(40, 0, _T(SimHei)); outtextxy(WIN_WIDTH / 2 - 120, WIN_HEIGHT / 2 - 20, _T(Game Over)); settextstyle(20, 0, _T(SimHei)); outtextxy(WIN_WIDTH / 2 - 110, WIN_HEIGHT / 2 30, _T(Press R to Restart)); } } int main() { initgraph(WIN_WIDTH, WIN_HEIGHT); srand((unsigned)time(NULL)); initGame(); BeginBatchDraw(); // 开始批量绘图 while (true) { handleInput(); if (!isPaused !isGameOver) updateGame(); renderGame(); FlushBatchDraw(); // 将缓冲区内容显示到屏幕 // 游戏结束后循环仍然继续方便按R重开 if (isGameOver) Sleep(50); else Sleep(speed); } EndBatchDraw(); closegraph(); return 0; }这段代码在Visual Studio EasyX环境下可以直接编译运行。如果你把源文件放在VSCode里用MinGW编译需要注意字符集设置建议把源码保存为UTF-8并把编译命令加-fexec-charsetGBK防止中文乱码。不过前面说过新手还是推荐VS。4.3 编译运行与常见调整运行起来后你会发现蛇移动得很有节奏每过一个固定间隔就走一格。这个间隔由Sleep(speed)控制speed初始是120毫秒吃到食物后会慢慢减少。我设置的最小值是60毫秒再快真的没法玩。如果你想改变游戏难度优先调整两个参数BLOCK格子像素大小调大窗口变大调小窗口变小。speed初始移动间隔改小更困难改大更休闲。如果你想改变蛇的初始长度可以改initGame函数里push_back的次数。这些都是很直观的调整改完重新编译就能看到效果。5. 常见问题与调试技巧5.1 编译报错找不到graphics.h这个错误几乎每个新手都会遇到但原因往往不同。VS安装EasyX之后还是找不到多半是安装时没识别到VS版本试着重新运行EasyX安装包或者在项目配置里手动附加包含目录。如果你是Dev-C或VSCode的MinGW用户需要下载EasyX提供的MinGW版库并在编译命令中手动加上-lgraphics等参数。我在第2章已经建议过最好不要在这块卡太久。最直接的排查方式新建一个空白Win32控制台项目只写#include graphics.h如果还是报错先把环境问题解决再写业务逻辑。5.2 键盘方向键没反应或者“串键”方向键无响应最常见的原因是程序窗口没有焦点或者用了_getch()等待输入但按键被缓冲区积累。我这里用的是GetAsyncKeyState函数该函数检测“某一按键此刻是否被按下”不需要阻塞排队所以方向键很灵敏。但注意GetAsyncKeyState如果按键一直按住它会在每一帧都返回按下状态导致方向持续改变如果玩家按住“上”后再按“左”可能出现“先向上再向左”而无法再转回上方的操作。实际游戏中这不算大问题因为蛇的转向本身有反方向限制。如果你想要严格一次按键只触发一个动作可以引入一个按键冷却标志或者改用_getch()配合_kbhit()在按下后清空输入缓冲。另外一个小坑空格和R键的检测我加了Sleep(200)。否则按一下空格游戏可能在一毫秒内检测到几十次按键直接变成“暂停又恢复”的闪烁效果。这个防抖技巧在图形游戏里很常用。5.3 窗口闪烁的终极解法双缓冲不用双缓冲的版本会看到窗口疯狂闪烁原因是程序每一帧先清屏再绘制清屏这一下会让画面瞬间空白人眼捕捉到就感觉在闪。简单说你一边擦黑板一边写字围观的就会看到黑板一会儿白一会儿黑。EasyX提供了双缓冲接口。思路是先在内存里把整幅画面画好再一次推给显示器。实现就是BeginBatchDraw()开启批处理每帧结束调用FlushBatchDraw()输出最后EndBatchDraw()收尾。我上面的代码全部加了这套逻辑如果你自己从头写一定记得加不加后面必后悔。5.4 随机数不随机 中文乱码不随机的问题十有八九是忘了srand((unsigned)time(NULL))。有些同学把这个放到generateFood里反复调用反而快速连续调用时间种子相同会生成同样的随机序列。正确姿势是在main函数里只调用一次。中文乱码问题分两种一种是编译报错一种是显示乱码。核心原因是EasyX内部使用宽字符而你输入的字符串可能是本地代码页。我代码里全部用了_T()宏和英文文本就是为了规避这个坑。如果你非要显示中文VS里把源文件保存为Unicode同时项目字符集选“使用Unicode字符集”基本能解决。5.5 问题速查表现象最常见原因解决方案找不到graphics.hEasyX未安装或未适配IDE重装EasyX换VS窗口闪退/一闪而过缺少等待输入或closegraph前退出main末尾加_getch()或观察控制台输出方向键没反应用了阻塞输入或窗口无焦点改用GetAsyncKeyState蛇移动太快/太慢Sleep参数不合理调speed初始值蛇头不吃食物坐标系统混乱像素坐标当格子坐标统一用格子坐标乘以BLOCK绘图画面闪烁未用双缓冲加BeginBatchDraw/FlushBatchDrawR或空格无效没有防抖按键后Sleep(200)食物生成在蛇身上未做有效性检查用do-while循环生成直到不重叠6. 从贪吃蛇到真正游戏开发进阶方向6.1 可以立即动手的5个扩展功能这个项目跑通之后我建议你选几个扩展功能自己动手改代码这样C的水平会提升非常快。我按难度从低到高列五个方向增加障碍物在游戏区里生成固定数量的墙块蛇碰到墙块游戏结束。增加游戏状态做一个开始菜单按空格才开始游戏结算画面显示最终得分和最佳记录。记录最高分把最高分写入文件下次启动时读取出来。这会用到C文件流。改变蛇的移动方式让蛇穿越墙壁从右边出去就从左边回来形成环形世界。做一个“AI蛇”写一个算法让蛇自动寻找食物哪怕算法不完美也能学到搜索和路径规划的思想。每个扩展功能都会逼你主动去查资料、去理解新的C特性。比如做最高分文件功能时你会去查ifstream和ofstream做开始菜单时你会去了解程序状态机做AI蛇时你会被迫理解广度优先搜索或者简单的贪心算法。这些知识全部嵌在“把游戏做好玩”的真实需求里比对着课本记牢固得多。6.2 代码重构从单文件到类设计当前代码虽然能跑但所有全局变量堆在一起维护起来累。当你决定给游戏加音效、加菜单、加关卡的时候你会发现自己陷入“改一个变量牵扯一片逻辑”的泥潭。这时候就该重构了。最自然的重构方式是把数据和行为封装成类Snake类内部持有蛇身节点、方向、移动方法、增长方法。Food类负责随机生成和位置信息。Game类负责游戏主循环、状态切换、分数统计、渲染调用。这样做的好处是函数之间不再靠全局变量传数据而是通过对象调用方法传参。比如Snake::move()返回是否吃到食物Game再决定要不要重新生成Food。这个思路也是很多商业游戏引擎的基础思想。如果你还想继续深入可以尝试把渲染逻辑从主循环中拆出去做一个简单的“渲染器”这样以后想换SDL2渲染只要替换渲染器部分游戏逻辑不需要动。6.3 给新手的学习路线建议写到这里我认为贪吃蛇最大的价值不是“做出了一个小游戏”本身而是它让你第一次遇到一个需要认真思考的完整项目。很多C学习者最大的问题不是不会语法而是不知道语法是怎么被组织成程序的。你从控制台学到类、指针、容器后一旦面对真实程序常被庞大的信息绕晕。贪吃蛇恰好是一个把复杂度控制在合理范围的项目核心逻辑简单但足够支撑你去练习“拆解需求、设计数据结构、组织函数、调试排错”这套完整的能力闭环。写完贪吃蛇之后你可以按这个顺序继续用控制台写一个学生管理系统练文件读写和链表用EasyX做一个打砖块练更多碰撞检测和多对象管理再往后就可以尝试SDL2做平台跳跃游戏。每一步都在前一步的基础上增加新技术但核心的游戏循环、状态处理、开发调试思路都是相通的。我自己的体会是玩游戏和做游戏是两个世界而贪吃蛇就是连接这两个世界的第一座桥。如果你照着代码敲一遍中间哪怕报错十次都把它修好再自己加一个新功能那么图形化编程和游戏逻辑这两件事你才算是真正入了门。
返回列表