每次看到还有人用黑底白字的控制台写贪吃蛇,我都想劝一句:试试EasyX。这个C语言图形库装上以后,贪吃蛇这个题目完全可以从“交作业”变成“拿得出手”的项目。今天要聊的项目,标题就很直白——EasyX图形库、C语言贪吃蛇、音乐、图片、界面比较美观。这三个关键词基本概括了这类项目的核心:游戏逻辑要跑通,资源要能加载,画面要能看。我把做这个项目过程中最值得讲的部分拆开,适合刚学完C语言、想做图形界面小游戏的人,也适合准备课程设计却不知道从哪下手的同学。
很多人一听“图形界面”就以为要学某个大框架,其实用EasyX就是给C语言开了个窗口,把绘图、按键、鼠标这些东西封装成普通函数,C语言基础够用就能写。贪吃蛇本身逻辑不难,真正的难点在于怎么把图片、音乐、界面这些“课外内容”整合进来。这篇文章会从整体设计、核心逻辑、图片加载、音乐播放、实操代码、常见问题几个方面讲,照着做你能得到一个能跑、有音乐、有图片、界面不丑的完整项目。
1. 项目整体设计与技术选型
1.1 为什么用EasyX而不是控制台?
先说个大实话:控制台版的贪吃蛇不是不能写,但写出来的东西几乎没法给人看。你可以在用Win32 API、SDL、OpenGL,可对一个刚学完结构体和指针的C语言新手来说,这些工具的学习曲线太陡。EasyX最大的价值是把“画点、画线、画矩形、贴图、播放声音”变成几行代码,而且它专门面向C/C++教学场景,不需要你去怼窗口消息循环,对新手极度友好。
我用EasyX做过好几个小游戏,最舒服的一点是它不改变你写C语言的思路。比如游戏循环还是while(1),按键检测还是_kbhit()和_getch(),只是把printf换成了putimage、把system("cls")换成了cleardevice。这意味着你已有的C语言基础不会浪费,学习成本基本集中在EasyX函数上。
另外一个现实原因:绝大多数学校课程设计允许用EasyX,不需要额外配置复杂环境。VS里装好EasyX之后,直接新建空项目、包含头文件就能写,比配置SDL省太多事。
1.2 项目功能模块拆解
这个标题里的项目虽然看起来简单,实际上包含了四个模块:游戏基础逻辑、图片资源、音频资源、界面状态管理。拆开看就清晰了:
| 模块 | 具体内容 | 关键技术点 |
|---|---|---|
| 游戏逻辑 | 蛇身移动、吃食物、得分、碰撞检测 | 数组或链表结构,方向枚举,随机数 |
| 图片资源 | 背景图、蛇头、蛇身、食物 | loadimage加载,putimage绘制 |
| 音频资源 | 背景音乐、吃食物音效、结束音效 | MCI接口mciSendString,winmm.lib |
| 界面状态 | 开始菜单、游戏中、暂停、游戏结束 | 状态机,不同状态调用不同绘制函数 |
我建议拿到需求后先别急着写代码,先把功能拆成上面这种表格。贪吃蛇这类项目最容易出现的错误是逻辑和界面混在一起写,最后改一个地方牵连整个文件。用状态机来管理界面,把游戏逻辑和绘制分开,后期加暂停、加难度都很方便。
1.3 技术方案取舍:图片素材还是纯绘图?
标题里写了“图片”和“界面比较美观”,但图片不一定非要外置素材。我自己做的时候先用纯图形函数把界面画出来,后面再换成图片资源,这样有两个好处:一是不会因为找不到素材卡住,二是EasyX绘图函数本身也能实现不错的效果。
比如蛇身不一定是绿色方块,可以用solidroundrect画圆角矩形,再根据蛇身索引调整颜色明暗,画面立刻有层次感。食物可以用solidcircle加高光点,背景可以setbkcolor配渐变色,这些都不需要任何图片文件。
如果你确实想用图片,建议准备三种素材:背景图、蛇头图、食物图。蛇身因为要分段移动,用图片反而麻烦,纯绘图更灵活。我的方案是“背景+食物用图片,蛇身用绘图”,这样既保证了画面效果,又不会把简单的事搞复杂。
2. 核心逻辑实现:贪吃蛇的数据模型与移动算法
2.1 蛇身数据结构
很多人写贪吃蛇第一反应是用链表,因为蛇身长度会变。但在EasyX这种小项目里,用固定数组完全够用,而且代码简单得多。关键在于理解“数组当蛇身,长度字段维护有效范围”。
#define MAX_SNAKE_LEN 1000 typedef struct Point { int x; int y; } Point; typedef enum Dir { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT } Dir; typedef struct Snake { Point body[MAX_SNAKE_LEN]; int length; Dir dir; } Snake;用这个结构体之后,蛇的每段身体就是一个坐标点。body[0]我建议存蛇头,方便后面移动和碰撞判断。每次移动,只需要做两件事:把整个数组从末尾到头部依次后移一位,然后根据当前方向算出新的蛇头坐标。
用数组不用链表还有个原因:显示的时候需要反复遍历蛇身,数组随机访问比链表方便,你还能轻松根据下标给不同片段画不同颜色。
2.2 移动、生长与方向控制
移动的核心代码很多人写得不对,常见错误是先从头部开始移动,结果下一段身体移动时把蛇头的新位置当成自己的新位置,蛇身直接串位。正确做法是“从尾部往头部覆盖”。
void moveSnake(Snake* snake) { // 从尾部往头部方向移动,让每一段都等于前一段的旧位置 for (int i = snake->length - 1; i > 0; i--) { snake->body[i] = snake->body[i - 1]; } // 更新蛇头坐标 switch (snake->dir) { case DIR_UP: snake->body[0].y -= UNIT; break; case DIR_DOWN: snake->body[0].y += UNIT; break; case DIR_LEFT: snake->body[0].x -= UNIT; break; case DIR_RIGHT: snake->body[0].x += UNIT; break; } }吃食物就更好办了:吃到了就把length加1,相当于尾部那一格没有删除,下一次移动时数组整体后移,自然就多出一格,实现了生长。不需要真去“插入”一个节点。
方向控制有个所有新手都会踩的坑:当前方向是LEFT时,不能按RIGHT。如果直接改方向,下一次移动时蛇头立刻反向,会和身体相撞。解决办法是记录一个“下一步方向”,只允许垂直方向改变水平方向,水平方向改变垂直方向。
void changeDir(Snake* snake, Dir newDir) { // 防止直接反向 if ((snake->dir == DIR_LEFT && newDir == DIR_RIGHT) || (snake->dir == DIR_RIGHT && newDir == DIR_LEFT) || (snake->dir == DIR_UP && newDir == DIR_DOWN) || (snake->dir == DIR_DOWN && newDir == DIR_UP)) { return; } snake->dir = newDir; }2.3 碰撞检测与游戏结束
碰撞检测分两种:撞墙和撞自己。撞墙的判断很简单,只要蛇头的坐标超出窗口边界就结束。但要注意窗口边界坐标从0开始,比如窗口宽度是800,那么x的范围应该是[0, 800),超出即结束。我建议动态读取窗口宽度,而不是写死,方便后续改窗口大小。
撞自己的判断需要用循环从body[1]开始遍历到body[length-1],看有没有坐标和body[0]重合。为什么从1开始?因为头部本身肯定是重合的,从自己开始比对没有意义。
int checkCollision(Snake* snake, int width, int height) { Point head = snake->body[0]; if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height) { return 1; } for (int i = 1; i < snake->length; i++) { if (head.x == snake->body[i].x && head.y == snake->body[i].y) { return 1; } } return 0; }实际项目里还要考虑一个问题:食物不能生成在蛇身上。判断食物坐标是否落在蛇身任意一段上,落在就重新生成。这个循环看起来不起眼,但如果不处理,食物偶尔会“消失”在蛇的身体下面,玩家会一脸懵。
3. 图片加载与界面美化实用方案
3.1 图片素材的准备与格式选择
提到图片,很多同学第一反应是下载一堆高清图,结果loadimage加载失败或者程序卡顿。EasyX使用loadimage加载图片时,对普通BMP、JPG、PNG都支持,但我想说一句:素材不是越大越好。背景图宽高最好和窗口一致,比如窗口800x600,背景图也做成800x600,加载时最多做一次缩放。
如果你有PNG带透明通道,EasyX可以直接显示透明效果,这是最省事的。但网上很多素材是JPG,带不了透明,你就会看到白色或黑色背景块。我的解决办法是:食物、小图标尽量找透明PNG,背景用不透明图片;蛇身这种需要灵活变形的部分干脆不贴图,用绘图函数画。
还有个细节:EasyX默认支持的是ANSI编码,如果你把项目设成Unicode字符集,字符串常量都要用_T()包起来。为了避免麻烦,我建议把项目字符集改成“使用多字节字符集”。这个问题不解决,loadimage里面的路径怎么都加载不出来,非常容易怀疑人生。
3.2 loadimage和putimage的正确打开方式
加载图片的API标准写法是:
#include <graphics.h> IMAGE bg; // 参数说明:图片对象、路径、缩放宽、缩放高、是否调整尺寸 loadimage(&bg, _T("res/bg.jpg"), 800, 600, true);然后绘制:
putimage(0, 0, &bg);这里最坑的是路径。C语言里反斜杠是转义字符,所以如果用"res\\bg.jpg"或者是正斜杠"res/bg.jpg"。我习惯用正斜杠,省得转义。
另外,loadimage第三个和第四个参数是缩放宽度和高度。很多教程直接不写,图片就会按原始尺寸绘制。如果你的图片比窗口大,可能只显示左上角一小块,不是代码错,是尺寸问题。第五个参数传true表示异步加载?我印象里不是。看EasyX文档:loadimage(IMAGE*, const TCHAR*, int width = 0, int height = 0, bool resize = false)。resize表示是否调整图片尺寸到width和height。如果传true且宽高为0,会调整到原始大小?实际上我们一般这样用:loadimage(&bg, _T("bg.jpg"), 800, 600, true);,直接把背景图缩放成窗口大小,简单直接。
3.3 蛇身、食物与背景的绘制细节
即使有图片,蛇身的绘制也会影响整个界面的美观程度。如果只用填充矩形,像素风很强,但谈不上“美观”。我用的是圆角矩形加渐变颜色,代码很简单:
void drawSnake(Snake* snake) { for (int i = snake->length - 1; i >= 0; i--) { int x = snake->body[i].x; int y = snake->body[i].y; // 根据位置调整颜色,蛇头亮、蛇尾暗 COLORREF color = HSVtoRGB(120, 0.6, 0.8 - 0.4 * i / snake->length); setfillcolor(color); solidroundrect(x + 2, y + 2, x + UNIT - 2, y + UNIT - 2, 10, 10); } }注意我绘制时从尾部开始,这样蛇头会最后绘制,覆盖蛇身连接处的小缝隙,画面会更顺眼。食物可以画成一个圆,再加一个白色高光小圆,立刻有立体感:
setfillcolor(RGB(220, 40, 40)); solidcircle(food.x + UNIT / 2, food.y + UNIT / 2, UNIT / 2 - 2); setfillcolor(RGB(255, 180, 180)); solidcircle(food.x + UNIT / 2 - 3, food.y + UNIT / 2 - 3, 4);背景图放上去之后,最好不要在实际游戏区域直接使用putimage,因为图片覆盖全窗口后会挡住其它文字。可以先把背景画到底层,再在上面绘制蛇和食物。EasyX没有真正的图层概念,你只需要按“背景→装饰→蛇→食物→文字”的顺序绘制,就能达到类似图层的效果。
3.4 多界面状态切换
界面美观不光是图片好看,还有整体流程。没有菜单直接进入游戏、死了直接关窗口,这种体验确实很“课程设计”。我建议用一个枚举类型保存游戏状态:
typedef enum GameState { STATE_MENU, STATE_PLAYING, STATE_PAUSE, STATE_OVER } GameState;游戏循环先判断当前状态,然后调用对应的绘制函数。菜单界面可以画一个标题文字,再画“开始游戏”“退出”两个按钮。EasyX里可以监听鼠标消息,用MOUSEMSG结构体读取鼠标位置,判断是否点击在按钮区域。
暂停界面最简单,只需要在PLAYING状态下按空格键切换到PAUSE,绘制的时候加一个半透明遮罩。这里有一个很多人会忽略的地方:绘制遮罩时不要用getimage去截屏保存,直接画一个半透明黑色矩形就行。EasyX没有内置半透明矩形,可以用一个做法:setfillcolor一个低透明度颜色配合fillrectangle,但颜色本身不支持alpha。我的做法是先用一个带透明度的宏,或者干脆让背景变暗:通过画一个纯色深色矩形再画游戏文字,视觉效果已经足够。
游戏结束界面要显示出得分,还要提示“按回车重新开始”。重新开始意味着重置蛇身、重置食物、重置得分,这里最好封装成一个resetGame函数,不要在循环里零散地赋值,否则容易漏。
4. 音乐与音效实现,以及背后的MCI细节
4.1 用mciSendString播放背景音乐
C语言要播放音乐,最省事的就是Windows的MCI(Media Control Interface)接口。mciSendString相当于一个命令行控制台,你给它传命令字符串,它去操作音频设备。需要包含mmsystem.h并链接winmm.lib:
#include <mmsystem.h> #pragma comment(lib, "winmm.lib")播放背景音乐就几步:
// 打开音乐文件,取一个别名 mciSendString(_T("open bgm.mp3 alias bgm"), NULL, 0, NULL); // 播放,repeat表示循环 mciSendString(_T("play bgm repeat"), NULL, 0, NULL);mciSendString的参数里有一个缓冲区和长度,如果只是发送命令不需要返回值,前三个参数可以传NULL和0。网上很多代码不检查返回值,结果音乐没出来也不知道为什么。你可以把返回值接住,用GetErrorText查错误信息,但大部分情况是路径问题或格式问题。
有个细节:MCI播放MP3需要你的系统有对应的解码器,Windows自带的Media Player能播的MP3一般都能用。如果遇到播不了,换WAV格式最保险,WAV的兼容性最好,缺点是文件大。背景音乐用MP3,音效用WAV,是最稳的组合。
4.2 吃食物音效与场景切换音效
背景音乐可以一直循环,但吃食物音效每次触发都要重新播放。如果只调用一次play,第二次吃食物时音乐可能在播放过程中,不会重新响。解决方法是先把同名设备关闭,再重新打开播放:
void playEatSound() { mciSendString(_T("close eat"), NULL, 0, NULL); mciSendString(_T("open eat.wav alias eat"), NULL, 0, NULL); mciSendString(_T("play eat"), NULL, 0, NULL); }这种方式每次关闭再打开,稍微有点延迟,但对吃食物这种短音效来说完全体感不到。如果你有多个音效,可以为每个音效起不同的别名,比如eat、over、click,避免相互抢占设备。游戏结束时播放一个低沉音效,再关闭背景音乐,体验会完整很多:
mciSendString(_T("close bgm"), NULL, 0, NULL); mciSendString(_T("open over.wav alias over"), NULL, 0, NULL); mciSendString(_T("play over"), NULL, 0, NULL);4.3 音乐模块常见坑
音频这块坑非常多,我遇到过几个典型情况。
第一个是文件路径。mciSendString里的路径同样是字符串,一样有反斜杠转义问题。如果文件放在项目根目录,直接写文件名就行;放在res目录,就要写"res/bgm.mp3"。路径中有空格时,MCI命令会解析错误,需要给完整路径加双引号,比如:
mciSendString(_T("open \"res/my bgm.mp3\" alias bgm"), NULL, 0, NULL);第二个是“无声”现象。有时候代码没报错,但就是听不到声音,可能是音量被系统混音器静音,也可能是没有声卡设备。这时候先拿系统播放器确认文件能播放,再检查MCI命令返回。排查时可以在播放前后打印返回值,返回非0基本就是命令错误。
第三个是关闭程序时背景音乐还在响。因为Windows的MCI设备是独立的,你需要在程序退出前发送close命令,或者直接调用mciSendString("close all", NULL, 0, NULL);。这既是礼貌,也让开发者模式调试时不会出现“上一个程序的音乐还在后台响”的情况。
5. 实操过程:从零搭建一个带音乐图片的贪吃蛇
5.1 环境配置
先装EasyX。去官网下载安装包,选择你当前VS版本,安装器会自动把库文件放到对应目录。然后新建一个C++空项目(注意:虽然你写的是C语言代码,但VS里用空项目加.cpp文件也能编译纯C语法,EasyX官方推荐用C++项目,因为图形库内部用了C++特性;你也可以建C项目,但需要把文件后缀改成.cpp或者调整配置,我建议直接用空项目,源文件放.cpp,代码语法按C写)。
项目属性里把字符集改成“使用多字节字符集”,这能避免很多TCHAR和_T()的兼容问题。当然如果你习惯用_T()宏,那也可以保持Unicode,但新手阶段还是少一个坑是一个。
包含头文件建议这样写:
#include <graphics.h> #include <conio.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <windows.h> #include <mmsystem.h> #pragma comment(lib, "winmm.lib")graphics.h是EasyX主头文件,conio.h里有_kbhit和_getch,这两个函数用于非阻塞按键检测,windows.h提供Sleep,mmsystem.h和winmm.lib负责音乐。
5.2 初始化窗口与资源加载
代码结构分成两部分:init函数负责初始化,resetGame函数负责每局开始时的数据重置。
#define WIDTH 800 #define HEIGHT 600 #define UNIT 20 IMAGE bg; IMAGE foodImg; Snake snake; Point food; int score; GameState state; void init() { initgraph(WIDTH, HEIGHT); srand((unsigned)time(NULL)); loadimage(&bg, _T("res/bg.jpg"), WIDTH, HEIGHT, true); loadimage(&foodImg, _T("res/food.png"), UNIT, UNIT, true); mciSendString(_T("open res/bgm.mp3 alias bgm"), NULL, 0, NULL); mciSendString(_T("play bgm repeat"), NULL, 0, NULL); resetGame(); }注意loadimage之后最好判断一下图片是否加载成功,可以用一个简单的办法:IMAGE对象初始时是空白,加载后可以通过IsEmpty判断。不过很多教程不判断,图片路径错了会在putimage时报错或者显示花屏。我习惯在加载失败时弹出一个消息框提示:
if (loadimage(&bg, _T("res/bg.jpg"), WIDTH, HEIGHT, true) == NULL) { MessageBox(NULL, _T("背景图片加载失败"), _T("错误"), MB_OK); }5.3 游戏循环主体框架
游戏循环是项目的骨架,每个状态都要在循环里被处理。我写了一个最常用的版本:
while (1) { BeginBatchDraw(); cleardevice(); putimage(0, 0, &bg); switch (state) { case STATE_MENU: drawMenu(); break; case STATE_PLAYING: processInput(); moveSnake(&snake); if (checkFood()) score++; if (checkCollision(&snake, WIDTH, HEIGHT)) state = STATE_OVER; drawGame(); break; case STATE_PAUSE: drawGame(); drawPause(); break; case STATE_OVER: drawOver(); break; } EndBatchDraw(); Sleep(80); }BeginBatchDraw和EndBatchDraw是EasyX的批绘制函数,用来防止闪烁。原理很简单:先不往屏幕上输出,把所有绘制命令放到内存缓冲区,最后一次性更新屏幕。虽然这里每个分支只画一帧,但批绘制能明显减少画面跳动。
这里没有把按键处理放外面,是因为菜单和游戏中的按键逻辑不同。比如在菜单界面按回车进入游戏,在游戏界面按空格暂停。如果你把所有按键处理集中在一个函数,再根据不同state调用,代码会很混乱。分开写更容易维护。
5.4 速度控制与帧率思路
Sleep(80)表示每一帧间隔80毫秒,也就是蛇大约每秒移动12.5格。这个速度对新手玩家来说比较舒服。但Sleep存在一个隐患:如果电脑性能波动或者你跑了很多东西,帧率会不稳定。更专业的做法是记录上一帧时间,计算时间差,超过固定阈值才更新逻辑。
我做过一个小改进:用分数控制速度。初始Sleep(120),每吃5个食物减少5毫秒,最低到30毫秒。这样游戏难度是递增的,玩家有挑战性。但要注意Sleep值不能太小,否则键盘输入和绘制可能跟不上,速度太快反而显得卡。
如果你想让画面更流畅,可以把逻辑更新和绘制分离:逻辑更新按固定步长,绘制每次循环都执行。但EasyX项目里这么做有点小题大做,反而把代码搞复杂。先按Sleep方案跑通,再考虑优化即可。
6. 常见问题与排错体验
6.1 方向键问题:一按方向键蛇就死
这个问题的根源几乎都是没有做反向禁止。当前方向是LEFT,你按RIGHT,下次移动时蛇头直接穿到下一格身体上,撞自己死了。解决办法就是写changeDir函数,像前面代码那样判断。还有一个细节:如果一帧内连续按了两个方向键,比如先按UP再按LEFT,最终方向取决于最后那个按键,但下一次移动只发生一次,中间不判断第二次移动,就不会反向自杀。
如果你用了_getch()检测方向键,注意方向键会返回两个字节,需要连续读两次。第一次返回值是224(小键盘)或0(方向键),第二次才是具体方向。很多新手第一次读到的值就拿来用,结果方向键完全没反应。
6.2 食物生成在蛇身里,导致画面诡异
食物随机生成后,理论上可能落在蛇身体的任何一个坐标上。如果不检查,玩家费劲追到那个位置,结果食物被蛇身盖住,看起来就像食物消失了。生成食物的函数要写个循环:
void generateFood() { do { food.x = rand() % (WIDTH / UNIT) * UNIT; food.y = rand() % (HEIGHT / UNIT) * UNIT; } while (isFoodOnSnake(&snake, food)); }这里要用do-while而不是先判断再生成,因为第一次无论如何都要生成。如果蛇已经很长,几乎占满整个窗口,循环可能会卡住。我实测过,窗口800x600、UNIT=20有1200个格点,蛇再长也很少超过300,碰撞概率可控,不会死循环。不过严谨一点,可以加一个最大尝试次数,超过就重置位置到第一个空位。
6.3 图片和音乐路径总是加载失败
我在第3章已经强调过路径问题,但这里再单独列出来,因为它是最高频错误。VS编译后,程序的工作目录默认是项目文件所在的目录,但有时候你调试时设置的是解决方案目录,就差了层。如果你把图片放在res文件夹,但运行时报错找不到文件,第一步先在代码里输出一下当前工作目录:
TCHAR buf[256]; GetCurrentDirectory(sizeof(buf), buf); OutputDebugString(buf);然后在对应位置放文件。最简单粗暴的做法是把图片和音乐直接放到项目目录,不建子文件夹,等程序跑通再整理。调试阶段不要折腾路径,不然会浪费大量时间。
6.4 窗口闪烁、画面拖影
没有用BeginBatchDraw,或者用了但每帧都调用cleardevice,会导致画面闪烁。正确姿势是:BeginBatchDraw和EndBatchDraw成对出现,中间所有绘制操作写完后统一刷新。如果你发现还是闪,检查你是否在绘制函数内部又调用了一次cleardevice,反复清屏就会出现白色闪动。
还有种情况:背景图加载失败,cleardevice清成黑色,再加白太刺眼。先处理图片加载,再看闪烁。
6.5 编译报错:找不到mmsystem.h或winmm.lib
这个问题多见于没有添加#pragma comment(lib, "winmm.lib"),或者项目环境不是Windows平台。EasyX和winmm都依赖Windows SDK,只要你的VS能创建Win32项目,就不会缺这个头文件。检查项目属性里是否有包含目录被手动清空,或者平台选择了Linux。确认平台设置为x86/x64,不要选成ARM或其他平台。
6.6 中文字体显示乱码或字体没效果
settextstyle里设置中文字体名,比如“楷体”“微软雅黑”,如果用Unicode字符集,字体名字符串最好用_T()包起来。显示中文时,如果源文件是UTF-8编码,在默认ANSI编译环境下容易乱码。建议源文件另存为ANSI编码(最简单是GB2312),或者使用settextstyle后再配合setbkmode(TRANSPARENT)让文字背景透明。乱码和字体效果是两回事,字体不生效往往是因为拼错字体名,系统里没有对应字体。
做这个项目的时候我有一个很深的体会:贪吃蛇本身逻辑很成熟,网上代码一抓一大把,但真正区分项目好坏的,是资源加载、界面状态切换、音频管理这些“容易忽略的细节”。把这些细节处理到位,项目质量立刻上了一个档次。最后再分享一个小技巧:把每一步的关键函数单独封装,不要全都堆在main函数里。我见过太多人把所有逻辑塞进一个while里,后面想改暂停、改图片,都要从头捋一遍,很痛苦。你按我说的模块化写法,改功能只是换一个函数的事,后面扩展自动寻路、双人模式、最高分存档都会方便得多。