news 2026/8/11 5:49:36

用Dev-C++和EasyX从零开发2D游戏:躺平发育实战教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Dev-C++和EasyX从零开发2D游戏:躺平发育实战教程

1. 项目概述:用经典工具开启游戏编程之旅

“躺平发育”这个游戏概念,相信很多朋友都不陌生,它描绘了一种轻松、休闲的成长体验。今天,我们不聊如何在游戏里“躺平”,而是聊一个更有意思的话题:如何亲手创造一个属于自己的“躺平发育”小游戏。你可能会想,这得用上Unity、Unreal这些大家伙吧?其实不然,对于初学者或者想重温编程纯粹乐趣的朋友来说,有一款经典、轻量且完全免费的工具就足够了——那就是Dev-C++ 5.11。

Dev-C++这个名字,对于很多从学校C/C++课程入门编程的朋友来说,简直是“时代的眼泪”。它是一款基于MinGW的Windows平台集成开发环境(IDE),由Bloodshed Software开发,后来由Embarcadero接手维护。它的界面在今天看来或许有些复古,但正是这种极致的简洁和轻量,让它成为学习基础编程、理解程序从代码到可执行文件整个过程的绝佳工具。选择Dev-C++ 5.11来制作一个“躺平发育”这样的2D小游戏,其核心价值在于“回归本质”。你将不再被复杂的引擎界面、繁多的插件和庞大的资源库所困扰,而是专注于最核心的游戏逻辑:如何用变量表示生命值和金币,如何用循环和条件判断控制游戏流程,如何用简单的图形库(比如EGE或EasyX)绘制出游戏角色和场景。这个过程,能让你透彻理解一个游戏是如何一砖一瓦构建起来的。

这个项目非常适合以下几类朋友:一是刚学完C语言基础语法,想找个有趣项目练手的学生;二是厌倦了“黑框框”控制台程序,渴望看到图形化成果的编程新手;三是想带孩子入门编程,需要一个直观、有趣项目的家长或老师。通过这个项目,你不仅能巩固分支、循环、数组、函数等核心知识,更能初步接触游戏编程的基本思想,如游戏循环、状态管理、简单碰撞检测等,为后续学习更高级的游戏引擎打下坚实的逻辑基础。

2. 开发环境搭建与核心工具选型

工欲善其事,必先利其器。虽然我们的目标是做游戏,但第一步仍然是搭建一个稳定、顺手的编程环境。Dev-C++ 5.11的安装过程本身非常简单,但其中一些细节配置却决定了后续开发体验的顺畅度。

2.1 Dev-C++ 5.11的安装与避坑指南

网络上流传的Dev-C++安装包来源繁杂,我强烈建议从Embarcadero的官方GitHub仓库或可信的镜像站下载。安装过程基本是“下一步”到底,但有几个关键点需要注意:

  1. 安装路径:尽量不要安装在包含中文或空格的路径下,例如D:\编程工具\Dev-Cpp就可能在某些情况下引发编译或链接的诡异错误。最稳妥的选择是像D:\Dev-Cpp这样的纯英文路径。
  2. 组件选择:安装程序通常会让你选择组件。对于我们的游戏开发,确保MinGW64编译器(或TDM-GCC)被勾选,这是编译C/C++代码的核心。其他如帮助文档、示例代码等,可以根据磁盘空间酌情选择。
  3. 首次运行配置:安装完成后首次启动,Dev-C++可能会提示你选择语言和编译器配置。语言选择简体中文即可。编译器配置通常会自动检测到安装的MinGW,如果没有,需要手动指定其bin目录的路径(例如D:\Dev-Cpp\MinGW64\bin)。

注意:网上有些教程会提到一个叫“小熊猫Dev-C++”的版本,这是一个由国内开发者维护的友好分支,界面更现代化,集成了更多适合教学的功能。如果你是完全的新手,从“小熊猫Dev-C++”开始体验可能会更友好。但本文基于原版Dev-C++ 5.11,其核心操作逻辑是相通的。

安装完成后,建议创建一个专门的目录来存放我们的游戏项目,例如D:\Projects\LyingFlatGame。在Dev-C++中,通过“文件”->“新建”->“项目”,选择“Windows应用程序”或“空项目”,并将项目保存到这个目录下。这样做的好处是所有源代码、资源文件和生成的可执行文件都在一起,管理起来非常清晰。

