File Coverage

blib/lib/Game/Collisions/AABB.pm
Criterion Covered Total %
statement 226 245 92.2
branch 60 86 69.7
condition 18 33 54.5
subroutine 48 49 97.9
pod 23 24 95.8
total 375 437 85.8


line stmt bran cond sub pod time code
1             # Copyright (c) 2018 Timm Murray
2             # All rights reserved.
3             #
4             # Redistribution and use in source and binary forms, with or without
5             # modification, are permitted provided that the following conditions are met:
6             #
7             # * Redistributions of source code must retain the above copyright notice,
8             # this list of conditions and the following disclaimer.
9             # * Redistributions in binary form must reproduce the above copyright
10             # notice, this list of conditions and the following disclaimer in the
11             # documentation and/or other materials provided with the distribution.
12             #
13             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
17             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23             # POSSIBILITY OF SUCH DAMAGE.
24             package Game::Collisions::AABB;
25             $Game::Collisions::AABB::VERSION = '0.4';
26 21     21   13381 use utf8;
  21         298  
  21         108  
27 21     21   743 use v5.14;
  21         69  
28 21     21   105 use warnings;
  21         35  
  21         481  
29 21     21   96 use List::Util ();
  21         42  
  21         336  
30 21     21   94 use Scalar::Util ();
  21         30  
  21         369  
31 21     21   92 use Carp 'confess';
  21         62  
  21         1090  
32              
33 21     21   129 use constant _X => 0;
  21         36  
  21         2041  
34 21     21   134 use constant _Y => 1;
  21         52  
  21         1076  
35 21     21   124 use constant _LENGTH => 2;
  21         35  
  21         998  
36 21     21   116 use constant _HEIGHT => 3;
  21         33  
  21         1073  
37 21     21   142 use constant _MAX_X => 4;
  21         47  
  21         1028  
38 21     21   128 use constant _MAX_Y => 5;
  21         32  
  21         1131  
39 21     21   118 use constant _PARENT_NODE => 6;
  21         38  
  21         1119  
40 21     21   125 use constant _LEFT_NODE => 7;
  21         34  
  21         924  
41 21     21   111 use constant _RIGHT_NODE => 8;
  21         34  
  21         1081  
42 21     21   158 use constant _USER_DATA => 9;
  21         48  
  21         1202  
43 21     21   156 use constant _DO_CALL_USER_DATA => 10;
  21         41  
  21         59699  
