298 lines
7.7 KiB
Go
298 lines
7.7 KiB
Go
// Package worldgen 确定性世界生成(场景/世界生成.md §3 管线)。
|
||
//
|
||
// 管线(pass 顺序固定):高度图 → 生物群系地表 → 洞穴 → 矿物 → 树(后处理)。
|
||
// 确定性铁律:同 seed 同参数 → 逐字节相同结果(噪声实例按 seed 派生、遍历顺序固定)。
|
||
package worldgen
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"mc/internal/block"
|
||
"mc/internal/chunk"
|
||
|
||
"github.com/ojrac/opensimplex-go"
|
||
)
|
||
|
||
// 生物群系(简化版:4 种,世界生态.md §2 双噪声判定)。
|
||
type biome uint8
|
||
|
||
const (
|
||
biomePlains biome = iota
|
||
biomeDesert
|
||
biomeForest
|
||
biomeSnow
|
||
)
|
||
|
||
// 海平面与地形常量。
|
||
const (
|
||
seaLevel = 62
|
||
dirtDepth = 3 // 地表下泥土厚度
|
||
)
|
||
|
||
// Generator 世界生成器。
|
||
type Generator struct {
|
||
seed int64
|
||
|
||
height opensimplex.Noise // 高度图(2D,fBm 近似:双 octave)
|
||
temp opensimplex.Noise // 温度噪声(生物群系)
|
||
humid opensimplex.Noise // 湿度噪声(生物群系)
|
||
cave opensimplex.Noise // 洞穴 3D 密度场
|
||
ore opensimplex.Noise // 矿物分布 3D
|
||
|
||
biomeScale float64
|
||
heightScale float64
|
||
caveDensity float64
|
||
|
||
// 常用方块状态(启动时从注册表解析一次)
|
||
stone, grass, dirt, bedrock, sand, water, snowB, log, leaves block.State
|
||
coal, iron, gold, diamond, redstone block.State
|
||
}
|
||
|
||
// New 创建生成器;必须的方块缺注册时报错。
|
||
func New(reg *block.Registry, seed int64, biomeScale, heightScale, caveDensity float64) (*Generator, error) {
|
||
g := &Generator{
|
||
seed: seed,
|
||
height: opensimplex.New(seed),
|
||
temp: opensimplex.New(seed + 101),
|
||
humid: opensimplex.New(seed + 202),
|
||
cave: opensimplex.New(seed + 303),
|
||
ore: opensimplex.New(seed + 404),
|
||
biomeScale: biomeScale,
|
||
heightScale: heightScale,
|
||
caveDensity: caveDensity,
|
||
}
|
||
// 解析必须方块 ID(缺失直接报错,避免生成到一半失败)
|
||
var err error
|
||
if g.stone, err = must(reg, "stone"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.grass, err = must(reg, "grass_block"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.dirt, err = must(reg, "dirt"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.bedrock, err = must(reg, "bedrock"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.sand, err = must(reg, "sand"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.water, err = must(reg, "water"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.snowB, err = must(reg, "snow_block"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.log, err = must(reg, "oak_log"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.leaves, err = must(reg, "oak_leaves"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.coal, err = must(reg, "coal_ore"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.iron, err = must(reg, "iron_ore"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.gold, err = must(reg, "gold_ore"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.diamond, err = must(reg, "diamond_ore"); err != nil {
|
||
return nil, err
|
||
}
|
||
if g.redstone, err = must(reg, "redstone_ore"); err != nil {
|
||
return nil, err
|
||
}
|
||
return g, nil
|
||
}
|
||
|
||
// must 从注册表取方块状态,缺注册返回错误。
|
||
func must(reg *block.Registry, name string) (block.State, error) {
|
||
id, ok := reg.ID(name)
|
||
if !ok {
|
||
return block.Air, fmt.Errorf("worldgen: 方块 %q 未注册", name)
|
||
}
|
||
return block.NewState(id, 0), nil
|
||
}
|
||
|
||
// heightAt 返回世界列 (wx, wz) 的地形高度(y)。
|
||
func (g *Generator) heightAt(wx, wz int32) int {
|
||
h := g.height.Eval2(float64(wx)*g.heightScale, float64(wz)*g.heightScale)
|
||
h += 0.35 * g.height.Eval2(float64(wx)*g.heightScale*2.5+100, float64(wz)*g.heightScale*2.5+100)
|
||
y := 64 + int(h*24)
|
||
if y < 8 {
|
||
y = 8
|
||
}
|
||
if y > chunk.Height-16 {
|
||
y = chunk.Height - 16
|
||
}
|
||
return y
|
||
}
|
||
|
||
// biomeAt 温度 + 湿度双噪声 → 生物群系(世界生态.md §2.1)。
|
||
func (g *Generator) biomeAt(wx, wz int32) biome {
|
||
t := g.temp.Eval2(float64(wx)*g.biomeScale, float64(wz)*g.biomeScale)
|
||
h := g.humid.Eval2(float64(wx)*g.biomeScale*0.9+50, float64(wz)*g.biomeScale*0.9+50)
|
||
switch {
|
||
case t > 0.35 && h < -0.1:
|
||
return biomeDesert
|
||
case t < -0.35:
|
||
return biomeSnow
|
||
case h > 0.3:
|
||
return biomeForest
|
||
default:
|
||
return biomePlains
|
||
}
|
||
}
|
||
|
||
// surfaceID 群系地表方块。
|
||
func (g *Generator) surfaceID(b biome) block.State {
|
||
switch b {
|
||
case biomeDesert:
|
||
return g.sand
|
||
case biomeSnow:
|
||
return g.snowB
|
||
default:
|
||
return g.grass
|
||
}
|
||
}
|
||
|
||
// isCave 洞穴判定(3D 密度场阈值;基岩层与高空不挖,世界生成.md §6 踩坑)。
|
||
func (g *Generator) isCave(wx, wy, wz int32) bool {
|
||
if wy > 56 || wy < 6 {
|
||
return false
|
||
}
|
||
n := g.cave.Eval3(float64(wx)*0.08, float64(wy)*0.12, float64(wz)*0.08)
|
||
n += 0.5 * g.cave.Eval3(float64(wx)*0.16+31, float64(wy)*0.24+17, float64(wz)*0.16+71)
|
||
return n > g.caveDensity
|
||
}
|
||
|
||
// oreAt 矿物分布(按深度区间,挖矿与矿物.md §2)。
|
||
func (g *Generator) oreAt(wx, wy, wz int32) block.State {
|
||
if wy > 128 {
|
||
return g.stone
|
||
}
|
||
n := g.ore.Eval3(float64(wx)*0.12, float64(wy)*0.12, float64(wz)*0.12)
|
||
switch {
|
||
case wy <= 16 && n > 0.86:
|
||
return g.diamond
|
||
case wy <= 16 && n > 0.78:
|
||
return g.redstone
|
||
case wy <= 32 && n > 0.74:
|
||
return g.gold
|
||
case wy <= 64 && n > 0.70:
|
||
return g.iron
|
||
case n > 0.64:
|
||
return g.coal
|
||
}
|
||
return g.stone
|
||
}
|
||
|
||
// Generate 生成整个区块(确定性;结果不标存档脏——生成属纯函数可重算)。
|
||
func (g *Generator) Generate(cx, cz int32) *chunk.Chunk {
|
||
c := chunk.New(cx, cz)
|
||
for lz := 0; lz < 16; lz++ {
|
||
for lx := 0; lx < 16; lx++ {
|
||
wx := int32(lx) + cx*16
|
||
wz := int32(lz) + cz*16
|
||
h := g.heightAt(wx, wz)
|
||
b := g.biomeAt(wx, wz)
|
||
surface := g.surfaceID(b)
|
||
|
||
// 海平面以下但高于地形:灌水
|
||
for y := h + 1; y < seaLevel; y++ {
|
||
c.Fill(lx, y, lz, g.water)
|
||
}
|
||
// 地形柱:基岩 → 石头/矿物 → 泥土 → 地表
|
||
for y := 0; y <= h; y++ {
|
||
wy := int32(y)
|
||
var s block.State
|
||
switch {
|
||
case y == 0:
|
||
s = g.bedrock
|
||
case y <= 4:
|
||
s = g.stone
|
||
case y == h:
|
||
s = surface
|
||
case y >= h-dirtDepth:
|
||
s = g.dirt
|
||
default:
|
||
s = g.stone
|
||
}
|
||
// 洞穴掏空(海平面以下洞内灌水)
|
||
if y > 4 && g.isCave(wx, wy, wz) {
|
||
if y <= seaLevel {
|
||
s = g.water
|
||
} else {
|
||
s = block.Air
|
||
}
|
||
} else if s == g.stone {
|
||
s = g.oreAt(wx, wy, wz)
|
||
}
|
||
c.Fill(lx, y, lz, s)
|
||
}
|
||
}
|
||
}
|
||
// 树后处理 pass(世界生态.md §3.1:边缘留白避免跨区块,树冠不出界)
|
||
g.placeTrees(c)
|
||
return c
|
||
}
|
||
|
||
// placeTrees 在森林/平原撒树;方块边缘 3 格内不撒(跨区块安全的最小方案)。
|
||
func (g *Generator) placeTrees(c *chunk.Chunk) {
|
||
for lz := 3; lz <= 12; lz++ {
|
||
for lx := 3; lx <= 12; lx++ {
|
||
wx := int32(lx) + c.CX*16
|
||
wz := int32(lz) + c.CZ*16
|
||
h := g.heightAt(wx, wz)
|
||
if h+7 >= chunk.Height {
|
||
continue
|
||
}
|
||
b := g.biomeAt(wx, wz)
|
||
prob := 0.80
|
||
if b == biomePlains {
|
||
prob = 0.95
|
||
}
|
||
if b != biomeForest && b != biomePlains {
|
||
continue
|
||
}
|
||
n := g.ore.Eval2(float64(wx)*0.2+777, float64(wz)*0.2+777)
|
||
if n < prob {
|
||
continue
|
||
}
|
||
// 只长在草上
|
||
if c.Block(lx, h, lz) != g.grass {
|
||
continue
|
||
}
|
||
g.growTree(c, lx, h, lz)
|
||
}
|
||
}
|
||
}
|
||
|
||
// growTree 生成一棵橡树:树干 5 格 + 树冠 5×5×3。
|
||
func (g *Generator) growTree(c *chunk.Chunk, lx, h, lz int) {
|
||
for y := h + 1; y <= h+5; y++ {
|
||
c.Fill(lx, y, lz, g.log)
|
||
}
|
||
for y := h + 3; y <= h+5; y++ {
|
||
for dx := -2; dx <= 2; dx++ {
|
||
for dz := -2; dz <= 2; dz++ {
|
||
x, z := lx+dx, lz+dz
|
||
if x < 0 || x >= 16 || z < 0 || z >= 16 {
|
||
continue // 树冠不出区块(边缘留白已保证不越界)
|
||
}
|
||
if dx == 0 && dz == 0 && y <= h+5 {
|
||
continue // 树干位置
|
||
}
|
||
// 角部修剪,更接近原版树冠形状
|
||
if y == h+5 && (dx*dx+dz*dz >= 5) {
|
||
continue
|
||
}
|
||
c.Fill(x, y, z, g.leaves)
|
||
}
|
||
}
|
||
}
|
||
}
|