ARTICLE DETAIL

资讯详情

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

C++与EasyX实战:从零构建图形化扫雷游戏

C++与EasyX实战:从零构建图形化扫雷游戏 1. 项目概述与核心价值扫雷这个几乎刻在每一个Windows用户DNA里的小游戏远不止是消磨时间的工具。对于C初学者和图形界面编程的入门者来说亲手实现一个扫雷游戏是一次绝佳的综合性实战演练。它麻雀虽小五脏俱全涵盖了二维数组、递归算法、随机数生成、鼠标交互、图形绘制、状态管理等多个核心编程概念。而EasyX作为一个专为C设计的轻量级图形库恰好为我们搭建了一座从枯燥的控制台“黑框框”通往生动图形化世界的桥梁。它封装了Windows的GDI绘图接口提供了诸如circle、rectangle、putpixel、outtextxy等直观易用的函数让我们无需深入Win32 API的复杂细节就能快速绘制出游戏界面。这个项目的目标就是带你从零开始用最纯粹的C语法和EasyX图形库一步步构建出一个功能完整、交互流畅的扫雷游戏。整个过程你将不再是被动的代码阅读者而是从游戏逻辑设计、数据结构定义、到每一行绘图代码、每一个鼠标点击事件处理的主动构建者。最终你将获得一个可以独立运行的.exe文件以及一份扎实的、能写进简历里的项目经验。无论你是刚学完C基础语法想找项目练手的学生还是希望重温经典算法与图形编程的开发者这个项目都能提供一条清晰、可执行的路径。2. 开发环境搭建与工具选型工欲善其事必先利其器。一个顺手的开发环境能极大提升编码效率和调试体验。虽然EasyX官方主要支持Visual Studio但在其他主流IDE上配置也并非难事。这里我会给出两种最主流的选择方案。2.1 集成开发环境IDE选择方案一Visual Studio 2022 (社区版)这是与EasyX兼容性最好、配置最简单的方案。VS社区版免费且功能强大其内置的MSVC编译器与EasyX的静态链接库无缝对接。优势一键安装EasyX调试功能强大智能提示IntelliSense优秀。安装从微软官网下载Visual Studio Installer在安装时勾选“使用C的桌面开发”工作负载即可。配置EasyX访问EasyX官网easyx.cn下载安装包。运行安装程序它会自动检测已安装的Visual Studio版本并进行配置。安装完成后在VS中新建一个“空项目”或“控制台应用”在代码中直接#include graphics.h即可使用。方案二VSCode MinGW-w64如果你偏爱轻量级、可高度定制的编辑器VSCode是绝佳选择。配合MinGW-w64编译器同样可以运行EasyX程序。优势轻快、跨平台Windows下、插件生态丰富。安装配置安装MinGW-w64推荐从 MSYS2 或 WinLibs 获取预编译的GCC工具链。安装后需将bin目录如C:\msys64\mingw64\bin添加到系统的PATH环境变量中。安装VSCode从官网下载安装。配置VSCode插件安装“C/C”扩展由Microsoft发布。配置EasyX这是关键步骤。EasyX官网提供了针对MinGW的适配库通常是一个.a静态库文件和一个修改过的graphics.h头文件。你需要将下载的graphics.h等头文件放入MinGW的include目录。将libeasyx.a等库文件放入MinGW的lib目录。在VSCode中配置项目的tasks.json编译任务和launch.json调试任务在编译参数中明确链接libeasyx.a和libgdi32.a、libole32.a等Windows系统库。注意对于新手强烈推荐方案一Visual Studio。它能避免大量环境配置的坑让你更专注于代码逻辑本身。本教程后续的代码和演示均基于Visual Studio环境。2.2 项目结构与代码管理在开始编码前建立清晰的项目结构是个好习惯。在你的项目目录下可以这样组织Minesweeper/ ├── MineSweeper.sln (Visual Studio解决方案文件) ├── MineSweeper.vcxproj (项目文件) ├── src/ (源代码目录) │ ├── main.cpp (程序入口主循环) │ ├── game.h (游戏核心逻辑头文件) │ ├── game.cpp (游戏核心逻辑实现) │ ├── graphics.h (图形绘制相关头文件可能封装EasyX调用) │ └── graphics.cpp (图形绘制实现) ├── resources/ (资源目录可选) │ └── images/ (存放数字、地雷等图片素材) └── README.md (项目说明文档)对于这样一个单文件或少量文件的小项目你也可以将所有代码写在一个main.cpp里这完全可行。但拆分成多个文件头文件.h和源文件.cpp有助于逻辑分离让代码更清晰、更易维护。例如将游戏状态计算和图形界面渲染分开符合“模型-视图”分离的思想。3. 游戏核心逻辑设计与数据结构在绘制任何图形之前我们必须先用代码构建出扫雷游戏的“大脑”——即它的数据模型和核心规则。这是整个项目的基石。3.1 游戏状态与棋盘表示扫雷棋盘本质上是一个二维网格。我们需要用数据结构来记录每个格子的多种状态。 首先定义一些游戏常量// 游戏难度常量 const int EASY_WIDTH 9; const int EASY_HEIGHT 9; const int EASY_MINES 10; const int MEDIUM_WIDTH 16; const int MEDIUM_HEIGHT 16; const int MEDIUM_MINES 40; const int HARD_WIDTH 30; const int HARD_HEIGHT 16; const int HARD_MINES 99; // 格子状态常量 const int CELL_UNKNOWN 0; // 未揭开 const int CELL_FLAGGED 1; // 已标记为旗子 const int CELL_QUESTION 2; // 标记为问号 const int CELL_REVEALED 3; // 已揭开 const int CELL_MINE 9; // 地雷 (用一个特殊值表示比如9)接下来设计核心的Game类来管理所有游戏状态class Game { private: int width_; // 棋盘宽度格子数 int height_; // 棋盘高度格子数 int mineCount_; // 地雷总数 int revealedCount_; // 已揭开的非雷格子数 bool gameOver_; // 游戏是否结束 bool firstClick_; // 是否是第一次点击用于安全开局 time_t startTime_; // 游戏开始时间 // 核心二维数组存储每个格子下的真实内容 // -1: 地雷, 0-8: 周围8格中的地雷数 std::vectorstd::vectorint board_; // 状态二维数组存储每个格子的当前显示状态 (CELL_UNKNOWN, CELL_FLAGGED等) std::vectorstd::vectorint state_; // 标记数组记录玩家标记的旗子位置用于判断是否标记了非雷格 std::vectorstd::vectorbool flags_; public: Game(int width, int height, int mineCount); void init(); // 初始化或重置游戏 void placeMines(int safeX, int safeY); // 放置地雷确保(safeX, safeY)周围无雷 int countAdjacentMines(int x, int y); // 计算(x,y)周围地雷数 bool reveal(int x, int y); // 揭开格子(x,y)返回是否触发地雷 void toggleFlag(int x, int y); // 切换标记旗子/问号/无 bool isWin() const; // 判断是否获胜 // ... 其他获取游戏状态的方法 };关键设计解析双棋盘结构board_存储“真相”哪里是雷周围有几颗雷state_存储“表象”玩家看到的状态。这种分离是游戏逻辑的核心。安全开局firstClick_和placeMines函数配合实现。在玩家第一次点击后再生成地雷并确保第一次点击的格子及其周围一圈3x3区域绝对没有地雷这是扫雷游戏的通用友好规则。递归揭开reveal函数是算法核心。当揭开的格子周围地雷数为0时需要自动递归揭开其周围所有未揭开的格子直到遇到数字格或边界为止。这涉及到经典的深度优先搜索DFS或广度优先搜索BFS算法。3.2 核心算法实现递归揭开与胜负判定reveal函数的实现是游戏逻辑的精华。bool Game::reveal(int x, int y) { // 边界检查或已揭开/已标记的格子直接返回 if (x 0 || x width_ || y 0 || y height_) return false; if (state_[y][x] CELL_REVEALED || state_[y][x] CELL_FLAGGED) return false; // 如果是第一次点击先布置地雷安全开局 if (firstClick_) { placeMines(x, y); firstClick_ false; startTime_ time(nullptr); // 记录开始时间 } // 如果踩中地雷游戏结束 if (board_[y][x] -1) { state_[y][x] CELL_REVEALED; gameOver_ true; return true; // 返回true表示触雷 } // 揭开当前格子 state_[y][x] CELL_REVEALED; revealedCount_; // 如果揭开的格子数字为0周围无雷则递归揭开周围8个格子 if (countAdjacentMines(x, y) 0) { // 使用递归DFS for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { if (dx 0 dy 0) continue; // 跳过自身 reveal(x dx, y dy); // 递归调用 } } // 也可以使用栈或队列进行BFS避免递归深度过大对于超大棋盘 } return false; // 未触雷 }胜负判定失败gameOver_标志被设为true即reveal函数返回true。胜利所有非地雷格子都被揭开。即revealedCount_ (width_ * height_ - mineCount_)。4. 基于EasyX的图形界面实现逻辑层完成后我们需要为其绘制一个“外壳”。EasyX让这个过程变得直观。4.1 窗口初始化与绘图常量首先我们需要根据棋盘大小和格子尺寸计算窗口大小并初始化图形窗口。#include graphics.h // EasyX图形库头文件 #include conio.h // 用于_getch()等 #include time.h // 绘图常量 const int CELL_SIZE 30; // 每个格子的像素大小 const int BORDER_LEFT 20; // 左边距 const int BORDER_TOP 60; // 顶边距留出空间显示雷数和计时器 const int INFO_AREA_HEIGHT 40; // 顶部信息区高度 // 颜色定义 const COLORREF COLOR_GRID_LINE LIGHTGRAY; const COLORREF COLOR_CELL_HIDDEN RGB(192, 192, 192); // 经典的未揭开格子灰色 const COLORREF COLOR_CELL_REVEALED WHITE; const COLORREF COLOR_FLAG RED; const COLORREF COLOR_MINE BLACK; const COLORREF COLOR_NUM[9] { // 数字1-8的颜色0不用显示 BLUE, // 1 RGB(0, 128, 0), // 2 (绿色) RED, // 3 RGB(0, 0, 128), // 4 (深蓝) RGB(128, 0, 0), // 5 (深红) RGB(0, 128, 128), // 6 (青色) PURPLE, // 7 GRAY // 8 }; void initGraphics(int boardWidth, int boardHeight) { int screenWidth BORDER_LEFT * 2 boardWidth * CELL_SIZE; int screenHeight BORDER_TOP INFO_AREA_HEIGHT boardHeight * CELL_SIZE; initgraph(screenWidth, screenHeight, SHOWCONSOLE); // SHOWCONSOLE方便调试 setbkcolor(WHITE); cleardevice(); settextstyle(20, 0, _T(宋体)); // 设置字体 settextcolor(BLACK); setlinecolor(COLOR_GRID_LINE); }4.2 绘制游戏界面棋盘、格子与数字绘制分为两部分静态的棋盘网格和动态的格子状态。void drawCell(int x, int y, int cellState, int cellValue) { // 计算格子左上角在屏幕上的坐标 int screenX BORDER_LEFT x * CELL_SIZE; int screenY BORDER_TOP y * CELL_SIZE; // 1. 绘制格子背景 if (cellState CELL_REVEALED) { setfillcolor(COLOR_CELL_REVEALED); } else { setfillcolor(COLOR_CELL_HIDDEN); } fillrectangle(screenX, screenY, screenX CELL_SIZE - 1, screenY CELL_SIZE - 1); // 2. 绘制格子边框凸起或凹陷效果增强立体感 if (cellState ! CELL_REVEALED) { // 未揭开左上亮右下暗模拟凸起按钮 setlinecolor(WHITE); line(screenX, screenY, screenX CELL_SIZE - 1, screenY); // 上边 line(screenX, screenY, screenX, screenY CELL_SIZE - 1); // 左边 setlinecolor(DARKGRAY); line(screenX CELL_SIZE - 1, screenY, screenX CELL_SIZE - 1, screenY CELL_SIZE - 1); // 右边 line(screenX, screenY CELL_SIZE - 1, screenX CELL_SIZE - 1, screenY CELL_SIZE - 1); // 下边 } else { // 已揭开简单的单线边框 setlinecolor(COLOR_GRID_LINE); rectangle(screenX, screenY, screenX CELL_SIZE, screenY CELL_SIZE); } // 3. 根据格子状态和内容绘制中心元素 switch (cellState) { case CELL_FLAGGED: // 绘制小红旗可以用多边形绘制或使用图片 setfillcolor(COLOR_FLAG); setlinecolor(COLOR_FLAG); // 简化版画一个红色实心三角形 solidpolygon(...); // 需要计算三角形三个顶点的坐标 break; case CELL_QUESTION: // 绘制蓝色问号 settextcolor(BLUE); outtextxy(screenX CELL_SIZE/2 - 5, screenY CELL_SIZE/2 - 10, _T(?)); break; case CELL_REVEALED: if (cellValue -1) { // 地雷 setfillcolor(COLOR_MINE); setlinecolor(COLOR_MINE); fillcircle(screenX CELL_SIZE/2, screenY CELL_SIZE/2, CELL_SIZE/3); } else if (cellValue 0) { // 数字1-8 TCHAR str[2]; _stprintf_s(str, _T(%d), cellValue); settextcolor(COLOR_NUM[cellValue - 1]); // 根据数字选择颜色 outtextxy(screenX CELL_SIZE/2 - 5, screenY CELL_SIZE/2 - 10, str); } // cellValue 0 时不绘制任何东西 break; // CELL_UNKNOWN 状态不需要绘制额外内容 } } void drawBoard(const Game game) { // 清屏并重绘所有格子 cleardevice(); for (int y 0; y game.getHeight(); y) { for (int x 0; x game.getWidth(); x) { int state game.getCellState(x, y); int value game.getCellValue(x, y); drawCell(x, y, state, value); } } // 绘制顶部信息剩余雷数、计时器、笑脸按钮等 drawGameInfo(game); }绘制技巧立体感通过改变边框线条的颜色左上白右下深灰可以模拟出Windows经典控件那种按下/弹起的立体效果极大提升UI质感。居中文本outtextxy的坐标是文本左上角。为了将单个数字字符居中在格子内通常需要将格子中心坐标减去字符预估宽度和高度的一半如CELL_SIZE/2 - 5和CELL_SIZE/2 - 10这里的5和10是经验值取决于字体大小。双缓冲如果发现画面闪烁可以在initgraph时使用EW_DBLBUF标志开启双缓冲并在每帧绘制完成后调用FlushBatchDraw()。4.3 鼠标交互与事件循环图形化游戏的核心是事件驱动。我们需要在主循环中不断获取鼠标消息并转换为游戏逻辑的输入。void gameLoop() { Game game(EASY_WIDTH, EASY_HEIGHT, EASY_MINES); initGraphics(game.getWidth(), game.getHeight()); ExMessage msg; // EasyX提供的消息结构体 bool running true; while (running) { // 1. 处理消息队列 while (peekmessage(msg, EX_MOUSE)) { // 只处理鼠标消息 if (msg.message WM_LBUTTONDOWN) { // 将屏幕坐标转换为棋盘格子坐标 int gridX (msg.x - BORDER_LEFT) / CELL_SIZE; int gridY (msg.y - BORDER_TOP) / CELL_SIZE; // 检查点击是否在有效棋盘区域内 if (gridX 0 gridX game.getWidth() gridY 0 gridY game.getHeight()) { bool hitMine game.reveal(gridX, gridY); if (hitMine) { // 游戏结束显示所有地雷 game.revealAllMines(); drawBoard(game); // 显示失败提示 outtextxy(..., _T(游戏结束你踩到雷了)); running false; } else if (game.isWin()) { drawBoard(game); // 显示胜利提示 outtextxy(..., _T(恭喜你胜利了)); running false; } } else { // 检查是否点击了“重新开始”按钮笑脸 // ... 处理按钮点击 } } else if (msg.message WM_RBUTTONDOWN) { int gridX (msg.x - BORDER_LEFT) / CELL_SIZE; int gridY (msg.y - BORDER_TOP) / CELL_SIZE; if (gridX 0 gridX game.getWidth() gridY 0 gridY game.getHeight()) { game.toggleFlag(gridX, gridY); // 右键标记 } } } // 2. 更新游戏状态如计时器 updateTimer(game); // 3. 重绘界面 if (!game.isGameOver()) { drawBoard(game); } // 4. 短暂延迟避免CPU占用率100% Sleep(10); // 单位毫秒 } // 游戏结束等待用户按键退出 _getch(); closegraph(); }交互细节坐标转换鼠标消息msg.x和msg.y是屏幕像素坐标必须通过减掉边距、除以格子大小并取整才能得到棋盘上的(gridX, gridY)索引。消息处理顺序使用peekmessage而不是getmessage可以避免阻塞实现流畅的实时循环。EX_MOUSE过滤器只获取鼠标消息提高效率。右键标记扫雷的经典操作是左键揭开右键循环标记无标记-旗子-问号-无标记。toggleFlag函数需要在Game类中实现此状态循环。5. 功能增强与性能优化一个基础版本完成后我们可以从用户体验和代码质量角度进行增强。5.1 添加游戏状态显示与交互控件顶部信息栏是游戏UI的重要组成部分。void drawGameInfo(const Game game) { // 1. 绘制剩余雷数 int remainingFlags game.getMineCount() - game.getFlagCount(); TCHAR mineStr[20]; _stprintf_s(mineStr, _T(雷数: %03d), remainingFlags); outtextxy(20, 20, mineStr); // 2. 绘制计时器 time_t currentTime game.isGameStarted() ? time(nullptr) : game.getStartTime(); int elapsedSeconds (currentTime - game.getStartTime()); TCHAR timeStr[20]; _stprintf_s(timeStr, _T(时间: %03d), elapsedSeconds); outtextxy(200, 20, timeStr); // 3. 绘制“重新开始”按钮笑脸 int btnX (getwidth() - 30) / 2; // 窗口中央 int btnY 15; setfillcolor(YELLOW); fillcircle(btnX 15, btnY 15, 15); // 绘制笑脸表情根据游戏状态改变正常/胜利/失败 if (game.isGameOver()) { if (game.isWin()) { // 画胜利的笑脸 setlinecolor(BLACK); arc(btnX 8, btnY 10, btnX 22, btnY 20, 0, PI); // 上扬的嘴 } else { // 画失败的哭脸 setlinecolor(BLACK); arc(btnX 8, btnY 20, btnX 22, btnY 30, PI, 2 * PI); // 下垂的嘴 } } else { // 画普通的笑脸 setlinecolor(BLACK); arc(btnX 8, btnY 10, btnX 22, btnY 20, 0, PI); } // 眼睛 setfillcolor(BLACK); solidcircle(btnX 10, btnY 10, 2); solidcircle(btnX 20, btnY 10, 2); }在鼠标事件处理中需要增加对笑脸按钮点击的检测点击后调用game.init()重置游戏。5.2 实现“双击数字”快速揭开这是扫雷的高级技巧当一个已揭开的数字格周围玩家标记的旗子数量等于该数字时双击该数字格可以自动揭开周围所有未标记的格子。这能极大提升游戏效率。void Game::chord(int x, int y) { // 只有已揭开的数字格才能被“和弦” if (state_[y][x] ! CELL_REVEALED || board_[y][x] 0) return; int flagCount 0; // 计算周围8格中标记为旗子的数量 for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { if (dx 0 dy 0) continue; int nx x dx, ny y dy; if (nx 0 nx width_ ny 0 ny height_) { if (state_[ny][nx] CELL_FLAGGED) flagCount; } } } // 如果旗子数等于格子数字则揭开周围未标记且未揭开的格子 if (flagCount board_[y][x]) { for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { if (dx 0 dy 0) continue; int nx x dx, ny y dy; if (nx 0 nx width_ ny 0 ny height_) { if (state_[ny][nx] CELL_UNKNOWN || state_[ny][nx] CELL_QUESTION) { reveal(nx, ny); // 可能触发地雷 } } } } } }在主循环的鼠标事件处理中需要监听WM_LBUTTONDBLCLK双击消息并在确认点击的是已揭开的数字格后调用game.chord(gridX, gridY)。5.3 性能优化与代码健壮性避免全局变量所有游戏状态和图形设置都应封装在类Game,Renderer中。这提高了代码的模块化和可测试性。局部重绘上述drawBoard函数每次重绘整个棋盘在格子多时可能效率不高。可以优化为“脏矩形”更新只重绘状态发生改变的格子。但对于扫雷这种格子数量有限最多30*16480格的游戏全屏重绘在60FPS下也完全不是问题。资源管理如果使用了图片素材如更精致的旗子、地雷图案需要使用EasyX的IMAGE对象和loadimage函数加载。务必在程序退出前用destroyimage释放。输入验证所有从鼠标坐标转换来的gridX、gridY都必须进行边界检查防止数组越界访问这是程序崩溃的常见原因。随机数质量地雷的随机布置应使用random库中的现代随机数发生器如std::mt19937而非古老的rand()和srand(time(nullptr))以保证更好的随机性分布。#include random std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution distrib(0, width_ * height_ - 1);6. 常见问题排查与调试技巧在开发过程中你几乎一定会遇到下面这些问题。6.1 编译与链接问题问题现象可能原因解决方案编译错误无法打开源文件 “graphics.h”EasyX头文件未正确包含或路径不对。在VS中确保已通过安装程序安装EasyX。如果是手动配置检查项目属性 - C/C - 常规 - 附加包含目录。链接错误无法解析的外部符号 __imp_xxx缺少链接库通常是Windows的GDI相关库。在VS项目属性 - 链接器 - 输入 - 附加依赖项中添加gdi32.lib和ole32.lib。EasyX安装器通常会自动配置。程序一闪而过通常是控制台程序执行完main函数就退出了。在main函数结尾或图形窗口循环后添加_getch();或system(“pause”);等待按键。或者使用initgraph时不显示控制台(SHOWCONSOLE)。在VSCodeMinGW中链接失败未正确链接EasyX for MinGW的静态库或系统库。在tasks.json的args中确保有-leasyx -lgdi32 -lole32等参数并且库文件路径正确。6.2 运行时逻辑错误问题现象调试思路解决方案鼠标点击位置错位屏幕坐标到格子坐标的转换公式错误。在drawCell和鼠标事件处理中打印(msg.x, msg.y)和计算后的(gridX, gridY)检查边距BORDER_LEFT、BORDER_TOP和CELL_SIZE是否正确。递归揭开时程序卡死或栈溢出递归函数reveal没有终止条件或条件错误导致无限递归。1. 确保递归前检查格子状态已揭开的格子不再递归。2. 确保递归只向周围8个方向进行不会重复访问自身。3. 对于极大棋盘考虑将递归改为显式的栈stack或队列queue实现的BFS。地雷生成在第一次点击位置placeMines函数在第一次点击前就被调用。确保在Game类构造函数或init()中只初始化数组不布置地雷。地雷布置应在firstClick_为true时在reveal函数内部首次点击后执行。胜利条件判断错误revealedCount_计数不准确。检查revealedCount_是否只在成功揭开非雷格时增加reveal函数中在确认不是地雷且状态变为CELL_REVEALED后。标记/取消标记操作不应影响此计数。画面闪烁严重直接绘图到屏幕导致。启用双缓冲。initgraph(width, height, SHOWCONSOLE6.3 调试心得与技巧善用控制台输出即使是在图形界面程序SHOWCONSOLE模式下的控制台也是强大的调试工具。在关键逻辑处使用printf或std::cout打印变量状态如board_和state_的值可以快速定位逻辑错误。可视化调试在开发初期可以修改drawCell函数即使格子未揭开也用小字在角落绘制出board_中的真实值地雷或数字。这让你一目了然地看到游戏内部状态方便验证地雷布置、数字计算是否正确。单元测试思维将游戏逻辑Game类与图形界面分离的好处之一是你可以为Game类编写简单的测试。例如创建一个固定布局的棋盘然后调用reveal检查state_变化是否符合预期。边界情况测试专门测试角落和边缘的格子周围不是8个邻居、第一次点击在角落、标记所有地雷后双击数字等边界情况。走到这里你已经拥有了一个完全由自己掌控的扫雷游戏。从抽象的数据结构设计到具体的像素绘制再到细微的交互体验每一个环节都浸透着你的思考与代码。这个项目最大的价值不在于复刻了一个经典游戏而在于你完整地走通了一个小型软件项目的生命周期需求分析、技术选型、架构设计、编码实现、调试测试。你会发现很多在课本上孤立的概念如数组、递归、类、消息循环在这里自然地串联起来成为了解决实际问题的工具。当你未来面对更复杂的项目时这次从零构建扫雷的经验——如何分层、如何设计状态、如何处理交互、如何调试——将成为你技术工具箱里最趁手的基础模型之一。
返回列表