news 2026/9/24 20:18:27

用C++和SDL2复刻超级玛丽:从框架到资源加载的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用C++和SDL2复刻超级玛丽:从框架到资源加载的完整实践

简介:基于C++的超级玛丽游戏源码包,包含完整的图片与背景音乐,是面向C++初学者和游戏开发入门者的经典练手项目。压缩包共33个文件,大小约7.4MB,涵盖14个mp3格式的音乐音效、6个bmp格式的图片素材,以及C++源码、工程配置和Visual Studio解决方案文件,解压后即可编译运行。目前已有734人学习下载,适合用于理解游戏主循环、碰撞检测、对象状态切换、音频播放等基础知识。源码采用面向对象思想编写,将角色、子弹、敌人、金币、关卡等抽象为独立模块,结构清晰、便于扩展,同时完整实现了跳跃、踩敌、吃金币、通关等玩法。通过研究这份代码,能够学习到游戏资源加载与管理的具体方法,也可以作为课程设计或毕业设计的参考基础。

1. 超级玛丽用 C++ 复刻:为什么多年后还值得自己动手写一遍

先说结论:标题里最值钱的不在“超级玛丽”,而在后半句。游戏逻辑写起来不难,真正让新手卡住的是图片素材怎么加载、背景音乐怎么循环、资源文件夹怎么组织。我用 C++ 从零写过一个可玩的版本,也翻过网上流传的同类源码,发现掉进同一个坑的代码不在少数:界面起来了,玛丽不动;图片动了,没有声音;声音响了,关卡过不去。这篇文章把从框架到资源加载的落地路径完整讲一遍,会用 SDL2 做底层库,代码按 C++11 标准写,能学习能改。适合已经读懂基本语法、想用一个小项目把指针、结构体、事件循环揉在一起练手的人,也适合拿到源码却编译不过、看不懂资源加载为何失败的读者。

2. 先立框架再谈玩法:主循环、地图与实体的基本设计

很多人拿到一份游戏源码,第一反应是找 main 函数,然后顺着往下读。这个习惯对于几百行的控制台程序没问题,但放到超级玛丽这种带渲染、输入、音频、碰撞的项目里,多半会读成黑匣子。我一般会先看三个东西:主循环在哪、地图数据用什么结构、游戏对象怎么表示。把这三件事理清,源码的骨架就出来了。

2.1 选型:为什么用 SDL2 而不是 EasyX 或控制台

如果你是在 vscode 配置 c/c++ 环境时顺手搜到这类源码,大概率会遇到三种库:控制台 API、EasyX、SDL2。控制台画字符表演示还行,做不了真正意义上的图片渲染;EasyX 简单但 Windows only,而且对音频支持约等于零;SDL2 跨平台、自带音频子系统、配合 SDL_image 和 SDL_mixer 能直接吃下图片和背景音乐,是社区里复刻 2D 平台游戏最常见的选择。

用 SDL2 还有一个现实原因:网上的超级玛丽源码,十个里有八个基于它。你学会读这一家的工程结构,再去看别的项目,迁移成本很低。唯一要忍受的是环境配置比 EasyX 麻烦一点,需要在链接器里加上 SDL2、SDL2_image、SDL2_mixer 的库路径和 dll。这个坑后面避坑章节会专门说。

2.2 主循环的最小骨架:先跑起来再谈画面

不管游戏多复杂,核心都是“处理输入—更新逻辑—渲染画面”这个循环。下面是去掉所有业务后最小可运行骨架:

#include <SDL.h> const int SCREEN_WIDTH = 800; const int SCREEN_HEIGHT = 600; int main(int argc, char* argv[]) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { SDL_Log("SDL_Init failed: %s", SDL_GetError()); return -1; } SDL_Window* window = SDL_CreateWindow( "C++ Super Mario", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN); SDL_Renderer* renderer = SDL_CreateRenderer( window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); bool running = true; SDL_Event event; while (running) { while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { running = false; } } update(); // 更新游戏逻辑:位移、动画、碰撞判断 render(); // 绘制背景、地图、人物、HUD SDL_Delay(16); // 近似 60 FPS } SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }

