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)
|
||||
}
|
||||
101
internal/world/world_test.go
Normal file
101
internal/world/world_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user