1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
101
102
103
104
105
106
107
108
109
110
111
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// Copyright 2016 Matthew D. Michelotti
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Collider is a library for continuous 2D collision detection,
//! for use with game developement.
//!
//! Most game engines follow the approach of periodically updating the
//! positions of all shapes and checking for collisions at a frozen snapshot in time.
//! [Continuous collision detection](https://en.wikipedia.org/wiki/Collision_detection#A_posteriori_.28discrete.29_versus_a_priori_.28continuous.29),
//! on the other hand, means that the time of collision is determined very precisely,
//! and the user is not restricted to a fixed time-stepping method.
//! There are currently two kinds of shapes supported by Collider: circles and rectangles.
//! The user specifies the positions and velocites of these shapes, which
//! they can update at any time, and Collider will solve for the precise times of
//! collision and separation.
//!
//! There are certain advantages that continuous collision detection
//! holds over the traditional approach.
//! In a game engine, the position of a sprite may be updated to overlap a wall,
//! and in a traditional collision system there would need to be a post-correction
//! to make sure the sprite does not appear inside of the wall.
//! This is not needed with continuous collision detection, since
//! the precise time and location at which the sprite touches the wall is known.
//! Traditional collision detection may have an issue with "tunneling," in which a
//! fast small object runs into a narrow wall and collision detection misses it,
//! or two fast small objects fly right through each other and collision detection misses it.
//! This is also not a problem for contiuous collision detection.
//! It is also debatable that continuous collision detection may be
//! more efficient in certain circumstances,
//! since the hitboxes may be updated less frequently and still maintain a
//! smooth appearance over time.
//!
//! Collider may be built with the `noisy-floats` feature, which will use the `R64` and `N64`
//! types from the `noisy_float` crate in place of `f64` types.
//! In either case, it is ultimately the user's responsibility to ensure
//! that they do not do anything that will result in improper floating point overflow or NaN.
//! For instructions for building a crate with a conditional feature,
//! see http://doc.crates.io/specifying-dependencies.html#choosing-features.
//!
//! #Example
//! ```
//! use collider::{Collider, Hitbox, Event};
//! use collider::geom::{PlacedShape, Shape, vec2};
//!
//! let mut collider: Collider = Collider::new(4.0, 0.01);
//!
//! let mut hitbox = Hitbox::new(PlacedShape::new(vec2(-10.0, 0.0), Shape::new_square(2.0)));
//! hitbox.vel.pos = vec2(1.0, 0.0);
//! collider.add_hitbox(0, hitbox);
//!
//! let mut hitbox = Hitbox::new(PlacedShape::new(vec2(10.0, 0.0), Shape::new_square(2.0)));
//! hitbox.vel.pos = vec2(-1.0, 0.0);
//! collider.add_hitbox(1, hitbox);
//!
//! while collider.time() < 20.0 {
//!     let time = collider.next_time().min(20.0);
//!     collider.set_time(time);
//!     if let Some((event, id1, id2)) = collider.next() {
//!         println!("{:?} between hitbox {} and hitbox {} at time {}.", event, id1, id2, collider.time());
//!
//!         if event == Event::Collide {
//!             println!("Speed of collided hitboxes is halved.");
//!             for id in [id1, id2].iter().cloned() {
//!                 let mut hitbox = collider.get_hitbox(id);
//!                 hitbox.vel.pos *= 0.5;
//!                 collider.update_hitbox(id, hitbox);
//!             }
//!         }
//!     }
//! }
//!
//! //the above loop prints the following events:
//! //  Collide between hitbox 0 and hitbox 1 at time 9.
//! //  Speed of collided hitboxes is halved.
//! //  Separate between hitbox 0 and hitbox 1 at time 13.01.
//! ```

//TODO when Rust 1.13 is released, change N64/R64 type aliases in the public API to macros, so that docs will just say f64 when compiled without noisy-floats, and update docs

#[cfg(feature = "noisy-floats")]
extern crate noisy_float;
extern crate fnv;

mod float;
pub mod geom;
mod geom_ext;
mod util;
mod core;
mod index_rect;

pub use core::*;

#[cfg(test)]
mod tests {
    use std::f64;
    use std::mem;
    use float::*;
    use super::{Collider, Hitbox, Event};
    use geom::{PlacedShape, Shape, vec2_f};

    fn advance_to_event(collider: &mut Collider, time: N64) {
        advance(collider, time);
        assert!(collider.next_time() == collider.time());
    }

    fn advance(collider: &mut Collider, time: N64) {
        while collider.time() < time {
            assert!(collider.next() == None);
            let new_time = collider.next_time().min(time);
            collider.set_time(new_time);
        }
        assert!(collider.time() == time);
    }

