首页 / 资讯中心 / 文章详情

用WebAssembly在浏览器里运行红色警戒2:从编译到多人对战的完整实践

用WebAssembly在浏览器里运行红色警戒2:从编译到多人对战的完整实践 ★ FEATURED ARTICLE
打开浏览器地址栏敲进一个网址几秒钟后一副横版的即时战略战场就铺满了整个网页——你可以在里面造基地、拉部队、点开“尤里的复仇”的战役甚至拉上隔壁城市的一个人开局对战。第一次跑通这个项目的时候我自己都愣了一下这玩意在二十多年前明明是装在光盘里的如今整个运行时连同资料片内容和多人对战架构全被塞进了浏览器。我花了不少个晚上折腾这件事把编译链、老的网络协议、扩展包加载机制挨个拆开揉碎再拼回去。这篇东西就记一下当时的手路、踩坑和最终的收尾方案给同样手痒的各位一个参考。很多人第一反应是“图个新鲜”但真去做了才发现难点不是“把游戏跑起来”而是“让一个为本地硬件写的老程序在浏览器的虚拟环境里还按原来的方式思考”。这里说的《红色警戒2》是一款经典即时战略游戏《尤里的复仇》是它的扩展包两者都是当初做给 Windows 98/2000 那批机器跑的。老游戏的问题很多直接调用本地的图形接口和声卡接口文件系统也是按光盘和绝对路径设计的更麻烦的是多人对战那块依赖局域网协议和固定帧率的实时同步逻辑。浏览器这边则是沙箱环境没有原始的图形上下文没有真正的网卡驱动甚至连进程线程模型都不一样。要把这套东西平移过来等于给一个只会说母语的软件当同声传译。我做的这套运行时的基本思路是先把游戏本体当作一个“黑盒”用能生成 WebAssembly 的编译链把它从原始二进制重新编译到浏览器可执行的字节码文件系统用虚拟内存和本地存储模拟图形接口和声音接口分别映射到 WebGL 和 WebAudio多人对战部分则自己写一套虚拟网卡把老游戏原本的局域网通信包翻译成浏览器的实时数据通道。简单说整个过程分三层编译层、兼容层、网络层。1. 项目从哪里切入为什么偏偏选它1.1 老即时战略游戏目前的两座大山先说第一座大山安装和启动。老游戏要跑起来靠的是光盘里的几百兆数据、注册表里的安装信息、系统安装的补丁有时还得有个物理光驱。到了今天“能不能玩”这个第一个门槛就拦住了一大部分人。你下载一个镜像、扒出数据、装好虚拟光驱还得手动调兼容模式一套组合拳下来多数人已经想关页面了。第二座大山联机。老即时战略的多人对战大多走的是局域网协议在当年这叫“串接线对战”或者局域网联机。现在人各在一处没有虚拟局域网原版联机基本就是物理层面断了。学校里同机房打一局很容易但天南海北的人想连上去门槛直接拉满。我选这个项目就是为了同时拆掉两座山。运行时在浏览器里数据文件由用户自己放进页面它不再需要安装程序网络部分我用浏览器的实时通道代替局域网红线启动和组队都在一个网页里完成。这个切口比单纯做怀旧模拟器更有意思的地方在于它不只是“让它能动”而是“让它在现代网络环境里还能社交”。1.2 浏览器运行时这个位置刚好卡在模拟器和容器之间曾经解决老游戏有两条路线一是纯模拟器把所有硬件虚拟出来但性能损耗大对图形和声音的还原也容易出偏差二是在本地创建一个完整的虚拟机或容器但这要求用户装一堆依赖背离了“点开即玩”的初衷。浏览器运行时走的是第三条不是全硬件模拟而是把系统调用做一层翻译。游戏本来要调用某个系统函数创建窗口、绘制位图、播放声音运行时把这些调用对应到浏览器的 WebGL、WebAudio和本地文件存储。这样性能开销比纯模拟低很多又能直接在网页上免安装运行。对即时战略这种画面不算极致、但逻辑非常复杂的类型来说这套方案简直像量身定做。除此之外浏览器运行时天然跨平台Windows、macOS、Linux、平板甚至手机只要浏览器支持 WebAssembly理论上都能跑。这在当初设计时是个加分项因为不用分别给三个操作系统各维护一个安装包。2. 编译链与兼容层把设计给 Windows 的程序请进网页2.1 搭桥工具Emscripten 与 WebAssembly整个方案的技术底座是 WebAssembly。它既不是脚本语言也不是新的框架而是一种能被浏览器直接执行的字节码规格。问题是游戏原本的 C/C 代码不能直接在 WebAssembly 上跑中间必须有编译器把它翻译过去。选择 Emscripten 的原因很直接它不仅能把 C/C 编译成 WebAssembly还把很多常见系统调用一起处理了。连 SDL 这类老游戏常用的图形/输入库都带了浏览器实现省了我大量重写时间。编译前要把游戏原本针对 Windows 的构建配置改一改主要是把原来链接的系统库替换成 Emscripten 提供的版本再设置好内存、文件系统等选项。我当时用的核心编译参数大致是emsdk install latest emsdk activate latest emmake cmake .. -DUSE_SDL2 -DCMAKE_BUILD_TYPERelease emmake make -j4 emcc game_sources.o -o ra2_browser.html \ -s WASM1 \ -s USE_SDL2 \ -s ALLOW_MEMORY_GROWTH1 \ -s EXIT_RUNTIME0 \ --shell-file shell_minimal.html解释一下几个关键参数的用意ALLOW_MEMORY_GROWTH1允许运行时按需扩展内存因为游戏越到后期单位和建筑越多内存占用不是一条直线EXIT_RUNTIME0表示游戏主循环结束后不立刻销毁运行时方便多人房间反复切进切出USE_SDL2会启用 Emscripten 自带的 SDL 支持省去每次自己处理键盘鼠标事件。2.2 文件系统让老游戏“假装”光盘还插着游戏跑起来的第一件事就是读盘。当年的数据文件几乎都是按光盘路径组织的有的模块一次打开几百个文件其中还夹杂着各种压缩包格式。浏览器没法直接给游戏一个盘符我就用 Emscripten 的虚拟文件系统搭了一座桥把用户放进去的游戏数据挂载到一个虚拟目录再让它成为游戏预设的光驱根路径。第一次尝试时我直接把所有文件一股脑塞进内存文件系统结果加载慢、内存占用高。后面改成按需读取数据文件仍然以压缩包形式留在浏览器本地存储里游戏访问某个文件时运行时再去解包、读入内存、交给游戏调用。这样首次加载快很多内存也稳住了。U盘拔掉这种事在这儿反而是正常的。2.3 图形与声音从 DirectDraw 到 WebGL从声卡到 WebAudio老版本的图形渲染走的是 DirectDraw直接在屏面上画各种位图讲究的是快、准、不啰嗦。浏览器里没有 DirectDraw我用 Emscripten 的 SDL 图形后端把它映射到 WebGL等于让老代码发出的每一句“我在屏幕上画这个点”都被实时翻译成一张纹理或一个三角形。大多数场景下效果是还原的但有一个坑老游戏默认的色深和分辨率很固定我们需要做一次画布缩放来匹配现代屏幕的宽高比否则画面要么拉伸变形要么四周黑边。声音部分更让我意外。老游戏的音效和音乐走的是声卡接口回放机制极其依赖系统底层的定时、混音。浏览器的 WebAudio 也有自己的一套时间模型两者之间必须做一层“按块填样板”的翻译。我在实现时把游戏的音频缓冲区改成了按固定大小提交每个提交周期恰好对应 WebAudio 的一帧处理时间。如果你本地试的时候发现音乐忽快忽慢多半是这层缓冲没有对齐而不是网速问题。3. 多人对战的关键老一代即时战略的同步协议怎么走出浏览器3.1 锁步同步技术的底子经典的即时战略多人对战绝大多数用的是“锁步同步”所有玩家各自运行同一个游戏逻辑但只保证接入网络的节点顺序一致。也就是说不通过网络传输整个战场状态只传输每个玩家在这一帧做了什么操作比如“造兵营”“让坦克往东移动”“选中三个单位”。每个节点收到大家的指令后以完全相同的顺序执行结果自然也是一样的。这属于一种用确定性模拟换带宽的做法特别适合几百个单位在场上跑的场景。缺点在于游戏逻辑必须保持确定性。如果有一帧两个玩家的计算结果不一致后面整个局势就开始出现“漂移”可能这头你看到坦克已经被打爆那头对手还觉得它活着。所以我在运行时里额外加了一个“帧哈希校验”每隔几帧把双方的指令队列摘要做一个校验一旦摘要不一致立刻暂停游戏并显示断线重同步提示。这个机制一开始是为排查问题加的后来发现它本身就是多人稳定性的一部分。3.2 原局域网协议在现代网络里的翻译层老游戏的联机协议是给局域网设计的里头有大量关于对方在线状态、房间列表、玩家昵称的广播。要接到浏览器上最直白的方法是写一个虚拟网卡把游戏跟局域网通信相关的系统调用全部接管翻译成浏览器的 WebRTC 数据通道消息。数据通道的特点是低延迟、双向、消息有序跟老式局域网通信在抽象层上很像但底层走的是用户数据报或者流式的传输策略。这里的难点不是“能不能发消息”而是“消息怎么组织”。老游戏联机时常常认为网络是可靠的、顺序的、且延迟在一个很小范围内但浏览器网络波动比局域网大得多。我的处理是游戏侧仍然按固定帧率生成操作指令指令缓存队列在网络拥堵时做延迟缓冲不强行追赶等待双方对齐之后再继续推进。同时把操作频率做了上限防止有人在浏览器端点一下鼠标产生几十条冗余指令反而拖垮同步。3.3 房间分配、信令与断线重连多人对战至少需要一个“大家怎么互相找到”的环节。浏览器里没有扫局域网广播我用一个最简的网页信令服务作为中转建房间的人在服务器拿一个随机房间码其他人输入这个码信令服务再把双方的浏览器连接信息互相交换等 WebRTC 通道建立成功信令服务就退到幕后不再接触游戏直播数据。做这套多人服务时我特意让游戏数据和信令数据分开原因很简单信令只负责初始握手延迟高一两秒无伤大雅游戏数据则是每帧都在跑绝对不能跟信令混在一起排队。断线重连则是另一个大刀阔斧的地方。老游戏天生没有这个设计网络断了就是断了。我在帧哈希校验的基础之上给每个玩家分了一个“同步界标”断线的人重新回到游戏时先要求其他玩家跳到最近的一个稳定界标再从那里继续。虽然会丢几秒的操作但比直接判负体验好太多。4. 尤里的复仇扩展包让资料片内容在浏览器里安家4.1 扩展包的导入与识别《尤里的复仇》不是单纯换一套皮肤它新增了战役、阵营、单位、音效和大量规则脚本。老版的安装方式是把新数据覆盖到原版目录运行时识别这些数据主要看文件清单和启动参数。我在浏览器版里做了两步第一步让用户把扩展包数据作为独立资源传入运行时扫描关键文件第二步在虚拟文件系统里把扩展包数据放到游戏目录之上使游戏优先读取扩展包的同名文件这样就成了天然的“后装”既保住了原版又不破坏扩展包自身的结构。实际操作中我碰到一个比较恼人的问题扩展包中的某些资源文件名与原版一致但内容格式不同游戏会先去读扩展包再决定是否覆盖原版内存里的对象。如果先后顺序错了轻则单位贴图花屏重则直接启动失败。后来我在扫描文件时额外读取了每个文件头部的格式签名逐个确认之后才挂载这个问题才算根治。4.2 战役、单位树和存档的兼容如果你只是想玩扩展包的遭遇战那加载路径很简单如果要把整个“尤里的复仇”战役打完运行时还需要额外处理脚本触发器和动画序列。老游戏的任务脚本是解释执行的它会在游戏内循环读取一堆触发器条件比如“某单位进入某区域”“计时器归零”“某个建筑被摧毁”每一个都对应到游戏内实体。浏览器运行时不影响这些逻辑但存档机制变成了一个隐患老游戏在本地写存档用的是绝对路径和扩展名浏览器环境里没有传统文件系统的概念。我把存档改为映射到浏览器的本地存储通过游戏自身的“读取/保存”界面做转换层。游戏仍然以为自己在读写一个本地文件实际底层已被替换成 IndexedDB 的记录。这个改法对原版和扩展包都通用唯一要注意的是存档版本如果扩展包改了游戏规则但没改版本号旧存档在浏览器端可能带进一些过期数据我就在读取时先把存档里的实体列表做一次合法性和版本双重校验避免“读档即崩”的尴尬。4.3 mod 加载顺便也打通了做完“尤里的复仇”之后我发现这套“文件覆盖虚拟目录优先级”的路子居然顺手把通用 mod 加载也打通了。老即时战略社区里存着一大批改单位数值、加阵营、换贴图的民间模组它们大多跟扩展包是同一套组织逻辑。原本我以为要针对每个 mod 做适配后来发现只要挂载时遵守“高位覆盖低位”的原则再把新的规则文件交给游戏本身去解释绝大多数流行 mod 都能直接跑。这里有个安全提醒凡是涉及注入数据、覆盖文件的操作运行时只认用户主动放入本地数据文件夹的内容不提供任何在线资源下载通道。这既是为了避免版权冲突也是为了保证多人在线环境里大家用的是完全一致的游戏版本不然一帧哈希校验立马爆炸。5. 实操避坑指南从编译到多人对战的几个真实问题5.1 编译链上的稳定军规编译阶段我印象最深的坑是 SDL 库的选择。Emscripten 自带 SDL 有好几套实现如果用错游戏界面会一片漆黑。我一开始用默认的 SDL1 兼容层结果鼠标键盘事件全部对不上号后来明确定位到 SDL2 的浏览器实现把事件轮询和渲染分开处理才开始稳定。还有内存问题。老游戏内部大量用裸指针而 WebAssembly 的内存模型是连续线性地址空间一旦越界就会立刻拉高整个浏览器沙箱的崩溃概率。我给游戏内存做了固定上限然后用编译参数预分配宁可多给一些页也不让运行时频繁动态扩展。动态扩展本身会触发垃圾回收停顿对战到一半突然卡一下那可比少几个单位严重多了。5.2 多人“不同步”的经典翻车现场我前前后后测试多人模式时遇到过无数次“两边看到的战场不一样”。排查过程写了几页纸最后基本归纳成三类一类是不同玩家加载的数据文件不一致常见于有人忘了传扩展包一类是随机数生成器的种子没有统一导致某些不明事件在不同机器上结果不同还有一类是指令包含浮点数各机器的浮点运算结果存在微差累积到几百帧后就形成了可见的偏差。针对第三类问题我的做法比较土但管用把所有联网游戏内使用的浮点中间结果全部强制转成整数存储或者在关键的随机事件上统一用同一个伪随机数生成器派生。即时战略游戏的单位坐标、伤害数值、建造队列在多数情况下用整数表示就够了浮点只存在于渲染层面。这样的约束可能听起来粗糙但确实让帧哈希校验的通过率从八成拉到了接近百分百。5.3 浏览器兼容速查表我给自己整理过一份速查表发出来给大家参考现象常见原因处理方式页面加载后黑屏图形后端映射失败画布没有初始化检查浏览器是否开启 WebGL重载页面并等一分钟音效断断续续WebAudio 缓冲与游戏帧率错位降低音频缓冲大小或把游戏帧率锁到 30/60 帧多人建房后别人进不来信令服务握手超时检查 WebRTC 连接类型NAT 严格的环境建议换网络读档后单位列表错乱存档数据新旧版本冲突清除浏览器本地存储重新导入游戏数据画面拉伸变模糊画布缩放算法太简单改用最近邻采样保留像素风鼠标光标飘移游戏内部坐标与浏览器坐标不一致启用指针捕获锁定鼠标在画布内以上每一条我都实测过前两条更是反复出现。尤其是黑屏问题第一次遇到的时候我还以为是编译参数写错查了半天才发现是用户浏览器默认关掉了 WebGL 硬件加速更新设置之后就正常了。6. 这一步做完之后的整体感受浏览器里跑经典即时战略听起来是怀旧做起来其实是工程。它不只是在 WebAssembly 上把一个老二进制盘活而是对“老程序如何在新环境里活下去”的一次完整实践。文件系统、图形接口、音效框架、网络协议每一个都是从上个时代的系统函数一路翻译到现代 API。这个过程里学到的不是某一个框架的用法而是如何在一个完全不同的运行环境里尊重老程序的原始逻辑再给它搭一座新的桥。我现在偶尔还会打开那个页面自己建一个“尤里的复仇”房间等朋友从另一边输入房间码进来。浏览器地址栏输入的那个瞬间这段代码本身就已经完全变了样。如果后面再有人往这个方向做我的建议是把重心放在“确定性”上只要同步不出乱子画质、加载速度、界面美化都是锦上添花。毕竟即时战略这类游戏真正的灵魂是“两边看到的战场必须一模一样”。浏览器支持 WebAssembly 这件事这几年已经非常成熟了未来完全可以把它扩展成一套通用的“老游戏浏览器运行时”框架让更多古典游戏靠一个网页就能重建当年的盛况。这条路还长但至少第一步我已经替各位蹚过来了。
阅读完成 · 觉得有帮助?
咨询建站