本地单机 + 局域网联机伪 3D 第一人称射击训练器。射线渲染(raycasting),Python + pygame,无外部素材(音效由 numpy 合成)。包含靶场练习、3v3 回合对抗、5v5 积分赛三种模式,以及同 Wi-Fi 下多人联机的局域网房间。
各系统双击 / 运行对应启动器,脚本会自动检查并安装依赖后再启动:
| 系统 | 启动器 |
|---|---|
| macOS | 双击 启动游戏.command |
| Windows | 双击 start.bat |
| Linux | 终端执行 ./start.sh(首次需 chmod +x start.sh) |
命令行(任意系统):
pip install -r requirements.txt
python3 main.py浏览器下载的 ZIP 里的文件都带 macOS 隔离标记(quarantine),无签名的 .command
脚本会被 Gatekeeper 拦下。任选其一解除:
方法 A(推荐,一次解决整个文件夹):打开「终端」,执行(文件夹不在下载目录就先 cd 过去,或把文件夹拖进终端窗口)。同时解隔离标记 + 补回可执行权限(GitHub 的 Download ZIP 不保留 +x):
cd ~/Downloads/geom-aim-main
xattr -cr .
chmod +x 启动游戏.command start.sh之后双击 启动游戏.command 即可。
方法 B(不敲命令):双击被拦后,打开 系统设置 → 隐私与安全性 → 滚到最下面, 点「仍要打开」并输入密码。
方法 C:用 git clone 拿代码(clone 下来的文件没有隔离标记,天然不触发此问题):
git clone https://github.com/Franky100-pig/geom-aim.git
cd geom-aim && ./启动游戏.command启动后进入标题界面,按 1 / 2 / 3 选择模式。
| 键 | 模式 | 说明 |
|---|---|---|
1 |
练习 | 5 种靶场训练(见下) |
2 |
对战 | 3v3 回合制对抗,含经济与买枪 |
3 |
积分赛 | 5v5 连续重生 TDM,先到 25 杀获胜 |
4 |
创建局域网房间 | 你当主机,跑权威模拟,朋友加入一起打 |
5 |
加入局域网房间 | 输入主机 IP 或 .local 主机名加入 |
对战与积分赛地图每局随机生成 180° 旋转对称 的掩体(半高箱 / 高箱);练习模式为固定柱。
| 按键 | 作用 |
|---|---|
W A S D |
移动 |
| 鼠标 | 转视角(含上下) |
| 鼠标左键 | 开火(点射 / 按住连发) |
| 鼠标右键 | 开镜(仅 SNIPER 练习模式,轻点切换) |
E |
蹲(缩小受击轮廓、降低被命中率) |
空格 |
跳(短暂抬升视角,可越过半高箱探头) |
G |
扔烟雾弹(练习免费;对战需购买;积分赛不可用) |
1–5 |
练习:切模式;对战:买枪阶段买枪;积分赛:随时换枪 |
C |
对战:在已买的枪里循环切换 |
R |
对战:换弹(2 秒) |
M |
3v3 / 积分赛:小地图放大一倍(再按还原) |
6 |
对战买枪阶段:买烟雾弹 |
H |
返回标题(任意状态) |
4 |
标题界面:创建局域网房间(主机) |
5 |
标题界面:输入主机地址加入(客户端) |
ESC |
菜单(灵敏度 / FOV / 准星 / 音效);对战内退回标题;房间界面取消 / 客户端断开 |
R |
重开(模式结束后) |
[ ] |
鼠标灵敏度(游戏中可调,±15% 步进) |
- = |
视场角 FOV |
M |
静音 |
F |
全屏(菜单内) |
你 + 2 名队友 AI 对抗 3 名敌人 AI,双方均会交火、买枪、使用烟雾弹。
- 赛制:best of 7,先到 4 回合胜。
- 流程:买枪(prep, 10s, 可移动) → 交火(live, 90s) → 结算(2.5s) → 下一回合。
- 死亡:一回合一条命。阵亡后先向后倒、视线朝天停留约 1 秒,再切到存活队友第一视角;观战期间视角锁定在队友身上,移动鼠标不会晃动画面。
- 胜负:一方 3 人全灭则另一方得分;时间到则存活多者胜,相同判平。
- 经济:起始 $800;胜 +3250 / 败 +1400;连败补偿递增;击杀按枪械分档奖励;上限 $16000。AI 按资金买枪。
- 武器库存:每回合重置——只带免费手枪上场(CS 式经济局)。同一回合内买的多把枪用
C循环切换,买枪阶段和交火中都行;1–5只在买枪阶段负责购买。 - 弹药与换弹:每把枪一个弹匣(手枪 12 / 冲锋枪·步枪 30 / 连狙 20 / 狙击 5)。打空后自动换弹,也可按
R主动换;换弹耗时 2 秒且期间不能开火,切枪会打断换弹(余弹保留)。右下角显示余弹与换弹进度。练习模式和积分赛不受弹药限制。 - 武器:手枪 / 冲锋枪 / 步枪 / 连狙 / 狙击,共 5 把(无护甲、无投掷物,烟雾弹单独购买)。爆头乘伤害倍率。
- 难度:4 档 + 逐回合自适应(依近 3 回合 KD 与胜率 ±20% 微调)。
| 档 | 反应延迟 | 瞄准误差 | 横移 |
|---|---|---|---|
| 简单 | 0.55s | 6.5° | 少 |
| 普通 | 0.38s | 4.0° | 中 |
| 困难 | 0.25s | 2.2° | 多 |
| 专家 | 0.16s | 1.2° | 多 |
- AI 判定:敌我共用同一套命中几何,瞄准点带高斯误差,超出人形轮廓即打偏;走位与掩体可真实降低被命中率。
- 小地图:左上角圆形小地图,只显示自己和队友(敌人位置保密);随视角旋转——正前方永远朝上,自己是中间的白色箭头。画出墙 / 柱子 / 半高箱 / 高箱的色块,防止在拼接地图里迷路;按
M放大一倍。自由练习模式不显示。 - AI 战术(偷背身 / 绕后包抄):目标把背露给 AI 时,AI 会压近偷袭而非原地横移;目标正面对枪、压不上去时,高难度 AI 会按冷却绕到目标身后包抄,到位/超时/低血/贴脸即收回正面对枪。可经
config.py的AI_FLANK_*系列参数调节(触发概率、冷却、包抄半径、最长时长、血量下限、开关)。 - 战绩:命中率 / KD / 爆头率,回合与整场结束均展示。
你 + 4 名队友 AI 对抗 5 名敌人 AI,无经济、可随时换枪。
- 胜负:先累计 25 击杀 的队伍获胜。
- 重生:阵亡后约 1.5s 重生,重生后 3s 无敌。阵亡观战规则同对战:先倒地 1 秒,再切到队友第一视角(鼠标锁定)。
- 换枪:
1–5随时免费换枪,无需购买。 - 小地图:与 3v3 同一套(左上角圆形,只显示队友,随视角旋转,
M放大一倍)。 - 地图:3×3 拼接(房间 25×17),随机对称掩体;玩家无烟雾弹,但 AI 会按判断使用烟雾弹——仅在中远距离封视线、低血撤退、或低频掩护推进时出手,且受开局冷静期与队伍配额限制,不会一开局就铺满。
- 命中 / 难度 / 战绩逻辑同对战模式。
同 Wi-Fi 下多人一起玩:一人当主机(跑权威模拟),其他人各开一份客户端加入。基于 UDP(JSON),零第三方依赖,纯标准库。
- 模式:人类 vs AI 协作。你当主机开一局 5v5 积分赛,房间最多 3 名真人(含你),空位由 AI 补满。
- 拓扑:客户端-服务器。主机跑全部模拟逻辑(移动、命中、AI、胜负);客户端只把本地键鼠输入上行、收到 30Hz 快照后在自己屏幕上渲染——所以即使某台机器很弱也不影响公平性。
- 加入方式:标题界面按
5→ 输入主机的 IP(如192.168.1.20)或.local主机名(如Frankys-Mac.local,macOS 自带,无需记 IP)→ 回车连接。主机可在房间界面看到本机局域网地址提示。 - 房间流程:主机按
4建房 → 等朋友加入(房间界面显示已连玩家)→ 回车 / 空格开局;按Esc取消。 - 朝向:服务器权威 yaw(防作弊 + 简单),客户端本地只算俯仰(pitch)做视觉。
- 断线:客户端
Esc退回标题即断开;主机侧超时(默认 4s 无输入)自动踢人并补回 AI。
快照按局域网 1500 MTU 自动 BASE64 分片重组,单包约 1.1KB,普通 Wi-Fi/Ethernet 下稳定送达;丢片则当帧丢弃、等下一帧快照。
| 模式 | 训练 |
|---|---|
| AIM BOTZ | 甩枪点靶,5–10 靶同时,清掉补位,进阶混入横移靶 |
| REFLEX | 闪现反应,单靶限时 0.7s→0.32s 递减并缩小 |
| TRACKING | 跟枪压枪,黏住高速乱跑的靶,断 0.85s 连击清零 |
| PEEK | 掩体探头,靶从柱后滑出才可击中 |
| SNIPER | 远距狙击,右键开/关镜 4x,左键单发需上栓 |
AK 手感:600 RPM,前几发近直上,后向右摆再回摆(player.py 的 SPRAY_YAW_PATTERN)。连发越久扩散越大,移动附加扩散,站定最准。爆头区在身高 78%–94%(250 分),躯干 100 分,连击第二起每级 +5%(封顶 2.5x)。
主要参数在 config.py,改完重开生效:
MOUSE_SENS:灵敏度([]游戏内可调)H_FOV_DEG:水平视场角,默认 90(-=可调)RECOIL_PX/RECOIL_RECOVER_PX:后坐力上抬与回正SPRAY_PER_SHOT/SPRAY_MAX:连发扩散RENDER_COL_STEP:射线精度,默认 2(卡则改 3/4,求锐利试 1)AI_DIFF_*/ECON_*/MATCH_*/SCORE_*:对战与积分赛数值TERRAIN_*:地形起伏开关 / 幅度 / 频率 / 种子(见下节)- 游戏内
L热重载配色
地图叠了一层平滑高度场(值噪声,与掩体种子独立、可复现):
- 玩起来的感觉:上下坡时相机会真实地升降(上坡视线抬高、下坡压低); 山脊能挡住视线、洼地能藏人——蹲在坡后是真掩体。
- AI 同样受地形影响:会按脚下高度开火与判视线,翻山越岭时和玩家同规则。
- 渲染:地面网格与墙脚跟着地形起伏,人和靶子会"踩"在坡面上。
调 config.py:
TERRAIN_ENABLED = True # 关掉即回到完全平整的旧地图
TERRAIN_AMP = 0.55 # 起伏幅度(墙高=1.0;想要更夸张可以试 0.8)
TERRAIN_FREQ = 0.16 # 越小山头越大越平缓
TERRAIN_SEED = 1337 # 换个数字换一张"地形脸"对战与积分赛地图每局随机生成,满足:180° 旋转对称、全图连通、出生点四周留空。
- 半高箱(高 0.70):高于站立眼高,站立不可穿透;蹲下(头顶 0.65 < 0.70)完全藏住,站立露头可见;跳起平射可越过。
- 高箱(高 1.0):等同满高墙,绝对遮挡。
满高外墙与房间墙始终为绝对遮挡,跳跃无法越过。
地图由 ARENA_TILES_X × ARENA_TILES_Y 个房间拼接,房间开门洞连通,外圈封外墙。改 config.py 顶部 ARENA_*:
ARENA_TILES_X/ARENA_TILES_Y:房间数(默认 4×3 = 12)ARENA_ROOM_W/ARENA_ROOM_H:单房间尺寸
默认约 109×57 格。对战 / 积分赛另有各自的 MATCH_* / SCORE_* 尺寸参数。
ADS_ZOOM:开镜倍率,默认 4BOLT_TIME:栓动延迟,默认 0.9s(设 0 变连点)SNIPER_SENS_MUL:开镜灵敏度乘子,默认 0.28SNIPER_DIST_MIN/SNIPER_DIST_MAX:远距靶刷新距离
main.py 主循环、命中判定、计分、标题三模式、对战/积分赛分支、阵亡观战
engine.py 伪 3D 射线渲染:地图 / 相机 / 投影 / DDA / 精灵可见性 / 掩体高度
player.py 移动碰撞、后坐力、弹道扩散、蹲跳、Weapon 抽象
targets.py 靶子与 5 种练习模式生成、移动、生命周期
weapons.py Weapon 数据类 + 5 把枪数值
ai.py Agent 状态机 + BFS 寻路 + AI 命中判定
match.py Match:对战/积分赛状态机、队伍、经济、胜负、重生、观战
net.py 局域网联机网络层(UDP/JSON、分片、主机/客户端)
hud.py 准星、计分板、枪模型、菜单、开镜镜框、对战/积分赛 HUD、标题
effects.py 曳光弹、枪口火光、碎片、飘字、命中标记
audio.py numpy 合成枪声与命中音
config.py 全部可调参数(AI_DIFF_* / ECON_* / MATCH_* / SCORE_* / COVER_* / TERRAIN_*)
tests/ 无头自检套件(练习 / 对战 / 积分赛 / 掩体 / 烟雾 / 返回标题)
python3 tests/test_core.py # 练习模式
python3 tests/test_match.py # 对战模式
python3 tests/test_score.py # 积分赛
python3 tests/test_cover.py # 掩体对称/连通/遮挡
python3 tests/test_nades.py # 烟雾弹
python3 tests/test_title_return.py # H 返回标题
python3 tests/test_net.py # 局域网联机:握手/输入/快照分片/超时
python3 tests/bench.py # 渲染开销基准全部为无头(dummy 驱动)测试,无需显示设备。改练习逻辑跑 test_core,改对战 / 积分赛跑对应用例,改联机跑 test_net。
python3 tests/bench.py(1280x720,CPU 侧,dummy 驱动):
天花板/地板渐变 0.068 ms
地板网格 0.158 ms
墙体光栅化 2.343 ms ← 主要开销,随 RENDER_COL_STEP 线性变化
HUD + 准星 + 枪 0.133 ms
整帧 update+draw 2.736 ms → 约 365 fps(拼接大地图)
对比 60 fps 的 16.7 ms 预算余量充足。
改 engine.py 前先看文件头注释。核心三条:
- 射线
ray = dir + plane_unit * s,dir分量恒为 1,DDA 参数即垂直距离,无鱼眼。 - 屏幕纵坐标
y = horizon + (H * vscale / d) * (EYE_HEIGHT - h),vscale = W / (2 * L * H)保证像素密度一致。 - 上下看靠平移地平线:
horizon = H/2 + pitch_px;准星在深度 d 处的世界高度为EYE_HEIGHT + pitch_px * d / (H * vscale),命中判定用之。
- pygame 2 窗口事件扁平:用
pygame.WINDOWFOCUSLOST/pygame.WINDOWMINIMIZED,无pygame.WINDOWEVENT子属性写法,否则切窗口瞬间崩。 Renderer建 Surface 前需检查pygame.display.get_init():无头环境Surface.convert()会抛异常。