SDL_Init的参数是位掩码,这里同时初始化视频和音频子系统,如果只写SDL_INIT_VIDEO,后面 SDL_mixer 初始化时会非常迷惑。SDL_CreateRenderer里我加了SDL_RENDERER_PRESENTVSYNC,让垂直同步去限制帧率,此时SDL_Delay(16)是兜底,防止某些环境换 vsync 失效后空转。两件事做同一件事,看起来冗余,实际是防止有一台机器不按常理出牌。

update()render()是分隔开的,这是整个工程最重要的一条纪律。以后你想加血条、加敌人、加音效,都往这两个函数里塞,但不要混在一起,否则改着改着就分不清哪个变量是“逻辑值”哪个是“绘制值”了。

2.3 地图数据:用字符串数组代替 Tile Map 编辑器

超级玛丽的地图本质上是一张瓷砖表,每一块是 32×32 或 48×48 的方块。很多源码会用一个 JSON 文件去描述地图,但对学习项目来说,字符串数组更直观,想改一处直接敲字符,不需要额外解析器。

#include <vector> #include <string> const int TILE_SIZE = 32; std::vector<std::string> mapData = { "############################", "#P...........?##..........#", "#.........####............#", "#..................M......#", "#...$..................####", "############################" };

字符约定可以自己定,我的习惯是这样:#是砖块,P是玛丽出生点,M是敌人,?是问号砖,$是金币。解析的时候按行、按列扫一遍:

for (int row = 0; row < (int)mapData.size(); ++row) { for (int col = 0; col < (int)mapData[row].size(); ++col) { char tile = mapData[row][col]; if (tile == '#') { spawnWall(col * TILE_SIZE, row * TILE_SIZE); } else if (tile == 'P') { playerStartX = col * TILE_SIZE; playerStartY = row * TILE_SIZE; } else if (tile == 'M') { spawnEnemy(col * TILE_SIZE, row * TILE_SIZE); } } }

col * TILE_SIZE是把格子坐标换算成像素坐标,这一步是新手最容易漏的。有人直接在数组下标上做碰撞,然后渲染时又乘 32,两边对不上,人物就像踩在空气上。解析时统一换算成像素,之后物理计算、渲染、碰撞都用同一套单位,不要混用。字符串数组的另一个好处是可以直接放进源码,不用处理外部文件缺失的问题;等你把逻辑跑通了,再改成读外部关卡文件也不迟。

3. 让玛丽“活”起来:图片渲染、动画帧与键盘响应

框架立起来之后,下一步是把图片变成“人物”。这一步有两类素材:一是单张角色图,二是把奔跑、跳跃、站立都放在同一张图集里的精灵表。大多数超级玛丽项目用后者,因为动画切换本质上是“裁剪同一张大图里的不同区域”。

3.1 加载图片:PNG 透明通道与纹理化

直接上 SDL 的SDL_LoadBMP加载位图会遇到一个恶心的问题:位图没有 alpha 通道,背景色会变成一个矩形贴在屏幕上。解决方法是加载后用SDL_SetColorKey指定一种颜色为透明色。但现在的主流做法是直接使用 PNG,交给 SDL_image 处理。

#include <SDL_image.h> SDL_Texture* loadTexture(SDL_Renderer* renderer, const char* path) { SDL_Surface* surface = IMG_Load(path); if (!surface) { SDL_Log("IMG_Load failed: %s", IMG_GetError()); return nullptr; } SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); SDL_FreeSurface(surface); // 纹理已经上传 GPU,surface 可以释放 return texture; }
// 使用示例 SDL_Texture* marioTex = loadTexture(renderer, "images/mario.png"); if (!marioTex) { // 在这里退出或走兜底逻辑,不要带着空指针往下跑 }

IMG_Load返回的SDL_Surface是一块内存像素数据,SDL_CreateTextureFromSurface才把它真正变成渲染器能高效使用的纹理。用完立刻释放 surface,这个习惯能让你在一张张加载图集时少占几十 MB 内存。

3.2 动画切换:计数器驱动的精灵裁剪

