194 lines
6.3 KiB
Go
194 lines
6.3 KiB
Go
|
|
package entity
|
||
|
|
|
||
|
|
import (
|
||
|
|
"github.com/df-mc/dragonfly/server/block/cube"
|
||
|
|
"github.com/df-mc/dragonfly/server/world"
|
||
|
|
"github.com/go-gl/mathgl/mgl64"
|
||
|
|
"math"
|
||
|
|
)
|
||
|
|
|
||
|
|
// MovementComputer is used to compute movement of an entity. When constructed, the Gravity of the entity
|
||
|
|
// the movement is computed for must be passed.
|
||
|
|
type MovementComputer struct {
|
||
|
|
Gravity, Drag float64
|
||
|
|
DragBeforeGravity bool
|
||
|
|
|
||
|
|
onGround bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// Movement represents the movement of a world.Entity as a result of a call to MovementComputer.TickMovement. The
|
||
|
|
// resulting position and velocity can be obtained by calling Position and Velocity. These can be sent to viewers by
|
||
|
|
// calling Send.
|
||
|
|
type Movement struct {
|
||
|
|
v []world.Viewer
|
||
|
|
e world.Entity
|
||
|
|
pos, vel, dpos, dvel mgl64.Vec3
|
||
|
|
rot cube.Rotation
|
||
|
|
onGround bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// Send sends the Movement to any viewers watching the entity at the time of the movement. If the position/velocity
|
||
|
|
// changes were negligible, nothing is sent.
|
||
|
|
func (m *Movement) Send() {
|
||
|
|
posChanged := !m.dpos.ApproxEqualThreshold(zeroVec3, epsilon)
|
||
|
|
velChanged := !m.dvel.ApproxEqualThreshold(zeroVec3, epsilon)
|
||
|
|
|
||
|
|
for _, v := range m.v {
|
||
|
|
if posChanged {
|
||
|
|
v.ViewEntityMovement(m.e, m.pos, m.rot, m.onGround)
|
||
|
|
}
|
||
|
|
if velChanged {
|
||
|
|
v.ViewEntityVelocity(m.e, m.vel)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Position returns the position as a result of the Movement as an mgl64.Vec3.
|
||
|
|
func (m *Movement) Position() mgl64.Vec3 {
|
||
|
|
return m.pos
|
||
|
|
}
|
||
|
|
|
||
|
|
// Velocity returns the velocity after the Movement as an mgl64.Vec3.
|
||
|
|
func (m *Movement) Velocity() mgl64.Vec3 {
|
||
|
|
return m.vel
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rotation returns the rotation, yaw and pitch, of the entity after the Movement.
|
||
|
|
func (m *Movement) Rotation() cube.Rotation {
|
||
|
|
return m.rot
|
||
|
|
}
|
||
|
|
|
||
|
|
// TickMovement performs a movement tick on an entity. Velocity is applied and changed according to the values
|
||
|
|
// of its Drag and Gravity.
|
||
|
|
// The new position of the entity after movement is returned.
|
||
|
|
// The resulting Movement can be sent to viewers by calling Movement.Send.
|
||
|
|
func (c *MovementComputer) TickMovement(e world.Entity, pos, vel mgl64.Vec3, rot cube.Rotation, tx *world.Tx) *Movement {
|
||
|
|
viewers := tx.Viewers(pos)
|
||
|
|
|
||
|
|
velBefore := vel
|
||
|
|
vel = c.applyHorizontalForces(tx, pos, c.applyVerticalForces(vel))
|
||
|
|
dPos, vel := c.checkCollision(tx, e, pos, vel)
|
||
|
|
|
||
|
|
return &Movement{v: viewers, e: e,
|
||
|
|
pos: pos.Add(dPos), vel: vel, dpos: dPos, dvel: vel.Sub(velBefore),
|
||
|
|
rot: rot, onGround: c.onGround,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// OnGround checks if the entity that this computer calculates is currently on the ground.
|
||
|
|
func (c *MovementComputer) OnGround() bool {
|
||
|
|
return c.onGround
|
||
|
|
}
|
||
|
|
|
||
|
|
// zeroVec3 is a mgl64.Vec3 with zero values.
|
||
|
|
var zeroVec3 mgl64.Vec3
|
||
|
|
|
||
|
|
// epsilon is the epsilon used for thresholds for change used for change in position and velocity.
|
||
|
|
const epsilon = 0.001
|
||
|
|
|
||
|
|
// applyVerticalForces applies gravity and drag on the Y axis, based on the Gravity and Drag values set.
|
||
|
|
func (c *MovementComputer) applyVerticalForces(vel mgl64.Vec3) mgl64.Vec3 {
|
||
|
|
if c.DragBeforeGravity {
|
||
|
|
vel[1] *= 1 - c.Drag
|
||
|
|
}
|
||
|
|
vel[1] -= c.Gravity
|
||
|
|
if !c.DragBeforeGravity {
|
||
|
|
vel[1] *= 1 - c.Drag
|
||
|
|
}
|
||
|
|
return vel
|
||
|
|
}
|
||
|
|
|
||
|
|
// applyHorizontalForces applies friction to the velocity based on the Drag value, reducing it on the X and Z axes.
|
||
|
|
func (c *MovementComputer) applyHorizontalForces(tx *world.Tx, pos, vel mgl64.Vec3) mgl64.Vec3 {
|
||
|
|
friction := 1 - c.Drag
|
||
|
|
if c.onGround {
|
||
|
|
if f, ok := tx.Block(cube.PosFromVec3(pos).Side(cube.FaceDown)).(interface {
|
||
|
|
Friction() float64
|
||
|
|
}); ok {
|
||
|
|
friction *= f.Friction()
|
||
|
|
} else {
|
||
|
|
friction *= 0.6
|
||
|
|
}
|
||
|
|
}
|
||
|
|
vel[0] *= friction
|
||
|
|
vel[2] *= friction
|
||
|
|
return vel
|
||
|
|
}
|
||
|
|
|
||
|
|
// checkCollision handles the collision of the entity with blocks, adapting the velocity of the entity if it
|
||
|
|
// happens to collide with a block.
|
||
|
|
// The final velocity and the Vec3 that the entity should move is returned.
|
||
|
|
func (c *MovementComputer) checkCollision(tx *world.Tx, e world.Entity, pos, vel mgl64.Vec3) (mgl64.Vec3, mgl64.Vec3) {
|
||
|
|
// TODO: Implement collision with other entities.
|
||
|
|
deltaX, deltaY, deltaZ := vel[0], vel[1], vel[2]
|
||
|
|
|
||
|
|
// Entities only ever have a single bounding box.
|
||
|
|
entityBBox := e.H().Type().BBox(e).Translate(pos)
|
||
|
|
blocks := blockBBoxsAround(tx, entityBBox.Extend(vel))
|
||
|
|
|
||
|
|
if !mgl64.FloatEqualThreshold(deltaY, 0, epsilon) {
|
||
|
|
// First we move the entity BBox on the Y axis.
|
||
|
|
for _, blockBBox := range blocks {
|
||
|
|
deltaY = entityBBox.YOffset(blockBBox, deltaY)
|
||
|
|
}
|
||
|
|
entityBBox = entityBBox.Translate(mgl64.Vec3{0, deltaY})
|
||
|
|
}
|
||
|
|
if !mgl64.FloatEqualThreshold(deltaX, 0, epsilon) {
|
||
|
|
// Then on the X axis.
|
||
|
|
for _, blockBBox := range blocks {
|
||
|
|
deltaX = entityBBox.XOffset(blockBBox, deltaX)
|
||
|
|
}
|
||
|
|
entityBBox = entityBBox.Translate(mgl64.Vec3{deltaX})
|
||
|
|
}
|
||
|
|
if !mgl64.FloatEqualThreshold(deltaZ, 0, epsilon) {
|
||
|
|
// And finally on the Z axis.
|
||
|
|
for _, blockBBox := range blocks {
|
||
|
|
deltaZ = entityBBox.ZOffset(blockBBox, deltaZ)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !mgl64.FloatEqual(vel[1], 0) {
|
||
|
|
// The Y velocity of the entity is currently not 0, meaning it is moving either up or down. We can
|
||
|
|
// then assume the entity is not currently on the ground.
|
||
|
|
c.onGround = false
|
||
|
|
}
|
||
|
|
if !mgl64.FloatEqual(deltaX, vel[0]) {
|
||
|
|
vel[0] = 0
|
||
|
|
}
|
||
|
|
if !mgl64.FloatEqual(deltaY, vel[1]) {
|
||
|
|
// The entity either hit the ground or hit the ceiling.
|
||
|
|
if vel[1] < 0 {
|
||
|
|
// The entity was going down, so we can assume it is now on the ground.
|
||
|
|
c.onGround = true
|
||
|
|
}
|
||
|
|
vel[1] = 0
|
||
|
|
}
|
||
|
|
if !mgl64.FloatEqual(deltaZ, vel[2]) {
|
||
|
|
vel[2] = 0
|
||
|
|
}
|
||
|
|
return mgl64.Vec3{deltaX, deltaY, deltaZ}, vel
|
||
|
|
}
|
||
|
|
|
||
|
|
// blockBBoxsAround returns all blocks around the entity passed, using the BBox passed to make a prediction of
|
||
|
|
// what blocks need to have their BBox returned.
|
||
|
|
func blockBBoxsAround(tx *world.Tx, box cube.BBox) []cube.BBox {
|
||
|
|
grown := box.Grow(0.25)
|
||
|
|
min, max := grown.Min(), grown.Max()
|
||
|
|
minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2]))
|
||
|
|
maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2]))
|
||
|
|
|
||
|
|
// A prediction of one BBox per block, plus an additional 2, in case
|
||
|
|
blockBBoxs := make([]cube.BBox, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)+2)
|
||
|
|
for y := minY; y <= maxY; y++ {
|
||
|
|
for x := minX; x <= maxX; x++ {
|
||
|
|
for z := minZ; z <= maxZ; z++ {
|
||
|
|
pos := cube.Pos{x, y, z}
|
||
|
|
boxes := tx.Block(pos).Model().BBox(pos, tx)
|
||
|
|
for _, box := range boxes {
|
||
|
|
blockBBoxs = append(blockBBoxs, box.Translate(mgl64.Vec3{float64(x), float64(y), float64(z)}))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return blockBBoxs
|
||
|
|
}
|