feat(world): 区块管理器——异步生成流水线、优先级队列、时间预算、火把光源联动(含测试)

This commit is contained in:
NianGao Dev
2026-08-15 22:37:15 +08:00
parent 90b22f8c87
commit 8bdd5b047f
2 changed files with 412 additions and 0 deletions

311
internal/world/world.go Normal file
View File

@@ -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)
}