9.7 KiB
9.7 KiB
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)获取实际组件
组件获取辅助函数:
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 字段获取世界位置
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):
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):
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):
// lym_1.f20_1 = atan2(i, e) = 俯仰角
// lym_1.h20_1 = -atan2(t, n) = 偏航角
获取相机偏航角的策略:
- 遍历 GameMode entity 和所有坦克 entity
- 策略1:查找有
kym_1/lym_1字段的 $r/gq 相机组件(值非零) - 策略2:查找有
qz9_1/gz9_1字段的 k7 相机控制器,从gz9_1获取相机组件 - 偏航角 =
-lym_1.h20_1
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(-272017744), 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 entitygm.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 旋转计算
将世界坐标旋转到相机坐标,使相机朝向对应雷达上的"上"方向:
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 类导出 |