Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion actor/rigidbody.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func (material Material) GetMass() float64 {

// RigidBody represents a rigid body in the physics simulation
type RigidBody struct {
// Useful to map to user data (e.g. entity id)
Id any

// Spatial properties
PreviousTransform Transform
Transform Transform
Expand All @@ -55,6 +58,7 @@ type RigidBody struct {
accumulatedForce mgl64.Vec3
accumulatedTorque mgl64.Vec3

IsTrigger bool
IsSleeping bool
SleepTimer float64

Expand Down Expand Up @@ -108,15 +112,23 @@ func NewRigidBody(transform Transform, shape ShapeInterface, bodyType BodyType,
return rb
}

func (rb *RigidBody) TrySleep(dt float64, timethreshold float64, velocityThreshold float64) {
// TrySleep check if a body can be set to sleep.
// returns 0 if no changes, 1 if set to sleep, 2 if waken
func (rb *RigidBody) TrySleep(dt float64, timethreshold float64, velocityThreshold float64) uint8 {
if rb.Velocity.Len() < velocityThreshold && rb.AngularVelocity.Len() < velocityThreshold {
rb.SleepTimer += dt // Incrémente le timer
if !rb.IsSleeping && rb.SleepTimer >= timethreshold {
rb.Sleep()

return 1
}
} else {
rb.WakeUp()

return 2
}

return 0
}

func (rb *RigidBody) Sleep() {
Expand Down
9 changes: 5 additions & 4 deletions actor/shape.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,21 +153,22 @@ func (b *Box) GetContactFeature(direction mgl64.Vec3, output *[8]mgl64.Vec3, cou
halfSize := b.HalfExtents

// Générer les 4 coins selon la face
if bestAxisIdx == 0 { // Face X
switch bestAxisIdx {
case 0:
x := sign * halfSize.X()
output[0] = mgl64.Vec3{x, -halfSize.Y(), -halfSize.Z()}
output[1] = mgl64.Vec3{x, -halfSize.Y(), halfSize.Z()}
output[2] = mgl64.Vec3{x, halfSize.Y(), halfSize.Z()}
output[3] = mgl64.Vec3{x, halfSize.Y(), -halfSize.Z()}
*count = 4
} else if bestAxisIdx == 1 { // Face Y
case 1:
y := sign * halfSize.Y()
output[0] = mgl64.Vec3{-halfSize.X(), y, -halfSize.Z()}
output[1] = mgl64.Vec3{-halfSize.X(), y, halfSize.Z()}
output[2] = mgl64.Vec3{halfSize.X(), y, halfSize.Z()}
output[3] = mgl64.Vec3{halfSize.X(), y, -halfSize.Z()}
*count = 4
} else { // Face Z
default:
z := sign * halfSize.Z()
output[0] = mgl64.Vec3{-halfSize.X(), -halfSize.Y(), z}
output[1] = mgl64.Vec3{halfSize.X(), -halfSize.Y(), z}
Expand Down Expand Up @@ -247,7 +248,7 @@ func (s *Sphere) GetAABB() AABB {
// ComputeMass calculates mass data for the sphere
func (s *Sphere) ComputeMass(density float64) float64 {
// Volume of sphere = (4/3) * π * r³
volume := (4.0 / 3.0) * math.Pi * math.Pow(s.Radius, 3)
volume := (4.0 / 3.0) * math.Pi * s.Radius * s.Radius * s.Radius

return density * volume
}
Expand Down
248 changes: 248 additions & 0 deletions event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package feather

import (
"unsafe"

"github.com/akmonengine/feather/actor"
"github.com/akmonengine/feather/constraint"
)

const (
TRIGGER_ENTER EventType = iota
COLLISION_ENTER
TRIGGER_STAY
COLLISION_STAY
TRIGGER_EXIT
COLLISION_EXIT
ON_SLEEP
ON_WAKE
)

type pairKey struct {
bodyA *actor.RigidBody
bodyB *actor.RigidBody
}

// makePairKey creates a normalized pair key with consistent ordering
func makePairKey(bodyA, bodyB *actor.RigidBody) pairKey {
ptrA := uintptr(unsafe.Pointer(bodyA))
ptrB := uintptr(unsafe.Pointer(bodyB))

if ptrB < ptrA {
bodyA, bodyB = bodyB, bodyA
}

return pairKey{bodyA: bodyA, bodyB: bodyB}
}

type EventType uint8

// Event interface - all events implement this
type Event interface {
Type() EventType
}

// Trigger events
type TriggerEnterEvent struct {
BodyA *actor.RigidBody
BodyB *actor.RigidBody
}

func (e TriggerEnterEvent) Type() EventType { return TRIGGER_ENTER }

type TriggerStayEvent struct {
BodyA *actor.RigidBody
BodyB *actor.RigidBody
}

func (e TriggerStayEvent) Type() EventType { return TRIGGER_STAY }

type TriggerExitEvent struct {
BodyA *actor.RigidBody
BodyB *actor.RigidBody
}

func (e TriggerExitEvent) Type() EventType { return TRIGGER_EXIT }

// Collision events
type CollisionEnterEvent struct {
BodyA *actor.RigidBody
BodyB *actor.RigidBody
}

func (e CollisionEnterEvent) Type() EventType { return COLLISION_ENTER }

type CollisionStayEvent struct {
BodyA *actor.RigidBody
BodyB *actor.RigidBody
}

func (e CollisionStayEvent) Type() EventType { return COLLISION_STAY }

type CollisionExitEvent struct {
BodyA *actor.RigidBody
BodyB *actor.RigidBody
}

func (e CollisionExitEvent) Type() EventType { return COLLISION_EXIT }

// Sleep/Wake events
type SleepEvent struct {
Body *actor.RigidBody
}

func (e SleepEvent) Type() EventType { return ON_SLEEP }

type WakeEvent struct {
Body *actor.RigidBody
}

func (e WakeEvent) Type() EventType { return ON_WAKE }

// EventListener - callback for events
type EventListener func(event Event)

// Events manager
type Events struct {
// Listeners by event type
listeners map[EventType][]EventListener

// Event buffer to send at flush
buffer []Event

// Collision tracking for Enter/Stay/Exit detection
previousActivePairs map[pairKey]bool
currentActivePairs map[pairKey]bool

sleepStates map[*actor.RigidBody]bool
}

func NewEvents() Events {
return Events{
listeners: make(map[EventType][]EventListener),
buffer: make([]Event, 0, 256),
previousActivePairs: make(map[pairKey]bool),
currentActivePairs: make(map[pairKey]bool),
sleepStates: make(map[*actor.RigidBody]bool),
}
}

// Subscribe adds a listener for an event type
func (e *Events) Subscribe(eventType EventType, listener EventListener) {
e.listeners[eventType] = append(e.listeners[eventType], listener)
}

// recordCollision is called during substeps to record a collision/trigger
func (e *Events) recordCollisions(constraints []*constraint.ContactConstraint) []*constraint.ContactConstraint {
n := 0
for _, c := range constraints {
pair := makePairKey(c.BodyA, c.BodyB)
e.currentActivePairs[pair] = true

if !c.BodyA.IsTrigger && !c.BodyB.IsTrigger {
constraints[n] = c
n++
}
}
constraints = constraints[:n]

return constraints
}

// processCollisionEvents compares current and previous pairs to detect Enter/Stay/Exit
// Should be called after all substeps
func (e *Events) processCollisionEvents() {
// Detect Enter and Stay events
for pair := range e.currentActivePairs {
// Skip if both bodies are sleeping, to avoid spamming events
if pair.bodyA.IsSleeping && pair.bodyB.IsSleeping {
continue
}

isTrigger := pair.bodyA.IsTrigger || pair.bodyB.IsTrigger

if e.previousActivePairs[pair] {
// Pair was active before and still is, Stay
if isTrigger {
e.buffer = append(e.buffer, TriggerStayEvent{
BodyA: pair.bodyA,
BodyB: pair.bodyB,
})
} else {
e.buffer = append(e.buffer, CollisionStayEvent{
BodyA: pair.bodyA,
BodyB: pair.bodyB,
})
}
} else {
// New pair, Enter
if isTrigger {
e.buffer = append(e.buffer, TriggerEnterEvent{
BodyA: pair.bodyA,
BodyB: pair.bodyB,
})
} else {
e.buffer = append(e.buffer, CollisionEnterEvent{
BodyA: pair.bodyA,
BodyB: pair.bodyB,
})
}
}
}

// Detect Exit events
for pair := range e.previousActivePairs {
if !e.currentActivePairs[pair] {
// Pair was active but is no longer, Exit
isTrigger := pair.bodyA.IsTrigger || pair.bodyB.IsTrigger

if isTrigger {
e.buffer = append(e.buffer, TriggerExitEvent{
BodyA: pair.bodyA,
BodyB: pair.bodyB,
})
} else {
e.buffer = append(e.buffer, CollisionExitEvent{
BodyA: pair.bodyA,
BodyB: pair.bodyB,
})
}
}
}

// Swap for next frame and clear current
e.previousActivePairs, e.currentActivePairs = e.currentActivePairs, e.previousActivePairs
clear(e.currentActivePairs)
}

func (e *Events) processSleepEvents(bodies []*actor.RigidBody) {
for _, body := range bodies {
trackedState, exists := e.sleepStates[body]
if !exists {
e.sleepStates[body] = body.IsSleeping
continue
}

if !trackedState && body.IsSleeping {
e.buffer = append(e.buffer, SleepEvent{Body: body})
e.sleepStates[body] = true
} else if trackedState && !body.IsSleeping {
e.buffer = append(e.buffer, WakeEvent{Body: body})
e.sleepStates[body] = false
}
}
}

// flush sends all buffered events and clears the buffer
func (e *Events) flush() {
e.processCollisionEvents()

for _, event := range e.buffer {
if listeners, ok := e.listeners[event.Type()]; ok {
for _, listener := range listeners {
listener(event)
}
}
}
e.buffer = e.buffer[:0]
}
Loading
Loading