作者 | Kuber Mehta 编译 | 苏宓
出品 | CSDN(ID:CSDNnews)
自 1993 年问世以来,《DOOM》(毁灭战士)早已超越了一款经典射击游戏的范畴,而是成为“极限移植”的代名词。从烤面包机、MacBook Touch Bar、智能冰箱,到我们此前报道过的 PDF 文档、TypeScript 类型系统,它几乎无处不在——“It Runs Doom”(万物皆可跑 DOOM)也因此成为一代程序员的狂欢梗。
最近,这一狂欢再度升级:一位开发者竟然将一款可玩的《DOOM》风格游戏完整封装进一个二维码中,扫码即可在浏览器中直接启动,无需下载,无需安装。
整个开发仅耗时一周,旨在突破了二维码在存储和压缩方面的极限,展示了将轻量级 Web 应用完全托管于二维码内的全新可能。
而更令人惊讶的是——整个游戏大小还不到 3KB。
目前,此项目已在 GitHub 上开源:https://github.com/Kuberwastaken/backdooms。二维码如下:
值得注意的是,iPhone、Android 等智能手机无法直接扫描,其只适配于浏览器端,需要借助在线二维码扫描器(https://scanqr.org/),扫描之后的游戏界面如下:
从荒诞构想到技术落地
这个项目由开发者 Kuber Mehta 发起,挑战是在不超过 3KB 的数据限制下,尽可能还原《DOOM》的游戏体验。
为什么是 3KB?
Kuber Mehta 解释说,这是二维码所能容纳的最大文本或二进制数据量。为了对比,原版《DOOM》中“机枪”(chaingun)贴图就需要约 1.2KB,而这次项目的总容量预算,仅为其两倍多一点。时下,Kuber Mehta 要在这个限制内完成整款游戏的逻辑与视觉呈现,可谓是极限挑战。
之所以会萌生这样的想法,是由于几年前 Kuber Mehta 偶然刷到一位 YouTube 博主 matttkc 发布的一条 YouTube 视频,其曾提出一个疯狂问题:“你能把一整个游戏塞进一个二维码里吗?”
这个设想从此深埋 Kuber Mehta 心底,但他一直没付诸实践——“因为我觉得自己可能不够聪明。”Kuber Mehta 自嘲道。
直到不久前,这个想法再次浮现,他决定不再犹豫,动手一试。可真正开始后,他很快发现:网上几乎找不到现成的 HTML 版本的 DOOM 实现可供复用。
资源有限的情况下,他退而求其次:“制作一个可玩的、受《Doom》启发的游戏,其大小不超过三段纯文本。”
正如他自己总结的:
“真的太难了,因为我的条件只有这些——
没有游戏引擎,只能写原生 HTML/JS;
没有素材资源,所有图形都得代码生成;
不能用任何库,因为每个字节都弥足珍贵。
但也正因如此,我学到了太多。”
所以它其实并不是《Doom》本尊——但 Mehta 的游戏确实很有《Doom》的味道。除了从 1993 年原版射击游戏中汲取灵感外,他还参考了迷因式的“迷离空间”恐怖故事 The Backrooms,于是他的项目被命名为“the backdooms”。
「这是不是一个愚蠢的决定?也许是。但结果酷不酷?毫无疑问——酷炸了!」,Kuber Mehta 说道。
极限代码压缩:每一个字节都算数
游戏是用 HTML 编写的,Mehta 必须让每个字符都物尽其用。
现实来看,要在二维码的 2,953 字节容量(QR Code Version 40)内嵌入一款游戏,他需要用到所谓的“压缩技术”,更准确说,是极限压缩。
Kuber Mehta 以一段游戏代码代码为例,展示了他具体的做法:
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;overflow:hidden;background:#000;cursor:crosshair}canvas{width:100vw;height:100vh}</style></head><body><canvas id=c></canvas><script>M=Math,c=document.getElementById("c"),c.width=320,c.height=240,h=c.getContext("2d"),x=4,y=4,a=0,H=100,am=25,rc=0,fl=0;f=(i,j)=>(Math.abs(i-4)<4&&Math.abs(j-4)<4)?"0":((((i+1000)%7)==3||((j+1000)%7)==3)?"0":(Math.random()<.05?"1":"0"));e=[{x:5,y:4,h:100},{x:4,y:5,h:100}],k={};onkeydown=e=>k[e.key]=1;onkeyup=e=>k[e.key]=0;onclick=_=>{if(am){am--;fl=2;rc=.2;e.forEach(o=>{d=M.hypot(o.x-x,o.y-y),r=M.atan2(o.y-y,o.x-x);if(d<5&&Math.abs(r-a)<.3)o.h-=50})}};R=_=>{rc=Math.max(0,rc-.02);fl=Math.max(0,fl-1);e=e.filter(o=>o.h>0);h.fillStyle="#000";h.fillRect(0,0,320,240);k.ArrowLeft&&(a-=.1);k.ArrowRight&&(a+=.1);m=.1;
如果你没看到 <!DOCTYPE html>,你可能都不敢相信这居然是 HTML。
再看一个例子,原始代码:
function drawWall(distance) { const height = 240 / distance; context.fillRect(x, 120 - height/2, 1, height); }
压缩后:
h.fillRect(i,120-240/d/2,1,240/d)
其中变量名都被压缩为单个字符,注释也完全消失。“从最终的代码看上去,有点像《Doom》里的恶魔挨了爆头一样令人震撼。”
地图生成机制
在游戏设计方面,起初,Kuber Mehta 打算采用 16x16 或 8x8 的固定小地图设计,这种尺寸在一个超小型游戏中本就算得上“合理”。但他并不满足于此,更希望玩家能获得“真正可玩”的体验。
因此,他决定引入无限地图生成,并辅以“种子(Seed)”机制。
这一做法与《Minecraft》中世界生成的思路类似:一个看似随机的字符串,就能决定整个游戏世界的布局。
Kuber Mehta 利用了这一原理,让玩家理论上可以通过特定种子值生成心仪的地图,并在代码中硬编码,从而每次运行都得到一致的世界:
SEED = Math.random() * 100;
在图形表现方面,Kuber Mehta 没有采用 WebGL 或 Canvas 的高级图形接口,而是回归原点,用了《DOOM》早期版本(1992 年)使用的光线投射(Raycasting)技术。这种方法以极低的资源消耗,模拟出类似 3D 的视觉效果。
其实现方式是:对屏幕上每一列像素投出一条方向略有偏差的射线,并计算这条射线与墙体的交点距离。墙体越近,绘制的柱状图形就越高,最终呈现出纵深感。
简版代码如下:
for (let i = 0; i < 320; i++) { const rayAngle = playerAngle + (i - 160) / 500; let distance = 0; while (!isWall(x + distance * cos(rayAngle), y + distance * sin(rayAngle))) { distance += 0.1; } drawColumn(i, distance); }
虽然这一逻辑只涉及基础的三角函数,但它在整个代码体积中占据了相当大的比重。Kuber Mehta 坦言,如果不是为了动态生成地图,他可能早就选择直接将 HTML 页面 Base64 编码后塞进 URL。但现在看来,这份坚持是值得的。
敌人机制
相比图形渲染,敌人系统的实现则更加艰难。项目早期版本中,敌人数量固定,分布于地图初始区域。玩家一旦走远,地图就变得空空如也——这在小地图中还能接受,但在无限生成的大地图中,问题立刻凸显。
Kuber Mehta 回忆,这部分开发过程“非常头痛”。在体积受限的前提下,想做出像样的射击反馈与敌人 AI,几乎是不可能完成的任务。他也坦言自己并不是游戏开发出身:“我……确实不太会做游戏。”
起初,敌人是完全静止的。后来,他陆续加入了简单的追踪机制,并最终实现了一个核心改进:玩家在移动时,系统会在其附近随机生成敌人,从而让整个地图始终保持紧张感和挑战性。
if ((k.ArrowUp || k.w || k.ArrowDown || k.s || k.ArrowLeft || k.ArrowRight) && e.length < 10 && Math.random() < .01) { t = Math.random() * 6.283; Rdist = 1 + Math.random(); X = x + M(t) * Rdist; Y = y + N(t) * Rdist; f(~~X, ~~Y) == "0" && e.push({ x: X, y: Y, h: 100 });}
完成以上版块之后,Kuber Mehta表示,“制作游戏只是挑战的一半,因为真正的挑战是将其放入二维码中。”
概念与可行性
标准最大的二维码(Version 40)最多只能容纳 2,953 字节,也就是大约 2.9KB。这个容量有多小?做个对比:
一个仅 1/15 秒的 Windows 音效文件,就已经 11KB
一张传统软盘(1.44MB)理论上可以存下将近 500 个这样的二维码
面对如此限制,Kuber Mehta 的首个版本大小达到了 3.4KB,超标严重。他的第一反应是:“完蛋了。”
为了解决这个问题,他连续四天进行疯狂压缩与优化,最终将游戏压缩至 2.4KB,虽然过程中做出了一些艰难但必要的取舍。
殊不知,一段折腾之后,更难的还在后面。
要知道二维码只能存储文本或二进制数据,而 HTML 并不属于这两种类型。直接嵌入 HTML 代码并不可行。
对此,不少开发者建议采用 Base64 编码,但这带来了 33% 的体积开销,几乎会吃掉本就所剩无几的存储空间,意味着实际可用空间不到 1.9KB。
在反复尝试后,Kuber Mehta 一度陷入绝望,甚至想要放弃。
后来,他连续两天轮番咨询 ChatGPT、DeepSeek 和 Claude 等 AI 模型,尝试上百种提示词,几乎每次都得到建议:“干脆托管在网站上会更容易。”直到某次,ChatGPT 随口提到了一句:DecompressionStream。
这成了转折点。
DecompressionStream 是一个现代浏览器支持的 Web API,能够解压 gzip 格式的数据流,简而言之,相当于浏览器内置了“WinRAR”。
Kuber Mehta 形容自己当时“仿佛被雷劈中”,豁然开朗。他确信:在尝试了几乎所有极限游戏优化技巧后,这是当前环境下唯一可行的解决方案。
整个游戏压缩嵌入流程如下:
输入 HTML 内容
进行 Gzip 压缩(最大压缩等级)
将压缩数据进行 Base64 编码
嵌入一个自解压的 HTML 包装器
转为 data:URI
尝试生成二维码(使用最高版本)
如果仍超出体积限制
→ 回头继续压缩优化
成功生成二维码!
Kuber Mehta 为此还编写了一个 Python 脚本自动化完成上述流程,期间测试了 34 个不同版本,不断调试压缩策略,最终,二维码成功诞生。
未来的可能性
受以上种种限制影响,the backdooms 的画面非常有限,你只会在灰色墙壁之间躲避红眼矩形,但它传达的“感觉”是到位的。
在 Kuber Mehta 看来,这不仅是一个小游戏的诞生,更是一个技术可能性的证明。他将这个项目(现已开源在 GitHub,https://github.com/Kuberwastaken/backdooms)视为一种宣言:
只要压缩得当,再加上 AI 助力,即便是在二维码这种极限载体上,也能运行真正的交互式游戏。
尽管不适合开发复杂项目,但这个方向打开了许多令人兴奋的应用场景。Kuber Mehta 表示,这种想法也可以在未来应用在更多场景中,如:
离线游戏传播:可通过海报、传单分享
极限编程展示(Demoscene):作为编程创意作品展示平台
低带宽环境下的教育小游戏:无需联网即可运行,具备实用性与趣味性
来源:
https://github.com/Kuberwastaken/backdooms?tab=readme-ov-file
https://kuber.studio/blog/Projects/How-I-Managed-To-Get-Doom-In-A-QR-Code
https://www.pcgamer.com/hardware/behold-a-single-qr-code-containing-a-miniaturized-take-on-doom-literally-the-entire-game/
https://news.ycombinator.com/item?id=43729683
推荐阅读:
▶全球裁员6000+人!这一次,连10年TypeScript老兵、AI总监都被微软“优化”掉了
▶腾讯出手了! 悄悄推出 AI 编程助手 CodeBuddy,开发者可以免费体验到国产 Cursor 了!
▶每天省5000美元、18PB数据大迁徙!Ruby On Rails之父冲刺“下云”的最后49天
CSDN创始人蒋涛「对谈」 浙大求是特聘教授方兴东。作为中国互联网30年的见证者,从鸿蒙操作系统的艰辛破晓之路,聊聊中国高科技的崛起大时代。华为数百人深度分享的技术传奇《鸿蒙开物》来了!直播间抽福袋,领「限定签章版」。