    #[test]
    fn smoke_test() {
        let mut collider = Collider::new(r64(4.0), r64(0.25));
        
        let mut hitbox = Hitbox::new(PlacedShape::new(vec2_f(-10.0, 0.0), Shape::new_square(r64(2.0))));
        hitbox.vel.pos = vec2_f(1.0, 0.0);
        collider.add_hitbox(0, hitbox);
        
        let mut hitbox = Hitbox::new(PlacedShape::new(vec2_f(10.0, 0.0), Shape::new_circle(r64(2.0))));
        hitbox.vel.pos = vec2_f(-1.0, 0.0);
        collider.add_hitbox(1, hitbox);
        
        advance_to_event(&mut collider, n64(9.0));
        assert!(collider.next() == Some((Event::Collide, 0, 1)));
        advance_to_event(&mut collider, n64(11.125));
        assert!(collider.next() == Some((Event::Separate, 0, 1)));
        advance(&mut collider, n64(23.0));
    }

    #[test]
    fn test_hitbox_updates() {
        let mut collider = Collider::new(r64(4.0), r64(0.25));
        
        let mut hitbox = Hitbox::new(PlacedShape::new(vec2_f(-10.0, 0.0), Shape::new_square(r64(2.0))));
        hitbox.vel.pos = vec2_f(1.0, 0.0);
        collider.add_hitbox(0, hitbox);
        
        let mut hitbox = Hitbox::new(PlacedShape::new(vec2_f(10.0, 0.0), Shape::new_circle(r64(2.0))));
        hitbox.vel.pos = vec2_f(1.0, 0.0);
        collider.add_hitbox(1, hitbox);
        
        advance(&mut collider, n64(11.0));
        
        let mut hitbox = collider.get_hitbox(0);
        assert!(hitbox.shape == PlacedShape::new(vec2_f(1.0, 0.0), Shape::new_square(r64(2.0))));
        assert!(hitbox.vel == PlacedShape::new(vec2_f(1.0, 0.0), Shape::new_square(r64(0.0))));
        assert!(hitbox.end_time == f64::INFINITY);
        hitbox.shape.pos = vec2_f(0.0, 2.0);
        hitbox.vel.pos = vec2_f(0.0, -1.0);
        collider.update_hitbox(0, hitbox);
        
        advance(&mut collider, n64(14.0));
        
        let mut hitbox = collider.get_hitbox(1);
        assert!(hitbox.shape == PlacedShape::new(vec2_f(24.0, 0.0), Shape::new_circle(r64(2.0))));
        assert!(hitbox.vel == PlacedShape::new(vec2_f(1.0, 0.0), Shape::new_circle(r64(0.0))));
        assert!(hitbox.end_time == f64::INFINITY);
        hitbox.shape.pos = vec2_f(0.0, -8.0);
        hitbox.vel.pos = vec2_f(0.0, 0.0);
        collider.update_hitbox(1, hitbox);
        
        advance_to_event(&mut collider, n64(19.0));
        
        assert!(collider.next() == Some((Event::Collide, 0, 1)));
        let mut hitbox = collider.get_hitbox(0);
        assert!(hitbox.shape == PlacedShape::new(vec2_f(0.0, -6.0), Shape::new_square(r64(2.0))));
        assert!(hitbox.vel == PlacedShape::new(vec2_f(0.0, -1.0), Shape::new_square(r64(0.0))));
        assert!(hitbox.end_time == f64::INFINITY);
        hitbox.vel.pos = vec2_f(0.0, 0.0);
        collider.update_hitbox(0, hitbox);
        
        let mut hitbox = collider.get_hitbox(1);
        assert!(hitbox.shape == PlacedShape::new(vec2_f(0.0, -8.0), Shape::new_circle(r64(2.0))));
        assert!(hitbox.vel == PlacedShape::new(vec2_f(0.0, 0.0), Shape::new_circle(r64(0.0))));
        assert!(hitbox.end_time == f64::INFINITY);
        hitbox.vel.pos = vec2_f(0.0, 2.0);
        collider.update_hitbox(1, hitbox);
        
        let hitbox = Hitbox::new(PlacedShape::new(vec2_f(0.0, 0.0), Shape::new_rect(vec2_f(2.0, 20.0))));
        collider.add_hitbox(2, hitbox);
        
        assert!(collider.next_time() == collider.time());
        let (mut event_1, mut event_2) = (collider.next(), collider.next());
        if event_1.unwrap().1 == 1 { mem::swap(&mut event_1, &mut event_2); }
        assert!(event_1 == Some((Event::Collide, 0, 2)));
        assert!(event_2 == Some((Event::Collide, 1, 2)));
        
        advance_to_event(&mut collider, n64(21.125));
        
        assert!(collider.next() == Some((Event::Separate, 0, 1)));
        
        advance(&mut collider, n64(26.125));
        
        collider.remove_hitbox(1);

        advance(&mut collider, n64(37.125));
    }
    
    //TODO test custom interactivities and interactivity changes...
}