Files
ngzz-mc/cmd/client/input.go
NianGao Dev 5da460c9a2 feat(ui): 小地图(左上角地表色图+玩家标记)与物品栏界面(E 键/Q 丢弃)
- 小地图(UI还原.md §3.5):96×96 地表色图(方块注册表→图集中心色,
  植被染色后采样),每 1.5s 重建,玩家标记烘焙进地图图(白点+黑描边);
  覆盖层 quad 批处理末尾颜色异常,标记改走烘焙规避
- 物品栏(UI还原.md §3.3):E 键开关屏幕状态 5,inventory.png 176×166
  窗口 + 27 主物品区 (7,83) + 热键栏行 (7,141),点击与手持栈交换/合并,
  Esc 关闭手持栈自动回收
- Q 丢弃选中槽 1 个物品(掉落在视线前方,UI与输入.md §5)
- cjkfont 多行渲染(设置界面按键教程 4 行列表)
- 动物攻击测试、原版掉落测试(草→泥土/动物掉肉)
2026-08-16 20:35:21 +08:00

246 lines
6.1 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
selectSlot int // -1 无
toggleInv bool // E 打开/关闭物品栏
dropItem bool // Q 丢弃选中槽 1 个物品
// 鼠标与 UI 事件(UI与输入.md §6 屏幕交互)
cursorX, cursorY float64
clickX, clickY float32
clicked bool
escPressed 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)
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, _ 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.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)
}
}
}
// 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
}
}
}
// 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 in.sprint {
speed *= 1.3
}
if in.sneak {
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
}
// 跳跃
if in.jump && p.OnGround {
p.Jump()
}
// 热键栏切换
switch in.selectSlot {
case -2:
s.Inv.Selected = (s.Inv.Selected + 1) % 9
case -3:
s.Inv.Selected = (s.Inv.Selected + 8) % 9
default:
if in.selectSlot >= 0 && in.selectSlot < 9 {
s.Inv.Selected = in.selectSlot
}
}
in.selectSlot = -1
// 左键:优先攻击实体(动物体系.md §5),未命中再破坏方块(按住累积)
if in.breakDown {
if !s.Attack() {
s.BeginBreak()
}
in.breakDown = false
}
if !in.breakHeld && s.BreakProgress() != 0 {
s.EndBreak()
}
// 放置(右键;命中工作台则打开合成界面,工作台与合成表.md §5)
if in.placeDown {
if !s.TryOpenCrafting() {
s.PlaceSelected()
}
in.placeDown = false
}
// 丢弃选中槽 1 个物品(UI与输入.md §5 Q 键)
if in.dropItem {
s.DropSelected()
in.dropItem = false
}
}
var _ = physics.Player{} // 保持引用(后续输入扩展用)