clok 发表于 2025-2-4 01:38:40

[Java核心开发]手把手使用Minestom带你解剖Minecraft的击退机制 ——实现精准可控的PVP击退效果

本帖最后由 clok 于 2025-2-5 01:13 编辑

---

# 手把手使用Minestom带你解剖Minecraft的击退机制
# ——实现精准可控的PVP击退效果

---

# 第一章 关于一个简单的击退原理介绍以及本篇文章的概要
## 读前须知
1. 该教程相对于其他的击退,**或许**(此处表示也许)更加**通俗易懂**(仅仅对于**我**而言)
2. 初中生来了也能**懂**,但是你如果初中**没有**学习**向量**的话可以稍微了解一点,稍微一点就会
3. 我代码写的很烂,别喷,能看懂就行,这里主要是计算
4. 该教程也是看着MinestomPVP总结出来的
5. 此处不包含任何**MoJang**代码,依赖与**Minestom**
6. 教程很长,大多数都是问题的解答,有实力的可以只看前两章,后两章只是解答问题
7. 本文章使用了AI帮我把我的话我的代码总结成md文档,本人表达能力有限。不得不说我觉得AI最好用的功能就是写wiki了,其他真的一般般
8. 测试环境:
    - 测试核心:Minestom
    - 核心版本:9803f2bfe3
    - 游戏版本:1.21.3


## 核心概念
在Minecraft中,击退(Knockback,简称KB)的本质是 **通过方向向量和速度合成实现的物理效果** 要实现优秀的PVP击退,需掌握以下核心要素:
1. **击退方向**: 由攻击者的面朝角度(Yaw)决定
2. **击退力度**: 由武器属性和状态效果调节
3. **速度合成**: 结合目标当前速度和击退力

---

## 零、代码展示
### 0.1 takeKnockback(对目标执行一个击退)
```kotlin

    @JvmStatic
    fun executeKnockback(
      target: LivingEntity,
      //source也许是一个空的
      source: Entity?,
      strength: Float,
      dx: Double,
      dz: Double,
      type: Type,
      attacker: Entity,
      verticalLimit: Double = 0.4
    ): Boolean {
      val event = EntityKnockbackEvent(target, source ?: attacker, type ,strength, verticalLimit).apply {
            EventDispatcher.call(this)
      }

      if (event.isCancelled) return false

      takeKnockback(target, event.strength, dx, dz, event.verticalLimit)
      return true
    }

    /*
    *空中连击为什么难?因为limit参数卡死了上升速度!
    *在20tick服务器,Y轴速度最大=0.4×20=8格/秒——
    *这就是你无法把对手打成卫星的根本原因!
    *
    * */
    @JvmStatic
    fun takeKnockback(entity: LivingEntity, str: Float, x: Double, z: Double, limit: Double = 0.4) {
      var strength = str
      if (strength > 0.0f) {
            strength *= ServerFlag.SERVER_TICKS_PER_SECOND.toFloat()
            //这是做了个归一化,如果不做归一化的话,可能会使击退数据出现异常,增加或降低
            val velocityModifier = Vec(x, z).normalize().mul(strength.toDouble())

            //在20tick服务器,Y轴速度最大=limit×20=8格/秒——
            val verticalLimit = limit * ServerFlag.SERVER_TICKS_PER_SECOND.toDouble()
            entity.velocity = Vec(
                entity.velocity.x() / 2.0 - velocityModifier.x(), if
                        (entity.isOnGround) min(verticalLimit, entity.velocity.y() / 2.0 + strength.toDouble())
                else entity.velocity.y(), entity.velocity.z() / 2.0 - velocityModifier.z()
            )
      }
    }


```
| 外部参数名称                   | 类型      | 默认值 | 作用域       | 功能描述                                                               |
|---------------------------|------------|--------|-------------|--------------------------------------------------------------------------|
| `entity: LivingEntity`   | 一只富有活力实体      | -      | 函数入参   | 被施加击退效果的目标实体(玩家/生物)                                        |
| `str: Float`               | 浮点数      | -      | 函数入参   | 基础击退强度(受武器、附魔、药水等影响)                                    |
| `x: Double`                | 双精度浮点| -      | 函数入参   | 击退方向的 X 轴分量(东西方向)                                             |
| `z: Double`                | 双精度浮点| -      | 函数入参   | 击退方向的 Z 轴分量(南北方向)                                             |
| `limit: Double`            | 双精度浮点| 0.4    | 函数入参   | 垂直速度上限(格/秒)                                                      |

