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

258 lines
6.5 KiB
Go
Raw Normal View History

// 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]
}