44              
45              
46             sub new
47             {
48 114     114 1 1268 my ($class, $args) = @_;
49             my $do_call_user_data = defined( $args->{user_data} )
50             && Scalar::Util::blessed( $args->{user_data} )
51 114   66     335 && $args->{user_data}->isa( 'Game::Collisions::UserData' );
52             my $self = [
53             $args->{x},
54             $args->{y},
55             $args->{length},
56             $args->{height},
57             $args->{x} + $args->{length},
58             $args->{y} + $args->{height},
59             undef, # parent node
60             undef, # left node
61             undef, # right node
62             $args->{user_data},
63 114         400 $do_call_user_data,
64             ];
65              
66 114         259 bless $self => $class;
67             }
68              
69              
70 657     657 1 1090 sub x { $_[0]->[_X] }
71 656     656 1 921 sub y { $_[0]->[_Y] }
72 298     298 1 439 sub length { $_[0]->[_LENGTH] }
73 298     298 1 414 sub height { $_[0]->[_HEIGHT] }
74 145     145 1 916 sub left_node { $_[0]->[_LEFT_NODE] }
75 120     120 1 202 sub right_node { $_[0]->[_RIGHT_NODE] }
76 182     182 1 347 sub parent { $_[0]->[_PARENT_NODE] }
77 3     3 1 13 sub user_data { $_[0]->[_USER_DATA] }
78 2     2   7 sub _do_call_user_data { $_[0]->[_DO_CALL_USER_DATA] }
79              
80              
81             sub set_left_node
82             {
83 73     73 1 175 my ($self, $node) = @_;
84 73         146 return $self->_set_node( $node, _LEFT_NODE );
85             }
86              
87             sub set_right_node
88             {
89 60     60 1 146 my ($self, $node) = @_;
90 60         109 return $self->_set_node( $node, _RIGHT_NODE );
91             }
92              
93             sub set_parent
94             {
95 138     138 1 207 my ($self, $parent) = @_;
96 138         178 my $current_parent = $self->[_PARENT_NODE];
97 138         164 $self->[_PARENT_NODE] = $parent;
98 138         186 return $current_parent;
99             }
100              
101             sub set_user_data
102             {
103 1     1 1 3 my ($self, $data) = @_;
104 1   33     10 my $do_call_user_data = defined( $data )
105             && Scalar::Util::blessed( $data )
106             && $data->isa( 'Game::Collisions::UserData' );
107 1         3 $self->[_USER_DATA] = $data;
108 1         1 $self->[_DO_CALL_USER_DATA] = $do_call_user_data;
109 1         2 return;
110             }
111              
112             sub resize_all_parents
113             {
114 32     32 1 47 my ($self) = @_;
115              
116 32         89 my @nodes_to_resize = ($self);
117 32         91 while( @nodes_to_resize ) {
118 71         97 my $next_node = shift @nodes_to_resize;
119 71 100       107 push @nodes_to_resize, $next_node->parent
120             if defined $next_node->parent;
121 71         106 $next_node->_resize_to_fit_children;
122             }
123              
124 32         48 return;
125             }
126              
127             sub does_collide
128             {
129 123     123 1 181 my ($self, $other_object) = @_;
130 123 100       257 return 0 if $self == $other_object; # Does not collide with itself
131 103         202 my ($minx1, $miny1, $length1, $height1, $maxx1, $maxy1) = @$self;
132 103         155 my ($minx2, $miny2, $length2, $height2, $maxx2, $maxy2) = @$other_object;
133              
134 103   66     662 return $maxx1 >= $minx2
135             && $minx1 <= $maxx2
136             && $maxy1 >= $miny2
137             && $miny1 <= $maxy2;
138             }
139              
140             sub does_fully_enclose
141             {
142 4     4 1 9 my ($self, $other_object) = @_;
143 4 50       13 return 0 if $self == $other_object; # Does not collide with itself
144 4         12 my ($minx1, $miny1, $length1, $height1, $maxx1, $maxy1) = @$self;
145 4         10 my ($minx2, $miny2, $length2, $height2, $maxx2, $maxy2) = @$other_object;
146              
147 4   66     43 return $maxx1 >= $maxx2
148             && $minx1 <= $minx2
149             && $maxy1 >= $maxy1
150             && $miny1 <= $miny2;
151             }
152              
153             sub root
154             {
155 1     1 0 6 my ($self) = @_;
156 1 50       41 return $self if ! defined $self->parent;
157            
158 1         6 my $current_parent = $self->parent;
159 1         3 while( defined $current_parent->parent ) {
160 2         4 $current_parent = $current_parent->parent;
161             }
162              
163 1         3 return $current_parent;
164             }
165              
166              
167              
168             sub find_best_sibling_node
169             {
170 34     34 1 70 my ($self, $new_node) = @_;
171              
172 34         60 my @nodes_to_check = ($self);
173 34         73 while( @nodes_to_check ) {
174 74         106 my $check_node = shift @nodes_to_check;
175 74 100       120 return $check_node if ! $check_node->is_branch_node;
176              
177 41         90 my $left_node = $check_node->left_node;
178 41         87 my $right_node = $check_node->right_node;
179              
180 41 100       104 if(! defined $left_node ) {
    100          
181 1 50       4 if(! $right_node->does_fully_enclose( $new_node ) ) {
    0          
182             # No left node, and we don't enclose the right, so
183             # the right should be our sibling
184 1         4 return $right_node;
185             }
186             elsif( $right_node->is_branch_node ) {
187             # Since right node is a branch node, and we enclose it,
188             # descend further without doing anything else.
189 0         0 push @nodes_to_check, $right_node;
190 0         0 next;
191             }
192             else {
193             # Right node is a leaf and we enclose it, so it's our
194             # sibling now
195 0         0 return $right_node;
196             }
197             }
198             elsif(! defined $right_node ) {
199 1 50       2 if(! $left_node->does_fully_enclose( $new_node ) ) {
    50          
200             # No right node, and we don't enclose the left, so
201             # the left should be our sibling
202 0         0 return $left_node;
203             }
204             elsif( $left_node->is_branch_node ) {
205             # Since left node is a branch node, and we enclose it,
206             # descend further without doing anything else
207 1         2 push @nodes_to_check, $left_node;
208 1         4 next;
209             }
210             else {
211             # Left node is a leaf and we enclose it, so it's our
212             # sibling now
213 0         0 return $left_node;
214             }
215             }
216              
217             # If we have both left and right nodes, then we have to decide which
218             # direction to go
219 39         73 my (undef, undef, $left_length, $left_height)
220             = $self->_calculate_bounding_box_for_nodes( $left_node, $new_node );
221 39         98 my (undef, undef, $right_length, $right_height)
222             = $self->_calculate_bounding_box_for_nodes( $right_node, $new_node);
223              
224 39         67 my $left_surface = $left_length * $left_height;
225 39         44 my $right_surface = $right_length * $right_height;
226 39 100       106 push @nodes_to_check,
227             ($left_surface > $right_surface) ? $right_node : $left_node;
228             }
229              
230             # How did we get here? It should have descended the tree until it
231             # came to the leaf and returned that. Just in case, return ourselves.
232 0         0 return $self;
233             }
234              
235             sub is_branch_node
236             {
237 263     263 1 330 my ($self) = @_;
238 263   100     1155 return (defined $self->[_LEFT_NODE]) || (defined $self->[_RIGHT_NODE]);
239             }
240              
241             sub dump_tree
242             {
243 0     0 1 0 my ($self, $spacing) = @_;
244 0   0     0 $spacing //= '';
245              
246 0 0       0 my $draw_chars = $self->is_branch_node
247             ? '├┐'
248             : '│├';
249 0         0 my $str = "$spacing├┤ " . join( ', ',
250             "$self",
251             $self->x,
252             $self->y,
253             $self->length,
254             $self->height,
255             );
256 0         0 $str .= "\n";
257 0 0       0 $str .= $self->left_node->dump_tree( $spacing . '┼' )
258             if defined $self->left_node;
259 0 0       0 $str .= $self->right_node->dump_tree( $spacing . '┼' )
260             if defined $self->right_node;
261              
262 0         0 return $str;
263             }
264              
265             sub move
266             {
267 2     2 1 22144 my ($self, $args) = @_;
268 2   50     8 my $add_x = $args->{add_x} // 0;
269 2   100     21 my $add_y = $args->{add_y} // 0;
270              
271 2         6 $self->[_X] = $self->[_X] + $add_x;
272 2         6 $self->[_Y] = $self->[_Y] + $add_y;
273 2         4 $self->[_MAX_X] = $self->[_MAX_X] + $add_x;
274 2         4 $self->[_MAX_Y] = $self->[_MAX_Y] + $add_y;
275              
276 2 100       7 if( $self->_do_call_user_data ) {
277 1         3 $self->user_data->on_aabb_move({
278             add_x => $add_x,
279             add_y => $add_y,
280             });
281             }
282 2         1167 $self->_reinsert;
283 2         141 return;
284             }
285              
286             sub insert_new_aabb
287             {
288 32     32 1 57 my ($self, $new_node) = @_;
289 32         65 my $best_sibling = $self->find_best_sibling_node( $new_node );
290              
291 32         68 my $min_x = List::Util::min( $new_node->x, $best_sibling->x );
292 32         60 my $min_y = List::Util::min( $new_node->y, $best_sibling->y );
293              
294 32         127 my $new_branch = Game::Collisions::AABB->new({
295             x => $min_x,
296             y => $min_y,
297             length => 1,
298             height => 1,
299             });
300              
301 32         87 my $old_parent = $best_sibling->parent;
302 32         74 $new_branch->set_left_node( $new_node );
303 32         66 $new_branch->set_right_node( $best_sibling );
304              
305 32         36 my $new_root;
306 32 100       82 if(! defined $old_parent ) {
307             # Happens when the root is going to be the new sibling. In this case,
308             # create a new node for the root.
309 10         21 $new_root = $new_branch;
310             }
311             else {
312 22 100       45 my $set_method = $best_sibling == $old_parent->left_node
313             ? "set_left_node"
314             : "set_right_node";
315 22         75 $old_parent->$set_method( $new_branch );
316             }
317              
318 32         85 $new_branch->resize_all_parents;
319 32         72 return $new_root;
320             }
321              
322             sub suggested_rotation
323             {
324 3     3 1 16 my ($self) = @_;
325 3         10 my $left_depth = $self->left_node->_depth(0);
326 3         10 my $right_depth = $self->right_node->_depth(0);
327 3         11 my $difference = abs( $left_depth - $right_depth );
328              
329 3 50       18 return $difference <= 1 ? 0 :
    100          
330             ($left_depth > $right_depth) ? -1 :
331             1;
332             }
333              
334             sub remove
335             {
336 4     4 1 25 my ($self) = @_;
337 4 100       17 confess "Can only remove leaf nodes" if $self->is_branch_node;
338              
339 3         11 my $parent = $self->parent;
340 3         11 $self->set_parent( undef );
341             # If there's only one node in the system, there will be no parent
342 3 100       15 $parent->_prune if defined $parent;
343              
344 3         8 return;
345             }
346              
347              
348             sub _prune
349             {
350 2     2   4 my ($self) = @_;
351 2 50       5 return unless $self->is_branch_node;
352 2         9 my $current_left = $self->left_node;
353 2         6 my $current_right = $self->right_node;
354              
355             # The main setters do things to try to keep things consistently
356             # connected, which isn't what we want here. Access internal strucutre
357             # directly.
358 2 100 66     6 if( (! defined $self->[_LEFT_NODE]->parent)
359             || ($self->[_LEFT_NODE]->parent != $self)
360             ){
361 1         2 $self->[_LEFT_NODE][_PARENT_NODE] = undef;
362 1         2 $self->[_LEFT_NODE] = undef;
363             }
364 2 100 66     7 if( (! defined $self->[_RIGHT_NODE]->parent)
365             || ($self->[_RIGHT_NODE]->parent != $self)
366             ){
367 1         2 $self->[_RIGHT_NODE][_PARENT_NODE] = undef;
368 1         3 $self->[_RIGHT_NODE] = undef;
369             }
370              
371             # Don't need to continue if we're the root node
372 2 100       5 return if ! defined $self->parent;
373 1 50 33     2 if( (! defined $self->left_node) && (defined $self->right_node) ) {
    0 0        
374             # Have right node but no left. Attach right node to our parent.
375 1 50       5 if( $self->parent->right_node == $self ) {
376 1         2 $self->parent->set_right_node( $current_right );
377             }
378             else {
379 0         0 $self->parent->set_left_node( $current_right );
380             }
381             }
382             elsif( (! defined $self->right_node) && (defined $self->left_node) ) {
383             # Have left node but no right. Attach left node to our parent.
384 0 0       0 if( $self->parent->right_node == $self ) {
385 0         0 $self->parent->set_right_node( $current_left );
386             }
387             else {
388 0         0 $self->parent->set_left_node( $current_right );
389             }
390             }
391              
392 1         2 $self->[_PARENT_NODE] = undef;
393 1         3 return;
394             }
395              
396             sub _depth
397             {
398 12     12   16 my ($self, $depth_so_far) = @_;
399             # TODO reimplement in iterative way
400 12 100       25 return $depth_so_far + 1 if ! $self->is_branch_node;
401 3         6 my $left_depth = $self->left_node->_depth( $depth_so_far + 1 );
402 3         7 my $right_depth = $self->right_node->_depth( $depth_so_far + 1 );
403              
404 3 100       12 return $left_depth > $right_depth
405             ? $left_depth
406             : $right_depth;
407             }
408              
409             sub _reinsert
410             {
411 3     3   14 my ($self) = @_;
412 3         48 my $current_parent = $self->parent;
413 3 100       11 return if ! defined $current_parent;
414 2         6 $self->_detach_from_parent;
415              
416 2         5 while( defined( my $possible_root = $current_parent->parent ) ) {
417 2         10 $current_parent = $possible_root;
418             }
419              
420             # $current_parent will now be the root of the tree
421 2         7 $current_parent->insert_new_aabb( $self );
422 2         5 return;
423             }
424              
425             sub _detach_from_parent
426             {
427 4     4   9 my ($self) = @_;
428 4         9 my $current_parent = $self->parent;
429 4 50       12 return unless defined $current_parent;
430              
431 4         10 my $current_grandparent = $current_parent->parent;
432 4         10 my $is_left = ($current_parent->left_node() == $self);
433 4 100       12 if(! defined $current_grandparent ) {
434             # Parent must have been root. Just detach ourselves.
435 2 100       6 if( $is_left ) {
436 1         3 $current_parent->set_left_node( undef );
437             }
438             else {
439 1         2 $current_parent->set_right_node( undef );
440             }
441             }
442             else {
443             # Our parent is removed, and our sibling takes its place in the
444             # grandparent
445 2 50       7 my $sibling = $is_left
446             ? $current_parent->right_node
447             : $current_parent->left_node;
448 2         4 my $is_parent_left
449             = ($current_grandparent->left_node == $current_parent);
450              
451 2 50       4 if( $is_parent_left ) {
452 2         4 $current_grandparent->set_left_node( $sibling );
453             }
454             else {
455 0         0 $current_grandparent->set_right_node( $sibling );
456             }
457             }
458            
459 4         19 $self->set_parent( undef );
460 4         7 return;
461             }
462              
463              
464              
465             sub _set_node
466             {
467 133     133   212 my ($self, $node, $index) = @_;
468 133 100       322 Scalar::Util::unweaken( $self->[$index] )
469             if defined $self->[$index];
470 133         162 $self->[$index] = $node;
471 133         313 Scalar::Util::weaken( $self->[$index] );
472 133 100       307 my $former_parent = defined $node
473             ? $node->set_parent( $self )
474             : undef;
475 133         233 return $former_parent;
476             }
477              
478             sub _resize_to_fit_children
479             {
480 71     71   108 my ($self) = @_;
481 71 50       124 return if ! $self->is_branch_node;
482 71         213 my ($x, $y, $length, $height) = $self->_calculate_bounding_box_for_nodes(
483             $self->[_LEFT_NODE],
484             $self->[_RIGHT_NODE],
485             );
486              
487 71         94 $self->[_X] = $x;
488 71         86 $self->[_Y] = $y;
489 71         85 $self->[_LENGTH] = $length;
490 71         83 $self->[_HEIGHT] = $height;
491 71         84 $self->[_MAX_X] = $x + $length;
492 71         91 $self->[_MAX_Y] = $y + $height;
493              
494 71         138 return;
495             }
496              
497             sub _calculate_bounding_box_for_nodes
498             {
499 149     149   208 my ($self, $node1, $node2) = @_;
500 149 100       227 return @$node1[_X, _Y, _LENGTH, _HEIGHT] if ! defined $node2;
501 148 50       210 return @$node2[_X, _Y, _LENGTH, _HEIGHT] if ! defined $node1;
502              
503 148         201 my $min_x = List::Util::min( $node1->x, $node2->x );
504 148         197 my $min_y = List::Util::min( $node1->y, $node2->y );
505 148         207 my $max_x = List::Util::max(
506             $node1->length + $node1->x,
507             $node2->length + $node2->x,
508             );
509 148         260 my $max_y = List::Util::max(
510             $node1->height + $node1->y,
511             $node2->height + $node2->y,
512             );
513              
514 148         214 my $length = $max_x - $min_x;
515 148         181 my $height = $max_y - $min_y;
516 148         269 return ($min_x, $min_y, $length, $height);
517             }
518              
519              
520             1;
521             __END__