假设 mario.png 是一张 96×32 的图集,里面从左到右是站立、走路 1、走路 2 三帧,每帧 32×32。渲染时用源矩形srcRect去裁剪图集,用目标矩形dstRect决定画在屏幕哪里:

const int FRAME_INTERVAL = 6; // 每 6 帧切换一次,60 FPS 下约每秒 10 帧 int frameCounter = 0; int marioFrame = 0; void updateAnimation() { frameCounter++; if (frameCounter >= FRAME_INTERVAL) { frameCounter = 0; marioFrame = (marioFrame + 1) % 3; } } void renderMario(SDL_Renderer* renderer, SDL_Texture* marioTex, int marioX, int marioY) { SDL_Rect srcRect = { marioFrame * 32, 0, 32, 32 }; SDL_Rect dstRect = { marioX, marioY, 32, 32 }; SDL_RenderCopy(renderer, marioTex, &srcRect, &dstRect); }

FRAME_INTERVAL控制动画速度,调大到 12 就是慢放,调小到 2 就是快放。注意这里不是按真实时间,而是按帧数计算的,如果哪一天你引入了动态帧率,这套就失效了,到时可以改用SDL_GetTicks()做时间驱动。SDL_RenderCopy的第三个参数传&srcRect,第四个传&dstRect,两个矩形语义完全不同,一个是“取原图哪里”,一个是“画到屏幕哪里”,写反了画面会直接花掉。

3.3 输入响应:SDL_KEYDOWN 与 SDL_GetKeyboardState 的选择

控制玛丽移动有两种常见写法。一种是接事件,在SDL_PollEvent循环里判断SDL_KEYDOWN/SDL_KEYUP;另一种是每帧直接查询键盘状态。平台游戏里角色需要持续按住方向键移动,更适合后者。

const Uint8* keys = SDL_GetKeyboardState(nullptr); if (keys[SDL_SCANCODE_LEFT]) { mario.vx = -MARIO_SPEED; mario.facingLeft = true; } else if (keys[SDL_SCANCODE_RIGHT]) { mario.vx = MARIO_SPEED; mario.facingLeft = false; } else { mario.vx = 0; } if (keys[SDL_SCANCODE_SPACE] && mario.onGround) { mario.vy = -JUMP_FORCE; mario.onGround = false; }

SDL_GetKeyboardState返回一个状态数组,用 SDL_SCANCODE 系列常量索引,keys[SDL_SCANCODE_SPACE]为 1 表示这一帧按键正被按下。“必须在地面才能跳”这个判断是硬编码进按键分支的,没有它,角色在空中按一下空格就无限连跳,直接破坏游戏手感。跳跃力度的数值JUMP_FORCE和重力加速度要先粗调一轮:跳太高,关卡设计白做;跳太低,管子跳不过去。这个没有玄学,只能一遍遍试。

4. 背景音乐与音效:资源加载、循环播放和释放顺序

到了音乐这一环,劝退率很高。图片只要路径对就能显示,音频却要处理初始化、格式、循环、音量、释放五件事,任何一环错了,表现都是“没声音”。但反过来说,音频也是最能提升“这东西像个游戏”的模块,一份带背景音乐的源码和一份静音源码,给人的完成度感受天差地别。

4.1 SDL_mixer 初始化:采样率、格式与缓冲区的选择

SDL_mixer 是 SDL 的官方音频扩展库,先Mix_OpenAudioMix_LoadMUS是一个固定流程。Mix_OpenAudio的四个参数分别是采样率、音频格式、声道数、缓冲块大小:

#include <SDL_mixer.h> if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { SDL_Log("Mix_OpenAudio failed: %s", Mix_GetError()); return -1; } Mix_Music* bgm = Mix_LoadMUS("audio/overworld.ogg"); if (!bgm) { SDL_Log("Mix_LoadMUS failed: %s", Mix_GetError()); return -1; } if (Mix_PlayMusic(bgm, -1) == -1) { SDL_Log("Mix_PlayMusic failed: %s", Mix_GetError()); }