| 内部变量名称                   | 类型      | 默认值 | 作用域       | 功能描述                                                               |
|---------------------------|------------|--------|-------------|--------------------------------------------------------------------------|
| `strength`               | 浮点数      | -      | 函数内部变量 | 经过 Tick 率换算后的实际击退强度                                          |
| `velocityModifier`         | Vec 向量    | -      | 函数内部变量 | 归一化后的击退方向向量 × 强度                                             |
| `verticalLimit`            | 双精度浮点| -      | 函数内部变量 | 根据服务器 Tick 率换算的垂直速度上限                                     |

### 0.2 runKnockback(执行一个简单的击退)
   ```kotlin
    private const val DEFAULT_KNOCKBACK_FACTOR = 0.5f
    private const val DEFAULT_VERTICAL_LIMIT = 0.4

    //这是Attack的击退!具体什么是attack击退你看第二章就知道了
    override fun processKnockback(
      attacker: Entity,
      target: LivingEntity,
      knockbackStrength: Int
    ): Boolean {
      if (knockbackStrength <= 0) return false

      return with(attacker.position) {
            val horizontalStrength = knockbackStrength * DEFAULT_KNOCKBACK_FACTOR
            val (dx, dz) = calculateAttackDirectionComponents(yaw)

            executeKnockback(
                target = target,
                source = attacker,
                strength = horizontalStrength, dx = dx, dz = dz, type = Type.ATTACK, attacker = attacker, DEFAULT_VERTICAL_LIMIT
            )
                .also {
                  success -> if (success && attacker is GamePlayer) attacker.afterMoveAttack()
                }

      }
    }

    private fun calculateAttackDirectionComponents(yaw: Float): Pair<Double, Double> {
      val radianYaw = Math.toRadians(yaw.toDouble())
      return sin(radianYaw) to -cos(radianYaw)
    }
   ```
#### 0.2.1
    这是Attack的击退的代码!具体什么是attack击退你看第二章就知道了,最好别跳着看,你可以先不知道,先往下看,这代码只是助于你理解的,所以叫attack击退
---

## 一、坐标系与基础参数
### 1.1 世界坐标系
- **X轴**:东(+) ↔ 西(-)
- **Z轴**:南(+) ↔ 北(-)
- **Y轴**:垂直方向(上+,下-)

### 1.2 面朝角度(Yaw)
- **0°**:正南(-Z方向)
- **90°**:西(-X方向)
- **180°**:正北(+Z方向)
- **270°**:东(+X方向)

---

## 二、击退方向计算
### 2.1 基础公式

击退方向由攻击者的Yaw角度通过三角函数计算:
```kotlin
   //将角度转为弧度制
   val radianYaw = Math.toRadians(yaw.toDouble())
   //计算生成一个Pair<Double,Doblue>的一对数值
   return sin(radianYaw) to -cos(radianYaw)
```
#### 关于2.1
   **你或许会有的疑问:**
      
      为什么dx不带负号而dz带了呢?:
      
         我们前面说过面朝的角度(Yaw)与方向的关系我们可以知道Yaw角度与方向的关系:
         Z轴和南北有关
         X轴与东西有关

         第一步:理解 Minecraft 的角度系统
            这与数学中的标准角度系统(0° 为东,逆时针旋转)不同,Minecraft 的 yaw 是 以正南为起点顺时针旋转
         可见我们需要调整三角函数的计算方式

         第二步:击退方向的反向逻辑
            当攻击者挥剑时,击退方向与攻击者的面朝方向相反(类似“向后推”)例如:
            攻击者面朝 正南(+Z) → 击退方向是 正北(-Z)
            攻击者面朝 东(+X) → 击退方向是 西(-X)
         可见为了反向,代码中需要将面朝方向的分量取反

         第三步:X 和 Z 分量的推导
            1. X 轴(东西方向)的分量:x = sin(yaw)
               在MC中,sin(θ) 对应 东西方向的分量
                  当玩家面朝 东(yaw=270°) 时:sin(270°) = -1 → 击退方向为 西(-X)
                  当玩家面朝 西(yaw=90°) 时:sin(90°) = 1 → 击退方向为 东(+X)
            2. Z轴(南北方向)的分量:z = -cos(yaw)
               cos(yaw) 原本对应 南北方向的分量,但需要反向
               当玩家面朝 南(yaw=0°) 时:cos(0°) = 1 → -cos(0°) = -1 → 击退方向为 北(-Z)
               当玩家面朝 北(yaw=180°) 时:cos(180°) = -1 → -cos(180°) = 1 → 击退方向为 南(+Z)
         第四步:举例子
            例1:攻击者面朝正南(yaw=0)
               x = sin(0°) = 0 → 无东西方向分量
               z = -cos(0°) = -1 → 击退方向为 北(-Z)
               ✅ 符合预期(面朝南,击退向北)
            例2:攻击者面朝东(yaw=270°)
               x = sin(270°) = -1 → 击退方向为 西(-X)
               z = -cos(270°) = 0 → 无南北方向分量
               ✅ 符合预期(面朝东,击退向西)
            例3:攻击者面朝西北(yaw=135°)
               x = sin(135°) ≈ 0.707 → 击退方向为 东南(+X)

               z = -cos(135°) ≈ 0.707 → 击退方向为 东南(+Z)
               ✅ 合成为 东南方向,与面朝西北相反

         你还可以这么想,通过诱导公式可以得出以下公式
            sin(-α) = -sinα
            cos(-α) = cosα
         这组诱导公式得到的sin都是相反的,但是cos不是,想要得到相反的直接加负号不就好了
      

