2026-08-15 22:42:21 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 14:21:08 +08:00
|
|
|
|
const (
|
|
|
|
|
|
AirMax = 15.0 // 氧气秒数(300 tick)
|
|
|
|
|
|
standHeight = 1.8
|
|
|
|
|
|
standEye = 1.62
|
|
|
|
|
|
sneakHeight = 1.5
|
|
|
|
|
|
sneakEye = 1.27
|
|
|
|
|
|
swimHeight = 0.6
|
|
|
|
|
|
swimEye = 0.4
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-15 22:42:21 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
|
2026-09-19 14:21:08 +08:00
|
|
|
|
OnGround bool
|
|
|
|
|
|
InWater bool // 脚或躯干在液体
|
|
|
|
|
|
EyeInWater bool // 眼睛在液体(氧气 / 水下雾)
|
|
|
|
|
|
Swimming bool
|
|
|
|
|
|
Flying bool
|
|
|
|
|
|
Air float64 // 剩余氧气秒数
|
|
|
|
|
|
FallSpeed float64 // 落地前一帧垂直速度(摔落判定)
|
2026-08-15 22:42:21 +08:00
|
|
|
|
|
|
|
|
|
|
rem [3]float64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewPlayer 创建默认玩家(站立尺寸)。
|
|
|
|
|
|
func NewPlayer(x, y, z float64) *Player {
|
|
|
|
|
|
return &Player{
|
|
|
|
|
|
Pos: [3]float64{x, y, z},
|
|
|
|
|
|
Width: 0.6,
|
2026-09-19 14:21:08 +08:00
|
|
|
|
Height: standHeight,
|
|
|
|
|
|
EyeHeight: standEye,
|
|
|
|
|
|
Air: AirMax,
|
2026-08-15 22:42:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-09-19 14:21:08 +08:00
|
|
|
|
p.OnGround = true
|
2026-08-15 22:42:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-19 14:21:08 +08:00
|
|
|
|
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])))
|
2026-08-15 22:42:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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]}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 14:21:08 +08:00
|
|
|
|
// 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 秒)。
|
2026-08-15 22:42:21 +08:00
|
|
|
|
func (p *Player) Tick(w World, dt float64) {
|
2026-09-19 14:21:08 +08:00
|
|
|
|
const gravity = 32.0
|
|
|
|
|
|
p.FallSpeed = p.Vel[1]
|
2026-08-15 22:42:21 +08:00
|
|
|
|
if !p.Flying {
|
2026-09-19 14:21:08 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-08-15 22:42:21 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
p.Vel[1] -= gravity * dt
|
2026-09-19 14:21:08 +08:00
|
|
|
|
if p.Vel[1] < -64 {
|
|
|
|
|
|
p.Vel[1] = -64
|
|
|
|
|
|
}
|
2026-08-15 22:42:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
p.Move(w, p.Vel[0]*dt, p.Vel[1]*dt, p.Vel[2]*dt)
|
2026-09-19 14:21:08 +08:00
|
|
|
|
p.TickAir(dt)
|
2026-08-15 22:42:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Jump 起跳(仅落地时生效;约 1.25 方块高度)。
|
|
|
|
|
|
func (p *Player) Jump() {
|
|
|
|
|
|
if p.OnGround {
|
|
|
|
|
|
p.Vel[1] = 9.0
|
|
|
|
|
|
p.OnGround = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-19 14:21:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|