Python Pygame 实现消消乐游戏:从零构建完整游戏逻辑与动画
1. 项目概述从一行代码到完整游戏最近在整理硬盘翻出来一个几年前用Python写的消消乐小游戏。当时写它纯粹是为了练手想看看用最基础的Python语法不依赖复杂的游戏引擎到底能做出一个什么样的可玩程序。没想到这个简单的项目后来成了我教新手入门Python时最受欢迎的案例之一。它麻雀虽小五脏俱全事件循环、图形绘制、碰撞检测、游戏逻辑、状态管理……一个游戏该有的核心要素它基本都涉及了。今天我就把这个项目的全代码分享出来并详细拆解其中的每一个模块。无论你是刚学完Python基础语法想找个项目练手还是对游戏开发背后的逻辑感到好奇这篇文章都能给你提供一个清晰的、可复现的路径。你会发现用百来行代码你也能创造出一个属于自己的、能运行、能交互的趣味世界。2. 核心思路与架构设计2.1 为什么选择Pygame当我们决定用Python做图形化小游戏时有几个库可以选择比如Pygame、Pyglet、Arcade等。我最终选择了Pygame原因很直接生态成熟、文档丰富、对2D游戏支持极好。对于消消乐这种基于网格、需要精确像素控制和事件响应的游戏Pygame提供的Surface画布、Rect矩形区域、事件队列和精灵Sprite系统非常趁手。它就像一个轻量级的画板你告诉它在哪里画什么颜色、什么形状它就能帮你呈现出来并且能捕捉鼠标点击、键盘按键等操作。虽然它的底层是C语言写的SDL库但Python接口封装得非常友好初学者理解起来没有太大障碍。注意安装Pygame通常使用pip命令pip install pygame。如果你的Python环境有多个版本请确保pip对应的是你正在使用的Python解释器。在终端输入python --version和pip --version可以快速确认。2.2 游戏状态机模型任何游戏的核心都是一个状态机。我们的消消乐也不例外。简单来说游戏在任何时刻都处于某种特定状态并根据玩家的输入或内部逻辑切换到另一种状态。对于这个游戏我定义了以下几个核心状态等待输入状态游戏界面稳定等待玩家点击第一个方块。方块选中状态玩家点击了一个方块该方块被高亮显示等待玩家点击相邻的第二个方块以尝试交换。交换动画状态两个方块正在执行交换位置的动画过程。消除检测状态交换完成后检查棋盘上是否有可以消除的行或列。消除动画状态高亮显示即将被消除的方块并播放消除效果如缩放、变淡。下落填充状态消除后上方的方块下落填补空缺同时顶部生成新的随机方块。连锁检测状态下落填充后可能形成新的可消除组合需要再次检测实现连锁消除。这个状态循环是游戏流畅运行的关键。在代码中我通常使用一个变量如game_state来标记当前状态并在主循环里根据这个状态执行不同的逻辑。这样做的好处是逻辑清晰易于调试。比如在“交换动画状态”下我们只关心两个方块的当前位置插值计算而不需要处理鼠标点击事件。2.3 数据结构如何表示游戏棋盘消消乐的棋盘本质上是一个二维网格。在Python中最自然的表示方式就是二维列表list of lists。例如一个8行8列的棋盘可以初始化为board [[0 for _ in range(8)] for _ in range(8)]。这里的0只是一个占位符我们需要用不同的数字来代表不同类型的方块比如1代表红色2代表蓝色等。但是仅仅存储类型是不够的。为了支持动画如交换、消除每个格子还需要存储更多信息例如方块类型决定其颜色和外观。屏幕坐标用于绘制。状态标志是否被选中、是否正在消除、是否正在下落等。因此一个更健壮的设计是为每个“格子”创建一个简单的类或使用字典。在这个项目中为了保持简洁我使用了二维列表存储类型同时维护了另一个二维列表来存储每个格子对应的Pygame.Rect对象用于绘制和碰撞检测。更复杂的属性如状态则通过全局变量或额外的数据结构来管理。3. 核心模块代码拆解与实现3.1 初始化与常量定义万事开头难但良好的初始化能让后续开发事半功倍。我们首先定义游戏所需的常量和初始化Pygame。import pygame import sys import random # 初始化pygame pygame.init() # 游戏常量定义 SCREEN_WIDTH 800 SCREEN_HEIGHT 600 GRID_SIZE 8 # 8x8的棋盘 CELL_SIZE 60 # 每个格子的像素大小 MARGIN 10 # 棋盘边距 FPS 60 # 游戏帧率 # 颜色定义 (R, G, B) BACKGROUND (240, 245, 250) GRID_COLOR (200, 210, 220) COLORS [ (255, 89, 94), # 红色 (255, 202, 58), # 黄色 (138, 201, 38), # 绿色 (25, 130, 196), # 蓝色 (106, 76, 147), # 紫色 ] # 计算棋盘左上角起始位置使其居中 BOARD_LEFT (SCREEN_WIDTH - GRID_SIZE * CELL_SIZE) // 2 BOARD_TOP (SCREEN_HEIGHT - GRID_SIZE * CELL_SIZE) // 2 # 创建游戏窗口 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(Python 消消乐) clock pygame.time.Clock()代码解读与心得常量分离将所有可能调整的参数如屏幕大小、格子尺寸、颜色定义为常量放在开头这是一个非常好的习惯。后期你想把棋盘改成9x9或者调整颜色主题只需要修改这里的一行代码而不必在程序逻辑里到处找数字。颜色选择我选择的这组颜色饱和度较高对比明显在消除时视觉反馈会更强烈。你可以从配色网站找自己喜欢的色板替换COLORS列表。居中计算BOARD_LEFT和BOARD_TOP的计算确保了棋盘始终绘制在窗口中央。这种动态计算比写死坐标要健壮得多无论你如何调整SCREEN_WIDTH或CELL_SIZE棋盘都会自动居中。3.2 游戏核心数据结构的初始化接下来我们初始化代表游戏棋盘的二维列表。我们需要确保初始化的棋盘没有直接形成可消除的组合三个或以上同行同色否则游戏一开始就会自动消除体验很怪。def create_board(): 创建一个随机但无初始可消除组合的棋盘 board [] for row in range(GRID_SIZE): board.append([]) for col in range(GRID_SIZE): # 随机选择一个颜色索引 color_index random.randint(0, len(COLORS) - 1) # 检查左边两个和上面两个是否同色避免初始三连 while (col 2 and board[row][col-1] color_index and board[row][col-2] color_index) or \ (row 2 and board[row-1][col] color_index and board[row-2][col] color_index): color_index random.randint(0, len(COLORS) - 1) board[row].append(color_index) return board # 初始化棋盘 game_board create_board() # 记录被选中的方块位置初始为None selected_cell None避坑指南create_board函数中的while循环是关键。它检查新生成的方块是否会和它左边两个或上边两个方块形成三连。这是一个“向后看”的简单算法虽然不能保证100%生成绝对无解的棋盘因为可能和右边或下边的方块形成消除但对于小规模棋盘和初始生成来说效果已经足够好且计算量小。selected_cell变量用于记录玩家第一次点击的方块坐标格式可以是(row, col)元组。这是实现“点击-再点击”交换操作的核心状态变量。3.3 主游戏循环与事件处理游戏的心脏是主循环。它每秒运行FPS次负责处理输入、更新状态、重新绘制画面。def main(): global selected_cell, game_board running True # 可以在这里定义更多的游戏状态变量例如 game_state WAITING while running: # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: if event.button 1: # 左键点击 mouse_pos pygame.mouse.get_pos() # 将鼠标坐标转换为棋盘网格坐标 col (mouse_pos[0] - BOARD_LEFT) // CELL_SIZE row (mouse_pos[1] - BOARD_TOP) // CELL_SIZE # 判断点击是否在棋盘范围内 if 0 row GRID_SIZE and 0 col GRID_SIZE: handle_click(row, col) # 2. 更新游戏状态此处暂时为空后续加入动画和逻辑更新 # update_game() # 3. 绘制画面 screen.fill(BACKGROUND) draw_board(screen, game_board) if selected_cell: highlight_selected(screen, selected_cell) pygame.display.flip() # 4. 控制帧率 clock.tick(FPS) pygame.quit() sys.exit()操作意图解析事件循环pygame.event.get()获取所有待处理的事件。我们主要关心退出事件(QUIT)和鼠标点击事件(MOUSEBUTTONDOWN)。坐标转换这是2D游戏开发的通用技巧。屏幕像素坐标(x, y)需要转换为棋盘逻辑坐标(row, col)。公式是col (x - BOARD_LEFT) // CELL_SIZE。//是整除运算符确保我们得到的是整数索引。边界检查转换后必须检查row和col是否在[0, GRID_SIZE)范围内防止玩家点击棋盘外部导致列表索引越界错误。3.4 绘制模块让棋盘可视化游戏的所有视觉表现都依赖于绘制函数。我们需要绘制背景、网格线以及填充颜色的方块。def draw_board(surface, board): 绘制整个棋盘 for row in range(GRID_SIZE): for col in range(GRID_SIZE): # 计算每个格子的矩形区域 rect pygame.Rect( BOARD_LEFT col * CELL_SIZE, BOARD_TOP row * CELL_SIZE, CELL_SIZE - 2, # 留一点边距使格子分开 CELL_SIZE - 2 ) # 获取颜色并绘制方块 color_index board[row][col] color COLORS[color_index] pygame.draw.rect(surface, color, rect, border_radius8) # 绘制圆角矩形 # 可选绘制网格线 # pygame.draw.rect(surface, GRID_COLOR, rect.inflate(4,4), 2, border_radius10) def highlight_selected(surface, cell_pos): 高亮显示被选中的方块 row, col cell_pos rect pygame.Rect( BOARD_LEFT col * CELL_SIZE, BOARD_TOP row * CELL_SIZE, CELL_SIZE - 2, CELL_SIZE - 2 ) # 绘制一个半透明的白色边框作为高亮效果 s pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA) s.fill((255, 255, 255, 80)) # 白色80的透明度 surface.blit(s, (rect.x, rect.y), special_flagspygame.BLEND_ALPHA_SDL2) # 或者绘制一个亮色边框 pygame.draw.rect(surface, (255, 255, 255), rect, 4, border_radius8)绘制技巧视觉优化CELL_SIZE - 2和border_radius8的运用让方块之间有了间隙并呈现圆角看起来比生硬的方块更美观。高亮效果highlight_selected提供了两种高亮思路。第一种是创建一个带透明度的Surface进行叠加实现“蒙版”效果。第二种是直接绘制一个更粗的白色边框。第二种方法性能更好且更简单我通常在项目中使用第二种。透明度效果虽然好看但需要处理SRCALPHA表面对性能有细微影响。3.5 核心游戏逻辑点击处理与交换现在来到游戏交互的核心——handle_click函数。它决定了点击一个方块后会发生什么。def handle_click(row, col): global selected_cell, game_board if selected_cell is None: # 第一次点击记录选中的方块 selected_cell (row, col) print(f选中了: ({row}, {col})) else: # 第二次点击 prev_row, prev_col selected_cell # 判断两次点击的方块是否相邻曼哈顿距离为1 if (abs(row - prev_row) 1 and col prev_col) or \ (abs(col - prev_col) 1 and row prev_row): print(f尝试交换: ({prev_row},{prev_col}) - ({row},{col})) # 尝试交换 if try_swap_and_check(prev_row, prev_col, row, col): # 交换成功且形成消除 print(交换成功并消除) # 这里应该触发消除和下落动画为了简化我们先直接更新棋盘 # 实际项目中此处应改变游戏状态进入“交换动画”-“消除检测”状态机 else: # 交换后无消除交换回来或不允许交换 print(交换无效无消除组合。) # 在实际动画中这里应该播放一个“交换回退”的动画 else: print(点击了不相邻的方块重新选择。) # 无论成功与否第二次点击后都清空选中状态或根据状态机逻辑调整 selected_cell None def try_swap_and_check(r1, c1, r2, c2): 尝试交换两个方块并检查交换后是否有消除。如果有则确认交换否则撤销交换。 global game_board # 1. 执行交换 game_board[r1][c1], game_board[r2][c2] game_board[r2][c2], game_board[r1][c1] # 2. 检查交换后两个位置是否参与形成消除组合 if has_match_at(r1, c1) or has_match_at(r2, c2): # 有消除交换有效 # 注意这里只做了即时检查实际游戏需要找出所有可消除项 return True else: # 无消除交换无效换回来 game_board[r1][c1], game_board[r2][c2] game_board[r2][c2], game_board[r1][c1] return False def has_match_at(row, col): 快速检查指定位置是否处于一个三连或以上的行或列中 color game_board[row][col] # 检查水平方向 left, right col, col while left - 1 0 and game_board[row][left - 1] color: left - 1 while right 1 GRID_SIZE and game_board[row][right 1] color: right 1 horizontal_count right - left 1 # 检查垂直方向 up, down row, row while up - 1 0 and game_board[up - 1][col] color: up - 1 while down 1 GRID_SIZE and game_board[down 1][col] color: down 1 vertical_count down - up 1 return horizontal_count 3 or vertical_count 3逻辑深度解析相邻判断使用曼哈顿距离abs(dx) abs(dy)判断是否相邻但这里简化成只允许上下左右四方向的直接相邻。这是消消乐的标准规则。交换验证try_swap_and_check函数实现了游戏的核心规则只有能导致消除的交换才被允许。它的策略是“先交换再检查”。如果检查通过则保留交换否则立刻撤销。这是一个非常直观且易于实现的逻辑。匹配检测算法has_match_at函数是检测消除的关键。它从目标位置向左右水平和上下垂直两个方向延伸统计连续相同颜色的方块数量。这种方法比遍历整个棋盘检查每一行每一列要高效得多因为我们只关心由这次交换可能引发消除的两个位置。3的判断条件意味着我们支持超过三个的连续消除四连、五连这能增加游戏的爽快感。4. 功能增强与动画实现基础版本已经可以运行和交互了但缺乏动画和连贯的消除流程显得很生硬。接下来我们为其注入灵魂——状态机和动画。4.1 引入游戏状态与动画时钟我们首先完善游戏的状态管理并引入一个简单的动画系统。# 在常量定义部分添加游戏状态和动画相关常量 ANIMATION_SPEED 10 # 像素/帧交换动画速度 STATE_WAITING waiting STATE_SWAPPING swapping STATE_MATCH_CHECKING matching STATE_FALLING falling # 在主函数或全局变量中初始化状态 game_state STATE_WAITING # 用于存储动画数据 animating_cells [] # 格式: [(r1,c1, target_x, target_y, current_x, current_y), ...] matches_to_remove [] # 存储待消除的方块位置 falling_columns {} # 记录每列需要下落的方块信息4.2 重构主循环基于状态更新主循环不再只是绘制静态棋盘而是根据game_state执行不同的逻辑。# 在主循环的“更新游戏状态”部分替换为 if game_state STATE_SWAPPING: update_swap_animation() elif game_state STATE_MATCH_CHECKING: # 检查消除如果找到则填充matches_to_remove并进入消除状态 found_matches find_all_matches() if found_matches: matches_to_remove found_matches game_state removing # 假设我们新增一个消除状态 else: # 无消除动画回退或者直接回到等待状态取决于规则 game_state STATE_WAITING elif game_state removing: update_removal_animation() elif game_state STATE_FALLING: update_falling_animation() # ... 其他状态4.3 实现交换动画交换动画的本质是让两个方块的屏幕坐标平滑地移动到对方的位置。def start_swap_animation(cell_a, cell_b): 启动两个方块交换的动画 global game_state, animating_cells r1, c1 cell_a r2, c2 cell_b # 计算它们的屏幕起始坐标和目标坐标 start_x1, start_y1 cell_to_pixel(r1, c1) target_x1, target_y1 cell_to_pixel(r2, c2) start_x2, start_y2 cell_to_pixel(r2, c2) target_x2, target_y2 cell_to_pixel(r1, c1) animating_cells [ (r1, c1, target_x1, target_y1, start_x1, start_y1), (r2, c2, target_x2, target_y2, start_x2, start_y2) ] game_state STATE_SWAPPING def update_swap_animation(): 更新所有正在交换的动画方块位置 global animating_cells, game_state, game_board all_finished True new_animating_list [] for anim in animating_cells: r, c, tx, ty, cx, cy anim # 计算朝向目标移动的方向向量 dx tx - cx dy ty - cy distance (dx**2 dy**2)**0.5 if distance ANIMATION_SPEED: # 已到达或非常接近目标 # 动画结束更新逻辑棋盘位置注意动画过程中逻辑棋盘并未真正交换 # 动画结束后才真正交换数据 new_animating_list.append((r, c, tx, ty, tx, ty)) # 保持最终位置 else: # 向目标移动一步 move_x dx / distance * ANIMATION_SPEED move_y dy / distance * ANIMATION_SPEED new_cx cx move_x new_cy cy move_y new_animating_list.append((r, c, tx, ty, new_cx, new_cy)) all_finished False animating_cells new_animating_list if all_finished: # 动画结束真正交换棋盘数据 # 这里假设animating_cells里只有两个元素且就是最初要交换的那一对 if len(animating_cells) 2: (r1, c1, _, _, _, _), (r2, c2, _, _, _, _) animating_cells game_board[r1][c1], game_board[r2][c2] game_board[r2][c2], game_board[r1][c1] animating_cells [] game_state STATE_MATCH_CHECKING # 进入消除检测状态 def cell_to_pixel(row, col): 将棋盘逻辑坐标转换为屏幕像素坐标方块中心点 x BOARD_LEFT col * CELL_SIZE CELL_SIZE // 2 y BOARD_TOP row * CELL_SIZE CELL_SIZE // 2 return x, y动画实现心得逻辑与渲染分离在动画播放期间逻辑棋盘game_board并没有立即交换。我们只是在屏幕上移动两个方块的绘制位置。直到动画播放完毕我们才真正交换game_board中的数据。这是游戏开发中常见的“表现层”与“数据层”分离的思想避免状态混乱。帧间移动update_swap_animation根据ANIMATION_SPEED和方向向量计算每一帧移动的距离。使用向量归一化 (dx / distance) 可以确保移动方向准确速度恒定。绘制适配在draw_board函数中需要修改。对于正在动画中的方块应该从animating_cells列表中获取其当前的屏幕坐标(cx, cy)进行绘制而不是根据其逻辑坐标(row, col)计算。这需要增加一个判断和查找逻辑。4.4 实现消除检测与方块下落交换动画完成后我们需要检查整个棋盘找出所有可消除的组合。def find_all_matches(): 扫描整个棋盘找出所有可消除的连续方块行或列3 matches_set set() # 使用集合避免重复添加一个方块可能同时处于横竖消除中 # 检查每一行 for row in range(GRID_SIZE): color game_board[row][0] count 1 start_col 0 for col in range(1, GRID_SIZE): if game_board[row][col] color: count 1 else: if count 3: for c in range(start_col, start_col count): matches_set.add((row, c)) color game_board[row][col] count 1 start_col col # 行末尾检查 if count 3: for c in range(start_col, start_col count): matches_set.add((row, c)) # 检查每一列 for col in range(GRID_SIZE): color game_board[0][col] count 1 start_row 0 for row in range(1, GRID_SIZE): if game_board[row][col] color: count 1 else: if count 3: for r in range(start_row, start_row count): matches_set.add((r, col)) color game_board[row][col] count 1 start_row row # 列末尾检查 if count 3: for r in range(start_row, start_row count): matches_set.add((r, col)) return list(matches_set) # 返回列表算法优化提示上面的find_all_matches函数是标准的双指针滑动窗口算法。它遍历每一行和每一列统计连续相同颜色的长度。当颜色改变或到达行尾时判断之前的连续长度是否3如果是则将这一串坐标加入集合。使用set是为了处理“十字形”消除的情况。例如一个方块同时处于一个横向三连和一个纵向三连的交点它会被两次加入列表用集合可以自动去重。找到所有匹配项后我们可以播放一个简单的消除动画比如让这些方块透明度逐渐降低或缩放然后将其从game_board中标记为“空”例如用-1表示。消除之后就是方块下落和新方块生成。def apply_gravity_and_fill(): 应用重力让悬空的方块下落并在顶部生成新方块 global game_board new_board [[-1 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)] # 1. 处理每一列的下落 for col in range(GRID_SIZE): write_row GRID_SIZE - 1 # 从最底部开始填充 for read_row in range(GRID_SIZE - 1, -1, -1): # 从下往上读 if game_board[read_row][col] ! -1: # 如果不是空位 new_board[write_row][col] game_board[read_row][col] write_row - 1 # 2. 剩余的空位顶部用随机新方块填充 while write_row 0: new_board[write_row][col] random.randint(0, len(COLORS) - 1) write_row - 1 game_board new_board # 下落完成后需要再次进入消除检测状态因为可能形成新的连锁消除 return True下落逻辑解析这个函数创建了一个新的临时棋盘new_board。对于每一列它从最底部(GRID_SIZE-1)开始从下往上遍历旧的棋盘(game_board)。遇到非空的方块就把它放到新棋盘的当前写入位置然后写入位置上移一格。这个过程就像把一列方块中的“空位”都挤到了顶部。遍历完旧棋盘后write_row变量指示了新棋盘顶部还有多少空位。这些空位就用随机的新方块填充。这个算法一次就完成了“下落”和“填充”两个步骤非常高效。5. 项目扩展与优化方向一个基础可玩的消消乐已经完成了。但如果你想让它更像一个完整的游戏或者作为学习项目进一步深入这里有几个明确的扩展方向。5.1 添加游戏分数与连击系统分数是游戏最直接的反馈。我们可以定义不同的得分规则基础消除消除3个方块得100分4个得200分5个得300分。连击Combo在一次操作交换后引发的连续多次消除如下落产生新消除中第二次及以后的消除得分翻倍例如1.5倍。同时消除如果一次消除了多组方块每组单独计算分数并累加。实现上需要在find_all_matches时计算本次消除了多少组、每组多少个然后根据当前连击数计算分数并更新到全局变量score。在draw_board函数中将score渲染到屏幕左上角。5.2 实现特殊方块与效果基础消消乐玩久了会腻引入特殊方块能极大增加策略性和趣味性。爆炸方块Bomb消除四个L型或T型方块时生成。当它被匹配或通过交换被引爆时能消除周围3x3范围内的所有方块。彩虹方块Color Clear消除五个及以上同行同列方块时生成。它可以与任何颜色的单一方块交换消除棋盘上所有与该单一方块颜色相同的方块。整行/整列消除方块消除四个同行或同列方块时生成。引爆时消除整行或整列。实现特殊方块需要在game_board中用特殊的数字或一个对象来标识。在消除检测、下落和绘制逻辑中都需要为这些特殊方块增加额外的处理分支。5.3 关卡设计与障碍物可以设计不同的关卡引入不可移动的障碍物如冰块、锁链、木箱。冰块覆盖在普通方块上需要其旁边的方块被消除一次或两次来打碎冰块才能露出下面的可移动方块。锁链将两个相邻方块锁在一起它们必须同时被交换即你需要选中其中一个进行交换另一个会跟随移动。木箱占据一个格子不能被交换只能通过其旁边的方块被消除产生的“爆炸”效果来摧毁。障碍物的引入会改变棋盘的数据结构每个格子可能需要存储方块类型和障碍物类型两个属性并大幅增加游戏逻辑的复杂度是很好的进阶练习。5.4 性能优化与代码重构当棋盘变大、特效变多时性能可能成为问题。脏矩形渲染Pygame默认每帧重绘整个屏幕screen.fill()。对于静态元素多的界面可以只重绘发生变化的部分区域但这在Pygame中实现起来较麻烦。一个更简单的方法是确保你的绘制代码足够高效。使用精灵组Sprite Group目前我们是手动循环绘制每个方块。Pygame的Sprite和Group系统可以更好地管理游戏对象并利用Group.draw()进行批量绘制在某些情况下更高效代码组织也更清晰。将游戏逻辑类化将棋盘数据、游戏状态、动画管理器等都封装到类里如GameBoard、GameState、AnimationManager而不是全部使用全局变量。这会使代码更模块化易于维护和扩展。例如你可以创建一个Block类它包含颜色、坐标、是否正在动画等属性以及draw()和update()方法。这个项目从零开始一步步构建了一个可玩、可扩展的消消乐游戏核心。它涉及了游戏循环、事件处理、图形绘制、碰撞检测坐标转换、状态机、二维数组算法、简单动画等多个编程核心概念。无论你是想巩固Python基础还是窥探游戏开发的门径亲手实现一遍这个项目再把上述的扩展功能逐一尝试收获一定会远超你的预期。编程的乐趣就在于看着自己写的代码从冰冷的逻辑变成屏幕上鲜活跳动的世界。

相关新闻