### 2.2 方向验证表
| 面朝方向 | Yaw| dx    | dz    | 击退方向 |
|----------|------|-------|-------|----------|
| 正南   | 0°   | 0.0   | -1.0| 正北   |
| 正东   | 270° | -1.0| 0.0   | 正西   |
| 东北   | 45°| 0.707 | 0.707 | 西南   |

---

## 三、归一化处理
### 3.1 问题描述
原始方向向量的长度不固定(如东北方向长度为√2≈1.414),直接使用会导致:
- 相同力度下,斜向击退距离比正方向远41%

### 3.2 解决方案
将方向向量转换为单位向量(长度=1):
原代码(Kotlin):
```kotlin
   val velocityModifier = Vec(dx, dz).normalize().mul(strength.toDouble())
```
实际表达的意思(Java):
```java
// 计算向量长度
double length = Math.sqrt(dx * dx + dz * dz);

// 归一化
if (length > 0) {
    dx /= length;
    dz /= length;
}
```

### 3.3 效果对比
| 原始向量 | 归一化结果 |
|----------|------------|
| (3, 4)   | (0.6, 0.8) |
| (1, 1)   | (0.707, 0.707) |

---

## 四、速度合成算法
### 4.1 计算公式
原代码:
```kotlin
      // 水平速度
      val newVelX = entity.velocity.x() / 2.0 - velocityModifier.x()
      val newVelZ = entity.velocity.z() / 2.0 - velocityModifier.z()
      // 垂直速度
      val newVelY = if (entity.isOnGround) min(verticalLimit, entity.velocity.y() / 2.0 + strength.toDouble()) else entity.velocity.y()
      entity.velocity = Vec(newVelX, newVelY, newVelZ)
```

### 4.2 参数说明
- **entity.velocity**:目标当前速度矢量
- **velocityModifier**:处理后的速度矢量,实际上就是原速度矢量*strength
- **/2.0**:模拟惯性衰减

---

## 五、实现优秀PVP击退的配置指南
### 5.1 参数推荐值
| 战斗风格       | strength | 适用场景          |
|----------------|----------|-------------------|
| 竞技精准       | 0.6~0.8| 1v1决斗         |
| 快速连击       | 0.4~0.6| 连招Combo         |
| 爆发控制       | 1.0~1.2| 群体PVP         |

### 5.2 进阶优化技巧
1. **地面限制增强**
```Kotlin
// 增强地面单位的垂直击退
val newVelY = if (entity.isOnGround) min(verticalLimit + 0.2, entity.velocity.y() / 2.0 + strength.toDouble() * 1.2) else entity.velocity.y()
```

2. **空中击退补偿**
```kotlin
// 对空中单位施加20%额外水平击退
if (!entity.isOnGround) entity.velocity = Vec(newVelX * 1.2, newVelY, newVelZ * 1.2)
```

