Skip to content

Commit 528ff87

Browse files
committed
feat: isTrigger property & events
triggers events (enter, stay, exit) + collision events (enter, stay, exit) + on sleep / on wake
1 parent 3de86b4 commit 528ff87

File tree

3 files changed

+288
-4
lines changed

3 files changed

+288
-4
lines changed

actor/rigidbody.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ func (material Material) GetMass() float64 {
3737

3838
// RigidBody represents a rigid body in the physics simulation
3939
type RigidBody struct {
40+
// Useful to map to user data (e.g. entity id)
41+
Id any
42+
4043
// Spatial properties
4144
PreviousTransform Transform
4245
Transform Transform
@@ -55,6 +58,7 @@ type RigidBody struct {
5558
accumulatedForce mgl64.Vec3
5659
accumulatedTorque mgl64.Vec3
5760

61+
IsTrigger bool
5862
IsSleeping bool
5963
SleepTimer float64
6064

@@ -108,15 +112,23 @@ func NewRigidBody(transform Transform, shape ShapeInterface, bodyType BodyType,
108112
return rb
109113
}
110114

111-
func (rb *RigidBody) TrySleep(dt float64, timethreshold float64, velocityThreshold float64) {
115+
// TrySleep check if a body can be set to sleep.
116+
// returns 0 if no changes, 1 if set to sleep, 2 if waken
117+
func (rb *RigidBody) TrySleep(dt float64, timethreshold float64, velocityThreshold float64) uint8 {
112118
if rb.Velocity.Len() < velocityThreshold && rb.AngularVelocity.Len() < velocityThreshold {
113119
rb.SleepTimer += dt // Incrémente le timer
114120
if !rb.IsSleeping && rb.SleepTimer >= timethreshold {
115121
rb.Sleep()
122+
123+
return 1
116124
}
117125
} else {
118126
rb.WakeUp()
127+
128+
return 2
119129
}
130+
131+
return 0
120132
}
121133

122134
func (rb *RigidBody) Sleep() {

trigger.go

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package feather
2+
3+
import (
4+
"unsafe"
5+
6+
"github.com/akmonengine/feather/actor"
7+
"github.com/akmonengine/feather/constraint"
8+
)
9+
10+
const (
11+
TRIGGER_ENTER EventType = iota
12+
COLLISION_ENTER
13+
TRIGGER_STAY
14+
COLLISION_STAY
15+
TRIGGER_EXIT
16+
COLLISION_EXIT
17+
ON_SLEEP
18+
ON_WAKE
19+
)
20+
21+
type pairKey struct {
22+
bodyA *actor.RigidBody
23+
bodyB *actor.RigidBody
24+
}
25+
26+
// makePairKey creates a normalized pair key with consistent ordering
27+
func makePairKey(bodyA, bodyB *actor.RigidBody) pairKey {
28+
ptrA := uintptr(unsafe.Pointer(bodyA))
29+
ptrB := uintptr(unsafe.Pointer(bodyB))
30+
31+
if ptrB < ptrA {
32+
bodyA, bodyB = bodyB, bodyA
33+
}
34+
35+
return pairKey{bodyA: bodyA, bodyB: bodyB}
36+
}
37+
38+
type EventType uint8
39+
40+
// Event interface - all events implement this
41+
type Event interface {
42+
Type() EventType
43+
}
44+
45+
// Trigger events
46+
type TriggerEnterEvent struct {
47+
BodyA *actor.RigidBody
48+
BodyB *actor.RigidBody
49+
}
50+
51+
func (e TriggerEnterEvent) Type() EventType { return TRIGGER_ENTER }
52+
53+
type TriggerStayEvent struct {
54+
BodyA *actor.RigidBody
55+
BodyB *actor.RigidBody
56+
}
57+
58+
func (e TriggerStayEvent) Type() EventType { return TRIGGER_STAY }
59+
60+
type TriggerExitEvent struct {
61+
BodyA *actor.RigidBody
62+
BodyB *actor.RigidBody
63+
}
64+
65+
func (e TriggerExitEvent) Type() EventType { return TRIGGER_EXIT }
66+
67+
// Collision events
68+
type CollisionEnterEvent struct {
69+
BodyA *actor.RigidBody
70+
BodyB *actor.RigidBody
71+
}
72+
73+
func (e CollisionEnterEvent) Type() EventType { return COLLISION_ENTER }
74+
75+
type CollisionStayEvent struct {
76+
BodyA *actor.RigidBody
77+
BodyB *actor.RigidBody
78+
}
79+
80+
func (e CollisionStayEvent) Type() EventType { return COLLISION_STAY }
81+
82+
type CollisionExitEvent struct {
83+
BodyA *actor.RigidBody
84+
BodyB *actor.RigidBody
85+
}
86+
87+
func (e CollisionExitEvent) Type() EventType { return COLLISION_EXIT }
88+
89+
// Sleep/Wake events
90+
type SleepEvent struct {
91+
Body *actor.RigidBody
92+
}
93+
94+
func (e SleepEvent) Type() EventType { return ON_SLEEP }
95+
96+
type WakeEvent struct {
97+
Body *actor.RigidBody
98+
}
99+
100+
func (e WakeEvent) Type() EventType { return ON_WAKE }
101+
102+
// EventListener - callback for events
103+
type EventListener func(event Event)
104+
105+
// Events manager
106+
type Events struct {
107+
// Listeners by event type
108+
listeners map[EventType][]EventListener
109+
110+
// Event buffer to send at flush
111+
buffer []Event
112+
113+
// Collision tracking for Enter/Stay/Exit detection
114+
previousActivePairs map[pairKey]bool
115+
currentActivePairs map[pairKey]bool
116+
117+
sleepStates map[*actor.RigidBody]bool
118+
}
119+
120+
func NewEvents() Events {
121+
return Events{
122+
listeners: make(map[EventType][]EventListener),
123+
buffer: make([]Event, 0, 256),
124+
previousActivePairs: make(map[pairKey]bool),
125+
currentActivePairs: make(map[pairKey]bool),
126+
sleepStates: make(map[*actor.RigidBody]bool),
127+
}
128+
}
129+
130+
// Subscribe adds a listener for an event type
131+
func (e *Events) Subscribe(eventType EventType, listener EventListener) {
132+
e.listeners[eventType] = append(e.listeners[eventType], listener)
133+
}
134+
135+
// recordCollision is called during substeps to record a collision/trigger
136+
func (e *Events) recordCollisions(constraints []*constraint.ContactConstraint) []*constraint.ContactConstraint {
137+
n := 0
138+
for _, c := range constraints {
139+
pair := makePairKey(c.BodyA, c.BodyB)
140+
e.currentActivePairs[pair] = true
141+
142+
if c.BodyA.IsTrigger == false && c.BodyB.IsTrigger == false {
143+
constraints[n] = c
144+
n++
145+
}
146+
}
147+
constraints = constraints[:n]
148+
149+
return constraints
150+
}
151+
152+
// emitSleep emits a sleep event (called from trySleep)
153+
func (e *Events) emitSleep(body *actor.RigidBody) {
154+
e.buffer = append(e.buffer, SleepEvent{Body: body})
155+
}
156+
157+
// emitWake emits a wake event (called from WakeUp)
158+
func (e *Events) emitWake(body *actor.RigidBody) {
159+
e.buffer = append(e.buffer, WakeEvent{Body: body})
160+
}
161+
162+
// processCollisionEvents compares current and previous pairs to detect Enter/Stay/Exit
163+
// Should be called after all substeps
164+
func (e *Events) processCollisionEvents() {
165+
// Detect Enter and Stay events
166+
for pair := range e.currentActivePairs {
167+
// Skip if both bodies are sleeping, to avoid spamming events
168+
if pair.bodyA.IsSleeping && pair.bodyB.IsSleeping {
169+
continue
170+
}
171+
172+
isTrigger := pair.bodyA.IsTrigger || pair.bodyB.IsTrigger
173+
174+
if e.previousActivePairs[pair] {
175+
// Pair was active before and still is, Stay
176+
if isTrigger {
177+
e.buffer = append(e.buffer, TriggerStayEvent{
178+
BodyA: pair.bodyA,
179+
BodyB: pair.bodyB,
180+
})
181+
} else {
182+
e.buffer = append(e.buffer, CollisionStayEvent{
183+
BodyA: pair.bodyA,
184+
BodyB: pair.bodyB,
185+
})
186+
}
187+
} else {
188+
// New pair, Enter
189+
if isTrigger {
190+
e.buffer = append(e.buffer, TriggerEnterEvent{
191+
BodyA: pair.bodyA,
192+
BodyB: pair.bodyB,
193+
})
194+
} else {
195+
e.buffer = append(e.buffer, CollisionEnterEvent{
196+
BodyA: pair.bodyA,
197+
BodyB: pair.bodyB,
198+
})
199+
}
200+
}
201+
}
202+
203+
// Detect Exit events
204+
for pair := range e.previousActivePairs {
205+
if !e.currentActivePairs[pair] {
206+
// Pair was active but is no longer, Exit
207+
isTrigger := pair.bodyA.IsTrigger || pair.bodyB.IsTrigger
208+
209+
if isTrigger {
210+
e.buffer = append(e.buffer, TriggerExitEvent{
211+
BodyA: pair.bodyA,
212+
BodyB: pair.bodyB,
213+
})
214+
} else {
215+
e.buffer = append(e.buffer, CollisionExitEvent{
216+
BodyA: pair.bodyA,
217+
BodyB: pair.bodyB,
218+
})
219+
}
220+
}
221+
}
222+
223+
// Swap for next frame and clear current
224+
e.previousActivePairs, e.currentActivePairs = e.currentActivePairs, e.previousActivePairs
225+
clear(e.currentActivePairs)
226+
}
227+
228+
func (e *Events) processSleepEvents(bodies []*actor.RigidBody) {
229+
for _, body := range bodies {
230+
trackedState, exists := e.sleepStates[body]
231+
if !exists {
232+
e.sleepStates[body] = body.IsSleeping
233+
continue
234+
}
235+
236+
if !trackedState && body.IsSleeping {
237+
e.buffer = append(e.buffer, SleepEvent{Body: body})
238+
e.sleepStates[body] = true
239+
} else if trackedState && !body.IsSleeping {
240+
e.buffer = append(e.buffer, WakeEvent{Body: body})
241+
e.sleepStates[body] = false
242+
}
243+
}
244+
}
245+
246+
// flush sends all buffered events and clears the buffer
247+
func (e *Events) flush() {
248+
e.processCollisionEvents()
249+
250+
for _, event := range e.buffer {
251+
if listeners, ok := e.listeners[event.Type()]; ok {
252+
for _, listener := range listeners {
253+
listener(event)
254+
}
255+
}
256+
}
257+
e.buffer = e.buffer[:0]
258+
}

world.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type World struct {
1616
Substeps int
1717
SpatialGrid *SpatialGrid
1818
Workers int
19+
20+
Events Events
1921
}
2022

2123
// AddBody adds a rigid body to the world
@@ -36,6 +38,13 @@ func (w *World) RemoveBody(body *actor.RigidBody) {
3638
if k != -1 {
3739
w.Bodies = append(w.Bodies[:k], w.Bodies[k+1:]...)
3840
}
41+
42+
delete(w.Events.sleepStates, body)
43+
for pair := range w.Events.previousActivePairs {
44+
if pair.bodyA == body || pair.bodyB == body {
45+
delete(w.Events.previousActivePairs, pair)
46+
}
47+
}
3948
}
4049

4150
func (w *World) Step(dt float64) {
@@ -49,6 +58,8 @@ func (w *World) Step(dt float64) {
4958
// Phase 2.1: Collision pair finding - narrow phase
5059
constraints := w.detectCollision()
5160

61+
constraints = w.Events.recordCollisions(constraints)
62+
5263
// Phase 3: Solver, only one iteration is required thanks to substeps
5364
w.solvePosition(h, constraints)
5465

@@ -61,6 +72,9 @@ func (w *World) Step(dt float64) {
6172

6273
w.trySleep(h)
6374
}
75+
76+
w.Events.processSleepEvents(w.Bodies)
77+
w.Events.flush()
6478
}
6579

6680
func (w *World) integrate(h float64) {
@@ -94,7 +108,7 @@ func (w *World) solveVelocity(h float64, constraints []*constraint.ContactConstr
94108
// trySleep sets the body to sleep if its velocity is lower than the threshold, for a given duration
95109
// this method is too simple to use a task, it slows down in multiple goroutines
96110
func (w *World) trySleep(h float64) {
97-
task(1, w.Bodies, func(body *actor.RigidBody) {
98-
body.TrySleep(h, 0.1, 0.05) // Seuil de vitesse pour le sleeping
99-
})
111+
for _, body := range w.Bodies {
112+
body.TrySleep(h, 0.1, 0.05)
113+
}
100114
}

0 commit comments

Comments
 (0)