跳转至

2.3 命中几何信息

hitVelocityhitPointhitNormal 三个 Vec3 字段共同构成了命中瞬间的几何快照。它们不仅用于角度修正和部位判定,也是护甲侧实现跳弹判定、斜穿修正覆写、命中部位装甲差异化的唯一信息来源。本节逐一说明这三个字段的含义、获取方式和常见处理模式。

命中速度矢量(hitVelocity)

hitVelocity 是弹丸在命中瞬间的速度矢量,单位 m/s,使用世界坐标系。它的作用远不止于"弹丸有多快"——角度修正需要方向、跳弹判定需要入射角、护甲侧的斜穿修正覆写也需要方向来推算等效厚度。

获取方式取决于弹丸的物理实现。如果你的弹丸是实体(Entity),最简单的做法是取 getDeltaMovement()——即前一帧到当前帧的位移。对于高帧率场景,这个位移近似于瞬时速度。注意 getDeltaMovement() 的单位是 blocks/tick,而协议要求 hitVelocity 的单位是 m/s,需要乘以 20(Minecraft 每秒 20 tick):

Vec3 hitVelocity = projectileEntity.getDeltaMovement().scale(20.0);

如果你的弹丸基于射线追踪(即时命中扫描),那么速度方向就是射线的方向,速度大小则从武器配置中读取:

Vec3 shotDirection = shooter.getLookAngle();
float muzzleVelocity = 1200f;   // 初速 m/s,从配置读取
// 按距离衰减
float remainingVelocity = muzzleVelocity * (float)Math.pow(0.999, distance);
Vec3 hitVelocity = shotDirection.scale(remainingVelocity);

不管是实体还是射线追踪,速度矢量都应该在命中瞬间的帧内获取——不能缓存弹丸出生时的初始速度,因为弹丸在飞行过程中可能已被重力或空气阻力衰减。

注意:hitVelocity 使用世界坐标系。hitPointhitNormal 同样使用世界坐标。三个字段共享同一个坐标空间,因此将 hitPoint 减去实体位置就可以得到命中点在实体局部空间中的偏移——护甲侧正是用这个方式做部位判定。

命中点(hitPoint)

hitPoint 是弹丸击中的精确位置,世界坐标。它的主要用途有两个。一是供武器侧在命中后播放粒子、生成弹坑或放置音效源——你应该在 hitPoint 处而非实体中心处产生效果,这样命中反馈才是位置上准确的。二是供护甲侧判定"打在了哪个部位"——将 hitPoint 转换到实体的局部坐标,根据前后左右高度等关系决定返回什么护甲等级。

从射线命中结果获取 hitPoint 是标准方式:

// 方块命中
BlockHitResult blockHit = ...;
Vec3 hitPoint = blockHit.getLocation();   // 命中方块面的精确坐标

// 实体命中
EntityHitResult entityHit = ...;
Vec3 hitPoint = entityHit.getLocation();  // 命中实体边界框的坐标

两者的 getLocation() 都返回世界坐标中的精确命中位置。

命中面法线(hitNormal)

hitNormal 是命中面的法线方向,指向面外侧——即命中发生时弹丸撞击的表面的外指向方向。对于方块命中,它是方块被击中的那个面的法线(北面的法线指向正 Z 方向,等等)。对于实体命中,它应该是子弹飞入实体碰撞箱的方向的反方向——粗略地等同于从命中点指向实体中心的方向。

BlockHitResult 获取法线最直接:

Vec3 hitNormal = new Vec3(
    blockHit.getDirection().getNormal().getX(),
    blockHit.getDirection().getNormal().getY(),
    blockHit.getDirection().getNormal().getZ()
);

BlockHitResult.getDirection() 返回的是 Direction 枚举(六个面的朝向),.getNormal() 返回整数 Vec3i(如 (1,0,0)(0,-1,0) 等),转为 Vec3 即可。注意这给出的只有六个离散方向——方块命中永远只能命中六个标准面之一。