44100 是 CD 音质采样率,对游戏音乐足够;MIX_DEFAULT_FORMAT是 16 位有符号整型;第二个参数 2 是双声道立体声;2048 是缓冲区大小,代表一次处理 2048 个采样帧,调大到 4096 可以降低爆音概率,但会增加播放延迟。如果你的代码没在SDL_Init里加SDL_INIT_AUDIO,这里会直接报错,日志永远指向音频设备,排查半天发现是初始化顺序问题。

4.2 音乐循环、音量与资源释放的正确姿势

这里要注意Mix_LoadMUSMix_LoadWAV的区别。背景音乐用前者,返回Mix_Music*;短音效(跳跃、吃金币)用后者,返回Mix_Chunk*。这两个类型不互通,有源码把音效当背景音乐加载,结果Mix_PlayMusic一直返回 -1。

// 背景音乐:无限循环 Mix_VolumeMusic(MIX_MAX_VOLUME / 2); Mix_PlayMusic(bgm, -1); // 音效:单独放通道,播放一次 Mix_Chunk* jumpSound = Mix_LoadWAV("audio/jump.wav"); if (jumpSound) { Mix_PlayChannel(-1, jumpSound, 0); } // 退出时的释放顺序:先停音乐,再释放音乐,最后关音频 Mix_HaltMusic(); Mix_FreeMusic(bgm); if (jumpSound) Mix_FreeChunk(jumpSound); Mix_CloseAudio();

Mix_PlayMusic的第二个参数是循环次数,-1 表示无限循环,0 表示只播一次。很多人写成 0,然后纳闷音乐怎么放完就没了。音量范围是 0 到MIX_MAX_VOLUME(128),MIX_MAX_VOLUME / 2就是一半音量。音效的Mix_PlayChannel第一个参数传 -1,让系统找一个空闲通道,避免手动分配通道号导致两个音效互相覆盖。

4.3 从网上拿到的 BGM 格式踩雷

我自己遇到过一种情况:下载的源码里配套音乐是 .mp3,本地开发环境放得好好的,打包给别人就静音。排查下来是 MP3 解码依赖 SDL_mixer 的特定插件,分发时少带一个 dll 就无声。最省心的做法是把音乐统一转成 OGG 或 WAV 再入库,OGG 体积小且解码库随 SDL_mixer 分发,几乎没有额外的版权顾虑。另一个常见问题是音乐文件本身有很长的结尾空白,听着像卡住,实际是音轨没剪干净,用音频工具裁到 2~3 秒内再进素材库。

5. 碰撞检测、敌人 AI 与常见问题排查

游戏源码里最容易让新人“翻车”的地方不在画面,在碰撞。超级玛丽这类平台跳跃游戏,所有手感都建立在“站得住、踩得准、跳得过去”这三个字上。很多源码能跑,但玩起来穿墙、陷地、踩不中敌人,问题都出在碰撞模型太粗糙。这一章我按自己做项目时踩过的坑来写。

5.1 AABB 碰撞检测:先懂原理再决定要不要用 SDL 自带的

SDL 自带SDL_HasIntersection,可以判断两个SDL_Rect是否相交。但直接用它做角色碰撞会有一个问题:它只告诉你“碰了”,不告诉你“从哪个方向碰的”。而平台游戏必须区分“头顶撞砖”和“脚底踩地”,所以我会手写一个分割轴判定:

bool checkCollision(const SDL_Rect& a, const SDL_Rect& b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; }

四个条件分别检查“a 的左边是否在 b 右边左边”“a 的右边是否在 b 左边右边”等,全部满足才算相交。要判断方向,更实用的做法是分轴移动:先水平移动,再垂直移动,每次移动后单独检测。也就是“x += vx; 检测水平碰撞; y += vy; 检测垂直碰撞”,而不是“x += vx; y += vy; 最后检测一次”。后一种写法在角色速度快一点时会出现隧穿——上一帧还在砖块左边,下一帧直接穿到右边,中间没有相交帧。

5.2 敌人 AI:一个 Goomba 级敌人的最小逻辑

