- 音频(音频.md §2):Session.Sound 事件回调,破坏/放置/攻击/村民环境音接入, 客户端 audio.Manager 3D 衰减 + 监听器跟随 + 音量可调 - 原版掉落:草方块→泥土、石头→圆石、树叶无掉落(no_drop);掉落物实体 用物品图标小广告牌渲染(物品纹理并入实体图集) - 动物攻击(动物体系.md §5):左键优先射线命中实体(半径 0.6 球体判定), 伤害/击退/死亡移除+肉类掉落;僵尸掉腐肉 - 村庄村民(村民系统.md §2):Generator.VillageAt 定位村庄中心,会话扫描 已加载区块在村庄生成 3 名村民 - 种子与灵敏度配置:client.yaml world.seed/name、mouse_sensitivity; 存档与种子联动;鼠标左右反转修复(右移应右转) - 设置界面(UI与输入.md §6):屏幕状态 4,灵敏度/音量滑条、世界种子显示、 按键教程多行列表(cjkfont 支持 \\n)、完成按钮;主菜单/暂停菜单入口 - 准星指向方块名:左下角 CJK 显示(lang 本地化,tick 光栅化渲染线程上传) - 截图统一存项目根目录 jietu/(paths.RootDir;dev 构建识别 bin/ 布局)
265 lines
6.7 KiB
Go
265 lines
6.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
|
||
}
|
||
|
||
// 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()
|
||
}
|
||
|
||
// 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 更新全部实体(激活区块内;冻结区由调用方过滤)。
|
||
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]
|
||
}
|