Files
TankIC/docs/Hornet_Radar_Experience.md
2026-07-28 01:16:15 +08:00

271 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 3DTank雷达开发经验记录
## 1. 项目背景
逆向分析 Tanki Online 中国版的 webpack 打包 JavaScript 文件(`main.b7114b43.js`),实现"3DTank雷达"功能:在屏幕右上角显示 2D 俯视图 Canvas 雷达,实时显示所有坦克相对位置,跟随相机朝向旋转。
## 2. 关键技术发现
### 2.1 Webpack 模块结构
- 主入口文件:`main.b7114b43.js`
- 解包后的模块文件:`3dtank.com/play/static/js/unpacked/module_XXXXX.js`
- 关键模块:
- `module_87072`:核心游戏逻辑(组件系统、相机、全局状态)
- `module_61378`:相机组件($r/gq 类)和实体组件系统(fe 类)
- `module_92980`:渲染相关(Sprite、着色器)
- `module_37036`:组件标识符函数 Jj(内部名 z_)
### 2.2 组件系统
**实体(Entity)基类 fe:**
- `entity.wyj_1.r2a_1` 是一个 `Map<组件标识, 组件列表>`
- Map 的 value 是列表类(s_),**不是单个组件**
- 必须调用 `valueList.g1(0)` 获取实际组件
**组件获取辅助函数:**
```javascript
function findComponentByField(entity, fieldName) {
if (!entity || !entity.wyj_1 || !entity.wyj_1.r2a_1) return null;
var result = null;
entity.wyj_1.r2a_1.forEach(function(valueList, key) {
if (result) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (comp && comp[fieldName]) result = comp;
} catch(e) {}
});
return result;
}
```
**关键组件:**
- **G$t(物理组件)**:`pz7_1`(O$向量)= 世界位置,字段 `f20_1`(x), `g20_1`(y), `h20_1`(z)
- **Kkt(hull组件)**:`czc_1`(局部位置), `dzc_1`(方向), `ezc_1`(水平方向)
- **$r/gq(相机组件)**:`jym_1`(FOV≈60°), `kym_1`(位置), `lym_1`(方向)
- **k7(相机控制器)**:`qz9_1`(C7参数), `gz9_1`(引用$r/gq相机组件), `hz9_1`(物理组件), `iz9_1`(hull组件)
### 2.3 坦克位置获取
**正确方法:** 通过 G$t 组件的 `pz7_1` 字段获取世界位置
```javascript
function getTankPosition(tank) {
var physicsComp = findComponentByField(tank, 'pz7_1');
if (physicsComp && physicsComp.pz7_1) {
return {
x: physicsComp.pz7_1.f20_1,
y: physicsComp.pz7_1.g20_1,
z: physicsComp.pz7_1.h20_1
};
}
return null;
}
```
**注意事项:**
- 玩家坦克 `ezr_1` 死亡后物理组件会变为 NULL
- 需要多级备用方案:`ezr_1` → `fzr_1` → 坦克列表第一个
### 2.4 相机方向获取(核心难点)
**全局状态 p7 类(module_87072 line 4409):**
- `kzb_1`(O$向量)= 相机位置
- `jzb_1`(O$向量)= 相机方向
- `fzb_1` = FOV(1.0471976 ≈ 60°)
- `g7()` 函数返回单例 `at`,**未导出**
**相机更新函数 h7(module_87072 line 4296-4299):**
```javascript
function h7(t, n, i, r) {
var e = t.qz9_1.qzb_1,
u = t.qz9_1.rzb_1;
// i 是偏航角,n 是相机位置
g7().jzb_1.f20_1 = -Math.sin(i); // 方向x
g7().jzb_1.g20_1 = Math.cos(i); // 方向y
g7().kzb_1.j26(n); // 位置 = n
...
}
```
**相机组件 $r/gq(module_61378 line 520-521):**
```javascript
function $r() {
oe.call(this);
this.jym_1 = 3.1415927/3; // FOV
this.kym_1 = new wn(); // 位置
this.lym_1 = new wn(); // 方向
}
```
**方向计算方法 z37(module_61378 line 2069-2076):**
```javascript
// lym_1.f20_1 = atan2(i, e) = 俯仰角
// lym_1.h20_1 = -atan2(t, n) = 偏航角
```
**获取相机偏航角的策略:**
1. 遍历 GameMode entity 和所有坦克 entity
2. 策略1:查找有 `kym_1`/`lym_1` 字段的 $r/gq 相机组件(值非零)
3. 策略2:查找有 `qz9_1`/`gz9_1` 字段的 k7 相机控制器,从 `gz9_1` 获取相机组件
4. 偏航角 = `-lym_1.h20_1`
```javascript
function getCameraHeading() {
var entities = [];
// 1. GameMode entity
try {
var gameScene = gm.rym().pyj_1;
var gmEntity = gameScene.eyl("GameMode");
if (gmEntity) entities.push(gmEntity);
} catch(e) {}
// 2. 所有坦克列表中的 entity
try {
var tankList = gm.xzq_1.z11b();
var tCount = tankList.e1();
for (var i = 0; i < tCount; i++) {
var t = tankList.g1(i);
if (t) entities.push(t);
}
} catch(e) {}
// 查找活跃相机组件
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (!entity || !entity.wyj_1 || !entity.wyj_1.r2a_1) continue;
var found = null;
entity.wyj_1.r2a_1.forEach(function(valueList, key) {
if (found) return;
try {
var comp = (valueList && typeof valueList.g1 === 'function') ? valueList.g1(0) : valueList;
if (!comp) return;
// 策略1:$r/gq 相机组件
if (comp.kym_1 && comp.lym_1 &&
comp.kym_1.f20_1 !== undefined && comp.lym_1.f20_1 !== undefined) {
if (Math.abs(comp.lym_1.h20_1) > 0.001 || Math.abs(comp.lym_1.f20_1) > 0.001) {
found = comp;
return;
}
}
// 策略2:k7 相机控制器
if (comp.qz9_1 && comp.gz9_1) {
var camComp = comp.gz9_1;
if (camComp && camComp.kym_1 && camComp.lym_1 &&
(Math.abs(camComp.lym_1.h20_1) > 0.001 || Math.abs(camComp.lym_1.f20_1) > 0.001)) {
found = camComp;
return;
}
}
} catch(e) {}
});
if (found) {
return {
yaw: -found.lym_1.h20_1, // 偏航角
pitch: found.lym_1.f20_1, // 俯仰角
pos: { x: found.kym_1.f20_1, y: found.kym_1.g20_1, z: found.kym_1.h20_1 }
};
}
}
return null;
}
```
### 2.5 坐标系统
- **x 和 y 是水平面**
- **z 是高度**
- 坦克位置范围:x(-29000~2595), y(-2720~17744), z(-228~382)
- 雷达使用 x, y 计算相对位置
### 2.6 GameMode 实例获取
通过 Hook `yht` 函数(module_87072 导出 `$_$.ac`)捕获 `ght` 实例:
- `gm.rym()` — 获取游戏场景
- `gm.rym().pyj_1` — jr 实例(场景容器)
- `gm.rym().pyj_1.eyl("GameMode")` — GameMode entity
- `gm.ezr_1` — 本地玩家坦克(可能为 null)
- `gm.fzr_1` — 当前坦克
- `gm.xzq_1` — Tht 坦克列表管理器
- `gm.xzq_1.z11b()` — 获取坦克列表
- `tankList.e1()` — 坦克数量
- `tankList.g1(i)` — 获取第 i 个坦克
## 3. 雷达实现方案
### 3.1 雷达参数
- Canvas: 320x320, 圆形, 右上角固定
- radius: 150
- radarRange: 30000(世界单位,显示全场)
- 坐标轴: x,y 为水平面, z 为高度
- 朝向: 相机偏航角(只跟随摄像机朝向,不跟随车轴朝向)
### 3.2 旋转计算
将世界坐标旋转到相机坐标,使相机朝向对应雷达上的"上"方向:
```javascript
var dx = pos.x - playerPos.x;
var dy = pos.y - playerPos.y;
// 旋转到相机朝向(相机朝上=北)
var cosA = Math.cos(playerAngle); // playerAngle = 相机偏航角
var sinA = Math.sin(playerAngle);
var rx = dx * cosA - dy * sinA;
var ry = dx * sinA + dy * cosA;
// 缩放到雷达范围(注意y轴反转,屏幕y向下)
var radarX = cx + (rx / radarRange) * radius;
var radarY = cy - (ry / radarRange) * radius;
```
### 3.3 友方/敌方区分
通过比较 `tank.qyj_1 === playerTank.qyj_1`(队伍对象引用相等)
## 4. 遇到的问题及解决方案
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 组件名都显示为 "s_" | Map value 是列表类,不是组件 | 调用 `valueList.g1(0)` 获取实际组件 |
| 雷达只显示玩家点 | 玩家坦克 `ezr_1` 死亡后物理组件为 NULL | 多级备用:`ezr_1` → `fzr_1` → 列表第一个 |
| 相对位置不正确 | 使用了 x,z 作为水平面 | 改为 x,y(z 是高度) |
| Cam: NULL | $r/gq 组件在坦克上值为0,不活跃 | 遍历所有 entity + k7 控制器策略 |
| 旋转方向反了 | 旋转角度符号错误 | `-playerAngle` → `playerAngle` |
| Fat.x12c_1 不是实时位置 | 是固定参考点 | 改用 $r/gq 组件的 kym_1/lym_1 |
## 5. 文件清单
### 工具脚本(tools/ 目录)
- `hook_gamemode.js` — Hook yht 捕获 ght 实例
- `probe_components.js` — 遍历坦克组件探查字段
- `probe_tank_fields.js` — 坦克字段深度探查
- `probe_position_camera.js` — 位置与相机探查
- `diagnose_radar.js` — 雷达诊断脚本
- `hornet_radar.js` — **3DTank雷达核心实现**
### 修改的游戏文件
- `main.b7114b43.js` — 已修改的 loader 文件
- `module_13431.js` — 已修改(禁用 Sentry)
## 6. 关键代码位置索引(module_87072.js)
| 行号 | 内容 |
|------|------|
| 1323 | `Jj = r.$_$.mf` — 组件标识符函数 |
| 1399 | `gq = _.$_$.c1` — 相机组件类(来自 module_61378) |
| 4286-4290 | `o7(t)` — 获取相机组件 `t.gz9_1` |
| 4296-4299 | `h7(t,n,i,r)` — 相机更新函数 |
| 4409-4423 | p7 类构造函数(全局状态) |
| 4428-4429 | `g7()` — 返回 p7 单例(未导出) |
| 4434-4458 | k7 类构造函数(相机控制器) |
| 51667-51669 | k7.vym — 获取 gq/G$t/Kkt 组件 |
| 68942-68943 | Kkt.w20 — 位置/方向设置 |
| 15234-15236 | Kkt 类构造函数 |
| 16259-16277 | G$t 类构造函数 |
| 125692-125693 | ght/yht 导出 |
## 7. 关键代码位置索引(module_61378.js)
| 行号 | 内容 |
|------|------|
| 520-521 | $r 类构造函数(gq相机组件) |
| 2069-2076 | $r 类方法(cyh/mym/z37) |
| 2482-2490 | fe.xyp/fe.yyp — 组件获取实现 |
| 3073 | `t.$_$.c1 = $r` — gq 类导出 |