Files
ngzz-mc/cmd/client/input.go
2026-09-19 14:21:08 +08:00

298 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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{} // 保持引用(后续输入扩展用)