Skip to content

Commit 2c069ca

Browse files
authored
feat: events & isTrigger property (#4)
* feat: isTrigger property & events triggers events (enter, stay, exit) + collision events (enter, stay, exit) + on sleep / on wake * feat(events): unit tests & file rename
1 parent 3de86b4 commit 2c069ca

File tree

5 files changed

+1125
-8
lines changed

5 files changed

+1125
-8
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() {

actor/shape.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,21 +153,22 @@ func (b *Box) GetContactFeature(direction mgl64.Vec3, output *[8]mgl64.Vec3, cou
153153
halfSize := b.HalfExtents
154154

155155
// Générer les 4 coins selon la face
156-
if bestAxisIdx == 0 { // Face X
156+
switch bestAxisIdx {
157+
case 0:
157158
x := sign * halfSize.X()
158159
output[0] = mgl64.Vec3{x, -halfSize.Y(), -halfSize.Z()}
159160
output[1] = mgl64.Vec3{x, -halfSize.Y(), halfSize.Z()}
160161
output[2] = mgl64.Vec3{x, halfSize.Y(), halfSize.Z()}
161162
output[3] = mgl64.Vec3{x, halfSize.Y(), -halfSize.Z()}
162163
*count = 4
163-
} else if bestAxisIdx == 1 { // Face Y
164+
case 1:
164165
y := sign * halfSize.Y()
165166
output[0] = mgl64.Vec3{-halfSize.X(), y, -halfSize.Z()}
166167
output[1] = mgl64.Vec3{-halfSize.X(), y, halfSize.Z()}
167168
output[2] = mgl64.Vec3{halfSize.X(), y, halfSize.Z()}
168169
output[3] = mgl64.Vec3{halfSize.X(), y, -halfSize.Z()}
169170
*count = 4
170-
} else { // Face Z
171+
default:
171172
z := sign * halfSize.Z()
172173
output[0] = mgl64.Vec3{-halfSize.X(), -halfSize.Y(), z}
173174
output[1] = mgl64.Vec3{halfSize.X(), -halfSize.Y(), z}
@@ -247,7 +248,7 @@ func (s *Sphere) GetAABB() AABB {
247248
// ComputeMass calculates mass data for the sphere
248249
func (s *Sphere) ComputeMass(density float64) float64 {
249250
// Volume of sphere = (4/3) * π * r³
250-
volume := (4.0 / 3.0) * math.Pi * math.Pow(s.Radius, 3)
251+
volume := (4.0 / 3.0) * math.Pi * s.Radius * s.Radius * s.Radius
251252

252253
return density * volume
253254
}

event.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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 && !c.BodyB.IsTrigger {
143+
constraints[n] = c
144+
n++
145+
}
146+
}
147+
constraints = constraints[:n]
148+
149+
return constraints
150+
}
151+
152+
// processCollisionEvents compares current and previous pairs to detect Enter/Stay/Exit
153+
// Should be called after all substeps
154+
func (e *Events) processCollisionEvents() {
155+
// Detect Enter and Stay events
156+
for pair := range e.currentActivePairs {
157+
// Skip if both bodies are sleeping, to avoid spamming events
158+
if pair.bodyA.IsSleeping && pair.bodyB.IsSleeping {
159+
continue
160+
}
161+
162+
isTrigger := pair.bodyA.IsTrigger || pair.bodyB.IsTrigger
163+
164+
if e.previousActivePairs[pair] {
165+
// Pair was active before and still is, Stay
166+
if isTrigger {
167+
e.buffer = append(e.buffer, TriggerStayEvent{
168+
BodyA: pair.bodyA,
169+
BodyB: pair.bodyB,
170+
})
171+
} else {
172+
e.buffer = append(e.buffer, CollisionStayEvent{
173+
BodyA: pair.bodyA,
174+
BodyB: pair.bodyB,
175+
})
176+
}
177+
} else {
178+
// New pair, Enter
179+
if isTrigger {
180+
e.buffer = append(e.buffer, TriggerEnterEvent{
181+
BodyA: pair.bodyA,
182+
BodyB: pair.bodyB,
183+
})
184+
} else {
185+
e.buffer = append(e.buffer, CollisionEnterEvent{
186+
BodyA: pair.bodyA,
187+
BodyB: pair.bodyB,
188+
})
189+
}
190+
}
191+
}
192+
193+
// Detect Exit events
194+
for pair := range e.previousActivePairs {
195+
if !e.currentActivePairs[pair] {
196+
// Pair was active but is no longer, Exit
197+
isTrigger := pair.bodyA.IsTrigger || pair.bodyB.IsTrigger
198+
199+
if isTrigger {
200+
e.buffer = append(e.buffer, TriggerExitEvent{
201+
BodyA: pair.bodyA,
202+
BodyB: pair.bodyB,
203+
})
204+
} else {
205+
e.buffer = append(e.buffer, CollisionExitEvent{
206+
BodyA: pair.bodyA,
207+
BodyB: pair.bodyB,
208+
})
209+
}
210+
}
211+
}
212+
213+
// Swap for next frame and clear current
214+
e.previousActivePairs, e.currentActivePairs = e.currentActivePairs, e.previousActivePairs
215+
clear(e.currentActivePairs)
216+
}
217+
218+
func (e *Events) processSleepEvents(bodies []*actor.RigidBody) {
219+
for _, body := range bodies {
220+
trackedState, exists := e.sleepStates[body]
221+
if !exists {
222+
e.sleepStates[body] = body.IsSleeping
223+
continue
224+
}
225+
226+
if !trackedState && body.IsSleeping {
227+
e.buffer = append(e.buffer, SleepEvent{Body: body})
228+
e.sleepStates[body] = true
229+
} else if trackedState && !body.IsSleeping {
230+
e.buffer = append(e.buffer, WakeEvent{Body: body})
231+
e.sleepStates[body] = false
232+
}
233+
}
234+
}
235+
236+
// flush sends all buffered events and clears the buffer
237+
func (e *Events) flush() {
238+
e.processCollisionEvents()
239+
240+
for _, event := range e.buffer {
241+
if listeners, ok := e.listeners[event.Type()]; ok {
242+
for _, listener := range listeners {
243+
listener(event)
244+
}
245+
}
246+
}
247+
e.buffer = e.buffer[:0]
248+
}

0 commit comments

Comments
 (0)