3. **方向随机扰动**(防预判,同时这也会为将要讲的DamageKnockback埋下伏笔)
```java
// 添加±5°随机偏移
val randomOffset = ThreadLocalRandom.current().nextDouble(-5.0, 5.0)
//这里的yaw是processKnockback里的那个calculateAttackDirectionComponents(yaw)里面的这个yaw
val adjustedYaw = yaw + randomOffset
//在processKnockback里的calculateAttackDirectionComponents(yaw)改成calculateAttackDirectionComponents(adjustedYaw)
```

---

## 六、调试与验证方法
### 6.1 调试工具
1. **F3调试界面**:实时查看实体坐标和速度
2. **Replay Mod**:录制并回放击退轨迹
3. **自定义ActionBar**:显示方向向量和力度数值

### 6.2 验证流程
1. 面朝正南攻击,确认目标向北移动(Z坐标递减)
2. 使用45°方向攻击,验证击退距离 = strength × √2
3. 连续攻击空中目标,检查垂直速度是否符合预期

---

## 七、常见问题解决方案
| 问题现象                | 排查重点                   | 解决方案               |
|-------------------------|----------------------------|--------------------------|
| 击退方向相反            | 检查dz是否使用`-cos(yaw)`| 修正符号               |
| 斜向击退距离异常      | 确认是否执行归一化         | 添加归一化处理         |
| 空中单位无击退效果      | 检查onGround判断逻辑       | 移除不必要的条件限制   |
| 连击时速度指数增长      | 验证速度衰减是否使用`/2.0` | 检查速度合成公式         |

---

# 第二章 深度解构Damage击退与Attack击退的协同机制——精确控制双重击退的叠加效应

## 提前预判你的疑问
    啥是Damage击退啊啥是Attack击退啊
      Damage击退就是每次攻击触发的
      Attack击退就是蓄力接近满格+击退触发的额外击退,这额外击退是在damage击退的基础上追加的一个加速度向量罢了
    哪Damage击退的代码跟Attack代码有什么区别?
    实际上我上一章讲到了一点attack击退,就是

## 核心概念重塑
### Damage击退与Attack击退的本质差异
| 特性                | Damage击退                        | Attack击退(暴击击退)            |
|---------------------|-------------------------------------|-------------------------------------|
| **触发条件**      | 所有攻击必定触发                  | 疾跑+蓄力满格时触发               |
| **力度基数**      | 基础值0.4f                        | 额外追加0.5f~1.0f                   |
| **方向基准**      | 攻击者与目标位置关系                | 攻击者面朝方向                      |
| **优先级**          | 底层基础                            | 上层叠加                            |

---

## 代码全景
### DamageKnockback.kt
```kotlin
//CONFIG
private const val MIN_DIRECTION_THRESHOLD = 1.0E-4
private const val RANDOM_DIRECTION_FACTOR = 0.01
private const val STRENGTH = 0.4f

object DamageKnockback :
    IDamageKnockback {

    //
    override fun processKnockback(damage: Damage, target: LivingEntity, knockbackStrength: Int): Boolean {
      val attacker = damage.attacker ?: return false

      val (dx, dz) = attacker.calculateKnockbackDirectionTo(target.position)

      target.sendHurtAnimation(dx, dz)

      return executeKnockback(target, attacker = attacker, source = damage.source, type = Type.DAMAGE, strength = STRENGTH, dx = dx, dz = dz)
    }

    private fun Entity.calculateKnockbackDirectionTo(target: Pos): Pair<Double, Double> {
      var dx = this.position.x - target.x
      var dz = this.position.z - target.z

      /*
      * 当方向向量过小时生成随机方向
      * 所以你在攻击的时候,与defender贴的很近,可能就会出现一种奇妙的现象,就是这个defender飞到你了你后面,或者左边或者右边
      *
      * */
      if (dx * dx + dz * dz < MIN_DIRECTION_THRESHOLD) {
            val random = ThreadLocalRandom.current()
            dx = random.randomDirectionComponent()
            dz = random.randomDirectionComponent()
      }
      return dx to dz
    }

    private fun ThreadLocalRandom.randomDirectionComponent() =
      nextDouble(-1.0, 1.0) * RANDOM_DIRECTION_FACTOR

    private fun LivingEntity.sendHurtAnimation(dx: Double, dz: Double) {
      //这个东西完全自愿选择!删了的话也没问题,客户端自行处理的动画没有太大差异
      if (this is Player) {
            val hurtDirection = calculateHurtDirection(dx, dz)
            sendPacket(HitAnimationPacket(entityId, hurtDirection))
      }
    }

    private fun Player.calculateHurtDirection(dx: Double, dz: Double): Float {
      val attackAngle = Math.toDegrees(atan2(dz, dx))
      return (attackAngle - position.yaw).toFloat().normalizeAngle()
    }
}
```

