1. 消灭星星核心玩法与整体架构拆解
1.1 为什么选择C语言加EasyX这套组合
做消灭星星这类消除类小游戏,技术选型其实比想象中要讲究。市面上的方案大致分三档:Unity、Godot这类引擎方案,Python加Pygame的脚本方案,以及C/C++配图形库的原生方案。我当初选C语言加EasyX,核心原因有三个。
第一是学习性价比。消灭星星的玩法逻辑——网格扫描、连通块判定、重力下落、列位移——本质上是二维数组操作和搜索算法的练习场。用引擎做,这些逻辑会被引擎的组件系统封装掉,你写的是“配置”而不是“算法”。用C语言手写,每一步都得自己想清楚,对理解游戏循环、状态管理、内存布局的帮助是实打实的。
第二是EasyX的轻量。EasyX是Windows平台下的一个图形库,封装了GDI的绘图接口,提供initgraph、BeginBatchDraw、putimage这类函数。它没有场景树、没有物理引擎、没有资源管理器,就是一个画布加一堆绘图API。这意味着整个游戏的架构完全由你掌控,不会出现“引擎帮你做了但你不懂”的黑盒。
第三是性能可控。消灭星星的棋盘通常是10乘10,最多100个格子。这个规模下,C语言的数组遍历速度远超脚本语言,即使每帧全量扫描连通块,帧率也稳得很。而且C语言没有GC,不会出现脚本语言那种“消除瞬间卡一下”的掉帧问题。
当然,这套组合也有代价。EasyX只支持Windows,跨平台就别想了;图形API偏底层,做动画、粒子、缓动都得自己写;没有内置的UI系统,按钮、文字、弹窗全得手搓。但对于一个消灭星星来说,这些代价完全可接受,反而逼着你把基础功练扎实。
1.2 游戏状态机的设计思路
消灭星星看起来简单,但状态流转其实不少。我把它拆成六个状态:初始化、等待输入、选中判定、消除结算、下落填充、游戏结束。每个状态负责一件事,状态之间通过事件驱动切换。
为什么用状态机而不是一堆if-else堆在主循环里?因为消除类游戏的状态切换有明确的先后依赖。比如你不能在“下落填充”还没结束时就去响应新的点击,否则会出现“方块还在掉,玩家又选了一组”的混乱。状态机强制你按顺序走流程,逻辑清晰,调试也方便。
具体实现上,我用一个枚举GameState表示当前状态,主循环里用switch分发。每个状态处理完自己的逻辑后,通过修改GameState变量触发切换。比如“选中判定”确认玩家选了一组可消除的星星后,把状态改成“消除结算”;“消除结算”播完消除动画,把状态改成“下落填充”;“下落填充”完成后,检查是否还有可消除的组合,有就回到“等待输入”,没有就进入“游戏结束”。
这里有个细节:动画和逻辑要分离。消除动画、下落动画都是表现层的东西,不应该阻塞逻辑层。我的做法是逻辑层瞬间完成数据变更(把要消除的格子标记为空格,把上方的星星下移),表现层用一个独立的动画队列去播放。这样即使动画还在播,逻辑层已经准备好了下一帧的数据,不会出现“等动画”的空转。
1.3 棋盘数据结构的选择与理由
棋盘数据我用的是一维数组模拟二维,而不是int board[10][10]这种二维数组。原因有两个。
一是内存连续性。一维数组在内存里是连续排列的,遍历时缓存命中率高。二维数组虽然逻辑上是二维的,但物理内存也是连续的,只是索引计算多一层。对于10乘10这种小规模,性能差异可以忽略,但一维数组在写连通块搜索时,索引计算更统一。
二是便于扩展。如果以后想把棋盘改成非矩形,或者做棋盘旋转,一维数组配合宽高变量更容易改。二维数组的board[i][j]写法在旋转时容易搞混行列。
具体定义是这样的:
#define BOARD_W 10 #define BOARD_H 10 #define EMPTY 0 int board[BOARD_W * BOARD_H]; // 索引转换 int idx(int x, int y) { return y * BOARD_W + x; }每个格子的值表示星星的颜色编号,0表示空格。颜色编号从1开始,方便和EMPTY区分。这个设计看起来简单,但后面所有的连通块搜索、重力下落、列位移都建立在这个基础上,所以一开始就要定清楚。
提示:不要用0表示有效颜色,否则空格和有效格子无法区分。我见过有人用0表示红色,结果消除后判断空格时把红色也当成空了,排查了半天。
2. 连通块搜索与消除判定的核心实现
2.1 连通块搜索为什么用BFS而不是DFS
消灭星星的核心判定是:玩家选中的两个及以上同色相邻星星构成一个连通块,可以消除。这里的“相邻”指上下左右四方向,不含对角线。搜索连通块的标准做法是BFS或DFS,我选BFS。
原因在于消除类游戏的连通块通常不大,但棋盘上可能有多个连通块。BFS用队列实现,天然适合“一层一层往外扩”的搜索模式,而且不会像DFS那样在极端情况下递归过深导致栈溢出。虽然10乘10的棋盘DFS也不会溢出,但BFS的代码结构更直观,调试时打印队列内容就能看到搜索过程。
BFS的实现需要一个队列和一个访问标记数组。访问标记我用一个和棋盘同大小的bool visited[],每次搜索前清零。队列用数组模拟,因为最大元素数就是棋盘格子数,不会超过100,用循环队列或者简单的头尾指针都行。
int queue[BOARD_W * BOARD_H]; bool visited[BOARD_W * BOARD_H]; int bfs(int startX, int startY, int color, int* outCells) { int head = 0, tail = 0; int count = 0; memset(visited, 0, sizeof(visited)); queue[tail++] = idx(startX, startY); visited[idx(startX, startY)] = true; while (head < tail) { int cur = queue[head++]; outCells[count++] = cur; int cx = cur % BOARD_W; int cy = cur / BOARD_W; // 四方向扩展 int dx[] = {0, 0, -1, 1}; int dy[] = {-1, 1, 0, 0}; for (int d = 0; d < 4; d++) { int nx = cx + dx[d]; int ny = cy + dy[d]; if (nx < 0 || nx >= BOARD_W || ny < 0 || ny >= BOARD_H) continue; int ni = idx(nx, ny); if (visited[ni]) continue; if (board[ni] != color) continue; visited[ni] = true; queue[tail++] = ni; } } return count; }这段代码里有个容易踩的坑:入队时就要标记visited,不能等出队再标记。如果出队才标记,同一个格子可能被多次入队,队列膨胀,严重时死循环。我第一版就犯了这个错,测试时发现消除一组星星后程序卡死,查了半天才发现是重复入队。
2.2 消除判定与得分计算
玩家点击两个星星后,先判断这两个星星颜色是否相同、是否相邻。如果满足,就以其中一个为起点做BFS,得到整个连通块。如果连通块大小大于等于2,就可以消除。
得分计算我参考了常见消灭星星的规则:n个星星消除得n的平方分。比如消除3个得9分,消除5个得25分。这个规则鼓励玩家攒大块再消,而不是见两个就消,增加了策略性。
int score = count * count; totalScore += score;为什么用平方而不是线性?线性的话,消两个得2分,消四个得4分,玩家没有攒块的动力。平方的话,消两个得4分,消四个得16分,差距拉大,玩家会主动寻找大连通块。这是消除类游戏设计的经典手法,用得分曲线引导玩家行为。
消除时把连通块内所有格子的board值设为EMPTY,同时记录消除的格子坐标,供动画层使用。这里要注意:先收集再清除,不要边遍历边清除,否则BFS的visited数组和board状态会不一致。
2.3 消除动画的实现细节
消除动画我做了个简单的缩放效果:星星先放大到1.2倍,再缩小到0,同时透明度从1降到0。EasyX本身不支持透明度混合,所以透明度效果我用的是颜色插值——把星星颜色逐渐向背景色靠拢,视觉上就像淡出。
动画用队列管理,每个动画元素记录:格子坐标、当前帧、总帧数、起始缩放、结束缩放。每帧更新所有动画元素的状态,画完后从队列移除已完成的。
typedef struct { int x, y; int color; int frame; int totalFrames; } AnimCell; AnimCell animQueue[100]; int animCount = 0;动画帧数我设的是15帧,按60帧每秒算就是0.25秒。太快看不清,太慢拖节奏。实测下来15帧是个比较舒服的值。
注意:动画期间要屏蔽玩家输入,否则玩家在星星还没消完时又点了一组,逻辑会乱。我的做法是在状态机里,只有“等待输入”状态才响应鼠标事件,其他状态一律忽略。
3. 重力下落与列位移的算法实现
3.1 单列重力下落的实现
消除完成后,每一列上方的星星要落到下方空位。这个操作的逻辑是:从下往上扫描每一列,用一个写指针记录当前应该填入的位置,遇到非空格就写到写指针位置,然后写指针上移。
void applyGravity() { for (int x = 0; x < BOARD_W; x++) { int writeY = BOARD_H - 1; for (int y = BOARD_H - 1; y >= 0; y--) { int i = idx(x, y); if (board[i] != EMPTY) { if (writeY != y) { board[idx(x, writeY)] = board[i]; board[i] = EMPTY; } writeY--; } } } }这段代码的关键是从下往上扫描。如果从上往下扫,遇到空格后把下面的星星移上来,会覆盖掉还没处理的星星。从下往上扫,写指针始终在扫描指针下方或重合,不会覆盖未处理的数据。
为什么不用“冒泡”方式一个个交换?因为冒泡是O(n²)的,虽然10行也就100次操作,但写指针法一次遍历就搞定,代码也更简洁。这是消除类游戏里非常经典的技巧,值得记住。
3.2 列位移与棋盘收缩
有些版本的消灭星星在消除后会把空列整体左移或右移,让棋盘收缩。我这一版没有做列位移,原因是列位移会让玩家的空间记忆失效——玩家刚记住某列有个大块,结果一消除整列移走了,体验反而不好。而且列位移后棋盘宽度变化,后续的连通块搜索和绘制都要跟着改,复杂度上升。
但如果你确实想做列位移,思路是这样的:先标记哪些列是空列,然后用一个写指针从左到右扫描,把非空列整体移到写指针位置。和重力下落是同一个套路,只是方向从垂直变成水平。
void shiftColumns() { int writeX = 0; for (int x = 0; x < BOARD_W; x++) { bool isEmpty = true; for (int y = 0; y < BOARD_H; y++) { if (board[idx(x, y)] != EMPTY) { isEmpty = false; break; } } if (!isEmpty) { if (writeX != x) { for (int y = 0; y < BOARD_H; y++) { board[idx(writeX, y)] = board[idx(x, y)]; board[idx(x, y)] = EMPTY; } } writeX++; } } }我个人的建议是:第一版先不做列位移,把核心消除和下落跑通,再加这个功能。否则一旦出bug,你分不清是消除逻辑错了还是位移逻辑错了。
3.3 下落动画与逻辑的同步
下落动画比消除动画麻烦一点,因为星星要移动位置。我的做法是:逻辑层瞬间完成下落(board数据已经更新),表现层记录每个星星的“起始位置”和“目标位置”,然后插值绘制。
具体来说,在下落前先保存一份下落前的board快照,下落完成后对比快照和当前board,找出每个移动了的星星,生成动画元素:起始坐标是快照里的位置,目标坐标是当前位置。动画播放时,星星从起始坐标线性移动到目标坐标。
typedef struct { int fromX, fromY; int toX, toY; int color; int frame; int totalFrames; } FallAnim;下落动画帧数我设的是10帧,比消除动画短,因为下落是“物理运动”,太快显得突兀,太慢显得拖沓。10帧约0.17秒,实测比较自然。
这里有个细节:多个星星同时下落时,动画要并行播放,不能一个播完再播下一个。所以动画队列要支持同时更新多个元素,每帧遍历整个队列更新状态。
实操心得:下落动画的缓动函数我用的是ease-out,即速度先快后慢,模拟重力加速然后落地减速的效果。线性移动看起来像“飘”,ease-out看起来像“掉”,差别很明显。
4. 常见问题排查与性能优化实录
4.1 消除后残留星星不消失的排查
这是我最开始遇到的一个bug:消除一组星星后,大部分消失了,但总有一两个残留。排查后发现是BFS的visited数组没有正确重置。我在每次BFS前用memset清零visited,但有一次在循环里调用BFS时忘了清零,导致上一轮的visited状态影响了下一轮。
排查这类问题的思路是:先确认数据层是否正确。我在消除后打印整个board,发现残留的星星在数据层已经是EMPTY了,说明消除逻辑没问题,问题出在绘制层。再查绘制层,发现绘制时用的是缓存的一份旧board数据,没有及时同步。所以教训是:数据变更后要立即同步到绘制层,不要用缓存。
4.2 点击响应错位的坐标转换问题
EasyX的鼠标事件返回的是屏幕坐标,而棋盘绘制在窗口的某个偏移位置。如果直接用屏幕坐标去算格子索引,会错位。我的做法是定义一个棋盘绘制区域的原点boardOriginX、boardOriginY和格子大小cellSize,点击时先减去原点,再除以格子大小。
int gx = (mouseX - boardOriginX) / cellSize; int gy = (mouseY - boardOriginY) / cellSize; if (gx < 0 || gx >= BOARD_W || gy < 0 || gy >= BOARD_H) return;这里有个坑:整数除法会向下取整,如果鼠标点在格子边界左侧,算出来的索引是对的;但如果点在边界右侧一点点,可能算到下一个格子。解决办法是用浮点除法然后取整,或者在接受点击时加一个容差判断。我实测下来,格子大小40像素时,边界误差在2像素以内,玩家基本感知不到,所以直接用整数除法也能接受。
4.3 帧率不稳与双缓冲
EasyX默认的绘图是直接画到屏幕上,如果每帧绘制内容多,会出现闪烁。解决办法是用双缓冲:先用BeginBatchDraw开启批量绘制,所有绘图操作先在内存里完成,最后用FlushBatchDraw一次性刷到屏幕。
BeginBatchDraw(); // 所有绘制操作 FlushBatchDraw();另外,主循环里要控制帧率,不能让它全速跑。我用的是Sleep加时间差的方式,目标60帧每秒,每帧约16毫秒。如果这一帧的逻辑加绘制只用了5毫秒,就Sleep(11)补足。
DWORD frameStart = GetTickCount(); // 逻辑和绘制 DWORD frameTime = GetTickCount() - frameStart; if (frameTime < 16) Sleep(16 - frameTime);注意:
Sleep的精度是毫秒级,实际可能会有1到2毫秒的误差,对于消灭星星这种休闲游戏完全够用。如果要做动作游戏,得用更高精度的计时方式。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 消除后残留星星 | 绘制层数据未同步 | 打印board对比 | 数据变更后立即重绘 |
| 点击错位 | 坐标未减原点 | 打印鼠标坐标和格子索引 | 减去棋盘原点再除格子大小 |
| 画面闪烁 | 未用双缓冲 | 观察闪烁频率 | BeginBatchDraw加FlushBatchDraw |
| 帧率过高 | 主循环无延时 | 打印每帧耗时 | Sleep补足到16毫秒 |
| BFS死循环 | visited入队未标记 | 打印队列长度 | 入队时立即标记visited |
| 下落覆盖数据 | 扫描方向错误 | 打印下落前后board | 从下往上扫描 |
| 动画期间逻辑乱 | 未屏蔽输入 | 观察动画时点击 | 状态机限制输入状态 |
| 得分不更新 | 得分变量未累加 | 打印totalScore | 消除时累加count乘count |
这张表是我在实际开发中踩过的坑的总结,基本上覆盖了消灭星星开发中80%的常见问题。建议在开发时对照检查,能省不少调试时间。
4.5 性能优化的几个实用技巧
消灭星星的性能瓶颈通常不在逻辑层,而在绘制层。10乘10的棋盘,每帧要画100个星星,如果每个星星都用fillcircle画圆,100次绘图调用在低端机上可能掉帧。我的优化方案是预渲染星星贴图。
具体做法是:游戏初始化时,把每种颜色的星星预先画到一个IMAGE对象里,尺寸是格子大小。绘制时直接用putimage贴图,比每次fillcircle快得多。EasyX的putimage是位块传输,底层是内存拷贝,速度远超逐像素绘制。
IMAGE starImages[COLOR_COUNT]; // 初始化时预渲染 for (int c = 1; c <= COLOR_COUNT; c++) { SetWorkingImage(&starImages[c]); cleardevice(); setfillcolor(colors[c]); fillcircle(cellSize/2, cellSize/2, cellSize/2 - 2); SetWorkingImage(NULL); }另一个技巧是脏矩形重绘。如果这一帧只有几个格子变化,不需要全屏重绘,只重绘变化的区域。EasyX没有直接的脏矩形API,但你可以用putimage的裁剪版本,只贴变化区域。不过对于消灭星星这种小棋盘,全屏重绘也就100次贴图,实测在普通电脑上稳60帧,所以脏矩形优化属于“锦上添花”,不是必须的。
实操心得:预渲染贴图时,记得把贴图的背景设为透明色。EasyX的
putimage支持透明贴图,但需要先用SetWorkingImage和putimage的掩码版本。如果背景不透明,星星会带着一个方块背景,看起来像贴了膏药。
5. 从能跑到好玩的体验打磨
5.1 选中高亮与预览反馈
玩家点击一个星星后,如果只是选中没有反馈,体验会很差。我加了两个反馈:选中高亮和连通块预览。
选中高亮是给选中的星星加一个白色描边,让玩家知道点到了哪个。连通块预览是当玩家选中一个星星后,自动高亮所有与它同色相邻的星星,让玩家在点击第二个星星前就能看到这一组有多大。
预览的实现是在“选中判定”状态里,对选中的星星做一次BFS,把连通块内的格子用半透明高亮画出来。这样玩家可以直观地判断“这一组值不值得消”。
// 预览高亮 for (int i = 0; i < previewCount; i++) { int x = previewCells[i] % BOARD_W; int y = previewCells[i] / BOARD_W; setlinecolor(WHITE); setlinestyle(PS_SOLID, 2); rectangle(boardOriginX + x*cellSize, boardOriginY + y*cellSize, boardOriginX + (x+1)*cellSize, boardOriginY + (y+1)*cellSize); }这个功能看起来小,但对体验提升很大。玩家不用“盲选”,可以规划消除顺序,策略性上来了。
5.2 得分飘字与连击反馈
消除时如果只是分数默默变化,玩家感知不到。我加了得分飘字:消除的星星中心位置弹出一个“+25”的文字,向上飘一段距离后淡出。
飘字用和动画队列类似的结构管理,记录起始位置、当前帧、总帧数、文字内容。每帧更新位置和透明度,画完移除。
typedef struct { int x, y; int score; int frame; int totalFrames; } ScorePopup;连击反馈是如果玩家连续多次消除都大于等于4个,屏幕边缘闪一下光效,给玩家“打得好”的正反馈。这个不是必须的,但加上后游戏节奏感更强。
5.3 游戏结束判定与重开
游戏结束的条件是:棋盘上不存在任何大小大于等于2的同色连通块。判定方法是遍历所有格子,对每个非空格子做BFS,如果找到任何一个连通块大小大于等于2,游戏继续;否则游戏结束。
bool hasMove() { for (int y = 0; y < BOARD_H; y++) { for (int x = 0; x < BOARD_W; x++) { int i = idx(x, y); if (board[i] == EMPTY) continue; int cells[100]; int count = bfs(x, y, board[i], cells); if (count >= 2) return true; } } return false; }这个判定每轮消除后都要做一次,复杂度是O(n²)乘BFS的O(n),对于100个格子来说就是10000次操作,完全可以接受。
游戏结束后弹出一个结算界面,显示总分和“再来一局”按钮。重开就是把board重新随机填充,分数清零,状态机回到“等待输入”。
注意:重开时要把动画队列、飘字队列、选中状态全部清空,否则上一局的残留动画会干扰新一局。我第一版重开时忘了清动画队列,结果新一局开始后上一局的消除动画还在播,画面很混乱。
5.4 难度曲线与随机填充策略
随机填充看起来简单,但填充策略直接影响游戏难度。如果完全随机,可能出现“开局就没有可消除组合”的死局,或者“满屏都是同色”的简单局。
我的做法是:填充时保证至少存在一个大小大于等于2的连通块。具体实现是填充完后检查hasMove(),如果没有就重新填充,最多重试10次。这样保证开局一定有解。
另外,颜色数量也影响难度。颜色越多,同色相邻的概率越低,连通块越小,难度越高。我默认用5种颜色,实测下来难度适中,既有大块可消,又不会太简单。如果想调难度,改成4种颜色会简单很多,6种颜色会难不少。
#define COLOR_COUNT 5这个参数建议做成可配置的,方便调试和调整。
5.5 音效与背景音乐的接入
EasyX本身不支持音频播放,但可以用Windows的PlaySound函数播放WAV文件。消除时播一个“啵”的音效,游戏结束时播一个结算音效,背景音乐循环播放。
#include <mmsystem.h> #pragma comment(lib, "winmm.lib") PlaySound(TEXT("pop.wav"), NULL, SND_FILENAME | SND_ASYNC);SND_ASYNC表示异步播放,不会阻塞主循环。如果同步播放,每次消除都会卡一下,体验很差。
音效文件建议用短小的WAV,采样率不用太高,22050赫兹足够。背景音乐可以用循环播放的WAV,但WAV文件体积大,如果在意体积可以转成MP3,但PlaySound不支持MP3,需要用其他库。对于消灭星星这种小游戏,WAV完全够用。
实操心得:音效播放频率要控制。如果玩家快速连续消除,音效叠加会刺耳。我的做法是加一个音效冷却时间,两次音效之间至少间隔100毫秒。
6. 代码组织与后续扩展方向
6.1 文件拆分与模块职责
单文件写消灭星星也能跑,但代码超过500行后维护就很痛苦。我把它拆成四个文件:main.c负责主循环和状态机,board.c负责棋盘数据和连通块搜索,render.c负责绘制和动画,game.c负责游戏逻辑和得分。
每个文件配一个头文件,暴露必要的接口,隐藏内部实现。比如board.h只暴露board_init、board_bfs、board_gravity这几个函数,visited数组和queue队列都是board.c内部的静态变量,外部不需要知道。
这样拆的好处是:改绘制不影响逻辑,改逻辑不影响数据。调试时也可以单独测试某个模块,比如写个测试函数只调board_bfs,验证连通块搜索是否正确。
6.2 可扩展的玩法方向
消灭星星的核心玩法跑通后,可以扩展的方向不少。我列几个我试过或考虑过的:
限时模式:给每局加一个倒计时,时间到就结算。这个改动主要在状态机里加一个计时器,每帧递减,归零时切到游戏结束状态。
道具系统:比如“炸弹”可以消除任意一个星星,“刷新”可以重新排列棋盘。道具用次数限制,增加策略性。
关卡模式:每关设定目标分数,达到就进下一关,难度递增(颜色增多、目标分数提高)。
排行榜:把最高分存到本地文件,启动时读取显示。用fopen和fprintf就能实现,不需要数据库。
这些扩展的共同点是:核心逻辑不变,只加外围系统。这也是为什么我建议先把核心玩法做扎实,再考虑扩展。核心不稳,扩展越多bug越多。
6.3 从EasyX迁移到其他平台的思路
如果以后想把这个游戏搬到其他平台,核心逻辑(棋盘数据、BFS、重力下落)是纯C语言,可以直接复用。需要重写的只有绘制层和输入层。
比如搬到Web端,可以用Emscripten把C代码编译成WebAssembly,绘制层用Canvas API重写。搬到移动端,可以用SDL2替换EasyX,SDL2跨平台支持好,API和EasyX类似,迁移成本不高。
关键是把逻辑层和表现层彻底分离。我的代码里,board.c和game.c不包含任何EasyX的调用,只操作数据。render.c才依赖EasyX。这样迁移时只需要重写render.c,逻辑层一行不用改。
这个分离原则在写第一版时可能觉得麻烦,但后期收益很大。我见过有人把绘制和逻辑混在一起,想加个新皮肤都得改核心代码,那才是真的痛苦。
6.4 我个人的开发节奏建议
最后分享下我做这个项目的节奏。第一版我只做了最核心的功能:棋盘显示、点击选中、连通块消除、重力下落。没有动画、没有音效、没有得分飘字。跑通后确认逻辑没问题,再加动画和音效。最后加得分、连击、结算这些外围系统。
这个顺序的好处是:每一步都有可运行的版本。如果一上来就做完整版,一旦出bug,你面对的是几百行代码,排查成本高。分步做的话,每一步只改一小块,出问题容易定位。
另外,每加一个功能就提交一次代码(如果用Git的话),这样即使改坏了也能回滚。我吃过亏,有一次加动画时把消除逻辑改乱了,又没有备份,只能从头重写。从那以后我养成了每完成一个小功能就提交的习惯。
实操心得:调试消除类游戏时,把棋盘数据打印成文本格式非常有用。我写了个
printBoard函数,用字符表示颜色,比如R红色、G绿色、.空格。每次消除前后各打印一次,对比就能看出逻辑对不对。这个习惯帮我省了大量时间。