星际防线:一款浏览器 3D 射击游戏的完整技术解剖
浏览器里跑 3D 射击游戏?以前这话会被前端工程师当笑话听。WebGL + Canvas 能渲染个旋转立方体就算不错了,大规模场景、实时碰撞、敌人 AI、受击反馈——这些向来是 Unity 和 Unreal 的地盘。
但这个项目告诉我:时代变了。
星际防线(kimi_fps_release)是一款完整的浏览器端第三人称射击游戏,5 个关卡、敌人 AI 状态机、武器系统、计分系统、医疗包、后处理效果——全部在浏览器里运行,不需要下载任何客户端。更关键的是,它用的是纯前端技术栈:React Three Fiber + TypeScript + Zustand,没有一行 C++。
这不是 Demo,不是概念验证。这是能玩的游戏。
一、架构选择:为什么用 React 做游戏?
很多人第一反应是:React 不是做 UI 的吗?拿它写游戏是不是选错工具了?
答案是:看场景。
传统游戏引擎(Unity/Unreal)的架构是帧驱动的——每帧按固定顺序执行:输入 → 物理 → 逻辑 → 渲染。而 React 是状态驱动的——状态变了,UI 自动更新。这两种范式打架吗?在 3D 场景里确实会。
但 React Three Fiber(R3F)做了件聪明的事:它把 Three.js 的渲染循环桥接到了 React 的组件树里。useFrame 钩子在每帧被调用,你可以在里面写任何 imperative 的代码,同时保留 React 组件化组织场景的能力。
结果是:场景组织用 React 组件树,每帧更新用 useFrame,全局状态用 Zustand。三者各司其职,不打架。
// 敌人系统就是一个 React 组件
function EnemySystem() {
const enemies = useGameStore((s) => s.enemies)
useFrame(() => {
// 每帧更新 AI 逻辑
})
return (
<group>
{enemies.map((enemy) => (
<EnemyUnit key={enemy.id} enemy={enemy} />
))}
</group>
)
}
这种架构的优势是开发效率。你想加一种新敌人?写个组件,往场景树里一插就行。React 的组件化心智模型在这里意外地好用。
代价也有:性能天花板比原生引擎低。但 R3F 做了不少优化——React 的调和过程只在属性变化时触发,Three.js 的渲染循环还是原生的,中间没有额外开销。
二、敌人 AI:从状态机到受击反馈
这个项目的 AI 系统比我想象的精致得多。不是简单的"朝玩家走",而是完整的五状态机:
1. 巡逻(Idle)——未警觉时原地漂浮,带微妙的正弦波动画 2. 警戒(Alert)——玩家进入 alertRange 后触发,切换到追击 3. 追击(Chase)——持续朝玩家移动,带跳跃运动 4. 攻击(Attack)——距离 ≤ 2 米时停止移动,触发攻击(1.5s CD) 5. 死亡(Dead)——血条清空后播放漂浮上升 + 放大 + 淡出动画
状态切换的代码很直接:
// 警戒检测
if (!enemyData.isAlerted && distToPlayer < levelConfig.alertRange) {
useGameStore.getState().alertEnemy(enemy.id)
}
// 追击逻辑
if (enemyData.isAlerted && distToPlayer > ATTACK_RANGE && !isStunned) {
const dir = playerPos.clone().sub(enemyPos).normalize()
// ...移动并更新位置
}
// 攻击逻辑
if (enemyData.isAlerted && distToPlayer <= ATTACK_RANGE && !isStunned) {
if (timeSinceLastAttack >= ATTACK_COOLDOWN) {
useGameStore.getState().hitPlayer(ENEMY_DAMAGE)
useGameStore.getState().setEnemyLastAttackTime(enemy.id, now)
}
}
但真正让我眼前一亮的不是状态机本身,而是受击反馈系统。很多 indie 游戏甚至商业游戏都忽略了这块——敌人中弹后只是血条减一下,没有体感。
这个项目做了全套:
1. 僵直(Hit Stop)
hitStunUntil: performance.now() + 300 // 0.3s 冻结
中弹后 0.3 秒内敌人停止移动和跳跃,子弹方向决定了身体倾斜角度。这叫hit-stop,格斗游戏和动作游戏的核心手感技术。
2. 身体倾斜
hit.leanAngle = -(0.52 + Math.random() * 0.26) // 30-45度后仰
modelGroupRef.current.rotation.x = hit.leanAngle
中弹瞬间身体向后倾斜,方向由子弹来向决定。不是写死的动画,是程序化生成的。
3. 头部后甩
hit.headSnap = (0.5 + Math.random() * 0.3) * (1 - timeSinceHit / 300)
headBoneRef.current.rotation.x = hit.headSnap
通过骨骼动画让头部产生剧烈后仰,随时间衰减。这需要找到 GLB 模型里的 head bone,实时操作。
4. 红色闪烁
mat.emissive.setRGB(hit.flashIntensity, 0, 0)
mat.emissiveIntensity = hit.flashIntensity * 2
全身材质发出红色自发光,强度随时间衰减。
5. 身体变形
// X 轴压缩 20%,Y 轴拉伸 20%
hit.scaleX = 1 - 0.2 * (deformPhase / 0.3)
hit.scaleY = 1 + 0.2 * (deformPhase / 0.3)
中弹瞬间身体被"压扁"再弹回,像果冻一样。这细节太狠了。
6. 蓝色血雾
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color('#4488ff'),
transparent: true,
opacity: 0.8,
depthWrite: false,
})
// 8-12 个粒子,带重力、速度、生命周期
中弹点生成蓝色血雾粒子,带重力下落和淡出。不是贴图,是实时生成的粒子系统。
这套反馈系统让每一次射击都有物理存在感。你打中敌人,敌人真的会"疼"。
三、自定义 GLSL 血条:三段式着色器
血条 UI 通常用 HTML/CSS 或 Canvas 2D 做,但这个项目选择用3D 空间中的 Billboard 平面 + 自定义 GLSL 着色器。为什么?
因为 HTML 覆盖在 WebGL 上会有层级问题,而且不能随 3D 场景自动缩放和透视。Billboard 平面始终在屏幕面向,完美解决。
更绝的是血条不是简单的"绿色变红色",而是三段式视觉效果:
// 三段区域
if (p.x <= fillX) {
// 填充区:正常颜色(绿/黄/红渐变)
} else if (p.x <= trailX) {
// 伤害轨迹区:暗色,随时间收缩
} else {
// 虚空区:纯黑
}
当敌人受到伤害,血条不是直接跳到新值,而是:
- 填充区(uFill):快速跟上当前血量(8倍速 lerp)
- 伤害轨迹区(uTrail):慢速跟上,留下一段暗色"伤痕"
- 颜色过渡:从绿到黄到红,慢速渐变
四、物理系统:跳跃不是简单的位移
这个项目的跳跃系统用了完整的抛物线物理:
const JUMP_HEIGHT = 1 // 米
const JUMP_TIME_UP = 0.883 // 秒
const JUMP_GRAVITY = (2 * JUMP_HEIGHT) / (JUMP_TIME_UP * JUMP_TIME_UP)
const JUMP_V0 = JUMP_GRAVITY * JUMP_TIME_UP
// 上升阶段
jumpY = JUMP_V0 * t - 0.5 * JUMP_GRAVITY * t * t
// 下降阶段
jumpY = JUMP_HEIGHT - 0.5 * JUMP_GRAVITY * td * td
不是 y += 0.1 然后 y -= 0.1 这种儿童物理,而是基于重力加速度公式计算。上升和下降时间相等(0.883s),对称的抛物线,手感可控。
敌人也有独立跳跃系统——追击时会不断跳跃,增加移动的不规则性,让玩家更难瞄准。
五、碰撞检测:AABB 简单但够用
敌人之间的碰撞用的是AABB(轴对齐包围盒),代码很简洁:
function aabbOverlap(ax, az, bx, bz, hw, hd) {
const dx = bx - ax, dz = bz - az
const overlapX = hw * 2 - Math.abs(dx)
const overlapZ = hd * 2 - Math.abs(dz)
// 沿最小穿透轴推开
if (overlapX < overlapZ) {
return { overlaps: true, pushX: sign * overlapX * 0.5, pushZ: 0 }
}
// ...
}
O(n²) 两两检查,注释写得很实在:"fine for ~20 enemies"。确实,20 个敌人每帧 400 次检查,浏览器轻松扛得住。
六、关卡设计的数学美学
5 个关卡的参数设计有规律:
| 关卡 | 地图 | 敌人数量 | 速度 | 血量 | 击杀目标 | 敌人缩放 |
|---|---|---|---|---|---|---|
| 1 | 50×50 | 8 | 1.5 | 100 | 10 | 1.0× |
| 2 | 100×100 | 12 | 2.0 | 120 | 12 | 2.0× |
| 3 | 150×150 | 16 | 2.5 | 150 | 15 | 4.0× |
| 4 | 200×200 | 20 | 3.0 | 200 | 18 | 8.0× |
| 5 | 250×250 | 24 | 3.5 | 250 | 20 | 16.0× |
这不是随意填的数字,是指数难度曲线设计。地图线性增长,但感知范围和视觉压迫感是指数增长,确保每关都有新的紧张感。
七、性能优化:在浏览器里跑 60fps
3D 游戏在浏览器里的头号敌人是性能。这个项目做了不少取舍:
<Canvas
dpr={[1, 1.5]} // 限制像素比,避免 Retina 屏爆显存
gl={{
antialias: false, // 禁用 MSAA,用后处理代替
toneMapping: 3, // ACES Filmic,好看且快
powerPreference: 'high-performance',
}}
camera={{ fov: 70, near: 0.1, far: 200 }}
>
后处理只开了 Vignette(暗角),注释写得很清楚:"Bloom is too expensive for web FPS games"。这取舍是对的——暗角营造氛围,Bloom 在浏览器里确实太烧了。
效果管理也有上限:
- 弹道轨迹:最多 10 条
- 弹壳:最多 20 个
- 火花:最多 200 个
- 通知:2 秒后自动清除
bulletTrails: [...].slice(-10), // 只保留最近10条
shells: [...].slice(-20), // 只保留最近20个
sparks: [...].slice(-200), // 最多200个粒子
这些 hard limit 是性能保险,防止粒子系统无限累积拖垮帧率。
八、音频系统:懒加载 + 变体循环
footsteps 用了真实的 MP3 文件,不是合成的:
class FootstepAudio {
private buffers: AudioBuffer[] = []
private nextIdx = 0
async load() {
const promises = Array.from({ length: 6 }, (_, i) =>
fetch(`/audio/step_walk_${i + 1}.mp3`)
.then((r) => r.arrayBuffer())
.then((arr) => ctx.decodeAudioData(arr))
)
this.buffers = await Promise.all(promises)
}
play(isRunning: boolean) {
src.buffer = this.buffers[this.nextIdx]
this.nextIdx = (this.nextIdx + 1) % this.buffers.length
src.playbackRate.value = isRunning ? 1.15 : 1.0
}
}
6 个不同的脚步声音频循环播放,防止机械重复。奔跑时音高加快(1.15×)、音量增大。懒加载设计——第一次用户交互时才加载音频,不浪费初始带宽。
九、连击系统:让射击有节奏感
if (now - state.lastComboTime < 3000) {
newCombo = state.combo + 1
} else {
newCombo = 1 // 超过3秒断连
}
if (newCombo >= 10) newMultiplier = 3.0
else if (newCombo >= 5) newMultiplier = 2.0
else if (newCombo >= 3) newMultiplier = 1.5
3 秒窗口期,3 连击 1.5 倍,5 连击 2 倍,10 连击 3 倍。这个设计来自经典射击游戏(Doom、Quake),它强迫玩家进入节奏状态——不是瞎射,而是有节奏地精准击杀,维持连击链条。
十、写在最后:浏览器游戏的未来
这个项目让我重新思考"浏览器能做什么"。以前总觉得浏览器游戏 = 2D Canvas 小游戏,但 React Three Fiber + Three.js 的组合已经能支撑相当复杂的 3D 体验了。
当然,它不会替代 Unity 或 Unreal 做 3A 大作。但对于以下场景,它是更好的选择:
- 即时可玩:发个链接就能玩,零下载
- 快速迭代:前端工具链(Vite HMR、TypeScript、ESLint)比游戏引擎轻快得多
- Web 原生集成:排行榜、社交分享、支付——浏览器的生态优势
- 跨平台:Windows、Mac、Linux、Android、iOS,一个代码库全跑
如果你想研究"如何用前端技术做 3D 游戏",这个项目是极好的参考。
GitHub: https://github.com/WhereIsHeroFrom/kimi_fps_release
#游戏开发 #ReactThreeFiber #WebGL #浏览器游戏 #3D游戏 #TypeScript #游戏AI #游戏设计
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens