自由相机状态机

[!WARNING] 本条目描述的是 FreecamController(camera/FreecamController.java),不是 core/FreecamCore.java。 FreecamCore 只是 FML 的 coremod 入口,只负责挂载早期 Mixin,详见 Mixin 加载阶段。

基本信息

属性 值
类型 特殊机制(客户端相机状态机)
实现类 com.caedis.freecam.camera.FreecamController
单例 静态 INSTANCE,FreecamController.instance() 获取;reset() 整体换新实例
触发条件 按下/松开 切换自由相机 的 F4;或按住 F4 时用数字键 1~9 选三脚架槽位
服务端同步 无——全部逻辑在客户端,对世界数据零影响

相机如何脱离玩家

enable()(FreecamController.java:111)的完整步骤:

  1. 前置检查:已激活 / 已被禁用 / thePlayer == null / theWorld == null 时直接返回;
  2. cameraEntity = new CameraEntity(mc.theWorld, mc.thePlayer)——在玩家当前位置与朝向新建脱离实体(见 脱离实体);
  3. cameraEntity.setCollisionMode(GeneralConfig.collisionMode);
  4. 备份 previousRenderViewEntity = mc.renderViewEntity 与 previousPerspective = mc.gameSettings.thirdPersonView;
  5. 强制 thirdPersonView = 0(第一人称)并把 mc.renderViewEntity 指向相机实体;
  6. 置 active = true、playerControlled = false、activeSlot = TripodSlot.NONE、三轴速度归零;
  7. 调用 applyPerspectiveOffset() 按 进入视角 偏移。

关键点:它不移动也不冻结玩家。玩家照常受输入控制,只是 renderViewEntity 被换走,且移动输入在 MixinMovementInputFromOptions 里被清零,所以移动键被相机接管。渲染侧的鼠标视角则由 MixinEntityRenderer 从 EntityClientPlayerMP.setAngles 重定向到 renderViewEntity.setAngles。

相机如何重新挂回

disable()(FreecamController.java:159)无条件还原:

字段 还原为
mc.renderViewEntity previousRenderViewEntity
mc.gameSettings.thirdPersonView previousPerspective
previousPerspective -1(哨兵值)
previousRenderViewEntity / cameraEntity null
active false
activeSlot TripodSlot.NONE

触发退出的路径(全部走 disable()):

  • 松开 F4 且本轮没有选过三脚架(ClientEventHandler.onKeyInput);
  • 重复按同一个三脚架槽位 → 视为"关闭该机位"(toggleTripod);
  • tick() 中 pendingDisable 为真(受伤排程,见 受伤时禁用);
  • tick() 中 已被禁用;
  • 玩家死亡(thePlayer.isDead);
  • cameraEntity.worldObj != mc.theWorld(换了世界);
  • 断线(onDisconnect → disable()),随后 reset() 换新单例。

FreecamController.reset() 在连上服务器时也会被调用(ClientConnectedToServerEvent), 因此换图/重连后所有 三脚架机位 与速度倍率都会归零。

每 tick 逻辑

tick()(FreecamController.java:252)在 ClientTickEvent.END 阶段被调用,顺序为:

  1. pendingDisable 或 已被禁用 → disable(),并清空 pendingDisable;
  2. 未激活 / 无相机 / mc.isGamePaused() → 返回(暂停时不推进相机);
  3. 玩家死亡 → disable();cameraEntity.worldObj != mc.theWorld → disable();
  4. 每 tick 重新下发 setCollisionMode(GeneralConfig.collisionMode),使碰撞模式可运行时热改;
  5. cameraEntity.onUpdate()(只刷新 prev*/lastTick* 插值字段,不做实体逻辑);
  6. playerControlled 为真 → 直接返回,相机不接受键盘输入;
  7. 读 WASD / 潜行 / 疾跑 7 个原版按键的原始 lwjgl 按键状态;
  8. speed = MovementConfig.speed * speedMultiplier,疾跑再 × 1.5;
  9. 按 移动模式 走 CREATIVE 或 STATIC 位移;
  10. clampToRenderDistance() 收边。

移动与转向

