feat(world): 区块管理器——异步生成流水线、优先级队列、时间预算、火把光源联动(含测试)
This commit is contained in:
311
internal/world/world.go
Normal file
311
internal/world/world.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user