EntityHitResult 获取法线则没有原生方法。标准的近似方式是反算命中点相对于实体包围盒的位置:

Entity target = entityHit.getEntity();
Vec3 hitPoint = entityHit.getLocation();

// 命中点相对于实体中心的方向(近似的命中面法线反方向)
Vec3 toCenter = target.position().subtract(hitPoint).normalize();
Vec3 hitNormal = toCenter.scale(-1);  // 反转为法线(指向面外侧)

这个近似方法在实体碰撞箱比较规整(接近方形)时效果良好。对于复杂形状的实体,更精确的做法是基于命中点与包围盒六个面的距离来推断命中面,但大多数场景下中心方向近似已经足够。

三个字段的联合使用

有了速度方向、命中点和法线,武器侧可以计算出全部命中几何信息:

// 入射角余弦:速度方向与命中面法线反方向的夹角
Vec3 shotDir = hitVelocity.normalize();
double cosTheta = Math.abs(shotDir.dot(hitNormal));

// 入射角(度)
double angleDeg = Math.toDegrees(Math.acos(cosTheta));

// 命中点到实体中心的距离(米)
double hitDistanceFromCenter = hitPoint.distanceTo(target.position());

// 命中点在实体局部坐标系中的方向(配合实体 YRot 判断前后左右)
Vec3 localHit = hitPoint.subtract(target.position());
double yRotRad = Math.toRadians(target.getYRot());
double forwardDist = localHit.x * Math.sin(-yRotRad) + localHit.z * Math.cos(-yRotRad);
double rightDist  = localHit.x * Math.cos(-yRotRad) - localHit.z * Math.sin(-yRotRad);

cosTheta 直接参与斜穿修正;hitDistanceFromCenter 可用于判定命中离中心多远(边缘命中 vs 正中);forwardDist 的正负可以区分正面和背面命中,rightDist 的正负区分左侧和右侧——这正是护甲侧做部位区分的基础。

几何信息缺失的场景

并非所有伤害都有完整的几何信息。在以下场景中,你只能提供 sourcebaseDamage,弹道字段使用零值:

  • 爆炸间接伤害:爆炸中心的弹片速度矢量繁多复杂,每个弹片的 hitPoint 和 hitNormal 取决于爆炸中心位置。如果不单独追踪每片弹片,就只能传递低信息量上下文。
  • 荆棘反伤:没有"方向"概念。
  • 虚空/指令伤害:无实体来源,同理没有几何信息。
  • DOT 持续伤害:如燃烧、中毒,通常不应走穿甲管线。

在这些情况下,hitVelocity 为零向量、hitPoint 为原点、hitNormal(0,1,0)(朝上,即垂直入射)。此时穿深为 0,getPenetrationLevel() 落在 UNARMORED_1。这正是合理的默认——环境伤害通常不应穿透装甲。

如果你想在爆炸等场景中提供更精确的几何信息(例如基于爆炸中心到实体的方向作为入射方向),可以在构造上下文时自行计算:

Vec3 centerToEntity = target.position().subtract(explosionCenter).normalize();
Vec3 hitNormal = centerToEntity.scale(-1);         // 从爆炸中心指向实体 = 命中面内侧→外侧
Vec3 hitPoint = target.position();                  // 近似为实体中心
// hitVelocity = centerToEntity.scale(estimatedSpeed);  // 如果需要速度

BFDamageContext ctx = BFDamageContext.builder()
    .source(explosionSource)
    .baseDamage(explosionDamage)
    .hitPoint(hitPoint)
    .hitNormal(hitNormal)
    .penetration(50f)  // 爆炸破片的中等穿深
    .build();

这种处理方式将爆炸视为"从中心向外发射中等穿深的破片",虽然不完美,但比纯粹的零信息量上下文能提供更合理的弹道判定。