Files
ngzz-mc/internal/entity/entity.go
2026-09-19 14:21:08 +08:00

294 lines
7.7 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 entity 实体系统生命周期、AI 状态机、掉落物(场景/实体系统.md
//
// 设计要点:
// - 实体与方块分离,随激活区块 tick卸载冻结
// - AI 决策限频0.250.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.3atan2(vx,vz)+π)。
func faceVelYaw(vx, vz float64) float64 {
return math.Atan2(vx, vz) + math.Pi
}