Files
ngzz-mc/cmd/client/main.go

218 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.
// 年糕历险记 —— 客户端入口。
//
// 装配顺序(架构.md §3.1 启动流程):配置 → 日志 → 内容包/图集 → 注册表 → 世界
// → 游戏会话(玩家/背包/相机)→ 渲染器 → 主循环tick 20Hz + render
package main
import (
"context"
"flag"
"os"
"os/signal"
"sync"
"time"
"mc/internal/assets"
"mc/internal/atlas"
"mc/internal/block"
"mc/internal/camera"
"mc/internal/config"
"mc/internal/engine"
"mc/internal/game"
"mc/internal/inventory"
"mc/internal/item"
"mc/internal/logx"
"mc/internal/mesh"
"mc/internal/physics"
"mc/internal/recipe"
"mc/internal/render"
"mc/internal/world"
"mc/internal/worldgen"
)
var devMode = flag.Bool("dev", devDefault, "开发模式:直接读取 assets/(默认随构建 tag")
// clientApp 客户端应用把会话与渲染粘合起来engine.Ticker + engine.Renderer
type clientApp struct {
sess *game.Session
rend *render.Renderer
reg *block.Registry
in *inputState
meshBuilder *mesh.Builder
mu sync.Mutex
pendingMesh []*worldMeshJob // tick 构建 → 渲染线程上传
winW, winH int
lastTime time.Time
}
// worldMeshJob 待上传网格。
type worldMeshJob struct {
cx, cz int32
m *mesh.ChunkMesh
}
// Tick 每 tick20Hz输入 → 玩家 → 会话 → 网格构建(渲染.md §3.3 dirty 重建)。
func (a *clientApp) Tick(dt float64) {
// 输入 → 玩家速度UI与输入.md §6 动作映射)
a.in.apply(a.sess)
a.sess.Tick(dt)
// 脏区块网格重建CPU 侧,预算内)
start := time.Now()
for _, c := range a.sess.World.ActiveChunks() {
if time.Since(start) > 6*time.Millisecond {
break // 帧预算(区块管理.md §4.3.2
}
if !c.Dirty.Load() {
continue
}
m := a.meshBuilder.Build(c, func(x, y, z int32) block.State {
return a.sess.World.Block(x, y, z)
})
a.mu.Lock()
a.pendingMesh = append(a.pendingMesh, &worldMeshJob{cx: c.CX, cz: c.CZ, m: m})
a.mu.Unlock()
c.Dirty.Store(false)
}
}
// Render 每帧渲染(渲染线程):上传待传网格 → 视图矩阵 → 三 pass 绘制。
func (a *clientApp) Render() {
// 上传 tick 构建好的网格GL 调用必须在渲染线程)
a.mu.Lock()
pending := a.pendingMesh
a.pendingMesh = nil
a.mu.Unlock()
for _, j := range pending {
a.rend.UploadChunk(j.cx, j.cz, j.m)
}
// 视图投影
cam := a.sess.Cam
aspect := float64(a.cfgW()) / float64(a.cfgH())
a.rend.SetViewProj(cam.View(), cam.Projection(aspect, 0.05, 1024))
// 清屏 + 绘制(视锥剔除简化为全量绘制,后置 LOD
a.rend.BeginFrame()
chunks := a.sess.World.ActiveChunks()
a.rend.DrawChunks(func(i int) (int32, int32) { return chunks[i].CX, chunks[i].CZ }, len(chunks))
a.rend.EndFrame()
}
// ShouldClose 窗口关闭判定。
func (a *clientApp) ShouldClose() bool { return a.rend.ShouldClose() }
// cfgW / cfgH 窗口尺寸(渲染用)。
func (a *clientApp) cfgW() int { return a.winW }
func (a *clientApp) cfgH() int { return a.winH }
func main() {
flag.Parse()
log, err := logx.New("logs/client.log", logx.LevelInfo)
if err != nil {
panic(err)
}
cfg, err := config.Load("config/client.yaml", config.DefaultClient())
if err != nil {
log.Warnf("配置加载失败,使用默认值:%v", err)
}
log.Infof("%s 客户端启动:%dx%d语言 %s视距 %d", cfg.Client.WindowTitle, cfg.Client.WindowWidth, cfg.Client.WindowHeight, cfg.Client.Language, cfg.Client.RenderDistance)
// 1) 内容包与图集(设计.md §1.3:扫描 mods/ + resourcepacks/
am := assets.New()
if err := am.Load("assets", "mods", "resourcepacks"); err != nil {
log.Warnf("内容包加载失败:%v", err)
}
log.Infof("已加载 %d 个内容包", len(am.Packs()))
keys, err := am.ExpandAtlas("blocks")
if err != nil {
panic(err)
}
atlasImg, uvRects, err := atlas.Build(am.ResolveTexture, keys, 16)
if err != nil {
panic(err)
}
log.Infof("blocks 图集:%d 纹理,%dx%d", len(uvRects), atlasImg.Bounds().Dx(), atlasImg.Bounds().Dy())
// UV 函数(渲染.md §4inset 防渗色)
uvFn := func(texKey string) (u0, v0, u1, v1 float32, ok bool) {
rect, found := uvRects[texKey]
if !found {
return 0, 0, 1, 1, false
}
W, H := float32(atlasImg.Bounds().Dx()), float32(atlasImg.Bounds().Dy())
const inset = 0.5
u0 = (float32(rect.Min.X) + inset) / W
v0 = (float32(rect.Min.Y) + inset) / H
u1 = (float32(rect.Max.X) - inset) / W
v1 = (float32(rect.Max.Y) - inset) / H
return u0, v0, u1, v1, true
}
// 2) 注册表(设计.md §6.1/§6.2 数据驱动)
reg, err := block.Load("assets/config/blocks.json")
if err != nil {
panic(err)
}
items, err := item.Load("assets/config/items.json")
if err != nil {
panic(err)
}
recipes, err := recipe.Load("assets/config/recipes.json")
if err != nil {
panic(err)
}
// 3) 世界与生成器(区块管理.md §4 异步流水线)
gen, err := worldgen.New(reg, 123456789, cfg.Client.RenderDistance/2000.0+0.004, 0.01, 0.1)
if err != nil {
panic(err)
}
w, err := world.New(reg, gen, cfg.Client.RenderDistance)
if err != nil {
panic(err)
}
defer w.Close()
// 4) 游戏会话(玩家先高空下落,区块加载后落地)
p := physics.NewPlayer(8.5, 100, 8.5)
cam := camera.New(p.Eye(), 0, 0, cfg.Client.FOV)
inv := inventory.New(items)
sess := game.NewSession(w, reg, items, recipes, p, inv, cam)
// 5) 渲染器GL 线程亲和在其内部锁定)
rend, err := render.New(cfg.Client.WindowTitle, cfg.Client.WindowWidth, cfg.Client.WindowHeight)
if err != nil {
panic(err)
}
defer rend.Shutdown()
rend.SetAtlas(atlasImg)
rend.SetCursorMode(true)
// 6) 输入(鼠标捕获 + 键鼠回调)
in := newInputState(rend.Window())
app := &clientApp{
sess: sess,
rend: rend,
reg: reg,
in: in,
meshBuilder: mesh.NewBuilder(reg, uvFn),
winW: cfg.Client.WindowWidth,
winH: cfg.Client.WindowHeight,
lastTime: time.Now(),
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
log.Infof("进入游戏主循环")
engine.Run(ctx, app, app)
log.Infof("客户端已退出")
}