引言
在 PvZ-Portable 的跨平台演进中,渲染后端统一至 OpenGL ES 2.0 解决了图形层的平台分裂。而在窗口管理、输入分发、音频输出与系统路径抽象上,项目自诞生起一直建立在 SDL2 与仓库内嵌的 SDL-Mixer-X 之上。
随着 SDL3 及配套生态(SDL3_mixer、Emscripten sdl3 port、vcpkg 与各 Linux 发行版软件源)趋于成熟,笔者在 PR #523 中将项目从 SDL2 全面迁移到了 SDL3。整个提交涉及 478 个文件,新增 1,275 行代码,删除 144,016 行代码——完成底层多媒体框架换代的同时,彻底移除了随仓库携带多年的 SDL-Mixer-X 源码树。
这次改造涉及音频引擎重写、高分屏(HiDPI)坐标系分离、输入事件扁平化以及七个目标平台构建链路的调整。作为系列的第一篇,本文先说明迁移的原因以及对玩家、编译者和打包维护者的实际影响,再从整体上梳理各子系统的改动脉络。具体技术细节在同系列其余三篇文章中展开:
为什么要迁移到 SDL3
项目此前已经在 Linux、Windows、macOS、Android、iOS、WebAssembly 和 Nintendo Switch 上稳定运行。决定在此时跨大版本迁移到 SDL3,主要出于以下几处现实问题。
摆脱内嵌的 14 万行 SDL-Mixer-X
《植物大战僵尸》的关卡音乐存放在单个 mainmusic.mo3 文件中,包含 30 个 tracker 通道,运行时需要通过跳转 Order 切换曲目,并在战斗进入 Burst 状态时逐通道实时调整鼓组与镲片的音量(见单流音乐系统博客)。标准 SDL2_mixer 没有暴露 MOD 音乐的逐通道音量接口,因此项目早期直接在 src/SexyAppFramework/sound/SDL-Mixer-X/ 下内嵌了整棵 SDL-Mixer-X 源码树。
这棵源码树有 14.3 万余行代码,包含了大量项目根本用不到的 MIDI 合成器、音色库、Qt 播放器示例乃至 VB6 绑定。每次全新编译时,相当一部分时间都花在编译这套庞大的第三方源码上;而在静态构建、交叉编译或 WebAssembly 移植时,它自带的旧版 CMake 脚本又经常与外部工具链产生摩擦。
SDL3 引入了支持拉取回调的 SDL_AudioStream,配合 SDL3_mixer 的轨道(MIX_Track)接口,可以直接把 libopenmpt 渲染出的浮点音频流挂接到混音器轨道上。原本需要内嵌 14 万行源码才能拿到的 MOD 通道控制能力,现在用不到 200 行桥接代码就能实现。
跨过 sdl2-compat 过渡层
在 Arch Linux、macOS Homebrew 等更新较快的软件源中,sdl2 包实际上已经被替换为基于 SDL3 实现的兼容层 sdl2-compat。这意味着程序编译时面向 SDL2 头文件,运行时却要先加载 libSDL2,再由 sdl2-compat 在内部通过 dlopen 动态加载 libSDL3。
这种双层结构给打包带来过不少麻烦。例如在 macOS CI 构建中,dylibbundler 通过分析 Mach-O 头部依赖来收集动态库,无法发现 sdl2-compat 在运行时 dlopen 的 libSDL3.dylib,过去只能在打包脚本里专门写一条命令把 libSDL3.dylib 手动拷进发布目录:
# 旧版 macOS CI 中针对 sdl2-compat 的特殊处理
install -m 755 "$(brew --prefix sdl3)/lib/libSDL3.dylib" dist/libs/libSDL3.dylib
既然主流环境的底层已经是 SDL3,直接迁移到原生 SDL3 接口,既省去了中间兼容层的转发,也让二进制依赖关系恢复直接。
原生 Wayland 策略与高分屏支持
在 SDL2 下,即便运行在 Linux Wayland 会话中,默认仍会优先选择 X11 后端(走 XWayland),只有显式设置环境变量 SDL_VIDEODRIVER=wayland 才会启用原生 Wayland。为此,项目在 Arch Linux 软件包中专门配了一个启动脚本 archlinux/pvz-portable.sh 来检测 WAYLAND_DISPLAY。
SDL3 在桌面 Linux 上将 Wayland 作为默认优先后端,并在窗口系统中原生区分了窗口逻辑尺寸与物理像素尺寸(SDL_WINDOW_HIGH_PIXEL_DENSITY)。迁移后,不再需要外部启动脚本干预视频驱动选择,高分屏环境下的渲染分辨率与鼠标坐标映射也能通过原生接口分离。
收敛 Android 构建链路
在 SDL2 架构下,Android 端的 SDLActivity 默认通过 System.loadLibrary("SDL2") 动态加载 SDL2,迫使我们在 vcpkg 静态依赖之外单独从源码编译一份 libSDL2.so。迁移到 SDL3 并调整 PvZPortableActivity 的库加载列表后,SDL3 与 SDL3_mixer 可以同其他依赖一样由 vcpkg 静态编译并直接链入单个 libmain.so,显著简化了 Android 的构建与打包过程。
对用户与打包维护者的影响
普通玩家:存档、资源与回放保持兼容
对直接下载预编译版本或使用在线 Web 版的玩家而言,这次迁移不需要做任何额外操作:
- 游戏资源不变:依然只需准备正版《植物大战僵尸:年度版》的
main.pak与properties/目录。 - 存档完全兼容:各平台的数据存储路径(由
SDL_GetPrefPath、Android 外部文件目录、iOSDocuments目录及浏览器 IndexedDB 管理)保持原样,userdata/下的用户存档与关卡进度存档可以直接读取。 - 确定性回放兼容:
.dmo演示文件的录制与回放格式没有破坏性变更,原有的回放文件仍可正常加载。 - 桌面体验改进:Linux Wayland 用户无需设置环境变量即可默认以原生 Wayland 窗口运行;macOS Retina 与桌面分数缩放屏幕下的窗口缩放、全屏切换更加稳定。
源码编译与打包维护:依赖变更说明
从源码自行编译 PvZ-Portable,或维护各 Linux 发行版、BSD 等平台软件包时,需要注意构建依赖的变化:
- 移除
SDL2,新增SDL3与SDL3_mixer:SDL3最低版本要求为 3.2.12。滚轮事件处理使用了 SDL 3.2.12 引入的event.wheel.integer_y字段,CMake 配置阶段会通过find_package(SDL3 3.2.12 CONFIG REQUIRED)检查版本。- 由于移除了仓库内嵌的
SDL-Mixer-X,现在需要系统或工具链提供官方的SDL3_mixer。
- 保留
libopenmpt:.mo3音乐解码不再经过混音器内置插件,而是由引擎直接调用libopenmpt解码后送入SDL_AudioStream,因此libopenmpt依然是必需的直接依赖。 - 常规动态构建不再显式查找二级音频依赖:在系统提供动态库的桌面构建中,顶层项目只需查找
SDL3、SDL3_mixer、libopenmpt、libpng、libjpeg-turbo和zlib;仅当静态链接libopenmpt且该静态归档未自包含解码依赖时,CMake 才会按需探测mpg123、vorbisfile、vorbis和ogg。
各主流平台更新后的依赖安装命令如下:
- Arch Linux:
sudo pacman -S --needed base-devel cmake libjpeg-turbo libopenmpt libpng ninja sdl3 sdl3_mixer - Debian / Ubuntu:
sudo apt install cmake ninja-build libjpeg-dev libopenmpt-dev libpng-dev libsdl3-dev libsdl3-mixer-dev - Windows (MSYS2 UCRT64):
pacman -S --needed base-devel mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-libjpeg-turbo mingw-w64-ucrt-x86_64-libopenmpt mingw-w64-ucrt-x86_64-libpng mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-sdl3 mingw-w64-ucrt-x86_64-sdl3-mixer - macOS (Homebrew):
brew install cmake dylibbundler jpeg-turbo libopenmpt libpng ninja sdl3 sdl3_mixer
Arch Linux 的打包脚本(archlinux/PKGBUILD 与 PKGBUILD-AUR)已同步将 depends 中的 sdl2-compat 替换为 sdl3 和 sdl3_mixer,并删去了 pvz-portable.sh 中手动设置 SDL_VIDEODRIVER 的代码。
迁移内容概览
从代码上看,除了把 #include <SDL.h> 替换为 #include <SDL3/SDL.h> 以及常规函数重命名外,实质性的改动集中在四个部分。
重构音频子系统
SDL3_mixer 放弃了沿用多年的“全局单混音器 + 固定整数通道号 + Mix_Chunk/Mix_Music 二元划分”设计,转向基于对象的 MIX_Mixer / MIX_Audio / MIX_Track 架构。借此机会,项目的音频层做了三项清理:
- 用
OpenMptMusic+SDL_AudioStream替代SDL-Mixer-X:新建了 190 行的OpenMptMusic类(src/SexyAppFramework/sound/OpenMptMusic.cpp),直接调用libopenmptC API 渲染双声道浮点音频,通过SDL_SetAudioStreamGetCallback按需向SDL_AudioStream喂入数据,再由MIX_SetTrackAudioStream将流挂载到MIX_Track上。曲目跳转(Order seek)和 30 通道独立音量控制全部收敛在OpenMptMusic内部,主线程与音频线程之间通过MIX_LockMixer同步。 - 消除
Music与MusicInterface的抽象泄漏:旧代码中,游戏层的Music.cpp直接包含了SDL_mixer_ext.h,甚至将mMusicInterface强转为SDLMusicInterface*去直接读写内部的mMusicMap和互斥锁。迁移后,LoadMusicFromMemory、GetMusicOrder、JumpToOrder和SetChannelVolume正式进入MusicInterface虚接口,Music.cpp不再感知任何底层音频库类型,此前为防止内存提前释放而保留的全局数组gMusicFileData也被清除。 - 移除手写的软件变调回调与
.au解码器:SDL3_mixer原生支持.au(Sun/NeXT $\mu$-law/PCM)格式,且MIX_Track原生提供MIX_SetTrackFrequencyRatio变调接口。因此,SDLSoundManager.cpp中 120 多行手写的.au解析与SDL_AudioCVT转换代码,以及SDLSoundInstance.cpp中近百行手动对int16_t样本做线性插值的SDLSoundPitchHandler回调全部删除,音效变调简化为一行函数调用。
这部分的设计与排错过程见《基于 SDL3_mixer 与 SDL_AudioStream 重构音频系统》。
窗口、高分屏双坐标系与输入事件更新
在窗口与输入层(Window.cpp、GLInterface.cpp、Input.cpp),改动主要围绕坐标精度与事件结构展开:
- 高分屏下的像素与窗口坐标分离:窗口创建启用了
SDL_WINDOW_HIGH_PIXEL_DENSITY。在 Retina 或分数缩放的 Wayland 桌面上,GLInterface::UpdateViewport()使用SDL_GetWindowSizeInPixels()获取物理像素尺寸来设置glViewport,同时使用SDL_GetWindowSize()获取窗口逻辑坐标,按比例换算出逻辑坐标系下的mPresentationRect。这保证了 OpenGL 以原生像素分辨率绘制,而升级为float类型的 SDL3 鼠标事件与输入法候选框位置(SDL_SetTextInputArea)仍在窗口逻辑坐标系下映射。 - 事件循环扁平化与滚轮整数刻度:原本嵌套在
SDL_WINDOWEVENT下的二级switch全部提升为顶层SDL_EVENT_WINDOW_*事件;鼠标滚轮改用 SDL 3.2.12 的event.wheel.integer_y,确保确定性回放(Demo)录制的 8 位滚轮增量在不同高精度滚动设备上行为一致。 SDLK_A命名变更背后的键码陷阱:SDL3 将字母键宏名从SDLK_a改为大写SDLK_A,但其底层枚举值依然是小写 ASCII'a'(0x61)。初次迁移时若误以为其值变成了'A',会导致所有字母快捷键与非 IME 字符输入失效。
这部分细节见《SDL3 窗口、高分屏双坐标系与输入事件迁移》。
清理平台遗留胶水代码
SDL3 将许多原本需要应用层自行编写平台分支的基础设施纳入了标准 API,使我们得以删去多处平台特判代码:
| 平台特判逻辑 | 迁移前(SDL2 + 平台 API) | 迁移后(SDL3 统一 API) |
|---|---|---|
| Windows 命令行 UTF-8 转换 | main.cpp 中 45 行 GetCommandLineW + CommandLineToArgvW + WideCharToMultiByte |
引入 <SDL3/SDL_main.h>,由 SDL3 入口统一提供 UTF-8 argv |
| Windows 纳秒休眠模拟 | Common.h 中 30 行 CreateWaitableTimer + WaitForSingleObject 实现的 nanosleep |
全平台统一调用 SDL_DelayNS() |
| iOS Documents 目录获取 | std::getenv("HOME") 拼接 "Documents" |
SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS) |
| 可执行文件目录获取 | char* p = SDL_GetBasePath() 后需手动 SDL_free(p) |
const char* p = SDL_GetBasePath()(SDL3 内部缓存,无需释放) |
重整七大平台构建与静态链接体系
代码层面的 API 迁移只占了整个 PR 的第一个提交(68de7dc2)。在 refactor/sdl3-migration 分支随后的 30 个提交中,大部分工作都围绕着 Linux(动态与 manylinux 静态)、Windows(MinGW 与 MSVC)、macOS、Android、iOS、WebAssembly 和 Nintendo Switch 七个平台的构建链路展开:
- 放弃在主项目
CMakeLists.txt中通过FetchContent隐式拉取构建依赖的尝试,回归标准find_package(SDL3 3.2.12 CONFIG REQUIRED)与find_package(SDL3_mixer CONFIG REQUIRED),保持构建入口单纯。 - Android 端通过 vcpkg 统一静态编译
sdl3与sdl3-mixer,全部链接进单个libmain.so,并在 Gradle 中编写downloadSdl3Java任务,自动解析vcpkg-configuration.json的 baseline 提交以拉取版本严格对齐的 SDL3 Java 层源码,同时兼顾 CI 与本地 Android Studio 构建。 - WebAssembly 端编写 14 行的
CMake/EmscriptenSDL3.cmake桥接 Emscripten 内置的-sUSE_SDL=3端口与源码编译的SDL3_mixer。 - 解决 Linux 全静态构建中
libopenmpt与SDL3_mixer内置stb_vorbis的符号冲突,以及 MinGW 静态目标的选择。
这些跨平台构建经验汇总在《SDL3 迁移中的跨平台构建与静态链接》中。
结语
从最终合并的结果来看,这次 SDL3 迁移更像是一次借框架换代完成的减法:
| 指标 | 迁移结果 |
|---|---|
| 仓库代码净变化 | +1,275 行 / -144,016 行(净减少逾 14.2 万行) |
| 内嵌第三方音频库 | 完全移除 SDL-Mixer-X 目录,改用 SDL3_mixer + 190 行 OpenMptMusic |
| 手写音频底层样板 | 移除 120 行 .au 解码器与 100 行软件重采样变调回调 |
| 平台特判胶水代码 | 移除 Win32 UTF-8 argv 转换、Win32 nanosleep、Wayland 启动脚本与 macOS libSDL3.dylib 补丁 |
| Android 原生库产物 | 从多个独立动态库收敛为单个静态集成的 libmain.so |
同系列的后续三篇文章将按子系统逐一拆解这次迁移中的技术细节与排错过程。
⚠️ 版权与说明
PvZ-Portable 严格遵守版权协议。游戏 IP(植物大战僵尸)属于 PopCap/EA。
本项目仅包含开源重实现的引擎代码,不含任何游戏美术、音效、关卡等受版权保护的资源文件。研究或使用此项目时,必须拥有正版游戏(如果没有,请在 Steam 或 EA 官网购买)。你需要从正版游戏中提取以下文件,放到 PvZ-Portable 的程序所在目录:
main.pakproperties/目录下的资源文件
PvZ-Portable 的源代码以 LGPL-3.0-or-later 许可证开源。
赞赏本文
| 支付宝 | 微信支付 |
|---|---|
![]() |
![]() |

