Class: Gosling::Collision

Inherits:
Object show all
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

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_bufferObject

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

Returns:

  • (Boolean)


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_infoObject

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_collisionObject

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_collisionObject

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