### AttackKnockback.kt
```kotlin
object AttackKnockback : IAttackKnockback {

    //
    private const val DEFAULT_KNOCKBACK_FACTOR = 0.5f
    private const val DEFAULT_VERTICAL_LIMIT = 0.4


    override fun processKnockback(
      attacker: Entity,
      target: LivingEntity,
      knockbackStrength: Int
    ): Boolean {
      if (knockbackStrength <= 0) return false

      return with(attacker.position) {
            val horizontalStrength = knockbackStrength * DEFAULT_KNOCKBACK_FACTOR
            val (dx, dz) = calculateAttackDirectionComponents(yaw)

            executeKnockback(
                target = target,
                source = attacker,
                strength = horizontalStrength, dx = dx, dz = dz, type = Type.ATTACK, attacker = attacker, DEFAULT_VERTICAL_LIMIT
            )
                .also {
                  //这afterMoveAttack()实际上是为了限制玩家的速度
                  success -> if (success && attacker is GamePlayer) attacker.
                  afterMoveAttack()
                }

      }
    }

    private fun calculateAttackDirectionComponents(yaw: Float): Pair<Double, Double> {
      val radianYaw = Math.toRadians(yaw.toDouble())
      return sin(radianYaw) to -cos(radianYaw)
    }

}
```

---

## 一、方向计算机制
### 1.1 基础向量
从攻击者到目标的向量:
```kotlin
var dx = attacker.x - target.x
var dz = attacker.z - target.z
```
| 坐标差类型 | 物理意义                |
|------------|-------------------------|
| dx > 0   | 攻击者在目标东侧      |
| dz < 0   | 攻击者在目标北侧      |

### 1.2 随机扰动触发条件
当向量长度平方小于阈值时触发随机方向:
```kotlin
if (dx*dx + dz*dz < MIN_DIRECTION_THRESHOLD) { // 默认1.0E-4
    // 生成随机方向分量
}
```
**典型场景**:
- 攻击者与目标坐标完全重合
- 两者距离小于0.01格(√(1E-4) = 0.01)

**举例子**:

    玩家A和玩家B在游戏中决斗,两人同时发动攻击,结果坐标几乎完全重合,距离小于0.01格。此时,游戏的随机扰动机制触发:
    原本玩家A的剑应该击中玩家B,但因扰动,剑锋偏转,擦肩而过。玩家B抓住机会,反手一击,反败为胜。玩家A无奈道:“就差0.01格,代码不让我赢!”

---

## 二、随机方向生成
### 2.1 随机分量算法
```kotlin
fun ThreadLocalRandom.randomDirectionComponent() =
    nextDouble(-1.0, 1.0) * RANDOM_DIRECTION_FACTOR // 默认0.01
```
生成范围:-0.01 ~ +0.01

### 2.2 随机方向意义
| 随机值    | 方向偏移          | 视觉效果            |
|-----------|-------------------|-----------------------|
| dx=0.01   | 向东轻微偏移      | 目标微微右弹          |
| dz=-0.008 | 向南轻微偏移      | 目标微微前弹          |

---

## 三、受伤动画同步
### 3.1 动画包发送
```kotlin
target.sendPacket(HitAnimationPacket(entityId, hurtDirection))
```
关键参数:
- **entityId**: 受击实体的唯一标识
- **hurtDirection**: 伤害方向角度(单位:度)

### 3.2 角度计算流程
1. 计算攻击向量与东轴的夹角:
   ```kotlin
   val attackAngle = Math.toDegrees(atan2(dz, dx)) // 范围(-180°~180°)
   ```
2. 转换为相对于玩家视角的角度:
   ```kotlin
   (attackAngle - playerYaw).normalizeAngle()
   ```

### 3.3 我猜你还会有疑问
为啥你都对它造成伤害了,哪不有受伤动画了吗,为啥还要发一次呀!
   
    我在一开始查资料的时候没有注意,我看了NMS源码,它也没有再发包,但是自己复刻出来发现缺点味,打起来不舒服,这不舒服来自窗口的抖动

    哪么到底是为什么呢!?
      欸欸欸这要解释的话太长了,我放第三章,额外章好好给你讲讲,感兴趣的可以直接看那里