常量 值 作用
DEFAULT_SPEED_SCALE 0.5 STATIC 模式的速度系数
CREATIVE_SPEED_SCALE 2.0 CREATIVE 模式的速度系数
CREATIVE_ACCELERATION 0.15 CREATIVE 模式下速度趋近目标的插值系数
CREATIVE_FRICTION 0.6 CREATIVE 模式下每 tick 的摩擦衰减
SPRINT_MULTIPLIER 1.5 按住疾跑键的倍率
DIAGONAL_FACTOR sin(45°) 同时前进+平移时的对角线归一化因子
SPEED_SCROLL_STEP 0.1F 滚轮每格的速度档位步进
SPEED_MULTIPLIER_MIN / MAX 0.1F / 10.0F 倍率上下限

两种模式的差别:

  • STATIC(tickDefaultMovement):直接把本 tick 的方向向量乘速度后 moveEntity,松键立即停住;
  • CREATIVE(tickCreativeMovement):先算出 target 速度,再对三个轴分别做 v += (target - v) * 0.15 的逼近,然后整体乘 0.6 摩擦;绝对值小于 0.001 的轴直接归零, 因此松键后会滑行一小段再停住。

朝向由 MixinEntityRenderer 把鼠标的 yaw/pitch 直接写进相机实体,控制器自身不处理旋转。

视角偏移(仅首次启用时)

applyPerspectiveOffset()(FreecamController.java:179)只在 enable() 末尾调用, enableTripod() 不调用它——这是 enable() 与 enableTripod() 的一个实质差异。

模式 距离常量 位移
INSIDE — 不偏移
FIRST_PERSON FIRST_PERSON_DISTANCE = 0.4 沿视线方向前移 0.4 格(眼睛前方)
THIRD_PERSON THIRD_PERSON_DISTANCE = 4.0 沿视线方向后移 4.0 格
THIRD_PERSON_MIRROR THIRD_PERSON_DISTANCE = 4.0 沿视线方向前移 4.0 格,并把 rotationYaw 与 prevRotationYaw 各 +180°

渲染距离收边

clampToRenderDistance()(FreecamController.java:298)是一个反作弊式护栏,不是性能限制:

  • maxDist = (renderDistanceChunks - CLAMP_MARGIN_CHUNKS) * 16.0,CLAMP_MARGIN_CHUNKS = 2(负值时钳到 0);
  • 对相机与玩家的三轴球面距离做比较,超出则按 scale = maxDist / dist 缩放偏移量;
  • 用 moveEntity 而非 setPosition 施加修正,使修正同样受 碰撞模式 约束, 避免玩家离开三脚架后相机被硬塞进方块里。

源码注释写明:把相机压在渲染距离边缘附近会 xray 到未加载区块的矿物。

玩家操控模式

togglePlayerControl()(FreecamController.java:217)在已激活时翻转 playerControlled, 并用 GTNHLib AboveHotbarHUD 在物品栏上方输出 msg.freecam.control.player / msg.freecam.control.camera。

开启后发生三处变化:

  • tick() 不再移动相机(由 移动模式 那段整体跳过);
  • 四个点击/拾取 Mixin 注入不再取消,允许玩家正常操作世界;
  • MixinEntityRenderer.getMouseOver 被取消并改用 thePlayer.rayTrace(reach, partialTicks), 即准星判定回到玩家身上。

反馈消息

全部通过 AboveHotbarHUD.renderTextAboveHotbar(text, 20, true, true) 输出,语言键见 assets/freecam/lang/en_US.lang:

语言键 触发
msg.freecam.enable / msg.freecam.disable toggle() 后按新状态二选一
msg.freecam.speed adjustSpeed(),格式 Speed: %.1fx
msg.freecam.tripod.open / msg.freecam.tripod.close toggleTripod(),格式带机位编号 %d
msg.freecam.tripod.reset resetTripods()
msg.freecam.control.player / msg.freecam.control.camera togglePlayerControl()

⚠️ 该 mod 只提供 en_US.lang,没有 zh_CN.lang,因此上述消息在中文环境下显示为语言键本身。

相关条目