游戏源码里最常见的敌人是左右巡逻型,地面走到底就回头。它的行为看起来简单,但有两个边界条件不处理就会穿模:一是碰到墙壁要回头,二是走到地面边缘要回头。

struct Enemy { float x, y; float speed; int direction; // 1 或 -1 }; void updateEnemy(Enemy& e) { int frontX = (e.direction == 1) ? e.x + TILE_SIZE : e.x - 1; int bottomY = e.y + TILE_SIZE; // 前方没有地板了,回头 if (tileAt(frontX, bottomY) != '#') { e.direction *= -1; } // 前方是墙壁,回头 if (tileAt(frontX, e.y) == '#') { e.direction *= -1; } e.x += e.direction * e.speed; }

tileAt把像素坐标换算回格子坐标,从地图二维数组里取出对应字符。注意“前方没有地板”的判断里用的是e.y + TILE_SIZE也就是脚底那一行,如果这一格不是#,说明敌人已经走到悬崖边,必须掉头。e.direction *= -1是翻转方向,用乘法比if分支更简洁。

5.3 避坑记录:现象、原因、解决

1. 图片加载成功但显示成一坨色块或黑方块。原因是 PNG 的 alpha 通道丢失,或者素材本身是 JPG 转存的伪 PNG。JPG 没有透明通道,背景色会被一起加载进来。解决:优先在美术阶段导出带透明通道的 PNG;如果你拿到的素材只有 BMP,用SDL_SetColorKey(surface, SDL_TRUE, SDL_MapRGB(surface->format, 255, 0, 255))把品红背景设为透明,注意这种方案的边缘会有锯齿。

2. 角色从高处落下,直接穿过地板。原因是自由落体速度累加过快,一帧内位移超过一个砖块高度,碰撞检测完全错过。解决:给下落速度设上限if (mario.vy > MAX_FALL_SPEED) mario.vy = MAX_FALL_SPEED;,比如 12 像素/帧,同时把碰撞改成“先移动、后检测、再回退”的分步逻辑。还有一招更稳:把一帧拆成两个子步,每步只移动一半位移,多次检测。

3. 编译过了,但 exe 拷到别的电脑提示找不到 VCRUNTIME140.dll。原因是编译时用了动态运行库/MD,目标机器没有安装对应的 Visual C++ Redistributable。解决:分发时要么让用户装一遍运行库,要么在 Visual Studio 里把“运行库”改为/MT静态链接。vscode 配置 c/c++ 环境时如果自己写 CMake,可以在 CMakeLists 里加set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"),麻烦一次,后面清净很多。

4. 背景音乐播放时断时续,或一按跳跃键音乐就卡一下。原因是音频缓冲太小、CPU 瞬时负载过高导致数据喂不上。解决:把Mix_OpenAudio的缓冲从 2048 调到 4096,同时检查主循环里是否把Mix_PlayMusic放进每帧调用——它只需要调用一次,循环内部会自动播,不要每帧重新触发。如果用的是 WAV 大文件,一次性加载会占内存,加载时界面瞬间卡顿,这时换成 OGG 压体积是治本。

6. 从“能跑”到“能玩”:这套源码值不值得继续投入的判断标准

很多人做完一个项目会想:这个源码到底算不算成功?我给自己定了三个验证标准,你也可以照着测自己手头的项目:第一,能否在干净环境里一条命令编译通过;第二,去掉音乐和图片后是否还会报错;第三,把窗口从 800×600 拉大到 1920×1080,角色是否还在地板上。第三条最阴险——如果碰撞逻辑里写死了窗口尺寸,全屏之后玛丽会直接掉出地图。

验证项判定标准常见失败信号
编译可复现新机器按 README 步骤能一键构建缺头文件、库路径写死绝对路径
资源独立图片/音乐缺失时给出明确报错黑屏静默失败、空指针崩溃
分辨率适配窗口缩放后地图和角色同步缩放角色跑到地图外、HUD 错位

这三点都通过了,源码就值得往深了投入。我会把下一步优先级放在“加状态机”上:站立、跑动、跳跃、下落、死亡五个状态用枚举管理,状态的进入条件和退出条件写清楚。很多源码只有一个update()里堆if,跑到后来每个新功能都要翻旧代码,这就是为什么有些游戏源码越改越烂。

还有一个值得做的方向是把关卡数据从源码中拆出去,改成读外部文件。字符串数组写死在代码里方便学习,但你要调关卡手感,每改一次都得重新编译。拆出去之后写一个简单的关卡编辑器,用字符拼地图,存成 txt,运行时加载,开发效率翻倍。这个改造也强迫你理解“数据与逻辑分离”这件事,比多写两百行业务代码划算得多。

我自己做这个项目时最后悔的是没在第一天就统一资源路径处理。Windows 下的相对路径以“当前工作目录”为基准,在 vscode 里调试和直接双击 exe,工作目录不一样,图片加载结果就不同,这种问题最耗时间。现在我都会在项目入口处打印SDL_GetBasePath(),把资源目录和程序目录绑定在一起,这个习惯帮我避开了一整类“换个启动方式就崩”的坑。如果你正卡在“快做完了但总有点不对劲”的状态,希望这些经验能帮你省下几个晚上的排查时间。

本文还有配套的精品资源,点击获取

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

559张三轮车数据集:YOLO/VOC双格式目标检测训练实战

简介&#xff1a;数据集包含559张三轮车实景图片&#xff0c;配套Pascal VOC与YOLO两种格式的标注文件&#xff0c;由labelImg手工绘制矩形框完成&#xff0c;类别统一为tricycle&#xff0c;共659个标注框。面向计算机视觉入门学习者和目标检测算法研究人员&#xff0c;尤其适…

作者头像 李华
网站建设 2026/9/24 20:18:05

从零实现听歌识曲API:声学指纹技术全解析

不知道你有没有认真想过&#xff0c;"听歌识曲 API"背后到底藏了多少技术活儿。手机里放一段5秒的录音上去&#xff0c;接口返回《晴天》周杰伦&#xff0c;词曲信息、专辑封面一并带回来——这个动作在音乐App、短视频平台、电台自动识别、KTV点歌系统里天天发生&am…

作者头像 李华
网站建设 2026/9/24 20:17:58

自然语言驱动Blender:DeepSeek Flash + MCP协议完整实战

最近我折腾了一个挺有意思的组合&#xff1a;本地起一个纯HTML聊天页面&#xff0c;后端挂上DeepSeek Flash模型&#xff0c;再通过MCP协议把Blender接进来。最终效果就是你用自然语言跟Blender对话——说“建一个立方体&#xff0c;放在原点偏右两米的位置&#xff0c;加一盏暖…

作者头像 李华
网站建设 2026/9/24 20:17:28

计及光伏快速无功响应的分布式电源优化配置方法

开头最近在做配电网分布式电源规划这块的项目&#xff0c;碰到一个挺典型的难题&#xff1a;传统分布式电源优化配置模型里&#xff0c;光伏电站大多被当成一个恒定功率因数的PQ节点来处理&#xff0c;也就是说只考虑了有功出力&#xff0c;无功这块基本按固定功率因数折算一下…

作者头像 李华
网站建设 2026/9/24 20:15:49

企业级AI项目复盘:写了没写进去,批了没批完

这期的标题写的是“写了但没写进去&#xff0c;批了但没批完”&#xff0c;如果你也在做企业级AI项目&#xff0c;大概率能瞬间get到这两个状态有多折磨人。我们团队这期迭代的主题是本地部署的知识库助手和底层的Java AI Agent应用平台&#xff0c;按理说方向明确、需求清楚&a…

作者头像 李华
网站建设 2026/9/24 20:15:39

多智能体系统多样性坍塌:机制、危害与十个对抗策略

1. 从“群体智慧”到“集体失明”&#xff1a;多样性坍塌到底是什么 你让三个Agent一起去修一个线上bug&#xff0c;它们讨论得热火朝天&#xff0c;结果一小时后提交的补丁一模一样&#xff0c;还是错的那个。你让五个Agent为新产品起名&#xff0c;以为能收到五十个创意&…

作者头像 李华