一个纯粹的安卓第一人称枪战游戏。零第三方依赖,零素材文件,零权限。
打开就能玩,不需要账号、不需要联网、不申请任何权限——不是「承诺不上传」,是技术上没有权限可用。
一个会追赶、会开火、会找掩体的敌人 AI,加上一套完整的局内循环:
敌人
- 四状态 AI:巡逻 → 追击 → 开火 → 躲回掩体
- 会朝你射击,命中率随距离和波次变化
- 每 3 波出现一次精英:血量 2.6 倍、伤害更高、外观带橙色护板
- 血量越低越红,一眼看得出该先打谁
战斗
- 爆头伤害 2.4 倍,命中数字单独显示
- 连杀:3.5 秒内连续击杀叠倍率,2 连 200 分最高叠到 600
- 三种武器,每 3 波轮换,手感各不相同
武器 弹匣 射速 单发伤害 特点 突击步枪 30 0.09s 34 全能 霰弹枪 8 0.72s 22×7 近战清场 狙击枪 5 1.10s 130 2.6 倍镜,一枪一个
生存
- 生命 100 + 护甲 100,护甲先扛 70% 伤害
- 护甲靠补给获得,脱战后自动回血
- 弹药箱 / 医疗包由敌人掉落,走过去自动拾取
- 受伤时屏幕边缘泛红 + 一圈方向弧,告诉你子弹从哪来
进度
- 波次递增:敌人更多、更厚、更快、更准
- 每波换一张程序生成的场地(共 3 张布局)
- 结算评级 S / A / B / C / D,由得分、波次、命中率、连杀、爆头共同决定
- 历史最高波次本地存档(SharedPreferences,不联网)
| 位置 | 动作 |
|---|---|
| 左半屏拖动 | 移动(虚拟摇杆) |
| 右半屏拖动 | 转视角 |
| 右下角大按钮 | 开火(按住连射),死亡后重开 |
| 右下角左侧三个圆钮 | 开镜 / 蹲下 / 跳跃 |
| 左上角 ⏸ | 暂停(切后台也会自动暂停) |
画面全部程序生成,仓库里没有任何 png / jpg / 模型 / 音频文件:
- 天空是一个真正的球体,带上下渐变的着色器,还有一个会反光的太阳位置
- 距离雾让远处几何体融进背景,而不是硬切一条边
- 每个敌人脚下有一块贴地的深色四边形当阴影
- 敌人由躯干、头、双腿、护板多个部件拼成,走路有摆动
- HUD 全部用 Canvas 画:血条护甲条、命中飘字(带黑描边)、右上角小地图、准星随散布张合、开镜后的瞄准环
音效是在内存里算出来的 PCM 波形,走 AudioTrack 播放:
- 枪声是低频正弦 + 白噪声,按包络衰减,三种武器音色不同
- 命中、击杀、爆头各是一段不同频率的上扫音
- 换弹是两下机械噪声,受伤是低通噪声,拾取是上扫正弦
这样既保住了「零素材」这条线,又不用把 wav 塞进仓库。
直链固定指向最新一版,不用在 Releases 页面里翻。也可以去 Releases 挑具体版本。
- 最低 Android 7.0(API 24),目标 API 35
- 支持任意 ABI(纯 Java + OpenGL ES 2.0,无 native 代码)
- 用仓库内的演示 keystore 签名,可直接安装
下载后如果系统提示「未知来源」,需要在设置里放行一次——APK 不是从应用商店来的, 这是安卓的默认行为,不是这个包有什么问题。
也可以自己构建:
echo "sdk.dir=/path/to/android-sdk" > local.properties
./gradlew assembleRelease需要 JDK 17 与 Android SDK 35。
app/src/main/java/com/icewocker/gunfire/
├── MainActivity.java 唯一 Activity:GLSurfaceView + HUD 叠层
├── audio/
│ └── SynthSound.java 程序合成的 PCM 音效
├── engine/ 渲染层,不碰游戏规则
│ ├── Vec3.java 三维向量
│ ├── Mat4.java 4x4 矩阵(投影 / 视图 / 模型变换)
│ ├── Shader.java GLSL 程序编译与 uniform 定位
│ ├── Mesh.java 三角网格(立方体、平面、球、圆柱)
│ └── Renderer.java GLSurfaceView.Renderer 实现,兼游戏循环
└── game/ 规则层,不碰 Android
├── GameWorld.java 世界状态、敌人 AI、物理、射线检测、计分
├── HudView.java Canvas 绘制的 HUD 与所有面板
└── TouchControls.java 触屏操作映射
关键的一条边界:GameWorld 不 import 任何 android.*。
这不是洁癖,是为了让游戏规则能在普通 JVM 上跑测试。app/src/test/ 下的
61 个用例覆盖了射线检测、敌人 AI、伤害结算、波次推进、移动碰撞、视角钳制、
矩阵运算——它们不需要模拟器,毫秒级跑完。
渲染层每帧从 GameWorld 读只读快照,输入层往里写事件,两者之间没有锁。
音效层也不碰规则:它盯着分数类计数器的增量来决定放哪个音,规则层里一行音频代码都没有。
打 tag 即发布,CI 会自动构建两条 APK 并挂到 Release 上:
git tag -a v1.1.0 -m "GunFire v1.1.0"
git push origin v1.1.0流水线做的事:单测 → Lint → 构建 debug/release → apksigner verify 验签 → 上传产物 → 发 Release。
仓库的 Actions → General 里 Workflow permissions 必须是 Read and write,
否则发 Release 那一步会以 Resource not accessible by integration 失败——构建是好的,只是没权限挂附件。
./gradlew testDebugUnitTest61 个用例,覆盖:
- 射击消耗弹药、弹匣打空自动换弹、换弹从备弹补充、换枪不吞备弹
- 射线命中敌人 / 被墙挡住 / 忽略已死目标 / 平行射线不产生假命中
- 爆头伤害不低于躯干、命中未击杀也给分、连杀窗口到期归零
- 敌人会移动、留在场内、状态机不越界、射程内会打中玩家
- 护甲分流伤害、脱战回血、零血绝不被回血拉起来、结算评级
- 移动不越出场地边界、被立柱阻挡、蹲下更慢、跳跃必须落回地面
- 暂停冻结世界、重开清空进度但保留历史最高波次
- 弹匣与备弹同时为空必须结束一局
- 矩阵运算有限性、单位矩阵乘法、零向量归一化不产生 NaN
这个项目从零写完,编译通过不代表能玩。以下问题都是写测试时暴露的:
1. 面向 -Z 时 forward 算成了 +Z
Camera.forward() 里 yaw 的符号写反,导致视角和移动方向不一致——按下前进键会倒着走。
2. 偏航角只做单次回绕
// 错的:一次给 10000 像素增量就残留到 1571 弧度
if (yaw > Math.PI) yaw -= 2 * Math.PI;
// 对的:必须循环归约
while (yaw > Math.PI) yaw -= 2 * Math.PI;
while (yaw < -Math.PI) yaw += 2 * Math.PI;3. 立柱挡在自己的射击走廊上
场地立柱按 x = -18, -12, ... , 18 均匀排布,正好有一颗立在 x=0。
玩家出生在中轴,向前直射先撞上这颗柱子——站着不动都打不中任何东西。
去掉中轴那颗,并留了一条常驻回归用例守住。
4. 散布角比目标张角还大
散布幅度是 spreadHeat * 0.035 弧度,而 20 米外目标半高只对应 0.0146 弧度——
枪口比靶子宽 2.4 倍,连射必脱靶。实测 5 个随机种子共 300 发,命中 0 次。
收敛到 0.012 以内,后坐力同步收敛。
5. 矮箱子把玩家挡死了
碰撞检测判断条件写反,所有高度大于零的方块都拦路。改成只考虑高于膝盖、低于头顶的实体。
6. 血量归零会被回血拉回来
update() 里先 regen 再判死亡。挨 1000 点伤害后血量归零,一帧后 regen 把它拉到 0.2,
死亡判定永远不成立——玩家变成打不死的。现在死亡判定提到恢复逻辑之前,并且
regen() 见到零血直接返回。zeroHealthNeverComesBack 守住这条。
7. 回血条件写反了
条件是 sinceDamage < REGEN_DELAY 就 return,等于「刚挨打才回血」。
更实际的问题是敌人持续开火,严格的脱战窗口根本等不到——实测跑满 12 秒从 60 掉到 44,
一滴没回。改成「最近 3.5 秒内被命中 2 次以上才视为被集火」。
8. 换一把枪就吞掉玩家攒的备弹
换枪时是 reserveAmmo = weapon().reserve 直接赋值。从突击步枪(150 备弹)换到
霰弹枪(40 备弹),手上 180 发瞬间变 40 发。改成取新枪基准量与现有量的较大者。
9. 补给踩在脚下却捡不到
拾取判定用了「点到点距离 + 高度容差」。补给躺在 y≈0.4,玩家 eye 在 1.68,
站到掩体顶上更高——高度差一超限就捡不起来。改成只看水平距离,这才是玩家的直觉。
- 没有声音之外的多媒体。音效是合成的,但没有背景音乐(音乐需要更长更复杂的 波形编排,属于另一个量级的工作)。
- 没有多人。这是单机游戏,没有网络代码。
- 敌人不会互相配合。每个敌人独立决策,没有包夹、没有火力压制。
- 场景是程序生成的。没有关卡编辑器,没有地图文件,三张布局写死在
GameWorld.buildArena()里。 - 手感仍未经真人验证。散布、后坐力、灵敏度、敌人命中率这些参数是按 「能打中靶子」推出来的,但没有人真的在手机上玩过。这需要你上手调。
MIT。见 LICENSE。