ARTICLE DETAIL

资讯详情

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

用Arduino复刻Simon记忆游戏:状态机、按键消抖与嵌入式实战

用Arduino复刻Simon记忆游戏:状态机、按键消抖与嵌入式实战 简介面向前端初学者与Web游戏爱好者的一套Simon西蒙记忆游戏完整源码资源源自1978年经典电子游戏规则用HTML、CSS、JavaScript三件套还原随机颜色音序、用户重复输入与胜负判定等核心玩法。资源共3个文件压缩包仅4KBHTML定义游戏面板与序列展示区域CSS负责四色圆形按钮、布局与交互样式JavaScript实现随机序列生成、点击监听、答案比对、游戏状态管理和音效反馈。代码全程带有详细中文注释便于逐行理解事件驱动编程与基础DOM操作。已有612人学习下载适合作为课程设计、期末作业或日常练手项目也可在此基础上扩展计分系统、难度等级和动画提示快速提升前端综合开发能力。通过亲手调试这套精简实现能直观掌握记忆类小游戏的完整逻辑闭环为后续更复杂的交互应用打好基础。 小时候在掌机上玩过的记忆游戏现在用一块开发板、四个按键和几个LED就能完整复现一遍。Simon游戏的核心机制并不复杂——随机生成一组颜色序列玩家按相同顺序复现每轮追加一个颜色直到按错为止。但真正动手做的时候你会发现难点全在细节里按键消抖怎么做才不误触序列如何随机生成又不重复玩家按错时如何判断而不产生歧义以及蜂鸣器发声和LED点亮的时序如何对齐。这篇文章我会从零开始把Simon游戏的完整制作过程拆开揉碎每一步都附上详细注释的代码对照着抄就能跑起来。这个项目特别适合刚入门单片机的朋友练手因为它覆盖了嵌入式开发最常用的几个知识点GPIO输入输出、按键检测与消抖、状态机设计、随机数生成以及简单的音效输出。整个项目不依赖任何外部库纯手写逻辑对理解底层原理非常有帮助。我自己在调试过程中踩了不少坑比如按键扫描放在循环里导致响应迟钝、随机序列出现连续重复、加上声音模块后时序完全错乱等这些问题我都会在文末集中整理出来希望能帮你绕开这些弯路。1. 项目分析与整体设计思路1.1 Simon游戏的玩法本质一段序列的记忆与复现Simon游戏的规则极简四个彩色区域红、蓝、绿、黄游戏开始时系统随机点亮一个颜色并播放对应音调玩家需要按下相同颜色的按键然后系统在原有序列末尾追加一个新的随机颜色玩家需要从第一个颜色开始完整复现整段序列。每成功一轮序列长度加一直到玩家按错或者超时游戏结束。拆解这个规则核心逻辑其实是一个“序列生成器”加一个“序列匹配器”。系统端的序列生成器负责随机产生颜色索引并追加到数组末尾玩家端的序列匹配器负责记录当前按到了序列中的第几个位置每次按键都和对应位置的预期颜色比对。这两个逻辑一分离整个程序的架构就非常清晰了。还有一个很容易被忽略的点是“玩法的成长曲线”。如果序列完全随机玩家到后期几乎不可能记住长达十几位的顺序游戏会迅速失去乐趣。所以很多版本的Simon游戏在序列生成时会做限制——相邻位不重复连续重复不超过两次这样既能保证可玩性又不会因为算法过于规整让玩家轻易猜出规律。我在代码里用的就是“避免连续三次相同”的生成策略。1.2 技术方案选型为什么用状态机而不是顺序流程不夸张地说Simon游戏是理解状态机的绝佳教材。如果不用状态机来组织代码你会发现在处理“系统演示序列”和“玩家输入验证”这两个阶段时程序逻辑会绕成一团乱麻。我采用的方案是三个核心状态SHOW_SEQUENCE系统演示、PLAYER_INPUT玩家输入、GAME_OVER游戏结束。状态之间通过标志位和事件触发切换每个状态内部只处理自己该做的事情。这样做的好处是代码逻辑线性化每个状态下只关心一类事件不容易出现逻辑交叉后续想扩展功能比如增加计分板、最高分记录、难度选择时只需要在对应状态里加分支不需要动整体框架调试时可以直接通过串口打印当前状态和关键变量问题定位非常快从这个角度看Simon游戏虽然小但它的状态机设计思想可以直接迁移到遥控器解码、菜单系统、通讯协议解析等更复杂的嵌入式项目里含金量很高。2. 硬件准备与电路搭建2.1 元器件清单与选型说明做Simon游戏核心硬件其实非常少。我这里用的是Arduino Nano开发板因为体积小巧、价格便宜且引脚资源足够。当然你用UNO也完全可以代码不需要改动。元器件数量说明Arduino开发板1块Nano或UNO均可轻触按键4个用于红、蓝、绿、黄四个输入LED红、蓝、绿、黄各1个用作颜色指示限流电阻220Ω或330Ω4个保护LED下拉电阻10kΩ4个确保按键未按下时引脚电平稳定蜂鸣器有源或无源1个发出对应音调面包板、杜邦线若干搭建电路使用选型方面有几点实践经验。LED颜色必须区分度高尤其是红色和橙色、蓝色和绿色建议直接买高亮度的标准色LED避免色差导致玩家看错。按键建议选手感清晰的轻触开关按下去有明确“咔哒”声为佳这样玩家能确认自己的操作被识别了。蜂鸣器这里要特别说明有源蜂鸣器和无源蜂鸣器的驱动方式完全不同。有源蜂鸣器内部带振荡电路通电就响但只能发出一个固定频率的声音无源蜂鸣器需要外部输入特定频率的方波驱动才能发声但好处是可以通过改变方波频率来产生不同音调。Simon游戏需要四种不同音调对应四个颜色所以必须用无源蜂鸣器。如果你手上只有有源蜂鸣器一个简单的替代方案是外接一个无源蜂鸣器元件或者在代码里用tone()函数配合音频模块实现但这会增加成本和接线复杂度。2.2 电路连接方式我的连接方案如下表所示颜色按键、LED和蜂鸣器的引脚定义和电路逻辑一一对应功能Arduino引脚接法说明红色LEDD2经220Ω电阻接GND蓝色LEDD3经220Ω电阻接GND绿色LEDD4经220Ω电阻接GND黄色LEDD5经220Ω电阻接GND红色按键D6一端接5V一端接引脚 10kΩ下拉电阻到GND蓝色按键D7同上绿色按键D8同上黄色按键D9同上无源蜂鸣器D10正极接引脚负极接GND关于按键的连接方式我推荐使用“按下为高”的接法按键一端接VCC5V另一端连接到Arduino引脚同时在引脚和GND之间接一个10kΩ下拉电阻。当按键按下时引脚读到高电平释放时下拉电阻将引脚拉低保证电平稳定。如果你手头刚好没有10kΩ下拉电阻也可以把代码改成启用Arduino内部上拉电阻的方案按键一端接GND一端接引脚然后在pinMode()中设置为INPUT_PULLUP检测时判断引脚为低电平表示按下。这个方案省了两个元件但逻辑是反相的——读到的数字是LOW时才是按下状态容易混淆新手建议还是用下拉电阻方案更直观。线路搭好后可以先写一个最简单的测试程序依次点亮四个LED并播放对应音调确认每个LED、蜂鸣器都正常工作再进入正式的Simon游戏逻辑开发。这一步能帮你提前发现虚接、短路等低级问题省得后面调试时怀疑人生。3. 软件逻辑与核心算法解析3.1 程序架构数据结构与模块划分整个Simon游戏的程序可以拆成三个模块序列管理模块、输入检测模块和音效输出模块。序列管理模块维护一个全局数组gameSequence[]记录当前需要玩家复现的完整序列输入检测模块负责扫描四个按键识别是哪一个被按下并返回对应的颜色索引音效输出模块根据颜色索引点亮LED并播放对应的音调。在代码组织上我建议把所有函数声明放在主程序上方注释要写清楚每个函数的输入、输出以及职责范围。这种模块化设计的好处是哪里出问题就查哪里不需要通读整个几百行的程序。核心数据结构包括#define SEQUENCE_LENGTH_MAX 100 // 最长序列长度防止越界 int gameSequence[SEQUENCE_LENGTH_MAX]; // 存储系统生成的序列 int sequenceLength; // 当前序列长度初始为1每成功一轮1 int playerIndex; // 玩家当前按到了序列中的哪一个位置这里有个关键的设计取舍序列长度上限设为100是因为人脑的记忆极限一般在20位左右100足够用了。而且Arduino的RAM有限Nano只有2KB数组开太大反而浪费。如果你计划做更多扩展功能也可以适度调整这个值但没必要设成几百上千。3.2 Simon周期查找与序列生成策略“simon周期查找”这个概念本质是研究序列生成的规律性问题。游戏中的序列是由伪随机数决定的random()函数生成的序列如果处理不好会出现两种情况一是相邻位置频繁重复玩家连续看到三个蓝色、四个黄色非常影响体验二是生成的序列缺少变化玩家总觉得“这局好像和上局很像”降低了新鲜感。我的解决方案是生成新颜色时加上约束条件新颜色不能和序列末尾两个元素完全相同也就是禁止连续三次出现同一个颜色。这个策略代码上非常好实现int generateRandomColor() { int newColor random(0, 4); // 生成0~3之间的随机数 // 如果新颜色和最后两个已存在的颜色都相同重新生成 while (sequenceLength 2 newColor gameSequence[sequenceLength - 1] newColor gameSequence[sequenceLength - 2]) { newColor random(0, 4); } return newColor; }这个“周期查找”的思路还可以延伸到难度调节上如果你想做不同难度等级只需要改这个生成函数的约束策略。比如简单模式限制“不能连续两次相同”困难模式则取消限制完全随机把游戏节奏的控制权交给玩家自己。另外提醒一点Arduino的random()函数在未调用randomSeed()之前每次上电生成的随机序列是完全一样的。所以初始化时一定要用randomSeed(analogRead(A0))这种方式通过读取悬空模拟引脚的噪声电压作为随机种子保证每次开机序列不同。3.3 按键检测与消抖的最佳实践按键检测是嵌入式开发里最常见也最容易出问题的地方。机械按键在按下的瞬间触点会因机械振动产生多次通断如果不做消抖处理一次按压会被程序误识别成多次触发。Simon游戏的按键是单次按下误触发会导致玩家明明只按了一下程序却判断他按了两下直接判负。消抖方案我推荐两种结合使用。第一种是延时消抖——检测到引脚电平变化后等待10~20ms再次读引脚如果电平保持稳定就确认按键有效。第二种是状态记录——只检测引脚电平变化沿从LOW到HIGH而不是持续检测高电平这样即使按键按住不放也只会触发一次。下面这段代码是我实际项目里用的按键扫描函数注释部分详细解释了每行代码的逻辑/** * 扫描并返回被按下的颜色索引如果没有按键按下则返回 -1。 * 消抖策略延时读取两次确认电平稳定。 */ int readButtonPress() { const int buttonPins[4] {6, 7, 8, 9}; const int debounceDelay 20; // 消抖延时单位毫秒 for (int i 0; i 4; i) { if (digitalRead(buttonPins[i]) HIGH) { delay(debounceDelay); // 等待信号稳定 if (digitalRead(buttonPins[i]) HIGH) { // 等按键释放避免按住时反复触发 while (digitalRead(buttonPins[i]) HIGH) { delay(10); } return i; // 返回颜色索引0红1蓝2绿3黄 } } } return -1; }这段代码里有几个细节值得展开说。第一层if检测到高电平后延时20ms再读一次是为了过滤机械抖动第二层while循环等待按键释放是为了防止玩家的手速过快或者按键卡住引起连续触发。不过这个阻塞式的等待按键释放有一个副作用在等待期间程序无法做其他事情。在Simon游戏里这个副作用其实影响不大因为玩家输入阶段本身就在等待按键但如果未来你想要做按键连发或者其他需要实时响应的功能这就会成为问题。3.4 状态机的完整状态流转现在把上述逻辑整合成状态机。整体流程如下初始化清空序列sequenceLength 0状态设为SHOW_SEQUENCESHOW_SEQUENCE状态在序列末尾追加一个随机颜色生成策略见3.2然后从第0位到sequenceLength - 1遍历逐一点亮对应LED并播放音效每个颜色持续400ms。全部演示完毕后playerIndex重置为0切换到PLAYER_INPUT状态PLAYER_INPUT状态循环调用readButtonPress()等待玩家按键。每读到一次按键将按键的颜色索引和gameSequence[playerIndex]比对相同点亮对应LED并播放音效playerIndex。如果playerIndex sequenceLength说明整段序列已复现正确回到SHOW_SEQUENCE状态进入下一轮不同进入GAME_OVER状态GAME_OVER状态播放错误提示音LED快速闪烁显示本次记住的序列长度。玩家按下任意按键后回到初始化状态重新开始这个状态机的切换逻辑非常对称代码量也不大但涵盖了几乎所有经典状态机的设计要素初始化、事件触发、状态跳转、循环结束条件。把这套思维吃透了后面写什么控制逻辑都顺手。4. 完整代码实现带详细注释4.1 全局定义与初始化下面给出完整代码。这段程序可以直接复制到Arduino IDE里编译烧录所有关键位置我都加了详细注释。/** * Simon 记忆游戏 * * 玩法系统随机生成一个颜色序列玩家按相同顺序复现。 * 每成功一轮序列长度加一直到按错结束。 * * 电路连接 * - LED: 红D2, 蓝D3, 绿D4, 黄D5均经220Ω电阻接地 * - 按键: 红D6, 蓝D7, 绿D8, 黄D9接5V带10kΩ下拉电阻 * - 蜂鸣器: D10无源蜂鸣器正极接引脚负极接地 */ // ------------------ 引脚定义 ------------------ const int redLEDPin 2; const int blueLEDPin 3; const int greenLEDPin 4; const int yellowLEDPin 5; const int redButtonPin 6; const int blueButtonPin 7; const int greenButtonPin 8; const int yellowButtonPin 9; const int buzzerPin 10; // ------------------ 颜色与音高映射 ------------------ // 颜色索引用 0~3 表示0红1蓝2绿3黄 const int ledPins[4] {redLEDPin, blueLEDPin, greenLEDPin, yellowLEDPin}; const int buttonPins[4] {redButtonPin, blueButtonPin, greenButtonPin, yellowButtonPin}; // 四个颜色对应的音高频率Hz采用 C大调自然音阶 // 红色: C5523Hz, 蓝色: D5587Hz, 绿色: E5659Hz, 黄色: G5784Hz const int noteFrequencies[4] {523, 587, 659, 784}; // ------------------ 游戏参数 ------------------ const int noteDuration 300; // 每个颜色点亮/发声的持续时间毫秒 const int sequenceLengthMax 50; // 序列最大长度防止数组越界 // ------------------ 全局变量 ------------------ int gameSequence[sequenceLengthMax]; // 系统生成的序列 int sequenceLength 0; // 当前序列长度 int playerIndex 0; // 玩家已经正确复现的位置 // 状态定义 enum GameState { STATE_SHOW_SEQUENCE, // 系统演示序列 STATE_PLAYER_INPUT, // 等待玩家输入 STATE_GAME_OVER // 游戏结束 }; GameState currentState STATE_SHOW_SEQUENCE;注意noteDuration这个参数。300ms是经过实际测试比较舒适的演示速度太快了玩家来不及看太慢了整局游戏的节奏会拖沓。你做的时候可以根据目标人群调整这个值——给小孩玩放慢到400ms给成年人玩可以压到250ms。4.2 工具函数点灯与发声/** * 点亮指定颜色的LED并播放对应音调 * param colorIndex 颜色索引0~3 */ void playColor(int colorIndex) { digitalWrite(ledPins[colorIndex], HIGH); // 点亮LED tone(buzzerPin, noteFrequencies[colorIndex]); // 播放对应频率的声音 delay(noteDuration); // 保持点亮和发声的时长 digitalWrite(ledPins[colorIndex], LOW); // 关闭LED noTone(buzzerPin); // 停止发声 }这一小段是整个游戏里最核心的“输出函数”。值得注意的是这里用的是阻塞式延时来保持LED和蜂鸣器的同步。这种写法在Simon游戏这种简单场景下完全没问题因为程序在演示序列时本来就不需要响应任何外部输入。但如果你想要LED熄灭后有短暂的视觉残留过渡可以在LOW之后再加一个短暂延时来实现“呼吸感”有兴趣可以自己试。不过在游戏结束后或者说在玩家输入阶段点亮LED时这个阻塞式延时就不能直接用了——因为每按一个键都要点亮300ms的话玩家快速连按会有卡顿感。我在玩家输入阶段单独写了一个flashColor函数点亮时间缩短到100ms而且不播放音调或者播放极短的音调给玩家的操作反馈更即时一些。/** * 玩家按键反馈用短暂点亮LED不阻塞太长时间 */ void flashColor(int colorIndex) { digitalWrite(ledPins[colorIndex], HIGH); tone(buzzerPin, noteFrequencies[colorIndex], 50); // 只响50ms delay(50); digitalWrite(ledPins[colorIndex], LOW); }4.3 核心逻辑序列生成、演示与玩家输入/** * 生成新颜色并追加到序列末尾。 * 采用周期查找约束不允许连续三次出现相同颜色保证序列变化更自然。 */ void appendNewColorToSequence() { int newColor random(0, 4); // 如果序列长度2且新颜色与序列最后两个颜色都相同则重新生成 while (sequenceLength 2 newColor gameSequence[sequenceLength - 1] newColor gameSequence[sequenceLength - 2]) { newColor random(0, 4); } gameSequence[sequenceLength] newColor; sequenceLength; } /** * 系统演示当前全部序列。 * 从第0个开始逐个点亮LED并播放音效让玩家记住顺序。 */ void showSequence() { for (int i 0; i sequenceLength; i) { playColor(gameSequence[i]); delay(50); // 每个颜色之间留50ms间隔方便区分 } delay(300); // 演示完毕后停300ms提示玩家轮到你输入了 } /** * 玩家输入处理。 * 阻塞等待玩家按键并判断按键顺序是否正确。 * return 返回0表示输入正确且序列已全部复现返回1表示输入错误返回-1表示玩家尚未按键 */到玩家输入处理这块我把状态机的主循环逻辑写清楚。这里用嵌套状态来区分“等待按键”和“校验按键”两个子阶段确保代码逻辑不会交叉混乱。4.4 主循环与状态机调度void setup() { randomSeed(analogRead(A0)); // 用悬空引脚的模拟噪声作为随机种子确保每次开机序列不同 // 初始化所有用到的引脚 for (int i 0; i 4; i) { pinMode(ledPins[i], OUTPUT); pinMode(buttonPins[i], INPUT); } pinMode(buzzerPin, OUTPUT); Serial.begin(9600); // 打开串口方便调试时查看状态信息 currentState STATE_SHOW_SEQUENCE; // 初始状态演示序列 } void loop() { switch (currentState) { case STATE_SHOW_SEQUENCE: // 每次进入此状态先在序列末尾追加一个新颜色 appendNewColorToSequence(); // 然后完整演示一遍序列 showSequence(); // 玩家输入索引归零 playerIndex 0; // 切换到玩家输入状态 currentState STATE_PLAYER_INPUT; break; case STATE_PLAYER_INPUT: int buttonPressed readButtonPress(); // 扫描按键 if (buttonPressed -1) { // 没有按键按下继续等待 break; } // 有按键按下与序列中对应位置的颜色比对 if (buttonPressed gameSequence[playerIndex]) { // 正确给出反馈音推进玩家索引 flashColor(buttonPressed); playerIndex; // 如果玩家已经复现完整个序列进入下一轮 if (playerIndex sequenceLength) { currentState STATE_SHOW_SEQUENCE; // 进入新一轮序列加长 } } else { // 错误进入游戏结束状态 currentState STATE_GAME_OVER; } break; case STATE_GAME_OVER: // 游戏结束闪烁LED并播放低沉错误音 for (int i 0; i 3; i) { for (int j 0; j 4; j) { digitalWrite(ledPins[j], HIGH); } tone(buzzerPin, 200, 200); delay(200); for (int j 0; j 4; j) { digitalWrite(ledPins[j], LOW); } delay(200); } Serial.print(Game Over! 序列长度: ); Serial.println(sequenceLength); // 等待玩家按任意键重新开始 while (readButtonPress() -1) { delay(50); } // 重置游戏 sequenceLength 0; memset(gameSequence, 0, sizeof(gameSequence)); currentState STATE_SHOW_SEQUENCE; break; } }整个主循环就是一个不断轮询状态机的过程。你可能会问为什么不在初始化时直接生成第一个颜色而是每进入一次SHOW_SEQUENCE状态才追加颜色这是因为第一轮时序列长度为0如果先在setup()里生成了第一个颜色玩家会少看一遍完整序列的正确过程体验上会有细微差异。统一放在状态机里处理逻辑更规整。4.5 完整代码的串口调试辅助调试阶段强烈建议在关键状态切换点加上Serial.println()输出。例如Serial.print(Sequence length: ); Serial.println(sequenceLength); Serial.print(Expected: ); Serial.println(gameSequence[playerIndex]); Serial.print(Got: ); Serial.println(buttonPressed);有了这些调试输出你能清楚地看到程序内部状态和预期是否一致。特别是排查“明明按了正确的键却判定错误”这类问题时串口日志一眼就能看出是按键消抖的问题还是序列索引不同步的问题省去大量盲猜的时间。5. 常见问题与调试心得5.1 按键误触发、连按和漏按的处理经验Simon游戏里最常见的Bug是按键误触发具体表现是玩家按一次系统判定按了两下。这个问题几乎全部出在消抖逻辑上。我测试过不同按键发现有些按键的抖动时间长达30ms如果消抖延时设得太短比如5ms根本滤不干净。另一个容易被忽略的是按键松开时的抖动。如果你只检测了按下沿而没有等待释放手速快的玩家可能在你以为他按了一次的情况下实际触发了二次按下。解决方式就是我在代码里写的while等待释放循环它确保一次完整按压只产生一次有效信号。还有一个比较隐蔽的问题是多个按键同时按下。代码里的for循环是按固定顺序扫描的如果玩家同时按下两个键系统会只识别扫描到先的那个。这个在Simon游戏里其实可以不管因为按规则玩家一次只能按一个键。但如果你想让代码更健壮可以在每次检测到某个按键后立即返回并且在函数开头加一个防重入标志。5.2 随机序列“连续重复”的处理与扩展思路在我早期的版本里随机序列生成完全不加限制结果测试时出现连续五个蓝色的情况看着就很滑稽。加了“禁止连续三次相同”的约束后序列看起来自然多了。不过这个约束策略其实还可以更细致。比如你还可以加上“相邻颜色不能相同”的限制即不允许连续两个颜色相同这会大幅提高游戏难度因为玩家需要更多关注颜色的切换而不是简单的“记住上一个位置”。想要做难度分级的可以试试// 简单模式允许相邻相同但不允许连续三次相同 // 困难模式不允许相邻相同 int generateRandomColor() { int newColor random(0, 4); while (sequenceLength 1 newColor gameSequence[sequenceLength - 1]) { newColor random(0, 4); } return newColor; }5.3 无源蜂鸣器不出声先排查这三点无源蜂鸣器不出声是我被问得最多的问题这种情况我一般建议按顺序排查第一确认蜂鸣器是有源的还是无源的。有源蜂鸣器直接接高电平就能响无源的必须给PWM方波或者调用tone()函数。很多人买的时候没注意拿到手的有源蜂鸣器虽然接上电会响但tone()函数反而不出正确音调。第二检查正负极是否接反。无源蜂鸣器的引脚长度通常有差异长脚接正极、短脚接负极如果反了可能只有微弱的交流声甚至完全无声。第三确认代码里有没有先调用noTone()又紧接着tone()的情况。Arduino的tone()和noTone()是互斥的如果在极短时间内反复切换蜂鸣器可能来不及发声。测试时最直接的办法是写一个简单的循环让蜂鸣器持续输出1kHz方波如果还是不响基本可以断定是接线或硬件问题而不是代码问题。5.4 调试心得善用串口和分解测试最后分享一个我自己的调试习惯。写嵌入式程序我永远不会等到全部代码写完才去硬件上测试。正确的顺序是分模块验证先单独写点亮LED的测试函数确认硬件连线无误再单独写按键扫描的测试用串口打印键值再单独写蜂鸣器音调的测试最后才把三个模块整合进状态机。每走一步都确认这一步没问题出错了也能立刻定位到是哪次改动引入的。Simon游戏虽小但五脏俱全。做完这个项目你对GPIO操作、状态机设计、按键消抖和模块化编程都会有实打实的理解。后续如果你想继续扩展比较有意义的方向有把序列做成双人模式一个人演示另一个人复现、通过LCD显示最高分记录、增加声音录制与回放功能或者做成物理按键触摸屏双输入的混合版本。基座已经很扎实了上面想怎么盖楼都行。本文还有配套的精品资源点击获取
返回列表