342 lines
9.9 KiB
Go
342 lines
9.9 KiB
Go
// 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
|
||
done chan struct{} // 关闭信号(goroutine 可退出)
|
||
|
||
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),
|
||
done: make(chan struct{}),
|
||
}
|
||
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 {
|
||
select {
|
||
case <-w.done:
|
||
return
|
||
case c := <-w.workers:
|
||
ch := w.gen.Generate(c.cx, c.cz)
|
||
// 生成完立即做天空光灌入(光照.md §3,单区块:邻居视为阻隔无影响)
|
||
light.SkyFill(chunkLightAdapter{ch, w.reg}, 0, 15, 0, 15)
|
||
select {
|
||
case w.results <- ch:
|
||
case <-w.done:
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 {
|
||
if y < 0 || y >= chunk.Height {
|
||
return block.Air
|
||
}
|
||
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
|
||
}
|
||
|
||
// InstallChunk 安装一个区块(存档载入/外部生成用):置为 active 并标 mesh 脏。
|
||
func (w *World) InstallChunk(c *chunk.Chunk) {
|
||
w.mu.Lock()
|
||
k := coordKey(c.CX, c.CZ)
|
||
w.chunks[k] = c
|
||
w.states[k] = stateActive
|
||
w.mu.Unlock()
|
||
c.Dirty.Store(true)
|
||
}
|
||
|
||
// 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.done)
|
||
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)
|
||
}
|
||
}
|
||
|
||
// Solid 碰撞判定(physics.World 接口实现,物理与碰撞.md §2 共用判定表)。
|
||
func (w *World) Solid(x, y, z int32) bool { return w.reg.IsSolid(w.Block(x, y, z)) }
|
||
|
||
// Fluid 液体判定(physics.World 接口实现)。
|
||
func (w *World) Fluid(x, y, z int32) bool { return w.reg.IsFluid(w.Block(x, y, z)) }
|
||
|
||
// 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)
|
||
}
|