2.2 图形库的引入:让黑窗口“活”起来

默认的Dev-C++项目只能创建控制台应用程序,也就是那个黑色的命令行窗口。我们要做游戏,必须要有图形界面。这就需要引入第三方图形库。对于Dev-C++,有两个非常经典且易用的选择:EasyXEGE (Easy Graphics Engine)

  • EasyX:历史更悠久,文档丰富,API设计非常简洁直观,特别适合初学者。它的绘图函数如circle,rectangle,putimage等,名字一看就懂。
  • EGE:相对较新,功能更强大一些,除了基本绘图,对动画、音频、键盘鼠标事件的支持也更现代,性能可能略好。

对于“躺平发育”这类2D休闲游戏,两者的功能都绰绰有余。我个人的建议是,如果你是第一次接触图形编程,从EasyX开始,它的学习曲线更平缓。下面以EasyX为例,说明如何将其集成到Dev-C++项目中:

  1. 下载EasyX库:访问EasyX的官方网站,下载针对Dev-C++的安装包。
  2. 安装:运行安装程序,它会自动检测已安装的Dev-C++路径,并将必要的头文件(.h)和库文件(.lib)复制到Dev-C++的MinGW目录中。这个过程通常是全自动的。
  3. 验证安装:在Dev-C++中新建一个源文件(.cpp),输入以下测试代码:
    #include <graphics.h> // EasyX图形库头文件 #include <conio.h> int main() { initgraph(640, 480); // 初始化一个640x480的图形窗口 circle(320, 240, 100); // 在窗口中心画一个半径为100的圆 getch(); // 按任意键继续 closegraph(); // 关闭图形窗口 return 0; }
  4. 项目配置(关键步骤):在Dev-C++中,点击菜单栏的“项目”->“项目属性”。在“参数”选项卡下的“链接器”框中,需要手动添加-lgdi32 -lole32 -luuid这几个链接库。这是因为EasyX底层调用了Windows的GDI等API,需要链接这些系统库才能正确编译。这是很多新手容易忽略,导致出现“undefined reference”链接错误的关键一步。

编译并运行这个测试程序,如果成功弹出一个画着白色圆形的窗口,那么恭喜你,图形环境配置成功!你的Dev-C++已经具备了创作游戏画面的能力。

3. “躺平发育”游戏核心逻辑设计与拆解

有了图形界面,我们就可以开始构思游戏本身了。“躺平发育”类游戏的核心玩法通常可以抽象为:玩家控制一个角色,在自动生成敌人的环境中,通过建造防御设施、升级自身能力来生存并击退敌人,最终达成某种目标(如生存一定时间、击败BOSS)。我们将这个核心拆解为几个可编程的模块。

3.1 游戏元素的数据抽象:用结构体与枚举定义世界

