feat(ui): 主菜单/暂停菜单屏幕状态机——中文标题「年糕历险记」、按钮点击、Esc 暂停(烟雾测试 10 秒无崩溃)
This commit is contained in:
@@ -45,6 +45,11 @@ type clientApp struct {
|
||||
uiR *ui.Renderer
|
||||
hud *ui.HUD
|
||||
|
||||
// 屏幕状态机:0 主菜单 / 1 游戏中 / 2 暂停(UI还原.md §2)
|
||||
screen int
|
||||
titleScreen *ui.TitleScreen
|
||||
pauseScreen *ui.PauseScreen
|
||||
|
||||
meshBuilder *mesh.Builder
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -62,7 +67,46 @@ type worldMeshJob struct {
|
||||
|
||||
// Tick 每 tick(20Hz):输入 → 玩家 → 会话 → 网格构建(渲染.md §3.3 dirty 重建)。
|
||||
func (a *clientApp) Tick(dt float64) {
|
||||
// 输入 → 玩家速度(UI与输入.md §6 动作映射)
|
||||
// 屏幕状态机(UI还原.md §2:主菜单/暂停不处理游戏输入)
|
||||
switch a.screen {
|
||||
case 0: // 主菜单
|
||||
if a.in.clicked {
|
||||
switch a.titleScreen.HitTest(a.in.clickX, a.in.clickY) {
|
||||
case "play":
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
case "quit":
|
||||
a.rend.Window().SetShouldClose(true)
|
||||
}
|
||||
a.in.clicked = false
|
||||
}
|
||||
a.in.dyaw, a.in.dpitch = 0, 0
|
||||
return
|
||||
case 2: // 暂停
|
||||
if a.in.escPressed {
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
}
|
||||
if a.in.clicked {
|
||||
switch a.pauseScreen.HitTest(a.in.clickX, a.in.clickY) {
|
||||
case "resume":
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
case "quit":
|
||||
a.rend.Window().SetShouldClose(true)
|
||||
}
|
||||
a.in.clicked = false
|
||||
}
|
||||
a.in.escPressed = false
|
||||
return
|
||||
}
|
||||
// 游戏中
|
||||
if a.in.escPressed {
|
||||
a.screen = 2
|
||||
a.rend.SetCursorMode(false)
|
||||
a.in.escPressed = false
|
||||
return
|
||||
}
|
||||
a.in.apply(a.sess)
|
||||
a.sess.Tick(dt)
|
||||
|
||||
@@ -107,7 +151,15 @@ func (a *clientApp) Render() {
|
||||
a.rend.DrawChunks(func(i int) (int32, int32) { return chunks[i].CX, chunks[i].CZ }, len(chunks))
|
||||
// UI pass(UI与输入.md §2:3D 之后、状态隔离)
|
||||
a.uiR.Begin()
|
||||
a.hud.Draw(a.uiR)
|
||||
switch a.screen {
|
||||
case 0:
|
||||
a.titleScreen.Draw(a.uiR)
|
||||
case 1:
|
||||
a.hud.Draw(a.uiR)
|
||||
case 2:
|
||||
a.hud.Draw(a.uiR) // 暂停时世界仍可见(UI还原.md §2)
|
||||
a.pauseScreen.Draw(a.uiR)
|
||||
}
|
||||
a.uiR.Flush()
|
||||
a.rend.EndFrame()
|
||||
}
|
||||
@@ -250,6 +302,13 @@ func main() {
|
||||
hud.SetItemAtlas(itemTex, itemUV)
|
||||
}
|
||||
|
||||
// 界面屏幕(中文标题「年糕历险记」+ 按钮,UI还原.md §2)
|
||||
lang, err := am.LoadLang(cfg.Client.Language)
|
||||
if err != nil {
|
||||
log.Warnf("语言加载失败: %v", err)
|
||||
}
|
||||
titleScreen, pauseScreen := buildScreens(uiR, lang)
|
||||
|
||||
// 6) 输入(鼠标捕获 + 键鼠回调)
|
||||
in := newInputState(rend.Window())
|
||||
|
||||
@@ -260,6 +319,8 @@ func main() {
|
||||
in: in,
|
||||
uiR: uiR,
|
||||
hud: hud,
|
||||
titleScreen: titleScreen,
|
||||
pauseScreen: pauseScreen,
|
||||
meshBuilder: mesh.NewBuilder(reg, uvFn),
|
||||
winW: cfg.Client.WindowWidth,
|
||||
winH: cfg.Client.WindowHeight,
|
||||
|
||||
@@ -31,6 +31,12 @@ type inputState struct {
|
||||
breakHeld bool
|
||||
placeDown bool
|
||||
selectSlot int // -1 无
|
||||
|
||||
// 鼠标与 UI 事件(UI与输入.md §6 屏幕交互)
|
||||
cursorX, cursorY float64
|
||||
clickX, clickY float32
|
||||
clicked bool
|
||||
escPressed bool
|
||||
}
|
||||
|
||||
// newInputState 创建输入状态并注册回调。
|
||||
@@ -63,7 +69,7 @@ func (in *inputState) keyCallback(_ *glfw.Window, key glfw.Key, _ int, action gl
|
||||
in.sprint = down
|
||||
case glfw.KeyEscape:
|
||||
if action == glfw.Press {
|
||||
in.win.SetShouldClose(true)
|
||||
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 {
|
||||
@@ -78,6 +84,9 @@ func (in *inputState) mouseButtonCallback(_ *glfw.Window, button glfw.MouseButto
|
||||
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:
|
||||
@@ -89,6 +98,7 @@ func (in *inputState) mouseButtonCallback(_ *glfw.Window, button glfw.MouseButto
|
||||
|
||||
// cursorCallback 鼠标移动:yaw/pitch 累积(渲染.md §2 主线程消费)。
|
||||
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
|
||||
|
||||
80
cmd/client/screens_gl.go
Normal file
80
cmd/client/screens_gl.go
Normal file
@@ -0,0 +1,80 @@
|
||||
//go:build gl
|
||||
|
||||
// 界面屏幕构建:中文标题与按钮(UI还原.md §2、§4 中文回退)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"mc/internal/assets"
|
||||
"mc/internal/cjkfont"
|
||||
"mc/internal/ui"
|
||||
)
|
||||
|
||||
// cjkPaths 系统 CJK 字体候选。
|
||||
var cjkPaths = []string{
|
||||
filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttc"),
|
||||
filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttf"),
|
||||
}
|
||||
|
||||
// buildScreens 构建主菜单与暂停菜单(按钮纹理 = 中文文本光栅化)。
|
||||
func buildScreens(r *ui.Renderer, lang *assets.Lang) (*ui.TitleScreen, *ui.PauseScreen) {
|
||||
cjk := loadCJK()
|
||||
text := func(key, fallback string, size float64) (uint32, float32, float32) {
|
||||
label := fallback
|
||||
if lang != nil {
|
||||
label = lang.Get(key)
|
||||
}
|
||||
if cjk != nil {
|
||||
img := cjk.Draw(label, color.RGBA{255, 255, 255, 255})
|
||||
tex := r.UploadImage(img)
|
||||
return tex, float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
|
||||
}
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
titleTex, titleW, titleH := text("menu.title", "年糕历险记", 48)
|
||||
if titleW == 0 {
|
||||
titleW, titleH = 320, 48 // 无 CJK 字体时占位
|
||||
}
|
||||
|
||||
// 主菜单按钮(居中列,UI还原.md §2)
|
||||
btn := func(key, fallback, action string, y float32, w, h int) ui.Button {
|
||||
tex, tw, th := text(key, fallback, 24)
|
||||
x := float32(w)/2 - tw/2
|
||||
return ui.Button{X: x, Y: y, W: tw, H: th, Tex: tex, Action: action}
|
||||
}
|
||||
title := &ui.TitleScreen{}
|
||||
title.TitleTex = titleTex
|
||||
title.TitleW = titleW
|
||||
title.TitleH = titleH
|
||||
title.Buttons = []ui.Button{
|
||||
btn("menu.singleplayer", "单人游戏", "play", 300, 1280, 720),
|
||||
btn("menu.multiplayer", "多人游戏", "", 350, 1280, 720),
|
||||
btn("menu.quit", "退出游戏", "quit", 400, 1280, 720),
|
||||
}
|
||||
_ = titleTex
|
||||
_ = titleH
|
||||
|
||||
pause := &ui.PauseScreen{}
|
||||
pause.TitleTex, pause.TitleW, pause.TitleH = text("menu.game", "游戏菜单", 32)
|
||||
pause.Buttons = []ui.Button{
|
||||
btn("gui.resume", "继续游戏", "resume", 300, 1280, 720),
|
||||
btn("menu.quit", "保存并退出", "quit", 350, 1280, 720),
|
||||
}
|
||||
return title, pause
|
||||
}
|
||||
|
||||
// loadCJK 加载系统 CJK 字体(失败返回 nil → 按钮退化不显示文本)。
|
||||
func loadCJK() *cjkfont.Renderer {
|
||||
for _, p := range cjkPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
if r, err := cjkfont.Load(p, 24); err == nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user