Files
ngzz-mc/internal/physics/player.go

115 lines
3.4 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 physics 玩家 AABB 碰撞与逐轴移动(场景/物理与碰撞.md §2)。
//
// 核心:分轴解算(X → Z → Y),每轴移动后检测 AABB 与固体方块重叠,重叠则回退该轴。
// 不用物理引擎:体素世界自己写最快、最可控。
package physics
import "math"
// World 物理层所需的世界接口(与渲染/交互共用同一张 solid 判定表)。
type World interface {
// Solid 是否碰撞(台阶/栅栏等多 AABB 形状后置,MVP 按整方块判定)。
Solid(x, y, z int32) bool
// Fluid 是否液体(游泳/浮力判定)。
Fluid(x, y, z int32) bool
}
// Player 玩家实体(设计.md §7.1:宽 0.6、高 1.8、眼高 1.62)。
type Player struct {
Pos [3]float64 // 脚底坐标
Vel [3]float64 // 速度(方块/秒)
Width float64
Height float64
EyeHeight float64
OnGround bool // 落地
Submerged bool // 在液体中
Flying bool // 飞行模式
// 累积的位移余量(亚步进精度,防边界抖动)
rem [3]float64
}
// NewPlayer 创建默认玩家(站立尺寸)。
func NewPlayer(x, y, z float64) *Player {
return &Player{
Pos: [3]float64{x, y, z},
Width: 0.6,
Height: 1.8,
EyeHeight: 1.62,
}
}
// Move 逐轴移动(物理与碰撞.md §2.2):X → Z → Y,每轴独立碰撞回退。
func (p *Player) Move(w World, dx, dy, dz float64) {
p.OnGround = false
deltas := [3]float64{dx, dy, dz}
for _, axis := range [3]int{0, 2, 1} {
p.Pos[axis] += deltas[axis]
if p.Collides(w) {
p.Pos[axis] -= deltas[axis]
p.Vel[axis] = 0
if axis == 1 && deltas[axis] < 0 {
p.OnGround = true // 垂直下落被阻挡 = 落地
}
}
}
// 液体状态更新(头部/脚部任一在水里)
headY := p.Pos[1] + p.Height*0.6
p.Submerged = w.Fluid(int32(math.Floor(p.Pos[0])), int32(math.Floor(p.Pos[1]+0.3)), int32(math.Floor(p.Pos[2]))) ||
w.Fluid(int32(math.Floor(p.Pos[0])), int32(math.Floor(headY)), int32(math.Floor(p.Pos[2])))
}
// Collides 当前位置 AABB 是否与固体方块重叠(含边界 epsilon 防抖动)。
func (p *Player) Collides(w World) bool {
const eps = 1e-6
minX := int32(math.Floor(p.Pos[0] - p.Width/2 + eps))
maxX := int32(math.Floor(p.Pos[0] + p.Width/2 - eps))
minY := int32(math.Floor(p.Pos[1] + eps))
maxY := int32(math.Floor(p.Pos[1] + p.Height - eps))
minZ := int32(math.Floor(p.Pos[2] - p.Width/2 + eps))
maxZ := int32(math.Floor(p.Pos[2] + p.Width/2 - eps))
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
}
// Eye 返回眼睛位置(相机位置)。
func (p *Player) Eye() [3]float64 {
return [3]float64{p.Pos[0], p.Pos[1] + p.EyeHeight, p.Pos[2]}
}
// Tick 重力与液体阻力积分(dt 秒;调用方随后调用 Move 应用位移)。
// 飞行/游泳模式由调用方在 Move 前覆写速度。
func (p *Player) Tick(w World, dt float64) {
const gravity = 32.0 // 方块/秒²
if !p.Flying {
if p.Submerged {
p.Vel[1] -= gravity * 0.3 * dt // 水中浮力近似
p.Vel[1] *= 1 - 2.0*dt // 阻力
} else {
p.Vel[1] -= gravity * dt
}
if p.Vel[1] < -64 {
p.Vel[1] = -64 // 终端速度
}
}
p.Move(w, p.Vel[0]*dt, p.Vel[1]*dt, p.Vel[2]*dt)
}
// Jump 起跳(仅落地时生效;约 1.25 方块高度)。
func (p *Player) Jump() {
if p.OnGround {
p.Vel[1] = 9.0
p.OnGround = false
}
}