[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
--- 居然有minestom的教程 必须支持一波 非常好的教程,我还是通过这篇文章了解到的minestom服务端,这个服务端的介绍、应用和面向的编程目前在中文社区还处于蓝海状态,且该服务端的实现看起来比较有趣。个人认为值得继续挖掘,感谢分享!
另外,经过编程开发版版主讨论,决定授予精华1评级,希望您能继续这方面的研究并分享。 同学yyds,赶紧发写好的核心,插件发到插件板块。 Yeqi 发表于 2025-2-6 14:37
同学yyds,赶紧发写好的核心,插件发到插件板块。
爱你{:q:kiss:} 大佬啊! 本帖最后由 Anzide 于 2025-2-10 01:20 编辑
还是Minestom大佬
顺便说一嘴,我创了一个Minestom开发交流群,欢迎加入:438752464
Edit: 发现你已经加入啦
嗯?
什么玩意在脑子里过了一下
页:
[1]