Files
ngzz-mc/internal/world/world.go

342 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}