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.
- checksums.yaml +5 -5
- data/lib/gosling.rb +4 -4
- data/lib/gosling/actor.rb +328 -328
- data/lib/gosling/circle.rb +65 -65
- data/lib/gosling/collision.rb +499 -499
- data/lib/gosling/image_library.rb +24 -24
- data/lib/gosling/inheritance_error.rb +4 -4
- data/lib/gosling/matrix_cache.rb +23 -21
- data/lib/gosling/object_cache.rb +48 -48
- data/lib/gosling/patches.rb +42 -42
- data/lib/gosling/polygon.rb +122 -122
- data/lib/gosling/rect.rb +41 -41
- data/lib/gosling/sprite.rb +50 -50
- data/lib/gosling/transformable.rb +413 -413
- data/lib/gosling/utils.rb +20 -20
- data/lib/gosling/vector_cache.rb +21 -21
- data/lib/gosling/version.rb +7 -7
- data/spec/actor_spec.rb +627 -627
- data/spec/circle_spec.rb +46 -46
- data/spec/collision_spec.rb +1755 -1755
- data/spec/image_library_spec.rb +19 -19
- data/spec/matrix_cache_spec.rb +25 -25
- data/spec/object_cache_spec.rb +22 -22
- data/spec/polygon_spec.rb +284 -284
- data/spec/rect_spec.rb +86 -86
- data/spec/spec_helper.rb +3 -3
- data/spec/sprite_spec.rb +45 -45
- data/spec/transformable_spec.rb +479 -479
- data/spec/vector_cache_spec.rb +80 -80
- metadata +3 -4
data/lib/gosling/circle.rb
CHANGED
@@ -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
|
data/lib/gosling/collision.rb
CHANGED
@@ -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
|