294 lines
7.7 KiB
Go
294 lines
7.7 KiB
Go
// 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
|
||
// ChunkLoaded 实体所在区块是否已加载(卸载区块冻结实体——
|
||
// 否则实体失去碰撞支撑会无限下坠到负数坐标,见 实体系统.md §2)
|
||
ChunkLoaded(x, z int32) bool
|
||
}
|
||
|
||
// Entity 实体。
|
||
type Entity struct {
|
||
ID ID
|
||
Kind Kind
|
||
Pos [3]float64 // 脚底坐标
|
||
Vel [3]float64
|
||
Yaw float64
|
||
Health float64
|
||
Dead bool
|
||
|
||
// HitFlash 受击白闪剩余秒数(客户端渲染用,打击反馈)
|
||
HitFlash float64
|
||
|
||
// AI 状态(实体系统.md §3 状态机)
|
||
state uint8
|
||
decisionIn float64 // 下次决策倒计时(限频)
|
||
attackIn float64 // 攻击冷却(僵尸与敌对生物.md §3)
|
||
wanderDir [2]float64
|
||
|
||
// Data 类型附加数据(掉落物 = *Drop,村民 = *Villager)
|
||
Data any
|
||
|
||
// AmbientT / WalkDist 音效时钟(音频.md §6.2;由 Session 推进)。
|
||
AmbientT int
|
||
WalkDist float64
|
||
}
|
||
|
||
// 僵尸 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()
|
||
}
|
||
|
||
// Get 按 ID 取实体(不存在返回 nil)。
|
||
func (m *Manager) Get(id ID) *Entity {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
return m.entities[id]
|
||
}
|
||
|
||
// 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 更新全部实体(激活区块内;冻结区跳过,实体系统.md §2
|
||
// ——未加载区块无碰撞支撑,实体继续 tick 会无限下坠并越界崩溃)。
|
||
func (m *Manager) Tick(dt float64, w WorldView, playerPos [3]float64) {
|
||
for _, e := range m.List() {
|
||
if e.Dead {
|
||
continue
|
||
}
|
||
if !w.ChunkLoaded(int32(e.Pos[0]), int32(e.Pos[2])) {
|
||
continue // 区块卸载:冻结
|
||
}
|
||
if e.HitFlash > 0 {
|
||
e.HitFlash -= dt // 受击白闪衰减
|
||
}
|
||
switch e.Kind {
|
||
case KindZombie:
|
||
e.tickZombie(dt, w, playerPos)
|
||
case KindAnimal:
|
||
e.tickAnimal(dt, w)
|
||
TickAnimal(e)
|
||
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 = faceVelYaw(e.Vel[0], e.Vel[2])
|
||
}
|
||
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
|
||
if e.Vel[0] != 0 || e.Vel[2] != 0 {
|
||
e.Yaw = faceVelYaw(e.Vel[0], e.Vel[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]
|
||
if e.Vel[0] != 0 || e.Vel[2] != 0 {
|
||
e.Yaw = faceVelYaw(e.Vel[0], e.Vel[2])
|
||
}
|
||
}
|
||
|
||
// faceVelYaw 盒模型 −Z 指向速度(渲染.md §5.3:atan2(vx,vz)+π)。
|
||
func faceVelYaw(vx, vz float64) float64 {
|
||
return math.Atan2(vx, vz) + math.Pi
|
||
}
|