Class: Gosling::Collision
- Includes:
- Singleton
- Defined in:
- lib/gosling/collision.rb
Overview
Very basic 2D collision detection. It is naive to where actors were during the last physics step or how fast they are moving. But it does a fine job of detecting collisions between actors in their present state.
Keep in mind that Actors and their subclasses each have their own unique shapes. Actors, by themselves, have no shape and will never collide with anything. To see collisions in action, you’ll need to use Circle, Polygon, or something else that has an actual shape.
Constant Summary collapse
- COLLISION_TOLERANCE =
0.000001
- @@collision_buffer =
[]
- @@global_position_cache =
{}
- @@global_vertices_cache =
{}
- @@global_transform_cache =
{}
- @@buffer_iterator_a =
nil
- @@buffer_iterator_b =
nil
Class Method Summary collapse
-
.buffer_shapes(actors) ⇒ Object
Adds one or more descendents of Actor to the collision testing buffer.
-
.clear_buffer ⇒ Object
Removes all actors from the collision testing buffer.
-
.get_collision_info(shapeA, shapeB, info = nil) ⇒ Object
Tests two Actors or child classes to see whether they overlap.
-
.is_point_in_shape?(point, shape) ⇒ Boolean
Tests a point in space to see whether it is inside the actor’s shape or not.
-
.next_collision_info ⇒ Object
Returns collision information for the next pair of actors in the collision buffer, or returns nil if all pairs in the buffer have been tested.
-
.peek_at_next_collision ⇒ Object
Returns the pair of actors in the collision buffer that would be tested during the next call to Collision.next_collision_info, or returns nil if all pairs in the buffer have been tested.
-
.skip_next_collision ⇒ Object
Advances the collision buffer’s iterators to the next pair of actors in the buffer without performing any collision testing.
-
.test(shapeA, shapeB) ⇒ Object
Tests two Actors or child classes to see whether they overlap.
-
.unbuffer_shapes(actors) ⇒ Object
Removes one or more descendents of Actor from the collision testing buffer.
Class Method Details
.buffer_shapes(actors) ⇒ Object
Adds one or more descendents of Actor to the collision testing buffer. The buffer’s iterators will be reset to the first potential collision in the buffer.
When added to the buffer, important and expensive global-space collision values for each Actor - transform, position, and any vertices - are calculated and cached for re-use. This ensures that expensive transform calculations are only performed once per actor during each collision resolution step.
If you modify a buffered actor’s transforms in any way, you will need to update its cached values by calling buffer_shapes again. Otherwise, it will continue to use stale and inaccurate transform information.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/gosling/collision.rb', line 170 def self.buffer_shapes(actors) type_check(actors, Array) actors.each { |a| type_check(a, Actor) } reset_buffer_iterators shapes = actors.reject { |a| a.instance_of?(Actor) } @@collision_buffer = @@collision_buffer | shapes shapes.each do |shape| unless @@global_transform_cache.key?(shape) @@global_transform_cache[shape] = MatrixCache.instance.get end shape.get_global_transform(@@global_transform_cache[shape]) unless @@global_position_cache.key?(shape) @@global_position_cache[shape] = VectorCache.instance.get end # TODO: can we calculate this position using the global transform we already have? @@global_position_cache[shape].set(shape.get_global_position) if shape.is_a?(Polygon) unless @@global_vertices_cache.key?(shape) @@global_vertices_cache[shape] = Array.new(shape.get_vertices.length) { VectorCache.instance.get } end # TODO: can we calculate these vertices using the global transform we already have? shape.get_global_vertices(@@global_vertices_cache[shape]) end end end |
.clear_buffer ⇒ Object
Removes all actors from the collision testing buffer. See Collision.unbuffer_shapes.
235 236 237 |
# File 'lib/gosling/collision.rb', line 235 def self.clear_buffer unbuffer_shapes(@@collision_buffer) end |
.get_collision_info(shapeA, shapeB, info = nil) ⇒ Object
Tests two Actors or child classes to see whether they overlap. This is similar to #test, but returns additional information.
Arguments:
-
shapeA: an Actor
-
shapeB: another Actor
Returns a hash with the following key/value pairs:
-
colliding: true if the Actors overlap; false otherwise
-
overlap: if colliding, the smallest overlapping distance; nil otherwise
-
penetration: if colliding, a vector representing how far shape B must move to be separated from (or merely
touching) shape A; nil otherwise
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/gosling/collision.rb', line 63 def self.get_collision_info(shapeA, shapeB, info = nil) if info info.clear else info = {} end info.merge!(actors: [shapeA, shapeB], colliding: false, overlap: nil, penetration: nil) return info if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor) return info if shapeA === shapeB get_separation_axes(shapeA, shapeB) return info if separation_axes.empty? smallest_overlap = nil smallest_axis = nil reset_projection_axis_tracking separation_axes.each do |axis| next if axis_already_projected?(axis) projectionA = project_onto_axis(shapeA, axis) projectionB = project_onto_axis(shapeB, axis) overlap = get_overlap(projectionA, projectionB) return info unless overlap && overlap > COLLISION_TOLERANCE if smallest_overlap.nil? || smallest_overlap > overlap smallest_overlap = overlap flip = (projectionA[0] + projectionA[1]) * 0.5 > (projectionB[0] + projectionB[1]) * 0.5 smallest_axis = axis smallest_axis.negate! if flip end end info[:colliding] = true info[:overlap] = smallest_overlap info[:penetration] = smallest_axis.normalize * smallest_overlap info end |
.is_point_in_shape?(point, shape) ⇒ Boolean
Tests a point in space to see whether it is inside the actor’s shape or not.
Arguments:
-
point: a Snow::Vec3
-
shape: an Actor
Returns:
-
true if the point is inside of the actor’s shape, false otherwise
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/gosling/collision.rb', line 112 def self.is_point_in_shape?(point, shape) type_check(point, Snow::Vec3) type_check(shape, Actor) return false if shape.instance_of?(Actor) global_pos = nil centers_axis = nil global_vertices = nil if shape.instance_of?(Circle) unless @@global_position_cache.key?(shape) global_pos = VectorCache.instance.get shape.get_global_position(global_pos) end centers_axis = VectorCache.instance.get point.subtract(@@global_position_cache.fetch(shape, global_pos), centers_axis) next_separation_axis.set(centers_axis) if centers_axis && (centers_axis[0] != 0 || centers_axis[1] != 0) else unless @@global_vertices_cache.key?(shape) global_vertices = Array.new(shape.get_vertices.length) { VectorCache.instance.get } shape.get_global_vertices(global_vertices) end get_polygon_separation_axes(@@global_vertices_cache.fetch(shape, global_vertices)) end reset_projection_axis_tracking separation_axes.each do |axis| next if axis_already_projected?(axis) shape_projection = project_onto_axis(shape, axis) point_projection = point.dot_product(axis) return false unless shape_projection.first <= point_projection && point_projection <= shape_projection.last end return true ensure VectorCache.instance.recycle(global_pos) if global_pos VectorCache.instance.recycle(centers_axis) if centers_axis global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices end |
.next_collision_info ⇒ Object
Returns collision information for the next pair of actors in the collision buffer, or returns nil if all pairs in the buffer have been tested. Advances the buffer’s iterators to the next pair. See Collision.get_collision_info.
243 244 245 246 247 248 249 250 |
# File 'lib/gosling/collision.rb', line 243 def self.next_collision_info reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil? return if iteration_complete? info = get_collision_info(@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b]) skip_next_collision info end |
.peek_at_next_collision ⇒ Object
Returns the pair of actors in the collision buffer that would be tested during the next call to Collision.next_collision_info, or returns nil if all pairs in the buffer have been tested. Does not perform collision testing or advance the buffer’s iterators.
One use of this method is to look at the two actors about to be tested and, using some custom and likely more efficient logic, determine if it’s worth bothering to collision test these actors at all. If not, the pair’s collision test can be skipped by calling Collision.skip_next_collision.
261 262 263 264 265 266 |
# File 'lib/gosling/collision.rb', line 261 def self.peek_at_next_collision reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil? return if iteration_complete? [@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b]] end |
.skip_next_collision ⇒ Object
Advances the collision buffer’s iterators to the next pair of actors in the buffer without performing any collision testing. By using this method in conjunction with Collision.peek_at_next_collision, it is possible to selectively skip collision testing for pairs of actors that meet certain criteria.
273 274 275 276 277 278 279 280 281 282 |
# File 'lib/gosling/collision.rb', line 273 def self.skip_next_collision reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil? return if iteration_complete? @@buffer_iterator_b += 1 if @@buffer_iterator_b >= @@buffer_iterator_a @@buffer_iterator_b = 0 @@buffer_iterator_a += 1 end end |
.test(shapeA, shapeB) ⇒ Object
Tests two Actors or child classes to see whether they overlap. Actors, having no shape, never overlap. Child classes use appropriate algorithms based on their shape.
Arguments:
-
shapeA: an Actor
-
shapeB: another Actor
Returns:
-
true if the actors’ shapes overlap, false otherwise
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/gosling/collision.rb', line 31 def self.test(shapeA, shapeB) return false if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor) return false if shapeA === shapeB get_separation_axes(shapeA, shapeB) reset_projection_axis_tracking separation_axes.each do |axis| next if axis_already_projected?(axis) projectionA = project_onto_axis(shapeA, axis) projectionB = project_onto_axis(shapeB, axis) return false unless projections_overlap?(projectionA, projectionB) end return true end |
.unbuffer_shapes(actors) ⇒ Object
Removes one or more descendents of Actor from the collision testing buffer. Any cached values for the actors are discarded. The buffer’s iterators will be reset to the first potential collision in the buffer.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/gosling/collision.rb', line 205 def self.unbuffer_shapes(actors) type_check(actors, Array) actors.each { |a| type_check(a, Actor) } reset_buffer_iterators @@collision_buffer = @@collision_buffer - actors actors.each do |actor| if @@global_transform_cache.key?(actor) MatrixCache.instance.recycle(@@global_transform_cache[actor]) @@global_transform_cache.delete(actor) end if @@global_position_cache.key?(actor) VectorCache.instance.recycle(@@global_position_cache[actor]) @@global_position_cache.delete(actor) end if @@global_vertices_cache.key?(actor) @@global_vertices_cache[actor].each do |vertex| VectorCache.instance.recycle(vertex) end @@global_vertices_cache.delete(actor) end end end |