gosling 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,65 +1,65 @@
1
- require_relative 'actor.rb'
2
- require_relative 'collision.rb'
3
-
4
- module Gosling
5
- ##
6
- # Represents an Actor with a circular shape, defined by a mutable radius. The circle is rendered relative to the
7
- # Circle's center (see Transformable#center).
8
- #
9
- class Circle < Actor
10
- ##
11
- # How many vertices to use when rendering circles. More vertices means more accurate rendering at the cost of
12
- # performance.
13
- #
14
- RENDER_VERTEX_COUNT = 16
15
-
16
- attr_reader :radius
17
-
18
- ##
19
- # Creates a new Circle with initial radius of zero.
20
- #
21
- def initialize(window)
22
- super(window)
23
- @radius = 0
24
- end
25
-
26
- ##
27
- # Sets this circle's radius. Radius must be a positive integer.
28
- #
29
- def radius=(val)
30
- raise ArgumentError.new("Circle.radius cannot be negative") if val < 0
31
- @radius = val
32
- end
33
-
34
- ##
35
- # Returns the angle's corresponding unit vector times this circle's radius.
36
- #
37
- def get_point_at_angle(radians, out = nil)
38
- raise ArgumentError.new("Expected Numeric, but received #{radians.inspect}!") unless radians.is_a?(Numeric)
39
- out ||= Snow::Vec3.new
40
- out.set(Math.cos(radians) * @radius, Math.sin(radians) * @radius, 0)
41
- end
42
-
43
- ##
44
- # Returns true if the point is inside the Circle, false otherwise.
45
- #
46
- def is_point_in_bounds(point)
47
- Collision.is_point_in_shape?(point, self)
48
- end
49
-
50
- private
51
-
52
- # TODO: keep a cached, class-level list of local vertices that can be re-used during rendering
53
-
54
- def render(matrix)
55
- # TODO: store these vertices in a cached, class-level array (see above)
56
- local_vertices = (0...RENDER_VERTEX_COUNT).map do |i|
57
- get_point_at_angle(Math::PI * 2 * i / RENDER_VERTEX_COUNT)
58
- end
59
- # TODO: retain an array of vertices in memory; write transformed vertices to this array
60
- global_vertices = local_vertices.map { |v| Transformable.transform_point(matrix, v) }
61
-
62
- fill_polygon(global_vertices)
63
- end
64
- end
65
- end
1
+ require_relative 'actor.rb'
2
+ require_relative 'collision.rb'
3
+
4
+ module Gosling
5
+ ##
6
+ # Represents an Actor with a circular shape, defined by a mutable radius. The circle is rendered relative to the
7
+ # Circle's center (see Transformable#center).
8
+ #
9
+ class Circle < Actor
10
+ ##
11
+ # How many vertices to use when rendering circles. More vertices means more accurate rendering at the cost of
12
+ # performance.
13
+ #
14
+ RENDER_VERTEX_COUNT = 16
15
+
16
+ attr_reader :radius
17
+
18
+ ##
19
+ # Creates a new Circle with initial radius of zero.
20
+ #
21
+ def initialize(window)
22
+ super(window)
23
+ @radius = 0
24
+ end
25
+
26
+ ##
27
+ # Sets this circle's radius. Radius must be a positive integer.
28
+ #
29
+ def radius=(val)
30
+ raise ArgumentError.new("Circle.radius cannot be negative") if val < 0
31
+ @radius = val
32
+ end
33
+
34
+ ##
35
+ # Returns the angle's corresponding unit vector times this circle's radius.
36
+ #
37
+ def get_point_at_angle(radians, out = nil)
38
+ raise ArgumentError.new("Expected Numeric, but received #{radians.inspect}!") unless radians.is_a?(Numeric)
39
+ out ||= Snow::Vec3.new
40
+ out.set(Math.cos(radians) * @radius, Math.sin(radians) * @radius, 0)
41
+ end
42
+
43
+ ##
44
+ # Returns true if the point is inside the Circle, false otherwise.
45
+ #
46
+ def is_point_in_bounds(point)
47
+ Collision.is_point_in_shape?(point, self)
48
+ end
49
+
50
+ private
51
+
52
+ # TODO: keep a cached, class-level list of local vertices that can be re-used during rendering
53
+
54
+ def render(matrix)
55
+ # TODO: store these vertices in a cached, class-level array (see above)
56
+ local_vertices = (0...RENDER_VERTEX_COUNT).map do |i|
57
+ get_point_at_angle(Math::PI * 2 * i / RENDER_VERTEX_COUNT)
58
+ end
59
+ # TODO: retain an array of vertices in memory; write transformed vertices to this array
60
+ global_vertices = local_vertices.map { |v| Transformable.transform_point(matrix, v) }
61
+
62
+ fill_polygon(global_vertices)
63
+ end
64
+ end
65
+ end
@@ -1,499 +1,499 @@
1
- require 'singleton'
2
-
3
- require_relative 'utils.rb'
4
-
5
- module Gosling
6
- ##
7
- # Very basic 2D collision detection. It is naive to where actors were during the last physics step or how fast they are
8
- # moving. But it does a fine job of detecting collisions between actors in their present state.
9
- #
10
- # Keep in mind that Actors and their subclasses each have their own unique shapes. Actors, by themselves, have no
11
- # shape and will never collide with anything. To see collisions in action, you'll need to use Circle, Polygon, or
12
- # something else that has an actual shape.
13
- #
14
-
15
- class Collision
16
- include Singleton
17
-
18
- COLLISION_TOLERANCE = 0.000001
19
-
20
- ##
21
- # Tests two Actors or child classes to see whether they overlap. Actors, having no shape, never overlap. Child
22
- # classes use appropriate algorithms based on their shape.
23
- #
24
- # Arguments:
25
- # - shapeA: an Actor
26
- # - shapeB: another Actor
27
- #
28
- # Returns:
29
- # - true if the actors' shapes overlap, false otherwise
30
- #
31
- def self.test(shapeA, shapeB)
32
- return false if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
33
-
34
- return false if shapeA === shapeB
35
-
36
- get_separation_axes(shapeA, shapeB)
37
-
38
- reset_projection_axis_tracking
39
- separation_axes.each do |axis|
40
- next if axis_already_projected?(axis)
41
- projectionA = project_onto_axis(shapeA, axis)
42
- projectionB = project_onto_axis(shapeB, axis)
43
- return false unless projections_overlap?(projectionA, projectionB)
44
- end
45
-
46
- return true
47
- end
48
-
49
- ##
50
- # Tests two Actors or child classes to see whether they overlap. This is similar to #test, but returns additional
51
- # information.
52
- #
53
- # Arguments:
54
- # - shapeA: an Actor
55
- # - shapeB: another Actor
56
- #
57
- # Returns a hash with the following key/value pairs:
58
- # - colliding: true if the Actors overlap; false otherwise
59
- # - overlap: if colliding, the smallest overlapping distance; nil otherwise
60
- # - penetration: if colliding, a vector representing how far shape B must move to be separated from (or merely
61
- # touching) shape A; nil otherwise
62
- #
63
- def self.get_collision_info(shapeA, shapeB, info = nil)
64
- if info
65
- info.clear
66
- else
67
- info = {}
68
- end
69
- info.merge!(actors: [shapeA, shapeB], colliding: false, overlap: nil, penetration: nil)
70
-
71
- return info if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
72
-
73
- return info if shapeA === shapeB
74
-
75
- get_separation_axes(shapeA, shapeB)
76
- return info if separation_axes.empty?
77
-
78
- smallest_overlap = nil
79
- smallest_axis = nil
80
- reset_projection_axis_tracking
81
- separation_axes.each do |axis|
82
- next if axis_already_projected?(axis)
83
- projectionA = project_onto_axis(shapeA, axis)
84
- projectionB = project_onto_axis(shapeB, axis)
85
- overlap = get_overlap(projectionA, projectionB)
86
- return info unless overlap && overlap > COLLISION_TOLERANCE
87
- if smallest_overlap.nil? || smallest_overlap > overlap
88
- smallest_overlap = overlap
89
- flip = (projectionA[0] + projectionA[1]) * 0.5 > (projectionB[0] + projectionB[1]) * 0.5
90
- smallest_axis = axis
91
- smallest_axis.negate! if flip
92
- end
93
- end
94
-
95
- info[:colliding] = true
96
- info[:overlap] = smallest_overlap
97
- info[:penetration] = smallest_axis.normalize * smallest_overlap
98
-
99
- info
100
- end
101
-
102
- ##
103
- # Tests a point in space to see whether it is inside the actor's shape or not.
104
- #
105
- # Arguments:
106
- # - point: a Snow::Vec3
107
- # - shape: an Actor
108
- #
109
- # Returns:
110
- # - true if the point is inside of the actor's shape, false otherwise
111
- #
112
- def self.is_point_in_shape?(point, shape)
113
- type_check(point, Snow::Vec3)
114
- type_check(shape, Actor)
115
-
116
- return false if shape.instance_of?(Actor)
117
-
118
- global_pos = nil
119
- centers_axis = nil
120
- global_vertices = nil
121
- if shape.instance_of?(Circle)
122
- unless @@global_position_cache.key?(shape)
123
- global_pos = VectorCache.instance.get
124
- shape.get_global_position(global_pos)
125
- end
126
- centers_axis = VectorCache.instance.get
127
- point.subtract(@@global_position_cache.fetch(shape, global_pos), centers_axis)
128
- next_separation_axis.set(centers_axis) if centers_axis && (centers_axis[0] != 0 || centers_axis[1] != 0)
129
- else
130
- unless @@global_vertices_cache.key?(shape)
131
- global_vertices = Array.new(shape.get_vertices.length) { VectorCache.instance.get }
132
- shape.get_global_vertices(global_vertices)
133
- end
134
- get_polygon_separation_axes(@@global_vertices_cache.fetch(shape, global_vertices))
135
- end
136
-
137
- reset_projection_axis_tracking
138
- separation_axes.each do |axis|
139
- next if axis_already_projected?(axis)
140
- shape_projection = project_onto_axis(shape, axis)
141
- point_projection = point.dot_product(axis)
142
- return false unless shape_projection.first <= point_projection && point_projection <= shape_projection.last
143
- end
144
-
145
- return true
146
- ensure
147
- VectorCache.instance.recycle(global_pos) if global_pos
148
- VectorCache.instance.recycle(centers_axis) if centers_axis
149
- global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices
150
- end
151
-
152
- @@collision_buffer = []
153
- @@global_position_cache = {}
154
- @@global_vertices_cache = {}
155
- @@global_transform_cache = {}
156
- @@buffer_iterator_a = nil
157
- @@buffer_iterator_b = nil
158
-
159
- ##
160
- # Adds one or more descendents of Actor to the collision testing buffer. The buffer's iterators will be reset to the
161
- # first potential collision in the buffer.
162
- #
163
- # When added to the buffer, important and expensive global-space collision values for each Actor - transform,
164
- # position, and any vertices - are calculated and cached for re-use. This ensures that expensive transform
165
- # calculations are only performed once per actor during each collision resolution step.
166
- #
167
- # If you modify a buffered actor's transforms in any way, you will need to update its cached values by calling
168
- # buffer_shapes again. Otherwise, it will continue to use stale and inaccurate transform information.
169
- #
170
- def self.buffer_shapes(actors)
171
- type_check(actors, Array)
172
- actors.each { |a| type_check(a, Actor) }
173
-
174
- reset_buffer_iterators
175
-
176
- shapes = actors.reject { |a| a.instance_of?(Actor) }
177
-
178
- @@collision_buffer = @@collision_buffer | shapes
179
- shapes.each do |shape|
180
- unless @@global_transform_cache.key?(shape)
181
- @@global_transform_cache[shape] = MatrixCache.instance.get
182
- end
183
- shape.get_global_transform(@@global_transform_cache[shape])
184
-
185
- unless @@global_position_cache.key?(shape)
186
- @@global_position_cache[shape] = VectorCache.instance.get
187
- end
188
- # TODO: can we calculate this position using the global transform we already have?
189
- @@global_position_cache[shape].set(shape.get_global_position)
190
-
191
- if shape.is_a?(Polygon)
192
- unless @@global_vertices_cache.key?(shape)
193
- @@global_vertices_cache[shape] = Array.new(shape.get_vertices.length) { VectorCache.instance.get }
194
- end
195
- # TODO: can we calculate these vertices using the global transform we already have?
196
- shape.get_global_vertices(@@global_vertices_cache[shape])
197
- end
198
- end
199
- end
200
-
201
- ##
202
- # Removes one or more descendents of Actor from the collision testing buffer. Any cached values for the actors
203
- # are discarded. The buffer's iterators will be reset to the first potential collision in the buffer.
204
- #
205
- def self.unbuffer_shapes(actors)
206
- type_check(actors, Array)
207
- actors.each { |a| type_check(a, Actor) }
208
-
209
- reset_buffer_iterators
210
-
211
- @@collision_buffer = @@collision_buffer - actors
212
- actors.each do |actor|
213
- if @@global_transform_cache.key?(actor)
214
- MatrixCache.instance.recycle(@@global_transform_cache[actor])
215
- @@global_transform_cache.delete(actor)
216
- end
217
-
218
- if @@global_position_cache.key?(actor)
219
- VectorCache.instance.recycle(@@global_position_cache[actor])
220
- @@global_position_cache.delete(actor)
221
- end
222
-
223
- if @@global_vertices_cache.key?(actor)
224
- @@global_vertices_cache[actor].each do |vertex|
225
- VectorCache.instance.recycle(vertex)
226
- end
227
- @@global_vertices_cache.delete(actor)
228
- end
229
- end
230
- end
231
-
232
- ##
233
- # Removes all actors from the collision testing buffer. See Collision.unbuffer_shapes.
234
- #
235
- def self.clear_buffer
236
- unbuffer_shapes(@@collision_buffer)
237
- end
238
-
239
- ##
240
- # Returns collision information for the next pair of actors in the collision buffer, or returns nil if all pairs in the
241
- # buffer have been tested. Advances the buffer's iterators to the next pair. See Collision.get_collision_info.
242
- #
243
- def self.next_collision_info
244
- reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
245
- return if iteration_complete?
246
-
247
- info = get_collision_info(@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b])
248
- skip_next_collision
249
- info
250
- end
251
-
252
- ##
253
- # Returns the pair of actors in the collision buffer that would be tested during the next call to
254
- # Collision.next_collision_info, or returns nil if all pairs in the buffer have been tested. Does not perform
255
- # collision testing or advance the buffer's iterators.
256
- #
257
- # One use of this method is to look at the two actors about to be tested and, using some custom and likely more
258
- # efficient logic, determine if it's worth bothering to collision test these actors at all. If not, the pair's collision test
259
- # can be skipped by calling Collision.skip_next_collision.
260
- #
261
- def self.peek_at_next_collision
262
- reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
263
- return if iteration_complete?
264
-
265
- [@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b]]
266
- end
267
-
268
- ##
269
- # Advances the collision buffer's iterators to the next pair of actors in the buffer without performing any collision
270
- # testing. By using this method in conjunction with Collision.peek_at_next_collision, it is possible to selectively
271
- # skip collision testing for pairs of actors that meet certain criteria.
272
- #
273
- def self.skip_next_collision
274
- reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
275
- return if iteration_complete?
276
-
277
- @@buffer_iterator_b += 1
278
- if @@buffer_iterator_b >= @@buffer_iterator_a
279
- @@buffer_iterator_b = 0
280
- @@buffer_iterator_a += 1
281
- end
282
- end
283
-
284
- private
285
-
286
- def self.iteration_complete?
287
- @@buffer_iterator_a >= @@collision_buffer.length
288
- end
289
-
290
- def self.reset_buffer_iterators
291
- @@buffer_iterator_a = 1
292
- @@buffer_iterator_b = 0
293
- end
294
-
295
- def self.get_normal(vector, out = nil)
296
- raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector[0] == 0 && vector[1] == 0
297
- out ||= Snow::Vec3.new
298
- out.set(-vector[1], vector[0], 0)
299
- end
300
-
301
- @@separation_axes = []
302
- @@separation_axis_count = 0
303
-
304
- def self.reset_separation_axes
305
- @@separation_axis_count = 0
306
- end
307
-
308
- def self.next_separation_axis
309
- axis = @@separation_axes[@@separation_axis_count] ||= Snow::Vec3.new
310
- @@separation_axis_count += 1
311
- axis
312
- end
313
-
314
- def self.separation_axes
315
- @@separation_axes[0...@@separation_axis_count]
316
- end
317
-
318
- @@gpsa_axis = Snow::Vec3.new
319
- def self.get_polygon_separation_axes(vertices)
320
- # TODO: special case for Rects - only return two axes to avoid duplicitous math
321
- vertices.each_index do |i|
322
- vertices[i].subtract(vertices[i - 1], @@gpsa_axis)
323
- if @@gpsa_axis[0] != 0 || @@gpsa_axis[1] != 0
324
- get_normal(@@gpsa_axis, @@gpsa_axis).normalize(next_separation_axis)
325
- end
326
- end
327
- nil
328
- end
329
-
330
- @@global_pos_a = nil
331
- @@global_pos_b = nil
332
- @@gcsa_axis = nil
333
- def self.get_circle_separation_axis(circleA, circleB)
334
- unless @@global_position_cache.key?(circleA)
335
- @@global_pos_a ||= Snow::Vec3.new
336
- circleA.get_global_position(@@global_pos_a)
337
- end
338
-
339
- unless @@global_position_cache.key?(circleB)
340
- @@global_pos_b ||= Snow::Vec3.new
341
- circleB.get_global_position(@@global_pos_b)
342
- end
343
-
344
- @@gcsa_axis ||= Snow::Vec3.new
345
- @@global_pos_a = @@global_position_cache.fetch(circleA, @@global_pos_a)
346
- @@global_pos_b = @@global_position_cache.fetch(circleB, @@global_pos_b)
347
- @@global_pos_b.subtract(@@global_pos_a, @@gcsa_axis)
348
- if @@gcsa_axis[0] != 0 || @@gcsa_axis[1] != 0
349
- @@gcsa_axis.normalize(next_separation_axis)
350
- end
351
- nil
352
- end
353
-
354
- def self.get_separation_axes(shapeA, shapeB)
355
- unless shapeA.is_a?(Actor) && !shapeA.instance_of?(Actor)
356
- raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeA.inspect}!")
357
- end
358
-
359
- unless shapeB.is_a?(Actor) && !shapeB.instance_of?(Actor)
360
- raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeB.inspect}!")
361
- end
362
-
363
- reset_separation_axes
364
- global_vertices = nil
365
-
366
- unless shapeA.instance_of?(Circle)
367
- unless @@global_vertices_cache.key?(shapeA)
368
- global_vertices = Array.new(shapeA.get_vertices.length) { VectorCache.instance.get }
369
- shapeA.get_global_vertices(global_vertices)
370
- end
371
- get_polygon_separation_axes(@@global_vertices_cache.fetch(shapeA, global_vertices))
372
- end
373
-
374
- unless shapeB.instance_of?(Circle)
375
- unless @@global_vertices_cache.key?(shapeB)
376
- global_vertices ||= []
377
- (shapeB.get_vertices.length - global_vertices.length).times do
378
- global_vertices.push(VectorCache.instance.get)
379
- end
380
- (global_vertices.length - shapeB.get_vertices.length).times do
381
- VectorCache.instance.recycle(global_vertices.pop)
382
- end
383
- shapeB.get_global_vertices(global_vertices)
384
- end
385
- get_polygon_separation_axes(@@global_vertices_cache.fetch(shapeB, global_vertices))
386
- end
387
-
388
- if shapeA.instance_of?(Circle) || shapeB.instance_of?(Circle)
389
- get_circle_separation_axis(shapeA, shapeB)
390
- end
391
-
392
- nil
393
- ensure
394
- global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices
395
- end
396
-
397
- @@poa_zero_z_axis = nil
398
- @@poa_local_axis = nil
399
- @@poa_intersection = nil
400
- @@poa_global_tf = nil
401
- @@poa_global_tf_inverse = nil
402
- def self.get_circle_vertices_by_axis(shape, axis)
403
- unless @@global_transform_cache.key?(shape)
404
- @@poa_global_tf ||= Snow::Mat3.new
405
- shape.get_global_transform(@@poa_global_tf)
406
- end
407
-
408
- @@poa_zero_z_axis ||= Snow::Vec3.new
409
- @@poa_zero_z_axis.set(axis[0], axis[1], 0)
410
-
411
- @@poa_global_tf_inverse ||= Snow::Mat3.new
412
- @@global_transform_cache.fetch(shape, @@poa_global_tf).inverse(@@poa_global_tf_inverse)
413
-
414
- @@poa_local_axis ||= Snow::Vec3.new
415
- @@poa_global_tf_inverse.multiply(@@poa_zero_z_axis, @@poa_local_axis)
416
-
417
- @@poa_intersection ||= Snow::Vec3.new
418
- shape.get_point_at_angle(Math.atan2(@@poa_local_axis[1], @@poa_local_axis[0]), @@poa_intersection)
419
-
420
- Transformable.transform_point(@@global_transform_cache.fetch(shape, @@poa_global_tf), @@poa_intersection, next_global_vertex)
421
-
422
- @@poa_intersection.negate!
423
- Transformable.transform_point(@@global_transform_cache.fetch(shape, @@poa_global_tf), @@poa_intersection, next_global_vertex)
424
- end
425
-
426
- @@global_vertices = nil
427
- @@global_vertices_count = 0
428
-
429
- def self.reset_global_vertices
430
- @@global_vertices ||= []
431
- @@global_vertices_count = 0
432
- end
433
-
434
- def self.next_global_vertex
435
- vertex = @@global_vertices[@@global_vertices_count] ||= Snow::Vec3.new
436
- @@global_vertices_count += 1
437
- vertex
438
- end
439
-
440
- @@projected_axes = nil
441
-
442
- def self.reset_projection_axis_tracking
443
- @@projected_axes ||= {}
444
- @@projected_axes.clear
445
- end
446
-
447
- def self.axis_already_projected?(axis)
448
- key = axis.to_s
449
- return true if @@projected_axes.key?(key)
450
- @@projected_axes[key] = nil
451
- end
452
-
453
- def self.project_onto_axis(shape, axis, out = nil)
454
- unless @@global_vertices_cache.key?(shape)
455
- reset_global_vertices
456
- if shape.instance_of?(Circle)
457
- get_circle_vertices_by_axis(shape, axis)
458
- else
459
- shape.get_global_vertices(@@global_vertices)
460
- @@global_vertices_count = shape.get_vertices.length
461
- end
462
- end
463
-
464
- min = nil
465
- max = nil
466
- @@global_vertices_cache.fetch(shape, @@global_vertices[0...@@global_vertices_count]).each do |vertex|
467
- projection = vertex.dot_product(axis)
468
- if min.nil?
469
- min = projection
470
- max = projection
471
- else
472
- min = projection if projection < min
473
- max = projection if projection > max
474
- end
475
- end
476
- out ||= []
477
- out[1] = max
478
- out[0] = min
479
- out
480
- end
481
-
482
- def self.projections_overlap?(a, b)
483
- overlap = get_overlap(a, b)
484
- overlap != nil && overlap > COLLISION_TOLERANCE
485
- end
486
-
487
- def self.get_overlap(a, b)
488
- raise ArgumentError.new("Projection array must be length 2, not #{a.inspect}!") unless a.length == 2
489
- raise ArgumentError.new("Projection array must be length 2, not #{b.inspect}!") unless b.length == 2
490
- a.sort! if a[0] > a[1]
491
- b.sort! if b[0] > b[1]
492
- return b[1] - b[0] if a[0] <= b[0] && b[1] <= a[1]
493
- return a[1] - a[0] if b[0] <= a[0] && a[1] <= b[1]
494
- return a[1] - b[0] if a[0] <= b[0] && b[0] <= a[1]
495
- return b[1] - a[0] if b[0] <= a[0] && a[0] <= b[1]
496
- nil
497
- end
498
- end
499
- end
1
+ require 'singleton'
2
+
3
+ require_relative 'utils.rb'
4
+
5
+ module Gosling
6
+ ##
7
+ # Very basic 2D collision detection. It is naive to where actors were during the last physics step or how fast they are
8
+ # moving. But it does a fine job of detecting collisions between actors in their present state.
9
+ #
10
+ # Keep in mind that Actors and their subclasses each have their own unique shapes. Actors, by themselves, have no
11
+ # shape and will never collide with anything. To see collisions in action, you'll need to use Circle, Polygon, or
12
+ # something else that has an actual shape.
13
+ #
14
+
15
+ class Collision
16
+ include Singleton
17
+
18
+ COLLISION_TOLERANCE = 0.000001
19
+
20
+ ##
21
+ # Tests two Actors or child classes to see whether they overlap. Actors, having no shape, never overlap. Child
22
+ # classes use appropriate algorithms based on their shape.
23
+ #
24
+ # Arguments:
25
+ # - shapeA: an Actor
26
+ # - shapeB: another Actor
27
+ #
28
+ # Returns:
29
+ # - true if the actors' shapes overlap, false otherwise
30
+ #
31
+ def self.test(shapeA, shapeB)
32
+ return false if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
33
+
34
+ return false if shapeA === shapeB
35
+
36
+ get_separation_axes(shapeA, shapeB)
37
+
38
+ reset_projection_axis_tracking
39
+ separation_axes.each do |axis|
40
+ next if axis_already_projected?(axis)
41
+ projectionA = project_onto_axis(shapeA, axis)
42
+ projectionB = project_onto_axis(shapeB, axis)
43
+ return false unless projections_overlap?(projectionA, projectionB)
44
+ end
45
+
46
+ return true
47
+ end
48
+
49
+ ##
50
+ # Tests two Actors or child classes to see whether they overlap. This is similar to #test, but returns additional
51
+ # information.
52
+ #
53
+ # Arguments:
54
+ # - shapeA: an Actor
55
+ # - shapeB: another Actor
56
+ #
57
+ # Returns a hash with the following key/value pairs:
58
+ # - colliding: true if the Actors overlap; false otherwise
59
+ # - overlap: if colliding, the smallest overlapping distance; nil otherwise
60
+ # - penetration: if colliding, a vector representing how far shape B must move to be separated from (or merely
61
+ # touching) shape A; nil otherwise
62
+ #
63
+ def self.get_collision_info(shapeA, shapeB, info = nil)
64
+ if info
65
+ info.clear
66
+ else
67
+ info = {}
68
+ end
69
+ info.merge!(actors: [shapeA, shapeB], colliding: false, overlap: nil, penetration: nil)
70
+
71
+ return info if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
72
+
73
+ return info if shapeA === shapeB
74
+
75
+ get_separation_axes(shapeA, shapeB)
76
+ return info if separation_axes.empty?
77
+
78
+ smallest_overlap = nil
79
+ smallest_axis = nil
80
+ reset_projection_axis_tracking
81
+ separation_axes.each do |axis|
82
+ next if axis_already_projected?(axis)
83
+ projectionA = project_onto_axis(shapeA, axis)
84
+ projectionB = project_onto_axis(shapeB, axis)
85
+ overlap = get_overlap(projectionA, projectionB)
86
+ return info unless overlap && overlap > COLLISION_TOLERANCE
87
+ if smallest_overlap.nil? || smallest_overlap > overlap
88
+ smallest_overlap = overlap
89
+ flip = (projectionA[0] + projectionA[1]) * 0.5 > (projectionB[0] + projectionB[1]) * 0.5
90
+ smallest_axis = axis
91
+ smallest_axis.negate! if flip
92
+ end
93
+ end
94
+
95
+ info[:colliding] = true
96
+ info[:overlap] = smallest_overlap
97
+ info[:penetration] = smallest_axis.normalize * smallest_overlap
98
+
99
+ info
100
+ end
101
+
102
+ ##
103
+ # Tests a point in space to see whether it is inside the actor's shape or not.
104
+ #
105
+ # Arguments:
106
+ # - point: a Snow::Vec3
107
+ # - shape: an Actor
108
+ #
109
+ # Returns:
110
+ # - true if the point is inside of the actor's shape, false otherwise
111
+ #
112
+ def self.is_point_in_shape?(point, shape)
113
+ type_check(point, Snow::Vec3)
114
+ type_check(shape, Actor)
115
+
116
+ return false if shape.instance_of?(Actor)
117
+
118
+ global_pos = nil
119
+ centers_axis = nil
120
+ global_vertices = nil
121
+ if shape.instance_of?(Circle)
122
+ unless @@global_position_cache.key?(shape)
123
+ global_pos = VectorCache.instance.get
124
+ shape.get_global_position(global_pos)
125
+ end
126
+ centers_axis = VectorCache.instance.get
127
+ point.subtract(@@global_position_cache.fetch(shape, global_pos), centers_axis)
128
+ next_separation_axis.set(centers_axis) if centers_axis && (centers_axis[0] != 0 || centers_axis[1] != 0)
129
+ else
130
+ unless @@global_vertices_cache.key?(shape)
131
+ global_vertices = Array.new(shape.get_vertices.length) { VectorCache.instance.get }
132
+ shape.get_global_vertices(global_vertices)
133
+ end
134
+ get_polygon_separation_axes(@@global_vertices_cache.fetch(shape, global_vertices))
135
+ end
136
+
137
+ reset_projection_axis_tracking
138
+ separation_axes.each do |axis|
139
+ next if axis_already_projected?(axis)
140
+ shape_projection = project_onto_axis(shape, axis)
141
+ point_projection = point.dot_product(axis)
142
+ return false unless shape_projection.first <= point_projection && point_projection <= shape_projection.last
143
+ end
144
+
145
+ return true
146
+ ensure
147
+ VectorCache.instance.recycle(global_pos) if global_pos
148
+ VectorCache.instance.recycle(centers_axis) if centers_axis
149
+ global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices
150
+ end
151
+
152
+ @@collision_buffer = []
153
+ @@global_position_cache = {}
154
+ @@global_vertices_cache = {}
155
+ @@global_transform_cache = {}
156
+ @@buffer_iterator_a = nil
157
+ @@buffer_iterator_b = nil
158
+
159
+ ##
160
+ # Adds one or more descendents of Actor to the collision testing buffer. The buffer's iterators will be reset to the
161
+ # first potential collision in the buffer.
162
+ #
163
+ # When added to the buffer, important and expensive global-space collision values for each Actor - transform,
164
+ # position, and any vertices - are calculated and cached for re-use. This ensures that expensive transform
165
+ # calculations are only performed once per actor during each collision resolution step.
166
+ #
167
+ # If you modify a buffered actor's transforms in any way, you will need to update its cached values by calling
168
+ # buffer_shapes again. Otherwise, it will continue to use stale and inaccurate transform information.
169
+ #
170
+ def self.buffer_shapes(actors)
171
+ type_check(actors, Array)
172
+ actors.each { |a| type_check(a, Actor) }
173
+
174
+ reset_buffer_iterators
175
+
176
+ shapes = actors.reject { |a| a.instance_of?(Actor) }
177
+
178
+ @@collision_buffer = @@collision_buffer | shapes
179
+ shapes.each do |shape|
180
+ unless @@global_transform_cache.key?(shape)
181
+ @@global_transform_cache[shape] = MatrixCache.instance.get
182
+ end
183
+ shape.get_global_transform(@@global_transform_cache[shape])
184
+
185
+ unless @@global_position_cache.key?(shape)
186
+ @@global_position_cache[shape] = VectorCache.instance.get
187
+ end
188
+ # TODO: can we calculate this position using the global transform we already have?
189
+ @@global_position_cache[shape].set(shape.get_global_position)
190
+
191
+ if shape.is_a?(Polygon)
192
+ unless @@global_vertices_cache.key?(shape)
193
+ @@global_vertices_cache[shape] = Array.new(shape.get_vertices.length) { VectorCache.instance.get }
194
+ end
195
+ # TODO: can we calculate these vertices using the global transform we already have?
196
+ shape.get_global_vertices(@@global_vertices_cache[shape])
197
+ end
198
+ end
199
+ end
200
+
201
+ ##
202
+ # Removes one or more descendents of Actor from the collision testing buffer. Any cached values for the actors
203
+ # are discarded. The buffer's iterators will be reset to the first potential collision in the buffer.
204
+ #
205
+ def self.unbuffer_shapes(actors)
206
+ type_check(actors, Array)
207
+ actors.each { |a| type_check(a, Actor) }
208
+
209
+ reset_buffer_iterators
210
+
211
+ @@collision_buffer = @@collision_buffer - actors
212
+ actors.each do |actor|
213
+ if @@global_transform_cache.key?(actor)
214
+ MatrixCache.instance.recycle(@@global_transform_cache[actor])
215
+ @@global_transform_cache.delete(actor)
216
+ end
217
+
218
+ if @@global_position_cache.key?(actor)
219
+ VectorCache.instance.recycle(@@global_position_cache[actor])
220
+ @@global_position_cache.delete(actor)
221
+ end
222
+
223
+ if @@global_vertices_cache.key?(actor)
224
+ @@global_vertices_cache[actor].each do |vertex|
225
+ VectorCache.instance.recycle(vertex)
226
+ end
227
+ @@global_vertices_cache.delete(actor)
228
+ end
229
+ end
230
+ end
231
+
232
+ ##
233
+ # Removes all actors from the collision testing buffer. See Collision.unbuffer_shapes.
234
+ #
235
+ def self.clear_buffer
236
+ unbuffer_shapes(@@collision_buffer)
237
+ end
238
+
239
+ ##
240
+ # Returns collision information for the next pair of actors in the collision buffer, or returns nil if all pairs in the
241
+ # buffer have been tested. Advances the buffer's iterators to the next pair. See Collision.get_collision_info.
242
+ #
243
+ def self.next_collision_info
244
+ reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
245
+ return if iteration_complete?
246
+
247
+ info = get_collision_info(@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b])
248
+ skip_next_collision
249
+ info
250
+ end
251
+
252
+ ##
253
+ # Returns the pair of actors in the collision buffer that would be tested during the next call to
254
+ # Collision.next_collision_info, or returns nil if all pairs in the buffer have been tested. Does not perform
255
+ # collision testing or advance the buffer's iterators.
256
+ #
257
+ # One use of this method is to look at the two actors about to be tested and, using some custom and likely more
258
+ # efficient logic, determine if it's worth bothering to collision test these actors at all. If not, the pair's collision test
259
+ # can be skipped by calling Collision.skip_next_collision.
260
+ #
261
+ def self.peek_at_next_collision
262
+ reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
263
+ return if iteration_complete?
264
+
265
+ [@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b]]
266
+ end
267
+
268
+ ##
269
+ # Advances the collision buffer's iterators to the next pair of actors in the buffer without performing any collision
270
+ # testing. By using this method in conjunction with Collision.peek_at_next_collision, it is possible to selectively
271
+ # skip collision testing for pairs of actors that meet certain criteria.
272
+ #
273
+ def self.skip_next_collision
274
+ reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
275
+ return if iteration_complete?
276
+
277
+ @@buffer_iterator_b += 1
278
+ if @@buffer_iterator_b >= @@buffer_iterator_a
279
+ @@buffer_iterator_b = 0
280
+ @@buffer_iterator_a += 1
281
+ end
282
+ end
283
+
284
+ private
285
+
286
+ def self.iteration_complete?
287
+ @@buffer_iterator_a >= @@collision_buffer.length
288
+ end
289
+
290
+ def self.reset_buffer_iterators
291
+ @@buffer_iterator_a = 1
292
+ @@buffer_iterator_b = 0
293
+ end
294
+
295
+ def self.get_normal(vector, out = nil)
296
+ raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector[0] == 0 && vector[1] == 0
297
+ out ||= Snow::Vec3.new
298
+ out.set(-vector[1], vector[0], 0)
299
+ end
300
+
301
+ @@separation_axes = []
302
+ @@separation_axis_count = 0
303
+
304
+ def self.reset_separation_axes
305
+ @@separation_axis_count = 0
306
+ end
307
+
308
+ def self.next_separation_axis
309
+ axis = @@separation_axes[@@separation_axis_count] ||= Snow::Vec3.new
310
+ @@separation_axis_count += 1
311
+ axis
312
+ end
313
+
314
+ def self.separation_axes
315
+ @@separation_axes[0...@@separation_axis_count]
316
+ end
317
+
318
+ @@gpsa_axis = Snow::Vec3.new
319
+ def self.get_polygon_separation_axes(vertices)
320
+ # TODO: special case for Rects - only return two axes to avoid duplicitous math
321
+ vertices.each_index do |i|
322
+ vertices[i].subtract(vertices[i - 1], @@gpsa_axis)
323
+ if @@gpsa_axis[0] != 0 || @@gpsa_axis[1] != 0
324
+ get_normal(@@gpsa_axis, @@gpsa_axis).normalize(next_separation_axis)
325
+ end
326
+ end
327
+ nil
328
+ end
329
+
330
+ @@global_pos_a = nil
331
+ @@global_pos_b = nil
332
+ @@gcsa_axis = nil
333
+ def self.get_circle_separation_axis(circleA, circleB)
334
+ unless @@global_position_cache.key?(circleA)
335
+ @@global_pos_a ||= Snow::Vec3.new
336
+ circleA.get_global_position(@@global_pos_a)
337
+ end
338
+
339
+ unless @@global_position_cache.key?(circleB)
340
+ @@global_pos_b ||= Snow::Vec3.new
341
+ circleB.get_global_position(@@global_pos_b)
342
+ end
343
+
344
+ @@gcsa_axis ||= Snow::Vec3.new
345
+ @@global_pos_a = @@global_position_cache.fetch(circleA, @@global_pos_a)
346
+ @@global_pos_b = @@global_position_cache.fetch(circleB, @@global_pos_b)
347
+ @@global_pos_b.subtract(@@global_pos_a, @@gcsa_axis)
348
+ if @@gcsa_axis[0] != 0 || @@gcsa_axis[1] != 0
349
+ @@gcsa_axis.normalize(next_separation_axis)
350
+ end
351
+ nil
352
+ end
353
+
354
+ def self.get_separation_axes(shapeA, shapeB)
355
+ unless shapeA.is_a?(Actor) && !shapeA.instance_of?(Actor)
356
+ raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeA.inspect}!")
357
+ end
358
+
359
+ unless shapeB.is_a?(Actor) && !shapeB.instance_of?(Actor)
360
+ raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeB.inspect}!")
361
+ end
362
+
363
+ reset_separation_axes
364
+ global_vertices = nil
365
+
366
+ unless shapeA.instance_of?(Circle)
367
+ unless @@global_vertices_cache.key?(shapeA)
368
+ global_vertices = Array.new(shapeA.get_vertices.length) { VectorCache.instance.get }
369
+ shapeA.get_global_vertices(global_vertices)
370
+ end
371
+ get_polygon_separation_axes(@@global_vertices_cache.fetch(shapeA, global_vertices))
372
+ end
373
+
374
+ unless shapeB.instance_of?(Circle)
375
+ unless @@global_vertices_cache.key?(shapeB)
376
+ global_vertices ||= []
377
+ (shapeB.get_vertices.length - global_vertices.length).times do
378
+ global_vertices.push(VectorCache.instance.get)
379
+ end
380
+ (global_vertices.length - shapeB.get_vertices.length).times do
381
+ VectorCache.instance.recycle(global_vertices.pop)
382
+ end
383
+ shapeB.get_global_vertices(global_vertices)
384
+ end
385
+ get_polygon_separation_axes(@@global_vertices_cache.fetch(shapeB, global_vertices))
386
+ end
387
+
388
+ if shapeA.instance_of?(Circle) || shapeB.instance_of?(Circle)
389
+ get_circle_separation_axis(shapeA, shapeB)
390
+ end
391
+
392
+ nil
393
+ ensure
394
+ global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices
395
+ end
396
+
397
+ @@poa_zero_z_axis = nil
398
+ @@poa_local_axis = nil
399
+ @@poa_intersection = nil
400
+ @@poa_global_tf = nil
401
+ @@poa_global_tf_inverse = nil
402
+ def self.get_circle_vertices_by_axis(shape, axis)
403
+ unless @@global_transform_cache.key?(shape)
404
+ @@poa_global_tf ||= Snow::Mat3.new
405
+ shape.get_global_transform(@@poa_global_tf)
406
+ end
407
+
408
+ @@poa_zero_z_axis ||= Snow::Vec3.new
409
+ @@poa_zero_z_axis.set(axis[0], axis[1], 0)
410
+
411
+ @@poa_global_tf_inverse ||= Snow::Mat3.new
412
+ @@global_transform_cache.fetch(shape, @@poa_global_tf).inverse(@@poa_global_tf_inverse)
413
+
414
+ @@poa_local_axis ||= Snow::Vec3.new
415
+ @@poa_global_tf_inverse.multiply(@@poa_zero_z_axis, @@poa_local_axis)
416
+
417
+ @@poa_intersection ||= Snow::Vec3.new
418
+ shape.get_point_at_angle(Math.atan2(@@poa_local_axis[1], @@poa_local_axis[0]), @@poa_intersection)
419
+
420
+ Transformable.transform_point(@@global_transform_cache.fetch(shape, @@poa_global_tf), @@poa_intersection, next_global_vertex)
421
+
422
+ @@poa_intersection.negate!
423
+ Transformable.transform_point(@@global_transform_cache.fetch(shape, @@poa_global_tf), @@poa_intersection, next_global_vertex)
424
+ end
425
+
426
+ @@global_vertices = nil
427
+ @@global_vertices_count = 0
428
+
429
+ def self.reset_global_vertices
430
+ @@global_vertices ||= []
431
+ @@global_vertices_count = 0
432
+ end
433
+
434
+ def self.next_global_vertex
435
+ vertex = @@global_vertices[@@global_vertices_count] ||= Snow::Vec3.new
436
+ @@global_vertices_count += 1
437
+ vertex
438
+ end
439
+
440
+ @@projected_axes = nil
441
+
442
+ def self.reset_projection_axis_tracking
443
+ @@projected_axes ||= {}
444
+ @@projected_axes.clear
445
+ end
446
+
447
+ def self.axis_already_projected?(axis)
448
+ key = axis.to_s
449
+ return true if @@projected_axes.key?(key)
450
+ @@projected_axes[key] = nil
451
+ end
452
+
453
+ def self.project_onto_axis(shape, axis, out = nil)
454
+ unless @@global_vertices_cache.key?(shape)
455
+ reset_global_vertices
456
+ if shape.instance_of?(Circle)
457
+ get_circle_vertices_by_axis(shape, axis)
458
+ else
459
+ shape.get_global_vertices(@@global_vertices)
460
+ @@global_vertices_count = shape.get_vertices.length
461
+ end
462
+ end
463
+
464
+ min = nil
465
+ max = nil
466
+ @@global_vertices_cache.fetch(shape, @@global_vertices[0...@@global_vertices_count]).each do |vertex|
467
+ projection = vertex.dot_product(axis)
468
+ if min.nil?
469
+ min = projection
470
+ max = projection
471
+ else
472
+ min = projection if projection < min
473
+ max = projection if projection > max
474
+ end
475
+ end
476
+ out ||= []
477
+ out[1] = max
478
+ out[0] = min
479
+ out
480
+ end
481
+
482
+ def self.projections_overlap?(a, b)
483
+ overlap = get_overlap(a, b)
484
+ overlap != nil && overlap > COLLISION_TOLERANCE
485
+ end
486
+
487
+ def self.get_overlap(a, b)
488
+ raise ArgumentError.new("Projection array must be length 2, not #{a.inspect}!") unless a.length == 2
489
+ raise ArgumentError.new("Projection array must be length 2, not #{b.inspect}!") unless b.length == 2
490
+ a.sort! if a[0] > a[1]
491
+ b.sort! if b[0] > b[1]
492
+ return b[1] - b[0] if a[0] <= b[0] && b[1] <= a[1]
493
+ return a[1] - a[0] if b[0] <= a[0] && a[1] <= b[1]
494
+ return a[1] - b[0] if a[0] <= b[0] && b[0] <= a[1]
495
+ return b[1] - a[0] if b[0] <= a[0] && a[0] <= b[1]
496
+ nil
497
+ end
498
+ end
499
+ end