30 · 小地图导航与目标视野检测

返回系列目录

三个不同问题

游戏或数字园区需要知道:目标离角色多远、在角色左边还是右边、是否落在视野角内。这三个问题分别使用长度平方、叉积的有向分量和点积。它们都不需要计算完整夹角。

示例在世界 XZ 平面工作,角色局部 +Z 为前方、+Y 为上方。方向 f 与目标位移 d 的二维表达取 (x, z),于是标量叉积为 f.x * d.z − f.z * d.x。在此约定下,正值表示左侧:当角色朝 +Z,世界 −X 在左边。不要把 XY 平面的符号说明不经检查地搬到 XZ 平面。

视野总角度为 φ,半角阈值是 cos(φ / 2):

f⋅d∣d∣≥cos⁡(φ/2),∣f∣=1. \frac{f\cdot d}{|d|}\ge\cos(\varphi/2),\quad |f|=1.

距离先用平方比较可省去大部分候选目标的平方根。

完整核心代码

import { createDemo } from './runtime.js';

const { THREE, scene, onFrame } = createDemo();
const actor = new THREE.Group();
scene.add(actor, new THREE.GridHelper(20, 20));
actor.add(new THREE.ArrowHelper(new THREE.Vector3(0, 0, 1), new THREE.Vector3(), 2));
const target = new THREE.Mesh(
  new THREE.SphereGeometry(0.25),
  new THREE.MeshBasicMaterial({ color: 0xff6644 })
);
scene.add(target);
const actorWorld = new THREE.Vector3();
const targetWorld = new THREE.Vector3();
const forward = new THREE.Vector3();
const delta = new THREE.Vector3();
const mapPoint = new THREE.Vector2();
const range = 6;
const cosineLimit = Math.cos(THREE.MathUtils.degToRad(90 / 2));

function inspectTarget() {
  actor.getWorldPosition(actorWorld);
  target.getWorldPosition(targetWorld);
  actor.getWorldDirection(forward); // Object3D 的世界 +Z;相机的实现不同。
  forward.y = 0;
  if (forward.lengthSq() < 1e-10) return { visible: false, side: '未知' };
  forward.normalize();
  delta.subVectors(targetWorld, actorWorld);
  delta.y = 0;
  const distanceSq = delta.lengthSq();
  if (distanceSq < 1e-10) return { visible: true, side: '重合' };
  const cross2 = forward.x * delta.z - forward.z * delta.x;
  const side = Math.abs(cross2) < 1e-5 ? '共线' : cross2 > 0 ? '左' : '右';
  const visible = distanceSq <= range * range &&
    forward.dot(delta) / Math.sqrt(distanceSq) >= cosineLimit;
  return { visible, side };
}

onFrame((dt, elapsed) => {
  target.position.set(Math.sin(elapsed) * 4, 0, Math.cos(elapsed) * 4);
  const result = inspectTarget();
  target.material.color.set(result.visible ? 0x44cc88 : 0xff6644);
  // 200×200 小地图,世界 XZ 的 [-10,10] 映射到像素。
  // 此处约定 +Z 朝地图下方,实际 UI 可用 mapPoint 定位图标。
  mapPoint.set((targetWorld.x + 10) * 10, (targetWorld.z + 10) * 10);
});

验证、边界与替代方案

目标绕一圈时只应在前方 90° 内变绿。侧向、正后方以及重合位置分别检查;“共线”包括同向和反向,必须结合点积判断前后。

本例忽略高度,适合地面导航。飞行器需要三维锥体检测;建筑楼层则应额外检查高度或楼层编号。本例的 visible 意为“距离和视角符合条件”,不表示没有墙遮挡。进一步从眼睛位置发射射线才可检测可见性。大量对象可先用空间网格、八叉树等缩小候选集合,再做精确查询。

练习(暂不提供答案)

  1. 将小地图改为北朝上、世界 +Z 朝上,调整像素转换。
  2. 让角色旋转,验证左右判断随角色朝向变化。
  3. 增加高度差限制,避免把楼上的目标当作同层目标。
  4. 添加遮挡墙,让视角通过但被墙挡住的目标显示第三种颜色。