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

9.7 KiB
Raw Permalink Blame History

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) = 偏航角

获取相机偏航角的策略:

  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
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(-290002595), 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 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 旋转计算

将世界坐标旋转到相机坐标,使相机朝向对应雷达上的"上"方向:

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 类导出