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

183 lines
4.5 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(x, y, z int32) bool
Fluid(x, y, z int32) bool
}
const (
AirMax = 15.0 // 氧气秒数300 tick
standHeight = 1.8
standEye = 1.62
sneakHeight = 1.5
sneakEye = 1.27
swimHeight = 0.6
swimEye = 0.4
)
// 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
InWater bool // 脚或躯干在液体
EyeInWater bool // 眼睛在液体(氧气 / 水下雾)
Swimming bool
Flying bool
Air float64 // 剩余氧气秒数
FallSpeed float64 // 落地前一帧垂直速度(摔落判定)
rem [3]float64
}
// NewPlayer 创建默认玩家(站立尺寸)。
func NewPlayer(x, y, z float64) *Player {
return &Player{
Pos: [3]float64{x, y, z},
Width: 0.6,
Height: standHeight,
EyeHeight: standEye,
Air: AirMax,
}
}
// Move 逐轴移动(物理与碰撞.md §2.2X → 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
}
}
}
p.updateFluid(w)
}
func (p *Player) updateFluid(w World) {
fx := int32(math.Floor(p.Pos[0]))
fz := int32(math.Floor(p.Pos[2]))
foot := w.Fluid(fx, int32(math.Floor(p.Pos[1]+0.3)), fz)
mid := w.Fluid(fx, int32(math.Floor(p.Pos[1]+p.Height*0.6)), fz)
p.InWater = foot || mid
eye := p.Eye()
p.EyeInWater = w.Fluid(int32(math.Floor(eye[0])), int32(math.Floor(eye[1])), int32(math.Floor(eye[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]}
}
// SetPose 切换潜行/游泳碰撞箱;站立会卡住则保持当前矮箱。
func (p *Player) SetPose(w World, sneak, swim bool) {
oldH, oldE := p.Height, p.EyeHeight
switch {
case swim && p.InWater:
p.Height, p.EyeHeight, p.Swimming = swimHeight, swimEye, true
case sneak:
p.Height, p.EyeHeight, p.Swimming = sneakHeight, sneakEye, false
default:
p.Height, p.EyeHeight, p.Swimming = standHeight, standEye, false
}
if p.Collides(w) {
p.Height, p.EyeHeight = oldH, oldE
if oldH <= swimHeight+0.01 {
p.Swimming = true
}
}
}
// TickAir 氧气:眼睛在水递减,出水回满(物理与碰撞.md §4
func (p *Player) TickAir(dt float64) {
if p.EyeInWater {
p.Air -= dt
if p.Air < 0 {
p.Air = 0
}
} else {
p.Air = AirMax
}
}
// Tick 重力与液体阻力积分dt 秒)。
func (p *Player) Tick(w World, dt float64) {
const gravity = 32.0
p.FallSpeed = p.Vel[1]
if !p.Flying {
if p.InWater {
p.Vel[1] -= gravity * 0.15 * dt
p.Vel[1] *= 1 - 3.0*dt
if p.Vel[1] < -4 {
p.Vel[1] = -4
}
} 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)
p.TickAir(dt)
}
// Jump 起跳(仅落地时生效;约 1.25 方块高度)。
func (p *Player) Jump() {
if p.OnGround {
p.Vel[1] = 9.0
p.OnGround = false
}
}
// SwimUp 水中上浮(不要求落地)。
func (p *Player) SwimUp() {
if p.InWater {
p.Vel[1] = 4.2
}
}
// SwimDown 水中下潜。
func (p *Player) SwimDown() {
if p.InWater {
p.Vel[1] = -3.2
}
}