Skip to content

Repository files navigation

GunFire

Release Android CI License: MIT

一个纯粹的安卓第一人称枪战游戏。零第三方依赖,零素材文件,零权限。

打开就能玩,不需要账号、不需要联网、不申请任何权限——不是「承诺不上传」,是技术上没有权限可用。

游戏内容

一个会追赶、会开火、会找掩体的敌人 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 塞进仓库。

下载

⬇️ 下载 app-release.apk

直链固定指向最新一版,不用在 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 testDebugUnitTest

61 个用例,覆盖:

  • 射击消耗弹药、弹匣打空自动换弹、换弹从备弹补充、换枪不吞备弹
  • 射线命中敌人 / 被墙挡住 / 忽略已死目标 / 平行射线不产生假命中
  • 爆头伤害不低于躯干、命中未击杀也给分、连杀窗口到期归零
  • 敌人会移动、留在场内、状态机不越界、射程内会打中玩家
  • 护甲分流伤害、脱战回血、零血绝不被回血拉起来、结算评级
  • 移动不越出场地边界、被立柱阻挡、蹲下更慢、跳跃必须落回地面
  • 暂停冻结世界、重开清空进度但保留历史最高波次
  • 弹匣与备弹同时为空必须结束一局
  • 矩阵运算有限性、单位矩阵乘法、零向量归一化不产生 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。

About

一个纯粹的安卓枪战游戏。623 KB,零第三方依赖,零权限。OpenGL ES 2.0 手写渲染,无引擎无素材。

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages