diff --git a/internal/world/world.go b/internal/world/world.go new file mode 100644 index 00000000..f4ddc6cd --- /dev/null +++ b/internal/world/world.go @@ -0,0 +1,311 @@ +// Package world 世界与区块管理:异步生成流水线 + 优先级队列 + 时间预算(场景/区块管理.md §3–§4)。 +// +// 线程模型(架构.md §4): +// - 主线程:Update 每帧调度(预算内); +// - worker pool:噪声生成 + 天空光灌入; +// - 本包同时实现 light.World 接口(跨区块光照访问)。 +package world + +import ( + "fmt" + "runtime" + "sort" + "sync" + "time" + + "mc/internal/block" + "mc/internal/chunk" + "mc/internal/coord" + "mc/internal/light" + "mc/internal/worldgen" +) + +// chunkState 区块生命周期状态(区块管理.md §3 状态机子集)。 +type chunkState uint8 + +const ( + stateUnloaded chunkState = iota // 不在内存 + stateQueued // 已入队等待生成 + stateGenerating // worker 生成中 + stateActive // 已挂回世界,可读可渲染 +) + +// World 世界实例。 +type World struct { + mu sync.Mutex + chunks map[uint64]*chunk.Chunk + states map[uint64]chunkState + + reg *block.Registry + gen *worldgen.Generator + + viewDistance int + playerX float64 + playerZ float64 + + // 异步生成流水线 + workers chan chunkCoord // 待生成坐标 + results chan *chunk.Chunk + + wg sync.WaitGroup +} + +// chunkCoord 区块坐标。 +type chunkCoord struct{ cx, cz int32 } + +// coordKey 区块坐标 → map key(禁止浮点 key,区块管理.md §2.2)。 +func coordKey(cx, cz int32) uint64 { return uint64(uint32(cx))<<32 | uint64(uint32(cz)) } + +// New 创建世界并启动 worker pool(数量 NumCPU-1,最少 1,区块管理.md §4.1)。 +func New(reg *block.Registry, gen *worldgen.Generator, viewDistance int) (*World, error) { + if reg == nil || gen == nil { + return nil, fmt.Errorf("world.New: 注册表与生成器必填") + } + n := runtime.NumCPU() - 1 + if n < 1 { + n = 1 + } + w := &World{ + chunks: make(map[uint64]*chunk.Chunk), + states: make(map[uint64]chunkState), + reg: reg, + gen: gen, + viewDistance: viewDistance, + workers: make(chan chunkCoord, n*2), + results: make(chan *chunk.Chunk, n*2), + } + for i := 0; i < n; i++ { + w.wg.Add(1) + go w.generateWorker() + } + return w, nil +} + +// generateWorker 生成 goroutine:纯 CPU 工作,不碰 GL、不持主线程锁。 +func (w *World) generateWorker() { + defer w.wg.Done() + for c := range w.workers { + ch := w.gen.Generate(c.cx, c.cz) + // 生成完立即做天空光灌入(光照.md §3,单区块:邻居视为阻隔无影响) + light.SkyFill(chunkLightAdapter{ch, w.reg}, 0, 15, 0, 15) + w.results <- ch + } +} + +// Update 每帧调度(主线程调用,区块管理.md §4.3 时间预算): +// 1) 收集结果挂回世界;2) 按距离重建优先级队列;3) 预算内派发生成任务。 +func (w *World) Update(playerX, playerY, playerZ float64, budget time.Duration) { + w.mu.Lock() + w.playerX, w.playerZ = playerX, playerZ + // 收集生成结果 → active + for { + select { + case ch := <-w.results: + k := coordKey(ch.CX, ch.CZ) + w.chunks[k] = ch + w.states[k] = stateActive + ch.Dirty.Store(true) // 首次挂载需建 mesh(渲染.md §3.3) + default: + goto drained + } + } +drained: + + // 重建优先级队列:视距内缺失区块按距离排序(区块管理.md §4.2) + pcx, pcz := coord.ChunkCoord(int32(playerX)), coord.ChunkCoord(int32(playerZ)) + vd := int32(w.viewDistance) + var queue []chunkCoord + for cx := pcx - vd; cx <= pcx+vd; cx++ { + for cz := pcz - vd; cz <= pcz+vd; cz++ { + k := coordKey(cx, cz) + if w.states[k] == stateActive || w.states[k] == stateGenerating { + continue + } + if w.states[k] == stateUnloaded { + w.states[k] = stateQueued + } + queue = append(queue, chunkCoord{cx, cz}) + } + } + sort.Slice(queue, func(i, j int) bool { + return w.dist(queue[i]) < w.dist(queue[j]) + }) + + // 预算内派发(主线程每帧最多做 8ms 调度工作,区块管理.md §4.3.2) + start := time.Now() + for _, c := range queue { + if time.Since(start) > budget { + break + } + select { + case w.workers <- c: + w.states[coordKey(c.cx, c.cz)] = stateGenerating + default: + // worker 繁忙:保持 queued,下帧重建队列时重试 + } + } + w.mu.Unlock() +} + +// dist 区块中心到玩家的欧氏距离(区块单位)。 +func (w *World) dist(c chunkCoord) float64 { + dx := float64(c.cx) - w.playerX/16 + dz := float64(c.cz) - w.playerZ/16 + return dx*dx + dz*dz +} + +// Block 世界级读方块(未加载区块视为空气)。 +func (w *World) Block(x, y, z int32) block.State { + w.mu.Lock() + ch := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))] + w.mu.Unlock() + if ch == nil { + return block.Air + } + return ch.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))) +} + +// SetBlock 世界级写方块(含火把光源联动,方块交互与动画.md §9): +// 放置光源 → 初始化亮度 + BFS;移除光源 → relight 重算。 +func (w *World) SetBlock(x, y, z int32, s block.State) { + w.mu.Lock() + ch := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))] + w.mu.Unlock() + if ch == nil { + return + } + lx, lz := int(coord.LocalCoord(x)), int(coord.LocalCoord(z)) + prev := ch.Block(lx, int(y), lz) + ch.SetBlock(lx, int(y), lz, s) + + prevLight := w.reg.Light(prev) + newLight := w.reg.Light(s) + switch { + case prevLight == 0 && newLight > 0: + // 放置光源:初始化自身亮度 + BFS(光照.md §4) + w.SetBlockLight(x, y, z, newLight) + light.Propagate(w, []light.Pos{{X: x, Y: y, Z: z}}) + case prevLight > 0 && newLight == 0: + // 移除光源:反向清除 + 重新灌入(光照.md §5) + light.RemoveLight(w, []light.Pos{{X: x, Y: y, Z: z}}, w.sourcesIn) + } +} + +// sourcesIn 扫描包围盒内现存光源(relight 阶段 2 用)。 +func (w *World) sourcesIn(minX, minY, minZ, maxX, maxY, maxZ int32) []light.Pos { + var seeds []light.Pos + for x := minX; x <= maxX; x++ { + for y := minY; y <= maxY; y++ { + for z := minZ; z <= maxZ; z++ { + if w.reg.Light(w.Block(x, y, z)) > 0 { + seeds = append(seeds, light.Pos{X: x, Y: y, Z: z}) + } + } + } + } + return seeds +} + +// ActiveChunks 返回已激活区块列表(渲染遍历用;返回副本避免并发写)。 +func (w *World) ActiveChunks() []*chunk.Chunk { + w.mu.Lock() + defer w.mu.Unlock() + out := make([]*chunk.Chunk, 0, len(w.chunks)) + for _, c := range w.chunks { + out = append(out, c) + } + return out +} + +// Count 返回已加载区块数(测试/统计)。 +func (w *World) Count() int { + w.mu.Lock() + defer w.mu.Unlock() + return len(w.chunks) +} + +// Close 停止 worker pool(goroutine 可退出,性能与内存.md §8)。 +func (w *World) Close() { + close(w.workers) + w.wg.Wait() +} + +// ---- light.World 接口实现(跨区块光照访问,光照.md §6)---- + +// Opaque 遮光判定:未加载区块与世界边界视为阻隔(延迟更新语义)。 +func (w *World) Opaque(x, y, z int32) bool { + if y < 0 || y >= chunk.Height { + return true + } + s := w.Block(x, y, z) + return w.reg.IsOpaque(s) +} + +// BlockLight 读方块光。 +func (w *World) BlockLight(x, y, z int32) uint8 { + if c := w.chunkOf(x, z); c != nil { + return c.BlockLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))) + } + return 0 +} + +// SetBlockLight 写方块光。 +func (w *World) SetBlockLight(x, y, z int32, v uint8) { + if c := w.chunkOf(x, z); c != nil { + c.SetBlockLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), v) + } +} + +// SourceLight 方块自身发光等级。 +func (w *World) SourceLight(x, y, z int32) uint8 { return w.reg.Light(w.Block(x, y, z)) } + +// SkyLight 读天空光。 +func (w *World) SkyLight(x, y, z int32) uint8 { + if c := w.chunkOf(x, z); c != nil { + return c.SkyLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))) + } + return 15 +} + +// SetSkyLight 写天空光。 +func (w *World) SetSkyLight(x, y, z int32, v uint8) { + if c := w.chunkOf(x, z); c != nil { + c.SetSkyLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), v) + } +} + +// chunkOf 定位区块(短锁)。 +func (w *World) chunkOf(x, z int32) *chunk.Chunk { + w.mu.Lock() + c := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))] + w.mu.Unlock() + return c +} + +// chunkLightAdapter 单区块光照适配器(生成阶段天空光灌入用,邻居视为阻隔)。 +type chunkLightAdapter struct { + c *chunk.Chunk + reg *block.Registry +} + +func (a chunkLightAdapter) Opaque(x, y, z int32) bool { + if y < 0 || y >= chunk.Height { + return true + } + return a.reg.IsOpaque(a.c.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))) +} +func (a chunkLightAdapter) BlockLight(x, y, z int32) uint8 { + return a.c.BlockLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))) +} +func (a chunkLightAdapter) SetBlockLight(x, y, z int32, v uint8) { + a.c.SetBlockLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), v) +} +func (a chunkLightAdapter) SourceLight(x, y, z int32) uint8 { + return a.reg.Light(a.c.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))) +} +func (a chunkLightAdapter) SkyLight(x, y, z int32) uint8 { + return a.c.SkyLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))) +} +func (a chunkLightAdapter) SetSkyLight(x, y, z int32, v uint8) { + a.c.SetSkyLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), v) +} diff --git a/internal/world/world_test.go b/internal/world/world_test.go new file mode 100644 index 00000000..f804c3c9 --- /dev/null +++ b/internal/world/world_test.go @@ -0,0 +1,101 @@ +package world + +import ( + "path/filepath" + "testing" + "time" + + "mc/internal/block" + "mc/internal/worldgen" +) + +// loadWorldReg 加载注册表。 +func loadWorldReg(t *testing.T) *block.Registry { + t.Helper() + r, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json")) + if err != nil { + t.Fatalf("加载注册表失败: %v", err) + } + return r +} + +// newTestWorld 创建小视距世界。 +func newTestWorld(t *testing.T, view int) *World { + t.Helper() + reg := loadWorldReg(t) + gen, err := worldgen.New(reg, 42, 0.005, 0.01, 0.1) + if err != nil { + t.Fatalf("创建生成器失败: %v", err) + } + w, err := New(reg, gen, view) + if err != nil { + t.Fatalf("创建世界失败: %v", err) + } + t.Cleanup(w.Close) + return w +} + +// waitChunk 轮询 Update 直到指定区块激活(异步生成流水线)。 +func waitChunk(t *testing.T, w *World, cx, cz int32) { + t.Helper() + deadline := time.Now().Add(15 * time.Second) + for time.Now().Before(deadline) { + w.Update(float64(cx*16+8), 64, float64(cz*16+8), 8*time.Millisecond) + if w.chunkOf(int32(cx*16+8), int32(cz*16+8)) != nil { + return + } + time.Sleep(2 * time.Millisecond) + } + t.Fatalf("区块 (%d,%d) 未在时限内生成", cx, cz) +} + +// TestLoadChunk 验证异步生成:基岩层存在、地表可站。 +func TestLoadChunk(t *testing.T) { + w := newTestWorld(t, 3) + waitChunk(t, w, 0, 0) + + bedrock, _ := w.reg.ID("bedrock") + if w.Block(8, 0, 8).ID() != bedrock { + t.Fatal("y=0 应为基岩") + } + // 地表:从顶向下找第一个非空气方块,其下应有实体方块 + var surf int32 = -1 + for y := int32(255); y >= 0; y-- { + if w.Block(8, y, 8) != block.Air { + surf = y + break + } + } + if surf < 8 { + t.Fatalf("地表高度异常: %d", surf) + } +} + +// TestTorchLightCycle 验证火把放置→照亮→移除→重算的完整闭环(方块交互与动画.md §9)。 +func TestTorchLightCycle(t *testing.T) { + w := newTestWorld(t, 2) + waitChunk(t, w, 0, 0) + + // 找地表 + var surf int32 + for y := int32(255); y >= 0; y-- { + if w.Block(8, y, 8) != block.Air { + surf = y + break + } + } + // 放置火把:地表上一格(地表方块不透明不受光,其周围空气受光) + torch, _ := w.reg.ID("torch") + w.SetBlock(8, surf+1, 8, block.NewState(torch, 0)) + if got := w.BlockLight(8, surf+1, 8); got != 14 { + t.Fatalf("火把自身光期望 14,实际 %d", got) + } + if got := w.BlockLight(7, surf+1, 8); got != 13 { + t.Fatalf("火把侧邻光期望 13,实际 %d", got) + } + // 移除火把:光应清零(relight) + w.SetBlock(8, surf+1, 8, block.Air) + if got := w.BlockLight(7, surf+1, 8); got != 0 { + t.Fatalf("移除火把后侧邻光期望 0,实际 %d", got) + } +}