//go:build gl // 客户端输入处理:键鼠状态采集(UI与输入.md §6)。 // // 设计:回调只更新状态,游戏逻辑在 tick 里消费状态(主循环解耦)。 package main import ( "math" "mc/internal/game" "mc/internal/physics" "github.com/go-gl/glfw/v3.3/glfw" ) // inputState 键鼠输入状态。 type inputState struct { win *glfw.Window // 按键状态 forward, back, left, right, jump, sneak, sprint bool // 鼠标 lastX, lastY float64 firstMouse bool dyaw, dpitch float64 // 累计角度增量(弧度,tick 消费) sens float64 // 灵敏度(设置界面可调,client.yaml mouse_sensitivity) // 按键事件(tick 消费) breakDown bool breakHeld bool placeDown bool placeHeld bool selectSlot int // -1 无 toggleInv bool // E 打开/关闭物品栏 dropItem bool // Q 丢弃选中槽 1 个物品 // 鼠标与 UI 事件(UI与输入.md §6 屏幕交互) cursorX, cursorY float64 clickX, clickY float32 clicked bool clickedRight bool escPressed bool toggleFS bool // F11 / Alt+Enter(渲染线程消费,渲染.md §5.4) textMode bool chars []rune backspace bool } // newInputState 创建输入状态并注册回调。 func newInputState(win *glfw.Window) *inputState { in := &inputState{win: win, firstMouse: true, selectSlot: -1, sens: 0.0025} win.SetKeyCallback(in.keyCallback) win.SetMouseButtonCallback(in.mouseButtonCallback) win.SetCursorPosCallback(in.cursorCallback) win.SetScrollCallback(in.scrollCallback) win.SetCharCallback(in.charCallback) return in } // SetSensitivity 设置鼠标灵敏度(弧度/像素,设置界面)。 func (in *inputState) SetSensitivity(s float64) { if s > 0 { in.sens = s } } // Sensitivity 当前灵敏度。 func (in *inputState) Sensitivity() float64 { return in.sens } // keyCallback 键盘回调(动作映射:WASD/空格/Shift/Ctrl/Esc)。 func (in *inputState) keyCallback(_ *glfw.Window, key glfw.Key, _ int, action glfw.Action, mods glfw.ModifierKey) { down := action != glfw.Release switch key { case glfw.KeyW: in.forward = down case glfw.KeyS: in.back = down case glfw.KeyA: in.left = down case glfw.KeyD: in.right = down case glfw.KeySpace: in.jump = down case glfw.KeyLeftShift: in.sneak = down case glfw.KeyLeftControl: in.sprint = down case glfw.KeyEscape: if action == glfw.Press { in.escPressed = true // 暂停/继续由屏幕状态机处理 } case glfw.KeyBackspace: if action != glfw.Release && in.textMode { in.backspace = true } case glfw.Key1, glfw.Key2, glfw.Key3, glfw.Key4, glfw.Key5, glfw.Key6, glfw.Key7, glfw.Key8, glfw.Key9: if action == glfw.Press { in.selectSlot = int(key) - int(glfw.Key1) } case glfw.KeyE: if action == glfw.Press { in.toggleInv = true // 物品栏开关(UI还原.md §3.3) } case glfw.KeyQ: if action == glfw.Press { in.dropItem = true // 丢弃物品(UI与输入.md §5) } case glfw.KeyF11: if action == glfw.Press { in.toggleFS = true } case glfw.KeyEnter, glfw.KeyKPEnter: if action == glfw.Press && mods&glfw.ModAlt != 0 { in.toggleFS = true } } } func (in *inputState) charCallback(_ *glfw.Window, char rune) { if !in.textMode { return } in.chars = append(in.chars, char) } // mouseButtonCallback 鼠标按键:左键破坏(按住累积)、右键放置(UI与输入.md §6 长按)。 func (in *inputState) mouseButtonCallback(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, _ glfw.ModifierKey) { switch button { case glfw.MouseButtonLeft: if action == glfw.Press { in.breakDown = true in.clicked = true in.clickX = float32(in.cursorX) in.clickY = float32(in.cursorY) } in.breakHeld = action != glfw.Release case glfw.MouseButtonRight: if action == glfw.Press { in.placeDown = true in.clickedRight = true in.clickX = float32(in.cursorX) in.clickY = float32(in.cursorY) } in.placeHeld = action != glfw.Release } } // cursorCallback 鼠标移动:yaw/pitch 累积(渲染.md §2 主线程消费)。 // 约定:鼠标右移 dx>0 应右转(yaw 减小——yaw=0 朝向 -Z,右转朝 +X); // 鼠标上移 dy<0 应抬头(pitch 增大)。 func (in *inputState) cursorCallback(_ *glfw.Window, x, y float64) { in.cursorX, in.cursorY = x, y if in.firstMouse { in.lastX, in.lastY = x, y in.firstMouse = false return } dx, dy := x-in.lastX, y-in.lastY in.lastX, in.lastY = x, y in.dyaw -= dx * in.sens in.dpitch -= dy * in.sens } // scrollCallback 滚轮切热键栏(UI与输入.md §6)。 func (in *inputState) scrollCallback(_ *glfw.Window, _, yoff float64) { if yoff > 0 { in.selectSlot = -2 // 上滚 } else if yoff < 0 { in.selectSlot = -3 // 下滚 } } // apply 每 tick 把输入状态转成玩家动作(game.Session 驱动)。 func (in *inputState) apply(s *game.Session) { cam := s.Cam p := s.Player // 视角(俯仰钳制在相机上,而非增量上——旧实现钳增量,抬头可翻转超过 ±90°) if in.dyaw != 0 || in.dpitch != 0 { cam.Yaw += in.dyaw cam.Pitch += in.dpitch in.dyaw, in.dpitch = 0, 0 const maxPitch = math.Pi/2 - 0.01 if cam.Pitch > maxPitch { cam.Pitch = maxPitch } if cam.Pitch < -maxPitch { cam.Pitch = -maxPitch } } // 游戏中左键=破坏,不是 UI 点击:清除遗留点击状态,防止暂停菜单收到过期点击 in.clicked = false // 水平移动(物理与碰撞.md §3:走/冲刺/潜行) f := cam.Forward() r := cam.Right() wish := [2]float64{} if in.forward { wish[0] += f[0] wish[1] += f[2] } if in.back { wish[0] -= f[0] wish[1] -= f[2] } if in.right { wish[0] += r[0] wish[1] += r[2] } if in.left { wish[0] -= r[0] wish[1] -= r[2] } speed := 4.3 if p.InWater { speed = 2.2 if in.sprint && in.forward { speed = 5.4 } } else if in.sprint { speed *= 1.3 } if in.sneak && !p.InWater { speed *= 0.3 } l := math.Sqrt(wish[0]*wish[0] + wish[1]*wish[1]) if l > 0 { p.Vel[0] = wish[0] / l * speed p.Vel[2] = wish[1] / l * speed } else { p.Vel[0] = 0 p.Vel[2] = 0 } // 跳跃 / 游泳(物理与碰撞.md §4:水中空格上浮,不要求落地) if in.jump { if p.InWater { p.SwimUp() } else if p.OnGround { p.Jump() } } if in.sneak && p.InWater { p.SwimDown() } p.SetPose(s.World, in.sneak && !p.InWater, in.sprint && in.forward && p.InWater) // 热键栏切换(UI与输入.md §6:上滚槽位−1,下滚+1,对齐原版 Java) switch in.selectSlot { case -2: s.Inv.Selected = (s.Inv.Selected + 8) % 9 case -3: s.Inv.Selected = (s.Inv.Selected + 1) % 9 default: if in.selectSlot >= 0 && in.selectSlot < 9 { s.Inv.Selected = in.selectSlot } } in.selectSlot = -1 // 左键:按下边沿攻击一次;按住连续破坏(方块交互与动画.md §3) if in.breakDown { if !s.Attack() { if _, ok := s.Cast(5); !ok { s.SwingMiss() } s.BeginBreak() } in.breakDown = false } else if in.breakHeld { if !s.Breaking() { s.BeginBreak() } else { s.ContinueBreak() } } else if s.Breaking() || s.BreakProgress() != 0 { s.EndBreak() } // 放置(右键;命中工作台/熔炉/箱子则打开界面) if in.placeDown { if !s.TryOpenCrafting() && !s.TryOpenFurnace() && !s.TryOpenChest() && !s.TryFlickLever() && !s.TryUseEntity() && !s.TryEat() { s.PlaceSelected() } in.placeDown = false } // 丢弃选中槽 1 个物品(UI与输入.md §5 Q 键) if in.dropItem { s.DropSelected() in.dropItem = false } } var _ = physics.Player{} // 保持引用(后续输入扩展用)