← 返回信息流
AI 资讯Hacker News·6 天前

OpenStrike:2004年掌机上的CS风格FPS现已发布

原标题:Shipping OpenStrike: A Counter-Strike-Shaped FPS on a 2004 Handheld

速览

OpenStrike是一款专为2004年手持设备设计的FPS游戏,其玩法与《反恐精英》高度相似,成功在低性能掌机上实现了流畅射击体验。该游戏展示了复古游戏开发的潜力,对怀旧玩家和硬件爱好者有一定意义。

AI 深度解读

在2004年掌机上运行《反恐精英》风格的FPS:OpenStrike深度解读

背景

2004年,索尼推出了PSP-1000掌机,其硬件规格在今天看来极为有限:单核MIPS CPU,主频333MHz,无乱序执行、无分支预测,最关键的是——没有JavaScript的JIT编译器,每个闭包每次帧执行都是解释型运行。内存仅32MB,用户程序可用24MB,甚至小于当前浏览器标签页的JavaScript堆内存。GPU是固定功能管线,无可编程着色器,仅能通过寄存器配置实现矩阵变换、颜色插值、纹理采样、深度测试和混合,输出分辨率为480×272,显存仅2MB。没有操作系统意义上的进程、虚拟内存、动态链接器,甚至没有console.log——整个游戏被编译成一个单独的可执行文件(EBOOT.PBP),运行即接管整机,崩溃即整机崩溃。

在这样的硬件上,今天有一款游戏诞生了:OpenStrike。它是一款单人FPS,使用《反恐精英》原版(GoldSrc时代)的地图,拥有机器人、弹道、后坐力、回合流程,以及一个用JSX编写的HUD(平视显示器)。回合规则用TypeScript编写,HUD是一个Solid应用,整个游戏在2004年的PSP上稳定锁定60帧,并且以单个文件形式发布,可直接放入Memory Stick。

核心内容

产品架构:两个“机器”的分工

OpenStrike的架构沿用了Web开发者熟悉的原生应用拆分模式:引擎用Rust,产品逻辑用JavaScript。

  • Rust侧openstrike-core + Pocket3D渲染器)负责所有必须每帧不丢帧的部分:玩家移动与碰撞、机器人AI、子弹、世界绘制。
  • JavaScript侧负责定义游戏规则:rules.ts包含回合流程、得分、武器和机器人调参表;hud.tsx是完整的HUD(生命值、弹药、准星、得分),是一个普通的Solid组件树,用Tailwind类样式,运行在PocketJS上。基础游戏不拥有任何Mod不具备的特权:修改rules.ts就相当于做了一个Mod。

嵌入可执行文件中的JavaScript引擎是QuickJS(Fabrice Bellard开发的完整ES2023引擎,编译后仅几百KB)。QuickJS之于PSP就像V8之于Node:宿主暴露strikeui两个API接口,同样的openstrike.js bundle在所有目标上启动。

帧循环:固定的时序

每秒60帧,每16.7毫秒(一个垂直空白周期):

  1. 读取手柄输入(pad → SimInput按钮+摇杆)
  2. 模拟更新(sim.tick(dt):移动、机器人、子弹)——Rust
  3. 一次QuickJS调用(strike.__dispatch(state, events))——JS
    • rules.ts反应并排队命令
    • hud.tsx仅重新渲染发生变化的绑定
  4. 排出命令到Rust核心(core.setPhaseconfigureWeapon等)
  5. 绘制(PVS→批次→GE)

数据流向下:普通的快照状态(hp、ammo、phase、aliveBots等)加上事件批次(hit、playerDied、roundReset)。意图向上:排队的命令。JavaScript每帧仅被咨询一次,且永远不会阻塞模拟——这正是PocketJS已经为UI实施的一帧一次跨边界策略,现在它掌控了游戏规则。

几何体打包器:为1999年的地图格式而生

GoldSrc时代的地图(《半条命》和原版《反恐精英》所用)不是三角形汤,而是BSP文件:通过二进制空间分割树预组织多边形,用平面递归分割世界,直到每个位置落入一个凸单元(称为叶节点)。该格式的关键特性是PVS(潜在可见集):对于每个叶节点,文件存储了预计算的、压缩的位集,记录从该叶节点出发可能看到的所有其他叶节点(无论通过门、越过箱子、任何角度)。计算PVS在1999年需要等级设计师的机器花费数分钟,而运行时只需微秒——这就像Web中的“锁定文件”式的遮挡剔除。

OpenStrike保留了这一1999年的精神,并做了现代扩展:将游戏机视为部署目标,在其前面加一个编译器。PSP无法在加载时解析BSP文件、解码纹理或重塑数据——这会消耗中间副本的RAM和CPU。因此,OpenStrike的构建步骤在笔记本电脑上运行pocket3d-cook,读取地图及其.wad纹理档案,输出一个.p3d文件:其字节恰好是PSP的GPU想读取的布局、对齐和字节顺序。

**“烘培”**是行业术语,指将运行时成本转移到构建时。OpenStrike的烘培非常激进:

  • 光照烘培到顶点颜色:GoldSrc地图自带小纹理阴影贴图(lightmap)。GE可以多纹理混合,但每个纹理单元都消耗带宽。烘培器将每个多边形细分为网格,将lightmap采样到顶点颜色通道中。GE免费对三角形进行颜色插值,因此阳光、阴影边缘和dust2的橙色光芒每帧无需任何成本——这是光照的内联。
  • 位置量化到16位整数:内存和GPU读取带宽都是浮点的一半。这解锁了本次移植最喜欢的硬件考古发现:在3D模式下,GE会静默地将整数顶点归一化(除以32768),这一行为在任何文档中都没有记载。反制措施是在模型矩阵中将世界缩放回32768倍——通过阅读模拟器源代码确认(在该平台上,模拟器源代码就是开发者文档)。
  • 纹理转换为CLUT8:8位索引到256色调色板,大小仅为RGBA的1/4。调整到2的幂次大小(GE硬性要求),进行swizzle(按GPU纹理缓存喜欢的瓦片模式重新排序,类似于“结构体数组,针对纹素”),并生成完整mip链(预缩小版本用于远处表面)。

所有这些构建时的执着换来了一个不存在的加载画面:.p3d文件被直接链接进可执行文件,加载时无需解析,直接映射到GPU希望的内存布局。

具体实现细节

  • 帧缓冲渲染:原生480×272输出,每个截图都是PSP可执行文件自己的帧缓冲,在模拟器中捕获。
  • 开源地址:pocket-stack/open-strike,是Pocket运行时家族(PocketJS底层架构)的第一个游戏,旨在证明“Web栈的易用性,但没有Web栈的累赘”可以扩展到实时3D游戏,且硬件早于iPhone。
  • 本文面向浏览器JavaScript开发者,无需嵌入式或图形学背景。

关键要点

  • 平台极限:在2004年的PSP(333MHz MIPS CPU,32MB RAM,2MB VR
查看原文 →pocketjs.dev