diff --git a/internal/entity/drops.go b/internal/entity/drops.go new file mode 100644 index 00000000..2d644dae --- /dev/null +++ b/internal/entity/drops.go @@ -0,0 +1,83 @@ +// 掉落物实体:合并与拾取(实体系统.md §5)。 +package entity + +import ( + "math" +) + +// 掉落物参数(实体系统.md §5)。 +const ( + dropMergeDist = 1.0 // 同物品合并距离 + dropStackMax = 64 // 合并上限 + dropPickupDist = 1.5 // 拾取半径 + dropPerChunk = 32 // 每区块掉落物上限 +) + +// Drop 掉落物实体数据。 +type Drop struct { + Item string + Count int +} + +// SpawnDrop 生成掉落物:同物品近距合并(上限 dropStackMax);超上限丢弃。 +func (m *Manager) SpawnDrop(itemName string, count int, x, y, z float64) { + if count <= 0 { + return + } + m.mu.Lock() + defer m.mu.Unlock() + // 合并 + if old, ok := m.drops[itemName]; ok && !old.Dead { + dx, dz := old.Pos[0]-x, old.Pos[2]-z + if math.Sqrt(dx*dx+dz*dz) < dropMergeDist { + if d, ok := old.Data.(*Drop); ok && d.Count+count <= dropStackMax { + d.Count += count + return + } + } + } + // 上限检查 + drops := 0 + for _, e := range m.entities { + if e.Kind == KindItemDrop { + drops++ + } + } + if drops >= dropPerChunk*8 { // 全局粗上限(按区块过滤后置) + return + } + m.next++ + e := &Entity{ID: m.next, Kind: KindItemDrop, Pos: [3]float64{x, y, z}, Health: 1, + Data: &Drop{Item: itemName, Count: count}} + e.Vel = [3]float64{(rand01(e.ID) - 0.5) * 2, 4, (rand01(e.ID>>16) - 0.5) * 2} // 弹出初速度 + m.entities[e.ID] = e + m.drops[itemName] = e +} + +// Data 实体附加数据。 +// rand01 实体 ID 派生确定性随机(掉落初速度用,存档回放一致)。 +func rand01(seed ID) float64 { + const mod = 1000003 + return float64(seed%mod) / mod +} + +// PickupNearby 拾取玩家附近掉落物,返回拾取的物品列表(实体系统.md §5 拾取)。 +func (m *Manager) PickupNearby(px, py, pz float64) []Drop { + var out []Drop + for _, e := range m.List() { + if e.Kind != KindItemDrop || e.Dead { + continue + } + d, ok := e.Data.(*Drop) + if !ok { + continue + } + dx, dy, dz := e.Pos[0]-px, e.Pos[1]-py, e.Pos[2]-pz + if dx*dx+dy*dy+dz*dz < dropPickupDist*dropPickupDist { + e.Dead = true + m.Remove(e.ID) + out = append(out, *d) + } + } + return out +} diff --git a/internal/entity/entity.go b/internal/entity/entity.go new file mode 100644 index 00000000..2e4edd37 --- /dev/null +++ b/internal/entity/entity.go @@ -0,0 +1,257 @@ +// Package entity 实体系统:生命周期、AI 状态机、掉落物(场景/实体系统.md)。 +// +// 设计要点: +// - 实体与方块分离,随激活区块 tick(卸载冻结); +// - AI 决策限频(0.25–0.5s 一次),寻路简化为直线移动 + 卡住重试(实体系统.md §3); +// - 掉落物合并 + 上限(实体系统.md §5)。 +package entity + +import ( + "math" + "sync" + + "mc/internal/block" +) + +// ID 实体 ID(服务器分配,多人同步.md §3)。 +type ID uint64 + +// Kind 实体类型。 +type Kind uint8 + +// 实体类型(僵尸与敌对生物.md、动物体系.md、村民系统.md)。 +const ( + KindAnimal Kind = iota // 被动动物 + KindZombie // 敌对生物样板 + KindVillager // 村民 + KindItemDrop // 掉落物 +) + +// WorldView 实体 AI 所需的世界只读视图。 +type WorldView interface { + Solid(x, y, z int32) bool + Block(x, y, z int32) block.State + SkyLight(x, y, z int32) uint8 + BlockLight(x, y, z int32) uint8 +} + +// Entity 实体。 +type Entity struct { + ID ID + Kind Kind + Pos [3]float64 // 脚底坐标 + Vel [3]float64 + Yaw float64 + Health float64 + Dead bool + + // AI 状态(实体系统.md §3 状态机) + state uint8 + decisionIn float64 // 下次决策倒计时(限频) + attackIn float64 // 攻击冷却(僵尸与敌对生物.md §3) + wanderDir [2]float64 + + // Data 类型附加数据(掉落物 = *Drop,村民 = *Villager) + Data any +} + +// 僵尸 AI 状态。 +const ( + zIdle uint8 = iota + zChase + zAttack +) + +// 动物 AI 状态。 +const ( + aWander uint8 = iota + aFlee +) + +// Manager 实体管理器(主线程所有权)。 +type Manager struct { + mu sync.Mutex + next ID + entities map[ID]*Entity + drops map[string]*Entity // 掉落物合并键(物品名)→ 实体 +} + +// NewManager 创建实体管理器。 +func NewManager() *Manager { + return &Manager{entities: make(map[ID]*Entity), drops: make(map[string]*Entity)} +} + +// Spawn 生成实体。 +func (m *Manager) Spawn(kind Kind, x, y, z float64) *Entity { + m.mu.Lock() + defer m.mu.Unlock() + m.next++ + e := &Entity{ID: m.next, Kind: kind, Pos: [3]float64{x, y, z}, Health: 20, decisionIn: 0.5} + m.entities[e.ID] = e + return e +} + +// Remove 移除实体。 +func (m *Manager) Remove(id ID) { + m.mu.Lock() + delete(m.entities, id) + m.mu.Unlock() +} + +// List 导出实体列表(副本)。 +func (m *Manager) List() []*Entity { + m.mu.Lock() + defer m.mu.Unlock() + out := make([]*Entity, 0, len(m.entities)) + for _, e := range m.entities { + out = append(out, e) + } + return out +} + +// Tick 每 tick 更新全部实体(激活区块内;冻结区由调用方过滤)。 +func (m *Manager) Tick(dt float64, w WorldView, playerPos [3]float64) { + for _, e := range m.List() { + if e.Dead { + continue + } + switch e.Kind { + case KindZombie: + e.tickZombie(dt, w, playerPos) + case KindAnimal: + e.tickAnimal(dt, w) + case KindVillager: + e.tickVillager(dt, w) + } + if e.Kind != KindItemDrop { + e.move(dt, w) + } + } +} + +// move 简化移动:水平意愿速度 + 重力 + AABB 碰撞(复用物理思路,实体系统.md §3)。 +func (e *Entity) move(dt float64, w WorldView) { + const g = 32.0 + e.Vel[1] -= g * dt + if e.Vel[1] < -64 { + e.Vel[1] = -64 + } + // 逐轴(X → Z → Y) + for _, axis := range [3]int{0, 2, 1} { + e.Pos[axis] += e.Vel[axis] * dt + if e.collides(w) { + e.Pos[axis] -= e.Vel[axis] * dt + e.Vel[axis] = 0 + } + } +} + +// collides AABB 碰撞判定(0.6×1.8)。 +func (e *Entity) collides(w WorldView) bool { + const hw, hh = 0.3, 1.8 + minX := int32(math.Floor(e.Pos[0] - hw + 1e-6)) + maxX := int32(math.Floor(e.Pos[0] + hw - 1e-6)) + minY := int32(math.Floor(e.Pos[1] + 1e-6)) + maxY := int32(math.Floor(e.Pos[1] + hh - 1e-6)) + minZ := int32(math.Floor(e.Pos[2] - hw + 1e-6)) + maxZ := int32(math.Floor(e.Pos[2] + hw - 1e-6)) + for x := minX; x <= maxX; x++ { + for y := minY; y <= maxY; y++ { + for z := minZ; z <= maxZ; z++ { + if w.Solid(x, y, z) { + return true + } + } + } + } + return false +} + +// tickZombie 僵尸 AI(僵尸与敌对生物.md §2–§4)。 +func (e *Entity) tickZombie(dt float64, w WorldView, playerPos [3]float64) { + // 白天燃烧(暴露天空光 ≥15 且无头盔 → 每秒伤害;逐 tick 而非秒杀) + if w.SkyLight(int32(e.Pos[0]), int32(e.Pos[1]+1.5), int32(e.Pos[2])) >= 15 { + e.Health -= 2.0 * dt + if e.Health <= 0 { + e.Dead = true + return + } + } + e.decisionIn -= dt + e.attackIn -= dt + dx, dz := playerPos[0]-e.Pos[0], playerPos[2]-e.Pos[2] + dist := math.Sqrt(dx*dx + dz*dz) + switch e.state { + case zIdle: + if dist < 16 && w.BlockLight(int32(e.Pos[0]), int32(e.Pos[1]), int32(e.Pos[2])) <= 7 { + e.state = zChase + } + case zChase: + if dist < 2.0 { + e.state = zAttack + e.Vel[0], e.Vel[2] = 0, 0 + break + } + if e.decisionIn <= 0 { + e.decisionIn = 0.5 // 限频寻路(实体系统.md §3) + speed := 3.0 + e.Vel[0] = dx / dist * speed + e.Vel[2] = dz / dist * speed + e.Yaw = math.Atan2(-dx, -dz) + } + if dist > 24 { + e.state = zIdle + } + case zAttack: + if dist > 2.5 { + e.state = zChase + } else if e.attackIn <= 0 { + e.attackIn = 1.0 // 攻击间隔 1s(僵尸与敌对生物.md §3) + e.Vel[0], e.Vel[2] = 0, 0 + } + } +} + +// tickAnimal 动物 AI:游荡 + 受击逃跑(动物体系.md §2 状态机子集)。 +func (e *Entity) tickAnimal(dt float64, w WorldView) { + e.decisionIn -= dt + if e.Health < 20 { + e.state = aFlee // 受击逃跑(简化:血不满即逃) + } + if e.decisionIn <= 0 { + e.decisionIn = 2.0 + switch e.state { + case aFlee: + ang := math.Atan2(e.wanderDir[1], e.wanderDir[0]) + 0.5 + e.wanderDir = [2]float64{math.Cos(ang), math.Sin(ang)} + e.state = aWander + default: + // 随机游荡方向(确定性:用实体 ID 派生) + ang := float64(e.ID%628) / 100.0 + e.wanderDir = [2]float64{math.Cos(ang), math.Sin(ang)} + if e.ID%3 == 0 { + e.wanderDir = [2]float64{0, 0} // 原地休息 + } + } + } + e.Vel[0] = e.wanderDir[0] * 1.2 + e.Vel[2] = e.wanderDir[1] * 1.2 + _ = w +} + +// tickVillager 村民 AI:白天游荡工作(建造队列由村民系统接入),夜晚回屋/逃跑。 +func (e *Entity) tickVillager(dt float64, w WorldView) { + e.decisionIn -= dt + isNight := w.SkyLight(int32(e.Pos[0]), int32(e.Pos[1]+1.5), int32(e.Pos[2])) < 5 + if e.decisionIn <= 0 { + e.decisionIn = 1.0 + if isNight { + e.wanderDir = [2]float64{0, 0} // 夜晚停下(回屋由村庄数据驱动,MVP) + } else { + ang := float64(e.ID%628) / 100.0 + e.wanderDir = [2]float64{math.Cos(ang) * 0.5, math.Sin(ang) * 0.5} + } + } + e.Vel[0] = e.wanderDir[0] + e.Vel[2] = e.wanderDir[1] +} diff --git a/internal/entity/entity_test.go b/internal/entity/entity_test.go new file mode 100644 index 00000000..bc171bda --- /dev/null +++ b/internal/entity/entity_test.go @@ -0,0 +1,157 @@ +package entity + +import ( + "path/filepath" + "testing" + "time" + + "mc/internal/block" + "mc/internal/world" + "mc/internal/worldgen" +) + +// flatWorld 测试世界:y<63 固体,其余空气;无光。 +type flatWorld struct{} + +func (flatWorld) Solid(x, y, z int32) bool { return y < 63 } +func (flatWorld) Block(x, y, z int32) block.State { return block.Air } +func (flatWorld) SkyLight(x, y, z int32) uint8 { return 15 } +func (flatWorld) BlockLight(x, y, z int32) uint8 { return 0 } + +// TestZombieChase 僵尸追玩家(僵尸与敌对生物.md §2)。 +func TestZombieChase(t *testing.T) { + m := NewManager() + z := m.Spawn(KindZombie, 0.5, 63, 0.5) + w := flatWorld{} + for i := 0; i < 200; i++ { + m.Tick(0.05, w, [3]float64{10, 63, 0.5}) + } + // 僵尸应向玩家移动(方块光 0 ≤ 7 满足感知条件;天空光 15 → 燃烧掉血但不影响移动) + if z.Pos[0] < 1.0 { + t.Fatalf("僵尸未向玩家移动: %+v", z.Pos) + } +} + +// TestZombieBurn 白天暴露燃烧致死(僵尸与敌对生物.md §4 逐 tick 伤害)。 +func TestZombieBurn(t *testing.T) { + m := NewManager() + z := m.Spawn(KindZombie, 0.5, 63, 0.5) + w := flatWorld{} // sky=15 + deadline := time.Now().Add(2 * time.Second) + for !z.Dead && time.Now().Before(deadline) { + m.Tick(0.05, w, [3]float64{100, 63, 100}) + } + if !z.Dead { + t.Fatal("暴露僵尸应在白天被烧死") + } + if z.Health > 0 { + t.Fatalf("死亡实体血量应 ≤0: %v", z.Health) + } +} + +// TestAnimalWander 动物游荡(确定性方向,实体系统.md §3 限频决策)。 +func TestAnimalWander(t *testing.T) { + m := NewManager() + a := m.Spawn(KindAnimal, 0.5, 63, 0.5) + w := flatWorld{} + for i := 0; i < 100; i++ { + m.Tick(0.05, w, [3]float64{100, 63, 100}) + } + if a.Pos[1] < 63 { + t.Fatalf("动物掉到地面以下: %+v", a.Pos) + } +} + +// TestDropMerge 掉落物合并(实体系统.md §5)。 +func TestDropMerge(t *testing.T) { + m := NewManager() + m.SpawnDrop("cobblestone", 5, 0.5, 64, 0.5) + m.SpawnDrop("cobblestone", 5, 0.6, 64, 0.5) // 距离 <1 → 合并 + m.SpawnDrop("cobblestone", 5, 10, 64, 10) // 距离远 → 新实体 + cnt := 0 + for _, e := range m.List() { + if e.Kind == KindItemDrop { + cnt++ + } + } + if cnt != 2 { + t.Fatalf("掉落物实体数期望 2,实际 %d", cnt) + } + // 拾取 + drops := m.PickupNearby(0.5, 64, 0.5) + if len(drops) != 1 || drops[0].Count != 10 { + t.Fatalf("拾取期望 1 组 10 个,实际 %+v", drops) + } +} + +// TestBlueprint 房屋蓝图确定性 + 建造执行顺序(村民系统.md §6)。 +func TestBlueprint(t *testing.T) { + a := Blueprint([3]int32{10, 64, 10}, 42) + b := Blueprint([3]int32{10, 64, 10}, 42) + if len(a) != len(b) { + t.Fatalf("蓝图确定性失败: %d != %d", len(a), len(b)) + } + for i := range a { + if a[i] != b[i] { + t.Fatalf("蓝图第 %d 项不一致", i) + } + } + // 顺序约束:地基(Order 0)先于墙与屋顶 + firstNonFoundation := -1 + for i, task := range a { + if task.Order != 0 { + firstNonFoundation = i + break + } + } + if firstNonFoundation <= 0 { + t.Fatal("蓝图缺少地基") + } + + // 建造执行:每 tick 2 块,全部放置成功 + v := &Villager{Profession: 1, BuildQueue: append([]BuildTask{}, a...)} + placed := 0 + place := func(x, y, z int32, id uint16, meta uint8) bool { placed++; return true } + for len(v.BuildQueue) > 0 { + v.BuildTick(place) + } + if placed != len(a) { + t.Fatalf("放置数量期望 %d,实际 %d", len(a), placed) + } +} + +// TestEntityWorldIntegration 实体跑在真实世界上(世界接入 smoke)。 +func TestEntityWorldIntegration(t *testing.T) { + reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json")) + if err != nil { + t.Fatalf("加载注册表失败: %v", err) + } + gen, err := worldgen.New(reg, 1, 0.005, 0.01, 1.0) // 无洞穴 + if err != nil { + t.Fatalf("创建生成器失败: %v", err) + } + w, err := world.New(reg, gen, 2) + if err != nil { + t.Fatalf("创建世界失败: %v", err) + } + defer w.Close() + deadline := time.Now().Add(15 * time.Second) + for time.Now().Before(deadline) { + w.Update(8, 64, 8, 8*time.Millisecond) + if w.Block(8, 0, 8) != block.Air { + break + } + time.Sleep(2 * time.Millisecond) + } + // 找地表放一只僵尸 + var surf int32 + for y := int32(255); y >= 0; y-- { + if w.Block(8, y, 8) != block.Air { + surf = y + break + } + } + m := NewManager() + m.Spawn(KindZombie, 8.5, float64(surf+1), 8.5) + m.Tick(0.05, w, [3]float64{12, float64(surf + 1), 8.5}) // 世界实现了 WorldView(Solid/Block/Sky/BlockLight) +} diff --git a/internal/entity/villager.go b/internal/entity/villager.go new file mode 100644 index 00000000..189475de --- /dev/null +++ b/internal/entity/villager.go @@ -0,0 +1,102 @@ +// 村民建造子系统:程序化房屋蓝图 + 逐块建造队列(村民系统.md §6 特色功能)。 +package entity + +import ( + "mc/internal/block" +) + +// Villager 村民附加数据(村民系统.md §3–§6)。 +type Villager struct { + Profession uint8 // 0 农民 1 建造者 2 铁匠 3 商人 + Home [3]int32 + // 建造队列(村民系统.md §6.3:按顺序逐块放置) + BuildQueue []BuildTask +} + +// BuildTask 一个建造任务(建造顺序已定)。 +type BuildTask struct { + X, Y, Z int32 + BlockID uint16 + Meta uint8 + Order uint32 // 地基 → 墙 → 门 → 屋顶 → 火把 +} + +// Blueprint 程序化房屋蓝图(村民系统.md §6.2): +// 矩形地基 W×D、墙高 3、门朝 +x、平顶、四角火把;确定性(seed 派生尺寸)。 +func Blueprint(origin [3]int32, seed uint64) []BuildTask { + w := 5 + int(seed%3) // 5–7 + d := 4 + int(seed%3) // 4–6 + wall := uint16(8) // oak_planks(与 assets/config/blocks.json 一致) + door := uint16(0) // 门后置:先留空 + roof := uint16(8) + torch := uint16(17) + + var tasks []BuildTask + order := uint32(0) + add := func(x, y, z int32, id uint16, o uint32) { + tasks = append(tasks, BuildTask{X: x, Y: y, Z: z, BlockID: id, Order: o}) + } + ox, oy, oz := origin[0], origin[1], origin[2] + // 1) 地基(order 0) + for x := 0; x < w; x++ { + for z := 0; z < d; z++ { + add(ox+int32(x), oy, oz+int32(z), wall, order) + } + } + order++ + // 2) 墙(order 1–2,高 3;门位置留空) + for y := 1; y <= 2; y++ { + for x := 0; x < w; x++ { + for z := 0; z < d; z++ { + edge := x == 0 || x == w-1 || z == 0 || z == d-1 + if !edge { + continue + } + if y == 1 && x == 0 && z == d/2 { + continue // 门洞 + } + add(ox+int32(x), oy+int32(y), oz+int32(z), wall, order) + } + } + order++ + } + // 3) 屋顶(order 3,平顶 + 一圈外沿) + for x := -1; x <= w; x++ { + for z := -1; z <= d; z++ { + edge := x == -1 || x == w || z == -1 || z == d + add(ox+int32(x), oy+3, oz+int32(z), roof, order+uint32(boolToInt(edge))) + } + } + order += 2 + // 4) 火把(order 5,四角) + for _, c := range [4][2]int{{0, 0}, {w - 1, 0}, {0, d - 1}, {w - 1, d - 1}} { + add(ox+int32(c[0]), oy+2, oz+int32(c[1]), torch, order) + } + _ = door + return tasks +} + +// boolToInt bool → int。 +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +// BuildTick 建造者村民执行建造队列:每 tick 1–2 块(村民系统.md §6.3 防瞬建突兀)。 +// place:放置回调(世界层 SetBlock);返回剩余任务数。 +func (v *Villager) BuildTick(place func(x, y, z int32, id uint16, meta uint8) bool) int { + n := 0 + for len(v.BuildQueue) > 0 && n < 2 { + t := v.BuildQueue[0] + v.BuildQueue = v.BuildQueue[1:] + if place(t.X, t.Y, t.Z, t.BlockID, t.Meta) { + n++ + } + } + return len(v.BuildQueue) +} + +// 方块状态工具(避免测试重复导入)。 +func stateOf(id uint16, meta uint8) block.State { return block.NewState(id, meta) } diff --git a/tools/zig.zip b/tools/zig.zip index a9ff323e..28d0af71 100644 Binary files a/tools/zig.zip and b/tools/zig.zip differ