在代码中,我们首先要定义游戏世界里的各种“东西”。用C语言的结构体(struct)和枚举(enum)来抽象它们是最清晰的方式。

  • 玩家角色:需要属性有坐标(x, y)、生命值(HP)、当前金币(gold)、攻击力(attack)、防御等级(defense)等。
    struct Player { int x, y; int hp; int maxHp; int gold; int attack; int defenseLevel; // 可以添加状态,如是否正在建造、升级等 bool isBuilding; };
  • 敌人(怪物):属性包括坐标、生命值、移动速度、攻击力、朝向、当前状态(移动、攻击、死亡)等。我们可以用一个数组或链表来管理多个敌人。
    enum EnemyState { MOVE, ATTACK, DEAD }; struct Enemy { int x, y; int hp; int speed; int attack; EnemyState state; // 简单AI:目标坐标(如冲向玩家基地) int targetX, targetY; };
  • 防御设施(如炮塔):属性包括坐标、攻击范围(range)、攻击间隔(coolDown)、当前冷却计时器(timer)、攻击力等。
    struct Turret { int x, y; int range; int coolDown; int timer; int attack; int level; };
  • 游戏状态:用一个全局的结构体或枚举来管理整个游戏的进行状态,例如菜单中、进行中、暂停、胜利、失败。
    enum GameState { MENU, PLAYING, PAUSED, WIN, LOSE }; GameState gameState = MENU;

通过这样的数据定义,游戏世界就从概念变成了代码中可操作的对象。接下来,就是让这些对象“动”起来。

3.2 游戏主循环:驱动一切的引擎

所有图形游戏的核心都是一个“游戏循环”。这个循环以极高的频率(例如每秒60次,即60FPS)不断重复做三件事:处理输入、更新逻辑、绘制画面。在EasyX/EGE中,我们可以用一个while循环配合kbhit()GetAsyncKeyState()函数来实现。

void gameLoop() { initGame(); // 初始化游戏数据 while (gameState != WIN && gameState != LOSE) { if (_kbhit()) { // 检测是否有键盘输入 processInput(); // 处理输入:移动、建造、升级等 } if (gameState == PLAYING) { updateGame(); // 更新游戏逻辑:敌人移动、攻击、炮塔冷却、碰撞检测等 } renderGame(); // 绘制当前帧的所有画面 // 控制帧率,让每次循环大约耗时16毫秒(约60FPS) Sleep(16); } showResult(); // 显示胜利或失败画面 }
  • processInput()函数:在这里检查用户按下了哪个键。例如,按方向键移动玩家,按‘B’键进入建造模式,按数字键选择要建造的炮塔类型,按空格键暂停等。GetAsyncKeyState(VK_LEFT)可以检测左箭头键是否被按下。
  • updateGame()函数:这是游戏逻辑的“大脑”。每一帧里,它要:
    1. 遍历所有敌人,根据其状态和AI逻辑更新位置(向目标点移动)。
    2. 遍历所有炮塔,减少其冷却计时器,如果冷却完毕,则检查范围内是否有敌人,有则发射“子弹”或直接造成伤害。
    3. 检测碰撞:敌人是否到达了基地(扣减玩家HP)?子弹是否击中了敌人(扣减敌人HP)?玩家是否走到了可建造区域?
    4. 更新游戏状态:如果玩家HP<=0,则gameState = LOSE;如果生存时间达到目标或击败所有敌人,则gameState = WIN
  • renderGame()函数:负责将内存中的游戏数据“画”到屏幕上。每一帧先清空上一帧的画面(cleardevice()),然后按顺序绘制背景、地图格子、玩家角色、所有敌人、所有炮塔、UI(金币数、生命条、建造菜单等)。

这个循环看似简单,却是所有游戏运行的基石。理解并实现好这个循环,你就掌握了游戏编程最核心的范式。

4. 关键功能模块的代码实现与详解

有了框架,我们来填充几个最关键的功能模块的代码细节。这些是实现“躺平发育”玩法不可或缺的部分。

4.1 图形绘制与动画:让游戏“看得见”

绘制是游戏给玩家的直接反馈。我们用EasyX的函数来实现。

  • 绘制静态元素:地图可以用rectangle画格子,基地可以用fillrectangle画一个实心矩形。
    // 绘制一个绿色的基地 setfillcolor(GREEN); fillrectangle(baseX, baseY, baseX + baseWidth, baseY + baseHeight); // 绘制地图网格 setlinecolor(DARKGRAY); for (int i = 0; i <= GRID_NUM; ++i) { line(startX, startY + i * GRID_SIZE, startX + MAP_WIDTH, startY + i * GRID_SIZE); // 横线 line(startX + i * GRID_SIZE, startY, startX + i * GRID_SIZE, startY + MAP_HEIGHT); // 竖线 }
  • 绘制动态角色:玩家和敌人可以用fillcircleputimage(贴图)来绘制。为了让角色动起来,我们需要在updateGame中更新他们的坐标,然后在renderGame中用新的坐标重新绘制。
    // 在renderGame函数中绘制玩家 setfillcolor(BLUE); fillcircle(player.x, player.y, PLAYER_RADIUS); // 绘制敌人 setfillcolor(RED); for (int i = 0; i < enemyCount; ++i) { if (enemies[i].hp > 0) { // 只绘制存活的敌人 fillcircle(enemies[i].x, enemies[i].y, ENEMY_RADIUS); } }
  • 绘制UI:使用outtextxy函数在指定位置输出文字,显示金币和生命值。
    char goldText[50]; sprintf(goldText, "Gold: %d", player.gold); // 将整数格式化为字符串 outtextxy(10, 10, goldText); // 绘制生命条(一个红色背景,一个根据当前HP比例画的绿色前景) int barWidth = 100; int barHeight = 10; int currentHpWidth = (int)((float)player.hp / player.maxHp * barWidth); setfillcolor(RED); fillrectangle(10, 30, 10 + barWidth, 30 + barHeight); setfillcolor(GREEN); fillrectangle(10, 30, 10 + currentHpWidth, 30 + barHeight);

4.2 简单敌人AI与碰撞检测

敌人需要自动向基地移动,并攻击沿途的防御设施或玩家。

  • 移动AI:最简单的AI就是让敌人每帧朝着目标点(基地坐标)移动一步。这需要一点向量运算。
    void updateEnemy(Enemy &e) { if (e.state == DEAD) return; // 计算朝向基地的方向向量 int dx = baseCenterX - e.x; int dy = baseCenterY - e.y; // 计算距离 float distance = sqrt(dx*dx + dy*dy); // 单位化方向向量,并乘以速度 if (distance > 0) { e.x += (int)((dx / distance) * e.speed); e.y += (int)((dy / distance) * e.speed); } // 如果非常接近基地,则切换为攻击状态 if (distance < ATTACK_RANGE) { e.state = ATTACK; } }
  • 碰撞检测:对于这种2D游戏,圆形和矩形之间的碰撞检测非常常用且高效。
    • 点与圆:判断点是否在圆内,用于鼠标点击选择炮塔。
      bool isPointInCircle(int px, int py, int cx, int cy, int radius) { int dx = px - cx; int dy = py - cy; return (dx*dx + dy*dy) <= radius*radius; }
    • 圆与圆:判断两个角色(如子弹和敌人)是否相撞。
      bool isCircleCollision(int x1, int y1, int r1, int x2, int y2, int r2) { int dx = x1 - x2; int dy = y1 - y2; int distanceSq = dx*dx + dy*dy; int radiusSum = r1 + r2; return distanceSq <= radiusSum * radiusSum; }
    • 圆与矩形:判断敌人是否进入炮塔的攻击范围(矩形区域)。
      bool isCircleRectCollision(int cx, int cy, int cr, int rx, int ry, int rw, int rh) { // 找到矩形上距离圆心最近的点 int closestX = max(rx, min(cx, rx + rw)); int closestY = max(ry, min(cy, ry + rh)); // 计算该点到圆心的距离 int dx = cx - closestX; int dy = cy - closestY; return (dx*dx + dy*dy) <= (cr * cr); }
    updateGame中,遍历所有炮塔和敌人,利用上述函数进行碰撞检测,如果炮塔冷却完毕且敌人进入其矩形攻击范围,则炮塔可以攻击该敌人。

4.3 经济与建造系统:游戏进程的驱动器

“躺平发育”的核心乐趣之一在于资源管理和策略建造。

  • 金币系统:金币可以随时间自动缓慢增长(player.gold += goldPerSecond * deltaTime),也可以通过击败敌人获得奖励。在updateGame中定期增加,在敌人死亡时增加。
  • 建造系统:我们需要一个“建造模式”状态。当玩家按下‘B’键,gameMode切换到BUILDING,屏幕上高亮显示可建造的区域(比如空的地图格子)。玩家用鼠标点击一个格子,再按数字键选择炮塔类型,如果金币足够,就在该位置创建一个新的炮塔结构体,并加入到炮塔数组中,同时扣除金币。
    void processBuildMode(int mouseX, int mouseY, int turretType) { // 1. 将鼠标坐标转换为地图网格坐标 int gridX = (mouseX - MAP_START_X) / GRID_SIZE; int gridY = (mouseY - MAP_START_Y) / GRID_SIZE; // 2. 检查该格子是否为空(未被占用) if (isGridEmpty(gridX, gridY)) { // 3. 根据turretType获取炮塔造价 int cost = getTurretCost(turretType); // 4. 检查金币是否足够 if (player.gold >= cost) { // 5. 创建新炮塔 Turret newTurret; newTurret.x = MAP_START_X + gridX * GRID_SIZE + GRID_SIZE/2; newTurret.y = MAP_START_Y + gridY * GRID_SIZE + GRID_SIZE/2; newTurret.level = 1; // ... 设置其他属性 // 6. 添加到数组 turrets[turretCount++] = newTurret; // 7. 扣钱 player.gold -= cost; } else { // 提示金币不足 } } }
  • 升级系统:类似建造,可以设计为当玩家选中一个已存在的炮塔(例如鼠标点击),出现升级按钮,消耗更多金币来提升其攻击力、射程或攻速。这只需要修改炮塔结构体中的对应属性即可。

5. 项目集成、调试与性能优化实录

当各个模块编码完成后,将它们整合到一个完整的项目中,并让游戏流畅运行,这个阶段会遇到最多的问题。

5.1 模块整合与编译链接问题排查

将输入处理、逻辑更新、图形渲染、数据管理的代码分别放到不同的函数甚至不同的.cpp源文件中是一个好习惯,比如input.cpp,update.cpp,render.cpp,game.cpp。在Dev-C++中,你需要确保所有这些源文件都被添加到项目中(在左侧项目管理器右键添加)。

这个阶段最常见的错误是链接错误(Linker Error),尤其是“undefined reference to `WinMain@16’”。这通常是因为:

  • 项目类型错误:你创建了一个“控制台项目”,但图形程序需要“Windows应用程序”的入口点WinMain。解决方案是确保创建项目时选择“Windows应用程序”,或者在项目属性中指定链接子系统为Windows(在“参数”->“链接器”中添加-mwindows)。
  • 库文件未链接:如前所述,忘记在链接器设置中添加-lgdi32 -lole32 -luuid
  • 函数声明与定义不匹配:在.h头文件中声明了函数,但在.cpp文件中定义的函数签名(返回值、参数列表)不一致。

另一个常见问题是运行时黑屏或闪退。这通常是由于:

  • 图形窗口初始化失败initgraph函数调用失败。检查分辨率是否设置过大,或者路径中是否有中文字符导致资源加载失败。
  • 数组越界:在遍历敌人或炮塔数组时,循环条件错误(如for (int i=0; i<=enemyCount; i++),应为i < enemyCount),访问了非法内存。
  • 空指针或未初始化:使用了未初始化的结构体指针或变量。

调试技巧:Dev-C++内置的调试器虽然简单,但很有用。在关键代码行设置断点(F5),然后使用“调试”->“运行到光标处”或单步执行(F7),可以观察变量值的变化,这是定位逻辑错误最有效的方法。对于图形程序,如果调试不方便,可以多用outtextxy在屏幕上打印关键的变量值(如敌人坐标、玩家金币数),这是一种原始的但非常有效的“日志调试法”。

5.2 性能优化与体验提升

当敌人和炮塔数量多起来后,你可能会发现游戏变卡了。这是因为我们使用了最朴素的算法,其计算量随着对象数量呈平方级增长(每个炮塔检查每个敌人)。这里有几个优化思路:

  1. 空间划分:最简单的优化是“距离剪枝”。在检查炮塔攻击范围时,先快速判断敌人与炮塔的直线距离是否“可能”在范围内(比如比较x、y坐标的差值),如果差得太远,就直接跳过精确的碰撞检测。
  2. 状态管理:不是每一帧都需要检测所有碰撞。例如,只有冷却完毕的炮塔才需要去查找目标。敌人只有处于移动状态时才需要更新路径。
  3. 固定时间步长:我们的游戏循环用了Sleep(16)来粗略控制帧率,但这并不精确,且受循环内计算量影响。更专业的做法是计算两帧之间的时间差(deltaTime),然后用这个时间差来更新所有与时间相关的量(如移动距离 = 速度 * deltaTime)。这样能保证在不同性能的电脑上,游戏逻辑速度是一致的。
    #include <time.h> clock_t lastTime = clock(); while (gameRunning) { clock_t currentTime = clock(); float deltaTime = (float)(currentTime - lastTime) / CLOCKS_PER_SEC; // 转换为秒 lastTime = currentTime; processInput(); updateGame(deltaTime); // 将deltaTime传入更新函数 renderGame(); // 可以不加Sleep,或者用一个更精确的帧率控制 }
  4. 资源管理:如果使用了图片(IMAGE对象),在游戏开始时一次性加载好(loadimage),而不是在每一帧的renderGame里加载。游戏结束时用delimage释放。

5.3 打包与分享你的作品

游戏做完了,怎么发给朋友玩呢?你直接发送项目文件夹里的.exe文件,对方很可能因为缺少运行库(主要是libgcc_s_seh-1.dll,libstdc++-6.dll,libwinpthread-1.dll等)而无法运行。

静态链接:最彻底的解决方案是在编译时进行静态链接,将这些依赖库打包进.exe文件。在Dev-C++的“项目”->“项目属性”->“参数”->“链接器”中,添加以下参数:

-static -static-libgcc -static-libstdc++

重新编译后,生成的.exe文件会变大不少,但几乎可以在任何64位Windows系统上独立运行。这是分享小型作品最推荐的方式。

完成这一步,你就拥有了一个完全由你编写、从零构建的“躺平发育”游戏。它可能画面简陋,玩法简单,但其中蕴含的每一个判断、每一行循环、每一次碰撞检测,都完全受控于你的代码。这种从无到有、掌控一切的成就感,是使用现成游戏引擎拖拽组件所无法比拟的。通过这个项目,你真正走通了一条从问题抽象、逻辑设计、代码实现到调试打包的完整开发路径,这才是本次创作之旅最宝贵的收获。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 5:49:35

一键生成论文工具工具箱:语法纠错+降重降AI一篇文章讲透

论文写完总觉得哪里不对劲&#xff1f;别急&#xff0c;AI 工具真的能帮你把初稿打磨成合格的终稿。每年毕业季&#xff0c;后台总能看到一堆类似的提问&#xff1a;“论文写完了&#xff0c;怎么改才能过审&#xff1f;”作为一个曾经被查重率 40% 惊到怀疑人生、最后靠工具救…

作者头像 李华
网站建设 2026/8/11 5:49:21

Go 并发服务开发短记:延迟怎么分段看

Go 并发服务开发短记&#xff1a;延迟怎么分段看 Go 服务的关键不是先堆组件&#xff0c;而是先确认请求上下文、goroutine 生命周期和依赖调用这条链路由谁维护、何时算完成。题目中的“延迟、吞吐与资源占用的性能调优”只在这条边界内展开。 Go 后端服务开发与并发编程模型解…

作者头像 李华
网站建设 2026/8/11 5:49:19

Unity WebGL部署实战:Nginx配置与跨域问题解决方案

1. 项目概述&#xff1a;从“打不开”到“跑起来”的必经之路如果你是一名Unity开发者&#xff0c;费尽心思将项目发布为WebGL&#xff0c;满心欢喜地部署到自己的服务器上&#xff0c;结果在Chrome浏览器里一打开&#xff0c;要么是一片空白&#xff0c;要么控制台报出一堆红色…

作者头像 李华
网站建设 2026/8/11 5:49:07

链表逆序相加算法解析与实现技巧

1. 两数相加问题背景与核心挑战这道题源自知名编程题库的热门题目集合&#xff0c;编号为第二题。题目要求处理两个非空链表&#xff0c;它们各自代表一个逆序存储的非负整数。我们需要将这两个数字相加&#xff0c;并以相同形式的链表返回结果。举个例子&#xff0c;如果输入是…

作者头像 李华
网站建设 2026/8/11 5:48:47

尚硅谷Hermes Agent教程,零基础玩转hermes爱马仕多智能体

尚硅谷精讲 Hermes&#xff0c;零基础玩转智能体开发 在人工智能从"单一模型对话"迈向"自主智能体应用"的关键节点&#xff0c;如何构建具备自主规划、工具调用与多步执行能力的 AI 系统&#xff0c;已成为开发者关注的焦点。在众多智能体框架中&#xff0…

作者头像 李华
网站建设 2026/8/11 5:47:31

Codex CLI命令速查手册:极简设计与高频场景实战指南

1. 项目概述&#xff1a;为什么你需要一份“无废话”的Codex命令手册&#xff1f;如果你正在寻找Codex的命令行工具&#xff08;CLI&#xff09;使用方法&#xff0c;大概率已经翻遍了官方文档、技术博客&#xff0c;甚至看了几个视频教程。但结果往往是&#xff1a;官方文档过…

作者头像 李华