Files
ngzz-mc/internal/entity/entity.go
NianGao Dev f25ae15077 feat(game): 音频接入、原版掉落、动物攻击、村庄村民、种子/灵敏度配置与设置界面
- 音频(音频.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/ 布局)
2026-08-16 20:18:52 +08:00

265 lines
6.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
}
// 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]
}