Files
ngzz-mc/internal/world/world.go
NianGao Dev f25ae15077 feat(game): 音频接入、原版掉落、动物攻击、村庄村民、种子/灵敏度配置与设置界面
- 音频(音频.md §2):Session.Sound 事件回调,破坏/放置/攻击/村民环境音接入,
  客户端 audio.Manager 3D 衰减 + 监听器跟随 + 音量可调
- 原版掉落:草方块→泥土、石头→圆石、树叶无掉落(no_drop);掉落物实体
  用物品图标小广告牌渲染(物品纹理并入实体图集)
- 动物攻击(动物体系.md §5):左键优先射线命中实体(半径 0.6 球体判定),
  伤害/击退/死亡移除+肉类掉落;僵尸掉腐肉
- 村庄村民(村民系统.md §2):Generator.VillageAt 定位村庄中心,会话扫描
  已加载区块在村庄生成 3 名村民
- 种子与灵敏度配置:client.yaml world.seed/name、mouse_sensitivity;
  存档与种子联动;鼠标左右反转修复(右移应右转)
- 设置界面(UI与输入.md §6):屏幕状态 4,灵敏度/音量滑条、世界种子显示、
  按键教程多行列表(cjkfont 支持 \\n)、完成按钮;主菜单/暂停菜单入口
- 准星指向方块名:左下角 CJK 显示(lang 本地化,tick 光栅化渲染线程上传)
- 截图统一存项目根目录 jietu/(paths.RootDir;dev 构建识别 bin/ 布局)
2026-08-16 20:18:52 +08:00

367 lines
11 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.RWMutex
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.RLock()
ch := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))]
w.mu.RUnlock()
if ch == nil {
return block.Air
}
return ch.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))
}
// WithReadLock 在持有世界读锁期间执行 fn。
// 网格构建用它包住整个 Build:邻居查询走 BlockUnlocked,免去每格一次加锁
// (旧实现每个面邻居查询各加一次锁,单区块约 40 万次加锁,mesh 构建极慢,
// 出生区块迟迟无网格,玩家透过脚下的洞看到相邻区块地下横截面——"进游戏黑屏"主因之一)。
func (w *World) WithReadLock(fn func()) {
w.mu.RLock()
defer w.mu.RUnlock()
fn()
}
// BlockUnlocked 无锁读方块(仅在 WithReadLock 回调内调用;未加载区块与世界边界视为空气)。
func (w *World) BlockUnlocked(x, y, z int32) block.State {
if y < 0 || y >= chunk.Height {
return block.Air
}
ch := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))]
if ch == nil {
return block.Air
}
return ch.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))
}
// VillageAt 返回 (x,z) 附近的村庄中心(村民生成定位,村民系统.md §2)。
func (w *World) VillageAt(x, z int32) ([2]int32, bool) { return w.gen.VillageAt(x, 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)
}