:没有一首 BGM 是音频文件)
系列《一个 Python 新手靠 AI 帮忙把贪吃蛇做成了蛇娘游戏》共 5 篇。本篇讲一个没资源的新手怎么搞定美术和音乐。前三篇把架构、画面、战斗都讲完了。这一篇我想聊一个特别现实的问题作为一个既不会画画、又不懂音乐、还没钱买素材的新手游戏里那些立绘、背景、音乐音效到底从哪来先说个可能让你意外的事实我这个游戏的assets文件夹里压根没有audio目录一个音频文件都没有。但你玩的时候点击有哒声、升级有上行音阶、四张地图各有各的背景音乐——这些声音全是程序在运行时用代码当场算出来的。这一篇就讲两件事我怎么用 AI 出图、再用脚本把图切成素材以及我怎么用 Python 合成全部音乐音效。后者是我整个项目里最魔幻、也最想分享的部分。一、美术AI 出图只是第一步能用才是难点角色立绘、场景背景我全是用 AI 生成的。但一开始我天真地以为AI 生成一张好看的图 素材搞定了结果发现差得远AI 出的图带着一整块背景可我要的是透明背景的角色有的图角落带着 AI 平台的水印尺寸、构图参差不齐直接放进游戏大小不一最麻烦的是蛇身——AI 给的是一整条蛇可贪吃蛇的身体需要能一节一节拼接、还要能收窄成尾巴。在AI的帮助下游戏用了一个切图脚本prepare_assets.py。二、切图管线一个脚本把 AI 原图批量变成游戏素材这个脚本的思路是我和 AI 约定了一套命名规范然后让脚本按规范自动处理我把 AI 生成的原图按{角色}_src_upper.png上半身、{角色}_src_scale.png鳞片/身体、{角色}_src_tail.png尾巴、{角色}_src_full.png全身立绘命名丢进一个目录脚本自动对每张图做去背景 → 去水印 → 自动裁掉多余空白 → 缩放到统一尺寸 → 柔化边缘蛇身那张还会被特殊处理成梯形条带一头宽一头窄这样游戏里拼接身体时才能自然收窄到尾巴最后按{角色id}/head.png、body_seg.png、tail_tip.png、full.png输出。这里面每一步我都是知道要干什么、但不知道具体怎么写全靠 AI 教。比如去背景我才知道原来有几种情况背景是纯色的可以用洪水填充从边缘往里漫背景是棋盘格的要按颜色容差判断。我把脚本里自动裁掉空白这一小段的意思简化一下你就明白它有多朴素# 教学简化版·非完整可运行·by 【外收内放】 def autocrop(im, pad6): 把图片四周的透明空白裁掉只留下有内容的部分再留一点边距。 # 扫描每个像素找出不透明像素的最小外接矩形 (l, t, r, b) l, t, r, b 找非透明区域的边界(im) # 按这个矩形裁一刀四周各留 pad 像素 return im.crop((l - pad, t - pad, r pad, b pad))有了这个脚本我后来加新角色时只要把 AI 出的四张原图按规范命名丢进去跑一下脚本素材就齐了。三、数据驱动一个角色其实就是一份 JSON美术搞定后角色的信息放哪还是老规矩——JSON。第一篇提过加角色不改代码这一篇把这份配置摊开看。一个角色在characters.json里是这么一条我节选了初始角色樱落删了一些长文本{ id: sakura, name: 樱落, rarity: SSR, title: 樱花神域的守护者, element: 樱, growth: 均衡, desc: 来自樱花神域的蛇娘性格温柔。等级越高斩击越锋利。, story: 樱落诞生于樱花神域最深处的古树之下……这里是一大段角色背景故事, egg_lines: { head: [哎呀…被你摸到头了呢花瓣都乱了。], body: [腰这里…是鳞片开始的地方痒痒的。], tail: [尾巴是我的骄傲轻一点哦。] }, kit: { actives: [ {id: petal_slash, type: petal_slash, name: 落樱·绯斩, key: 1, color: [255, 150, 190], vfx: slash, desc: 向前突进斩击路径敌人受伤并获得短暂护盾。} ], passive: {id: p_bloom_guard, name: 花守, desc: 击杀敌人有概率回复 1 点生命。} }, head: characters/sakura/head.png, full: characters/sakura/full.png, body_seg: characters/sakura/body_seg.png, tail_tip: characters/sakura/tail_tip.png }story和desc给角色写背景故事和人设。虽然没人规定贪吃蛇要有剧情但我觉得有故事的角色才活egg_lines这是我特别得意的一个小设计——玩家在角色详情页摸头、摸腰、摸尾巴会触发不同的台词彩蛋。这些台词也全在 JSON 里我想加就加kit.actives里的color和vfxcolor是这个角色技能特效的配色vfx是特效样式slash 斩击 / star 星芒 / wave 波纹…。六个角色共用同一套连招机制第 3 篇讲的靠这两个字段就长出了各自不同的视觉风格desc每个技能的一句话说明角色详情页直接读它显示所以我又给每个技能都写了介绍。对我这种新手来说把内容全塞进 JSON 最大的好处是我想加角色、改台词、调配色都不用去碰那堆我看着就头疼的战斗代码改完 JSON 存盘就生效。四、重头戏没有一首 BGM 是音频文件全用代码算出来好来到这篇我最想讲的部分。我最初的认知和大多数人一样游戏音乐 一堆 mp3/ogg 文件。可问题来了——我没有音乐素材网上下的又有版权风险我自己更不会作曲。我甚至一度想让游戏静音算了。是 AI 给我打开了一扇窗。它说了一句我至今记得的话声音的本质就是一条波形的曲线而曲线是可以用数学算出来的。你没有音频文件但你可以用代码把波形一个点一个点地算出来。我半信半疑但它真的带我一步步做出来了。核心其实就四个小函数我把它们理解成作曲的四块积木积木一_tone()—— 算出一个音一个音本质就是让空气按某个频率振动。代码里就是逐个采样点算正弦值# 教学简化版·非完整可运行·by 【外收内放】 def _tone(freq, dur, wavesine, vol0.5, slide_toNone): 算一段单音返回一串 [-1, 1] 之间的采样点。 n int(dur * SAMPLE_RATE) # 这段音一共有多少个采样点 out, phase [0.0] * n, 0.0 for i in range(n): p i / n # 频率可以从 freq 平滑滑到 slide_to就做出滑音效果 f freq if slide_to is None else freq (slide_to - freq) * p phase 2 * math.pi * f / SAMPLE_RATE v math.sin(phase) # 正弦波换个数学公式就是方波/三角波/锯齿波 # …再乘一段渐强渐弱的包络避免开头结尾啪地爆音… out[i] v * vol return out我当时连采样率波形包络这些词都是第一次听是 AI 一个个给我解释采样率就是一秒取多少个点点越密声音越细腻正弦波听着柔和、方波听着电子、锯齿波听着粗糙包络就是让声音慢慢起来、慢慢消失不然会有爆音。懂了之后我发现调这几个参数就能调出完全不同音色的音特别有意思。积木二和三_seq()拼旋律_mix()叠和声有了单个音把它们一前一后接起来就是旋律同时叠起来就是和声# 教学简化版·非完整可运行·by 【外收内放】 def _seq(segments): # 顺序拼接一个音接一个音 → 旋律 out [] for s in segments: out.extend(s) return out def _mix(tracks): # 叠加多轨几个音同时响 → 和声 n max(len(t) for t in tracks) out [0.0] * n for t in tracks: for i, v in enumerate(t): out[i] v return out最后再用_to_bytes()把这串[-1, 1]的数字转成 pygame 能播放的 16-bit 字节流一个音就做好了。用积木拼出音效有了这四块积木各种音效就是配方问题了。我截几个真实的例子# 教学简化版·非完整可运行·by 【外收内放】 # 点按钮又高又短的方波哒 ui_click: _tone(1200, 0.04, square, 0.32) # 升级Do-Mi-Sol-Do 四个音依次上行一听就是升级了 level_up: _seq([_tone(N[k], 0.07, sine, 0.38) for k in (C5, E5, G5, C6)]) # 受伤低频方波往下滑 一点噪声闷闷的咚 hurt: _mix([_tone(110, 0.15, square, 0.36, slide_to70), _noise(0.15, 0.22)])上面的N[C5]是一个音名 → 频率的字典_noise()是生成噪声的积木用来做打击感。用积木拼出 BGM四张地图四首不同的曲子背景音乐比音效复杂一点但思路一样。我给每个场景配一组**和弦走向 音色 速度**然后让代码把它渲染成高音琶音 低音铺底叠在一起的循环# 教学简化版·非完整可运行·by 【外收内放】 if name battle_campus_garden: # 校园庭院明亮大调、中速、正弦波 chords [(C4,E4,G4), (G3,B3,D4), (A3,C4,E4), (F3,A4,C4)] note_dur, wave 0.20, sine elif name battle_deep_sea: # 深海遗迹低八度、慢速、三角波幽深压迫 chords [(D3,F3,A3), (C3,E3,G3), (D3,F3,A3), (E3,G3,B3)] note_dur, wave 0.30, tri # …霓虹夜市用小调方波紧张、樱花神域用高音颤音空灵各有各的配方… # 把和弦拆成高音琶音 lead 低音铺底 bass叠起来就是一段 BGM lead [] for (a, b, c) in chords: for f in (a, b, c, b): lead.extend(_tone(N[f], note_dur, wave, 0.15)) bass [] for (a, _b, _c) in chords: bass.extend(_tone(N[a] / 2, note_dur * 4 * 0.9, sine, 0.11)) return _mix([lead, bass])四张地图就这样有了四首听感明显不同的曲子全程没有用到任何一个音频文件。我还留了一手以后想换真音乐丢文件进去就行虽然我现在全靠合成但 AI 提醒我别把路走死。所以我们设计成混合素材程序会先去assets/audio/bgm/找有没有对应的真实音频文件.ogg/.wav/.mp3有就用真文件没有才用代码合成兜底。也就是说将来我哪天找到了一首心仪的、没有版权问题的曲子只要把文件按约定的名字丢进那个目录游戏就会自动用它替换掉合成音——一行代码都不用改。这个留后路的设计是我从这个项目里学到的另一个重要习惯。五、这套思路让一个三无新手也撑起了一个游戏回头看我既没美术、也没音乐、也没预算能把这个游戏做成现在这样靠的就是两条内容数据驱动角色、场景、技能、台词全在 JSON 里加内容不碰代码素材程序生成图靠 AI 脚本切声音靠代码合成绕开了没有资源这个死结。这两条合起来让我一个人也能撑起一个有 6 个角色、4 张地图、几十种音效和 4 首 BGM 的游戏。对和我一样什么都不会、但想做点东西的新手来说我觉得这比任何具体代码都更值得记住资源不够就用思路来凑。当然这一篇里的合成代码绝大部分是 AI 写的我更多是听懂原理、然后敢改参数。我不敢说这些代码写得多专业但至少它跑起来了还挺好听。