271 lines
9.7 KiB
Markdown
271 lines
9.7 KiB
Markdown
# 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 类导出 |
|