个人重生点与睡眠状态机
睡袋之所以能「让某个玩家在某个睡袋上出生」,靠的是 Forge 的
IExtendedEntityProperties扩展属性 + 每 tick 的服务端钩子。本条目覆盖ExtendedPlayer与EventHandler两个类。
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 特殊机制(无指令、无配置项) |
| 承载类 | ru.npo6ka.sleepingbag.ExtendedPlayer(IExtendedEntityProperties) |
| 驱动类 | ru.npo6ka.sleepingbag.EventHandler |
| 扩展属性名 | ExtendedPlayer(EXT_PROP_NAME 常量声明了它,但 register 里用的是字面量 "ExtendedPlayer") |
| 挂载时机 | EntityEvent.EntityConstructing 中对每个 EntityPlayer 调用 |
| 触发条件 | 玩家放置睡袋 / 玩家睡醒 |
| 指令 | 无 |
| 配置文件 | 无(源码中没有任何 Config 读取) |
存储字段
ExtendedPlayer 有 5 个字段,全部只在内存里:
| 字段 | 类型 | 含义 | 写入点 |
|---|---|---|---|
lastCoord |
ChunkCoordinates |
放置睡袋时玩家自己站的位置(floor(posX), (int) posY, floor(posZ)) |
放置睡袋时 |
bedCoord |
ChunkCoordinates |
睡袋脚部方块的坐标 | 放置睡袋时 |
lastSpawnCoord |
ChunkCoordinates |
放置睡袋时玩家原本的原版出生点(entity.getBedLocation(dimensionId)) |
放置睡袋时 |
isSleeping |
int |
睡眠状态机(0 / 1 / 2) | 放置睡袋 + 服务端 tick |
isWakeUp |
boolean |
唤醒标志,触发一次重生点重设 | 睡醒(setBedOccupied) |
睡眠状态机(isSleeping 三态)
isSleeping 不是一个布尔值而是三态计数器,由 EventHandler.onTick(仅 TickEvent.ServerTickEvent 且 phase == END)推进:
| 状态 | 含义 | 下一 tick 行为 |
|---|---|---|
0 |
未在睡(默认) | 无动作 |
1 |
睡袋已铺好,等待唤醒 | 置为 2 |
2 |
真正调用睡袋方块 | 对 bedCoord 处方块调用 onBlockActivated(...),然后置回 0 |
可观测结果:从放下睡袋到真正躺进去有 2 个服务端 tick(≈100 ms)的延迟。 放置时 storeOriginalPosition 写入 1,第一个 tick 升到 2,第二个 tick 才真正触发放睡动画。
被调用的参数固定为 (side = 1, hitX = 0.5f, hitY = 0.25f, hitZ = 0.5f),正好落在 0.25 高的睡袋顶面中央。
唤醒与重生点重设
BlockSleepingBag.setBedOccupied 每次被调用(无论 occupied 传 true 还是 false,方法体根本不读这个参数)都会 setWakeUpFlag(true)。服务端 tick 随后执行:
player.setSpawnChunk(ep.getlastSpawnCoord(), false, player.worldObj.provider.dimensionId);
ep.setWakeUpFlag(false);
即把玩家当前的原版重生点重新写回放置睡袋之前记录的那个坐标 lastSpawnCoord。配合 getBedSpawnPosition 覆写(返回 lastCoord),形成完整闭环:
- 战点判定 / 判怪点走
getBedSpawnPosition→ 睡袋旁边的lastCoord - 实际重生走原版
setSpawnChunk→ 睡袋铺之前记下的lastSpawnCoord - 两者分离,所以睡袋不会把全服战点搬到野外,也不会因为离睡袋太远而在半夜被判定成「离床太远」
客户端:躺姿下移
EventHandler.onRenderPlayer(RenderPlayerEvent.Pre,@SideOnly(Side.CLIENT)):
if (player.isPlayerSleeping()) GL11.glTranslated(0.0, -0.3, 0.0);
睡觉中的玩家模型整体下移 0.3 格,用于匹配 0.25 高的睡袋碰撞箱,避免人物悬空或陷进方块。
事件注册
CommonProxy.init() 把同一个 EventHandler 实例注册到两条总线:
| 总线 | 响应的事件 |
|---|---|
MinecraftForge.EVENT_BUS |
RenderPlayerEvent.Pre(仅客户端)、EntityEvent.EntityConstructing |
FMLCommonHandler.instance().bus() |
TickEvent.ServerTickEvent |
Core 的 preInit / init / postInit 全部只是转发给 CommonProxy,postInit 是空实现。
已知限制
- 不持久化:
saveNBTData/loadNBTData/init三个接口方法全是空方法体,NBTUtil也只是一个空的 final 类。ItemSleepingBag.TAG_POSITION = "player_pos_sleeping_bag"声明了但没有任何引用。所以睡袋坐标只活在内存里,玩家登出 / 服务器重启后需要重新睡一次才能生效 - 只遍历主世界:
onTick用getMinecraftServerInstance().getEntityWorld()取世界,只处理主世界的world.playerEntities。在其他维度(如 GTNH 的口袋维度)睡觉的玩家不会被这套 tick 逻辑处理 - 无出生点会 NPE:
lastSpawnCoord来自entity.getBedLocation(dimensionId),玩家从未设置过原版出生点时返回null,setSpawnChunk(null, ...)会抛异常 - 死亡/重生事件未挂钩:
EventHandler只监听RenderPlayerEvent、EntityConstructing、ServerTickEvent,没有LivingDeathEvent/PlayerRespawnEvent;「睡袋消失后重生点如何回退」完全依赖上面lastSpawnCoord的重写逻辑 Utils类未被使用:RAND/toString/debug三个成员在整个源码中没有任何调用点
相关条目
- 睡袋方块(Sleeping Bag Block) -
getBedSpawnPosition覆写与拆袋回收逻辑 - 睡袋(Sleeping Bag) - 写入
lastCoord/bedCoord/lastSpawnCoord的入口