脱离实体
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 特殊机制(伪造的玩家实体) |
| 实现类 | 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 |
子步数上限,防止传送级位移触发巨量碰撞扫描 |
流程:
- 若 碰撞模式 为
NONE→noClip = true,直接setPosition(posX+dx, …)后返回; - 否则计算
dist = √(dx²+dy²+dz²);dist > 0.2时steps = min(64, ceil(dist / 0.2)),否则steps = 1; - 循环
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)自己复刻原版的分轴解算顺序:
- 用
boundingBox.addCoord(dx, dy, dz)得到目标箱; - 取出过滤后的碰撞箱列表
collisions; - 先解 Y:依次
calculateYOffset,然后boundingBox.offset(0, dy, 0); - 再解 X:依次
calculateXOffset,然后offset(dx, 0, 0); - 最后解 Z:依次
calculateZOffset,然后offset(0, 0, dz); - 三个轴的
posX/Y/Z全部由碰撞箱中心反推; - 状态位:
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 逻辑,
在自由相机激活期间都会读到原点坐标。
相关条目
- 自由相机状态机 - 谁调用
moveEntity,以及渲染距离收边为什么也走碰撞感知路径 - 相机碰撞模式 - 4 个取值如何改变
shouldIgnoreBlock与是否启用子步进 - 三脚架相机槽位 - 同一
CameraEntity类被复用为 9 个长期驻留的机位 - Mixin 加载阶段 - 相机不可见/无眼高由哪些渲染补丁配合