---

## 四、配置参数详解
| 参数名称                   | 默认值      | 作用域               | 调整建议         |
|---------------------------|-------------|----------------------|--------------------|
| MIN_DIRECTION_THRESHOLD   | 1.0E-4      | 方向计算             | 保持默认         |
| RANDOM_DIRECTION_FACTOR   | 0.01      | 随机扰动幅度         | 根据战斗风格调整(最好保持默认就行,你玩家挤在一起的机会很少)   |
| Type.DAMAGE.strength      | 0.4f      | 基础击退力度         | 参考PVP平衡需求(其他需求可以看第一章第五节)    |

---

## 五、调试技巧
### 5.1 方向可视化
1. 在攻击者与目标之间绘制粒子线
2. 当触发随机方向时显示特殊标记

### 5.2 数据监控
```kotlin
// 调试输出示例
println("原始方向: dx=$dx, dz=$dz")
if (isRandomized) println("随机方向: $randDx, $randDz")
```
---

## 六、与Attack击退的对比
| 特性                | Damage击退               | Attack击退             |
|---------------------|--------------------------|------------------------|
| 方向基准            | 攻击者位置               | 攻击者面朝角度         |
| 垂直控制            | 无特殊处理               | 地面限制               |
| 适用场景            | 弹射物、环境伤害         | 近战攻击               |
| 方向随机性          | 近距离强制随机         | 可配置扰动             |

### 6.1 我猜你有疑问
- 为什么**attack击退**的计算与**damage击退**的计算有这么大的差异!?
   
      一 Damage击退:位置差决定方向
            1 空想
                想象攻击者(A)和目标(B)在平面上:
                  A坐标:(x₁, z₁)
                  B坐标:(x₂, z₂)
                击退方向 = B到A的反方向
            2 计算
                  dx = A的x坐标 - B的x坐标
                  dz = A的z坐标 - B的z坐标
                比如A在(5,5),B在(0,0),dx=5-0=5,dz=5-0=5
                反向处理的话直接(-dx, -dz)即可
            3. 为什么这样设计?
                符合直觉:就像你推别人,对方会朝你相反方向后退
                自动适应:无论从哪个方向攻击,方向自动计算
      二、Attack击退:面朝角度决定方向(第一章有详细的计算过程和详解)
            1. 角度系统速成(虽然已经说过无数遍了)
                0°:面朝正南
                90°:面朝西
                180°:面朝正北
                270°:面朝东
            2. 计算步骤
                把角度转为弧度:
                  弧度 = 角度 × (π/180)
                  比如90° → 1.5708弧度
                用三角函数计算方向:
                  dx = sin(弧度)
                  dz = -cos(弧度)
      三、为什么两种方式不同?
            1. Damage击退的特点
                适合远程攻击:箭、火球等
                无需玩家控制:方向自动计算(方向其实就是那碰撞点决定的)

            2. Attack击退的特点
                适合近战操作:玩家可以主动控制方向
                战术性强:可以通过调整面朝角度精确击退

      四、实战验证方法
            1. Damage击退测试
                让朋友站在(0,0)
                你站在(5,0)攻击
                观察朋友被击退到(-5,0)左右的方向(一定不是-5,0,因为会有衰减的)
            2. Attack击退测试
                面朝正南(0°)攻击
                朋友会被击退到正北方向
                转身90°再攻击,观察方向变化
      

      你现在应该能理解:
            Damage击退像被球砸中,方向由碰撞点决定
            Attack击退像用棍子推人,方向由你拿棍子姿势控制
---

# 第三章 精准控制客户端表现的关键设计——sendHurtAnimation

## 前情概要
    这章开始往后都是问题篇章了,这章的问题在第二章3.3中提到

## 一、动画触发的本质区别

### 1.1 服务端与客户端的职责分离
| 触发源         | 动画类型       | 控制权归属   | 必要性               |
|----------------|----------------|-------------|----------------------|
| 伤害计算       | 实体抖动+红屏| 客户端   | 自动触发(不可靠)    |
| HitAnimationPacket | 击退方向动画   | 服务端   | 必须手动控制          |

- **客户端自动动画**:当客户端收到伤害事件时自动播放基础受伤效果,但:
- ❌ 不包含精确的击退方向信息
- ❌ 不同客户端版本表现不一致
- ❌ 无法与自定义击退逻辑同步

