gosling 2.3.0 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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