脱离实体

基本信息

属性 值
类型 特殊机制(伪造的玩家实体)
实现类 com.caedis.freecam.camera.CameraEntity extends EntityPlayer
GameProfile new GameProfile(null, "FreeCamEntity")(无 UUID、无名称)
碰撞箱 setSize(0.4F, 0.425F),宽 0.4 / 高 0.425(与原版玩家一致)
眼高偏移 yOffset = 1.62F,但 getEyeHeight() 被覆写为 0.0F
初始位置 构造时复制 thePlayer 的 posX/Y/Z 与 rotationYaw/Pitch,并把 prevPos*/lastTickPos*/prevRotation* 同步到该值

选择继承 EntityPlayer 而不是 Entity 的原因体现在覆写清单上:渲染管线、方块高亮、 手持物、光照采样等大量客户端逻辑只对 EntityPlayer 做 instanceof 判断,继承它可以避免大量补丁。

位置与插值

方法 行为
setPosition(x, y, z) 覆写:不调用父类逻辑,直接写 posX/Y/Z,并以碰撞箱中心重建 boundingBox(min = 中心 - 半宽/半高,max = 中心 + 半宽/半高)
onUpdate() 覆写:只把当前位置复制到 prevPosX/Y/Z、lastTickPosX/Y/Z、prevRotationYaw/Pitch、prevRotationYawHead,不执行任何实体逻辑(不寻路、不燃烧、不上浮)

onUpdate() 的插值字段仍然有意义:EntityRenderer 的平滑取样依赖它们,所以相机移动在画面上仍是平滑的。

位移:子步进碰撞

moveEntity(dx, dy, dz)(CameraEntity.java:94)是本类最复杂的部分。

常量 值 作用
MAX_STEP 0.2D 单个子步的最大位移,必须小于碰撞箱尺寸,防止高速相机穿墙
MAX_STEPS 64 子步数上限,防止传送级位移触发巨量碰撞扫描

流程:

  1. 若 碰撞模式 为 NONE → noClip = true,直接 setPosition(posX+dx, …) 后返回;
  2. 否则计算 dist = √(dx²+dy²+dz²);dist > 0.2 时 steps = min(64, ceil(dist / 0.2)),否则 steps = 1;
  3. 循环 steps 次调用 moveStep(sx, sy, sz),把位移等分。

moveStep(CameraEntity.java:114)按模式分派:

  • FULL:noClip = false,调用 super.moveEntity(原版 EntityPlayer 完整碰撞), 随后修正 posY——原版把 posY 推导为 bb.minY + yOffset - ySize,而本实体要的是碰撞箱中心, 故重写为 (bb.minY + bb.maxY) / 2.0;
  • 其他模式:noClip = false,调用自实现的 moveEntityFiltered。

moveEntityFiltered(CameraEntity.java:126)自己复刻原版的分轴解算顺序:

  1. 用 boundingBox.addCoord(dx, dy, dz) 得到目标箱;
  2. 取出过滤后的碰撞箱列表 collisions;
  3. 先解 Y:依次 calculateYOffset,然后 boundingBox.offset(0, dy, 0);
  4. 再解 X:依次 calculateXOffset,然后 offset(dx, 0, 0);
  5. 最后解 Z:依次 calculateZOffset,然后 offset(0, 0, dz);
  6. 三个轴的 posX/Y/Z 全部由碰撞箱中心反推;
  7. 状态位:isCollidedHorizontally = dx != dx || dz != dz(利用 calculateXOffset 把 NaN 作为"不可移动"信号 的原版技巧)、isCollidedVertically = origDy != dy、onGround = isCollidedVertically && origDy < 0.0D、 isCollided = 水平 || 垂直。

碰撞箱的筛选

getFilteredCollisionBoxes(area)(CameraEntity.java:157):

  • 复用成员字段 collisionScratch(List<AxisAlignedBB>)并在每次调用时 clear(), 避免高速移动时每个子步都分配新列表;
  • 先用 MathHelper.floor_double 算出遍历范围,其中 X/Z 用 max = floor(area.max + 1.0);
  • 双重循环 bx/bz 时先做 if (!worldObj.blockExists(bx, 64, bz)) continue;——Y 固定取 64 只是 "该列区块是否已加载"的探测值,不表示只扫描 y=64;
  • 内层 by 从 minY - 1 到 maxY - 1(向下多扫一层)取 Block,先过 shouldIgnoreBlock 再 block.addCollisionBoxesToList(...) 填入列表。

shouldIgnoreBlock(CameraEntity.java:185)按 碰撞模式 决定放行哪些方块:

模式 过滤条件
IGNORE_TRANSPARENT !block.isOpaqueCube() → 所有非不透明方块放行
IGNORE_OPENABLE block instanceof BlockDoor || BlockTrapDoor || BlockFenceGate
FULL / NONE 不过滤(false)

被裁掉的行为

以下方法全部被覆写,目的是让这个"假玩家"完全不参与服务端语义:

方法 返回/行为 目的
isInvisible() true 不渲染模型
getEyeHeight() 0.0F 视线相关计算不再抬高
isEntityAlive() true 永远存活,避免被当作待移除实体
isEntityInsideOpaqueBlock() false 避免相机卡墙时触发第三人称穿墙处理
canBePushed() / canBeCollidedWith() false 不被水流/活塞推动,其它实体也不会撞上它
isSneaking() false 强制不潜行
attackEntityFrom(...) false 免疫一切伤害
damageEntity(...) 空实现 兜底
addChatMessage(...) 空实现 相机不产生聊天输出
canCommandSenderUseCommand(...) false 相机不能执行命令
getPlayerCoordinates() new ChunkCoordinates(0, 0, 0) 不暴露真实坐标
getHeldItem() null 无手持物
getEquipmentInSlot(int) null 无护甲
setCurrentItemOrArmor(int, ItemStack) 空实现 拒绝写入
getLastActiveItems() new ItemStack[0] 物品栏为空数组

⚠️ getPlayerCoordinates() 恒返回 (0,0,0):任何依赖该方法取玩家位置的第三方 mod 逻辑, 在自由相机激活期间都会读到原点坐标。

相关条目