---

## 二、HitAnimationPacket 的核心作用

### 2.1 数据包结构解析()
```java
HitAnimationPacket(
    int entityId,    // 受击实体ID
    float yaw// 伤害方向角度(单位:度)
)
```

### 2.2 关键功能实现
| 参数         | 客户端行为                                                               |
|--------------|--------------------------------------------------------------------------|
| entityId   | 确定要播放动画的实体                                                      |
| direction    | 控制以下行为:<br>1. 受伤实体头部转向<br>2. 客户端预计算击退 |

---

## 三、两组动画实验得出真相

### 3.1 实际表现对比
| 调用情况                | 客户端视觉效果                        | 网络流量分析                     |
|-------------------------|----------------------------------------|----------------------------------|
| 不发送数据包            | 仅有轻微抖动,无方向性动画            | 减少1个数据包(约20字节)      |
| 发送HitAnimationPacket | 完整击退方向动画+实体头部转向         | 增加关键方向信息同步             |

### 3.2 代码层面验证
在 `LivingEntity#damage()` 源码中:
```java
// Minestom核心类
public void damage(@NotNull Damage damage) {
    // 自动触发的客户端效果仅限于:
    // 1. 生命值变化
    // 2. 基础受伤音效
    // 3. 无方向性的实体抖动
    // 没有击退方向动画!
}
```

---

## 四、伟大的科学!

### 4.1 角度计算公式
```kotlin
fun calculateHurtDirection(dx: Double, dz: Double): Float {
    val attackAngle = Math.toDegrees(atan2(dz, dx)) // [-180°, 180°]
    val relativeAngle = attackAngle - position.yaw// 转换为玩家视角坐标系
    return normalizeAngle(relativeAngle)            // 标准化到[-180°, 180°]
}
```

### 4.2 同步必要性演示
假设场景:
- 攻击者坐标:(5, 0, 5)
- 受击者坐标:(0, 0, 0)
- 攻击者Yaw:45°

不发送数据包时:
```
客户端计算方向角度 = atan2(0,0) = 0° // 无效值
实际表现:随机方向抖动
```

发送数据包时:
```
服务端计算:
attackAngle = atan2(5,5) = 45°
relativeAngle = 45° - 45° = 0°
最终动画:正对攻击者的完美方向同步,抖动更加舒服了,使我达到高潮
```

---
### 5.2 流量消耗测试(理论)
| 战斗强度       | 每秒数据包增量 | 带宽消耗       |
|----------------|----------------|----------------|
| 单人PVP      | 2-5 packets/s| <1 KB/s      |
| 50人团战       | 100 packets/s| ≈20 KB/s       |

---

通过以上步骤的系统化实施,你将能够精确控制Minecraft的击退机制,打造出既符合物理直觉又具备竞技深度的PVP体验

如果还有问题请加入我们
- QQ:995070869
---

PixelRPG 发表于 2025-2-4 02:56:05

居然有minestom的教程 必须支持一波

ABlueCat 发表于 2025-2-4 17:45:51

非常好的教程,我还是通过这篇文章了解到的minestom服务端,这个服务端的介绍、应用和面向的编程目前在中文社区还处于蓝海状态,且该服务端的实现看起来比较有趣。个人认为值得继续挖掘,感谢分享!
另外,经过编程开发版版主讨论,决定授予精华1评级,希望您能继续这方面的研究并分享。

Yeqi 发表于 2025-2-6 14:37:15

同学yyds,赶紧发写好的核心,插件发到插件板块。

clok 发表于 2025-2-6 14:56:41

Yeqi 发表于 2025-2-6 14:37
同学yyds,赶紧发写好的核心,插件发到插件板块。

爱你{:q:kiss:}

一路繁华的夏 发表于 2025-2-6 15:37:39

大佬啊!

Anzide 发表于 2025-2-10 01:18:05

本帖最后由 Anzide 于 2025-2-10 01:20 编辑

还是Minestom大佬
顺便说一嘴,我创了一个Minestom开发交流群,欢迎加入:438752464

Edit: 发现你已经加入啦

MSCAX 发表于 3 天前

嗯?
什么玩意在脑子里过了一下
页: [1]
查看完整版本: [Java核心开发]手把手使用Minestom带你解剖Minecraft的击退机制 ——实现精准可控的PVP击退效果