2.4 回调与事件处理¶
伤害执行之后,武器侧往往需要知道"刚才发生了什么"——弹丸击穿了、被挡住了、还是跳飞了?产生的破片是否该生成二次弹丸?BFDamageHandler 接口正是为这些需求设计的。通过将 handler 注入上下文,武器侧的代码可以在协议管线执行完毕后接收事件回调,并根据结果触发后续效果。
回调触发的时机¶
回调在 BFDamageApi.hurt() 方法的末尾触发——此时伤害已经执行完毕(target.hurt() 已调用并返回),但上下文栈尚未出栈(因此护甲侧在回调中仍然可以通过 getContextFor 读取上下文)。这个时机意味着两点:第一,你可以在回调中安全地读取上下文的任何字段;第二,你不会在伤害执行之前就错误地触发效果——如果伤害被原版机制拦截(hurt 返回 false),dealt 为 0,但回调仍会触发,因为你可能需要知道"发射了但被挡住了"。
五个事件回调¶
BFDamageHandler 提供了五个事件回调方法,全部带有 default 空实现。你只需覆写你关心的那几个。
public class MyGunHandler implements BFDamageHandler {
@Override
public void onPenetrated(BFHurtTarget target, BFDamageContext ctx) {
// 击穿:在命中点产生穿透火花粒子
spawnPenetrationParticles(ctx.hitPoint(), ctx.hitNormal());
}
@Override
public void onBlocked(BFHurtTarget target, BFDamageContext ctx) {
// 未击穿:播放金属撞击音效,产生火花粒子
playImpactSound(ctx.hitPoint(), SoundEvents.ANVIL_LAND);
spawnRicochetParticles(ctx.hitPoint(), ctx.hitNormal());
}
@Override
public void onRicochet(BFHurtTarget target, BFDamageContext ctx) {
// 跳弹:播放跳弹音效,产生偏转火花,并在偏转方向生成跳弹实体
playRicochetSound(ctx.hitPoint());
Vec3 ricochetDir = calculateRicochet(ctx); // 根据入射角和法线计算偏转方向
spawnRicochetBullet(ctx.hitPoint(), ricochetDir);
}
@Override
public void onOvermatch(BFHurtTarget target, BFDamageContext ctx) {
// 超匹配(碾压):弹丸完整穿透,碎甲不碎弹
// 产生更激烈的穿透效果——大块火花 + 装甲碎片粒子
spawnOvermatchParticles(ctx.hitPoint(), ctx.hitNormal());
spawnArmorFragments(ctx.hitPoint(), ctx.hitNormal());
}
@Override
public void onSpall(BFHurtTarget target, BFDamageContext ctx) {
// 破片:弹体碎裂,产生二次破片弹丸
int fragmentCount = 3 + random.nextInt(5);
for (int i = 0; i < fragmentCount; i++) {
spawnFragment(ctx.hitPoint(), randomSpread(ctx.hitNormal(), 30f));
}
}
}
每个回调方法的参数都包含 BFHurtTarget target 和 BFDamageContext ctx,因此你在回调中既能知道打在谁身上(target),也能知道打在哪儿、怎么打的(ctx)。target 是 BFHurtTarget 接口类型——如果你需要访问原始实体,可以将其转型为 Entity(因为实现 BFHurtTarget 的类几乎总是 Entity 的子类)。
回调的触发顺序¶
在管线内部,回调分为两类:主结果回调和派生效果回调。
主结果回调由 PenetrationResult 决定,三选一触发:
- PENETRATED → 触发
onPenetrated - BLOCKED → 触发
onBlocked - RICOCHET → 触发
onRicochet
派生效果回调则只由武器侧 handler 的判断方法决定。主结果回调完成后,协议会分别调用 isOvermatch(target, ctx, result) 和 isSpall(target, ctx, result);哪个返回 true,就触发哪个对应回调:
if (handler.isOvermatch(target, ctx, result)) {
handler.onOvermatch(target, ctx);
}
if (handler.isSpall(target, ctx, result)) {
handler.onSpall(target, ctx);
}
这意味着协议本身不会硬编码"跳弹一定不产生破片"或"超匹配一定排斥破片"。这些是默认 handler 判定表达的常见弹药模型,而不是协议层的限制。如果你的弹药会在跳弹时碎裂,覆写 isSpall 返回 true 即可;如果某种特殊弹药同时需要触发 onOvermatch 和 onSpall,也可以让两个判断方法都返回 true。
一次命中至少会触发一个主结果回调,也可能追加触发零个、一个或两个派生效果回调。如果你只需要知道最终穿甲结果而不关心超匹配/破片的细分,只覆写 onPenetrated、onBlocked、onRicochet 三个主回调即可。
超匹配与破片的自动判定¶
isOvermatch 和 isSpall 的默认实现在 BFDamageHandler 接口中定义,它们的判定逻辑如下。
超匹配(Overmatch)的条件:击穿 (PENETRATED) 并且修正后的穿深 > 目标的 RHA 等效厚度 × 1.5。物理含义是"穿深远超装甲厚度,弹体完整穿透,像热刀切黄油一样碾压装甲"。默认的 1.5 倍阈值意味着如果装甲有 100mm RHA,那么 150mm 以上的穿深才算超匹配。你可以覆写此方法来调整阈值:
@Override
public boolean isOvermatch(BFHurtTarget target, BFDamageContext ctx, PenetrationResult result) {
if (result != PenetrationResult.PENETRATED) return false;
float rha = target.getRHA(ctx);
float modifiedPen = target.modifyPenetration(ctx);
return modifiedPen > rha * 2.0f; // 更严格的阈值
}
破片(Spall)的默认条件:未击穿 (BLOCKED)——弹体在装甲表面碎裂——或者击穿但非超匹配——弹体在穿透过程中碎裂。跳弹默认不产生破片。逻辑可以概括为:凡是默认模型中弹体碎裂的场景都产生破片;跳弹(弹体不碎)和碾压(弹体不碎)不产生。
// 默认实现
default boolean isSpall(BFHurtTarget target, BFDamageContext ctx, PenetrationResult result) {
if (result == PenetrationResult.RICOCHET) return false;
if (result == PenetrationResult.BLOCKED) return true;
return !isOvermatch(target, ctx, result);
}
你可以覆写这两个判定方法来适配自己模组的弹药行为。例如,某些弹药类型(HESH 碎甲弹)可能在任何命中下都产生破片;某些弹药(全口径 AP)可能在特定角度下才碎裂;某些低质量弹丸可能即使击穿也不产生二次破片。
dealDamage 便捷方法¶
BFDamageHandler 接口提供了一个便捷方法 dealDamage,它会自动将自身注入上下文再调用 BFDamageApi.hurt():
// 传统写法
BFDamageContext ctx = BFDamageContext.builder()
.source(source).baseDamage(35f).penetration(120f)
.build();
float dealt = BFDamageApi.hurt(target, ctx.withHandler(this));
// 便捷写法
float dealt = this.dealDamage(target, ctx);
两种写法在逻辑上完全等价。使用 dealDamage 可以省略显式的 withHandler(this) 调用,并且让代码的语义更清晰——"我这个 handler 对目标造成了一次协议伤害"。在同一个类同时实现 BFDamageHandler 和武器逻辑的场景下(很常见),dealDamage 是最自然的选择。
Handler 作为类型标记¶
除了接收回调,BFDamageHandler 还有第二个作用:护甲侧可以通过 ctx.getHandler() instanceof MyGunHandler 来判断"是谁在打我"。
这个模式对于需要精细判定的护甲系统特别有用。例如,爆反装甲可能只对化学能弹药(HEAT)生效,对动能弹药(APFSDS)不生效。枪械模组可以在构造上下文时注入特定类型的 handler,护甲侧在 modifyPenetration 中检查:
// 护甲侧
@Override
public float modifyPenetration(BFDamageContext ctx) {
float base = ctx.penetration();
if (ctx.getHandler() instanceof HeatRoundHandler) {
// HEAT 弹药:爆反拦截生效
return base * 0.4f; // 穿深衰减至 40%
}
return base; // 动能弹药:爆反不生效
}
这个用法的前提是武器模组将 handler 的类暴露为公共 API,且 handler 的类型能传达弹药的类别信息。由于 handler 通常也实现了回调逻辑,将它与"弹药类型"绑定是自然的——一种弹药类型 = 一种 handler 实现。
完整实例:子弹击中后的全部反馈¶
以下是一个完整的 handler 实现,展示了一次子弹命中后武器侧可能做的所有反馈:
public class RifleBulletHandler implements BFDamageHandler {
private final Level level;
public RifleBulletHandler(Level level) {
this.level = level;
}
@Override
public void onPenetrated(BFHurtTarget target, BFDamageContext ctx) {
// 穿透命中标记(红色)
spawnHitMarker(ctx.hitPoint(), 0xFF0000);
// 穿透火花
level.addParticle(ParticleTypes.LAVA,
ctx.hitPoint().x, ctx.hitPoint().y, ctx.hitPoint().z, 0, 0, 0);
}
@Override
public void onBlocked(BFHurtTarget target, BFDamageContext ctx) {
// 未击穿命中标记(白色)
spawnHitMarker(ctx.hitPoint(), 0xFFFFFF);
// 金属撞击音效
level.playSound(null, ctx.hitPoint().x, ctx.hitPoint().y, ctx.hitPoint().z,
SoundEvents.ANVIL_LAND, SoundSource.PLAYERS, 0.3f, 1.2f);
}
@Override
public void onRicochet(BFHurtTarget target, BFDamageContext ctx) {
// 跳弹音效 + 偏转粒子
level.playSound(null, ctx.hitPoint().x, ctx.hitPoint().y, ctx.hitPoint().z,
SoundEvents.ARROW_HIT, SoundSource.PLAYERS, 0.5f, 1.5f);
spawnRicochetSparks(ctx.hitPoint(), ctx.hitNormal());
}
@Override
public void onSpall(BFHurtTarget target, BFDamageContext ctx) {
// 破片:在法线反方向(指向目标内部)产生二次破片弹丸
Vec3 inward = ctx.hitNormal().scale(-1);
for (int i = 0; i < 4; i++) {
Vec3 spreadDir = inward.add(
(random.nextFloat() - 0.5) * 0.5,
(random.nextFloat() - 0.5) * 0.5,
(random.nextFloat() - 0.5) * 0.5
).normalize();
spawnFragmentBullet(ctx.hitPoint(), spreadDir, 0.3f);
}
}
// 发起伤害的入口
public float fireAt(Object target, DamageSource source, float baseDamage, float penetration,
Vec3 hitVelocity, Vec3 hitPoint, Vec3 hitNormal) {
BFDamageContext ctx = BFDamageContext.builder()
.source(source)
.baseDamage(baseDamage)
.hitVelocity(hitVelocity)
.hitPoint(hitPoint)
.hitNormal(hitNormal)
.penetration(penetration)
.build();
return this.dealDamage(target, ctx);
}
}
这个 handler 将回调逻辑和伤害发起逻辑封装在同一个类中——fireAt 方法负责构造上下文并发起协议伤害,回调方法负责接收事件并产生视觉和音效反馈。这种"一体两面"的设计是武器模组中最常见的 handler 使用模式。