File Coverage

blib/lib/Game/Collisions/AABB.pm
Criterion Covered Total %
statement 226 245 92.2
branch 60 86 69.7
condition 21 39 53.8
subroutine 48 49 97.9
pod 23 24 95.8
total 378 443 85.3


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.5';
26 22     22   12111 use utf8;
  22         273  
  22         98  
27 22     22   629 use v5.14;
  22         66  
28 22     22   99 use warnings;
  22         34  
  22         482  
29 22     22   96 use List::Util ();
  22         28  
  22         285  
30 22     22   90 use Scalar::Util ();
  22         37  
  22         341  
31 22     22   93 use Carp 'confess';
  22         43  
  22         1079  
32              
33 22     22   116 use constant _X => 0;
  22         32  
  22         1902  
34 22     22   140 use constant _Y => 1;
  22         33  
  22         1004  
35 22     22   110 use constant _LENGTH => 2;
  22         35  
  22         888  
36 22     22   113 use constant _HEIGHT => 3;
  22         32  
  22         966  
37 22     22   128 use constant _MAX_X => 4;
  22         45  
  22         954  
38 22     22   116 use constant _MAX_Y => 5;
  22         34  
  22         982  
39 22     22   111 use constant _PARENT_NODE => 6;
  22         33  
  22         989  
40 22     22   115 use constant _LEFT_NODE => 7;
  22         32  
  22         1001  
41 22     22   106 use constant _RIGHT_NODE => 8;
  22         29  
  22         1016  
42 22     22   120 use constant _USER_DATA => 9;
  22         41  
  22         1047  
43 22     22   132 use constant _DO_CALL_USER_DATA => 10;
  22         35  
  22         54824  
44              
45              
46             sub new
47             {
48 116     116 1 1239 my ($class, $args) = @_;
49             my $do_call_user_data = defined( $args->{user_data} )
50             && Scalar::Util::blessed( $args->{user_data} )
51 116   66     311 && $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 116         371 $do_call_user_data,
64             ];
65              
66 116         238 bless $self => $class;
67             }
68              
69              
70 633     633 1 878 sub x { $_[0]->[_X] }
71 632     632 1 808 sub y { $_[0]->[_Y] }
72 286     286 1 351 sub length { $_[0]->[_LENGTH] }
73 286     286 1 345 sub height { $_[0]->[_HEIGHT] }
74 142     142 1 706 sub left_node { $_[0]->[_LEFT_NODE] }
75 117     117 1 178 sub right_node { $_[0]->[_RIGHT_NODE] }
76 181     181 1 318 sub parent { $_[0]->[_PARENT_NODE] }
77 3     3 1 12 sub user_data { $_[0]->[_USER_DATA] }
78 2     2   8 sub _do_call_user_data { $_[0]->[_DO_CALL_USER_DATA] }
79              
80              
81             sub set_left_node
82             {
83 73     73 1 163 my ($self, $node) = @_;
84 73         136 return $self->_set_node( $node, _LEFT_NODE );
85             }
86              
87             sub set_right_node
88             {
89 61     61 1 128 my ($self, $node) = @_;
90 61         108 return $self->_set_node( $node, _RIGHT_NODE );
91             }
92              
93             sub set_parent
94             {
95 140     140 1 186 my ($self, $parent) = @_;
96 140         162 my $current_parent = $self->[_PARENT_NODE];
97 140         154 $self->[_PARENT_NODE] = $parent;
98 140         169 return $current_parent;
99             }
100              
101             sub set_user_data
102             {
103 1     1 1 2 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         2 $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 44 my ($self) = @_;
115              
116 32         55 my @nodes_to_resize = ($self);
117 32         81 while( @nodes_to_resize ) {
118 69         83 my $next_node = shift @nodes_to_resize;
119 69 100       90 push @nodes_to_resize, $next_node->parent
120             if defined $next_node->parent;
121 69         103 $next_node->_resize_to_fit_children;
122             }
123              
124 32         40 return;
125             }
126              
127             sub does_collide
128             {
129 121     121 1 154 my ($self, $other_object) = @_;
130 121 100       239 return 0 if $self == $other_object; # Does not collide with itself
131 101         192 my ($minx1, $miny1, $length1, $height1, $maxx1, $maxy1) = @$self;
132 101         142 my ($minx2, $miny2, $length2, $height2, $maxx2, $maxy2) = @$other_object;
133              
134 101   66     861 return $maxx1 >= $minx2
135             && $minx1 <= $maxx2
136             && $maxy1 >= $miny2
137             && $miny1 <= $maxy2;
138             }
139              
140             sub does_fully_enclose
141             {
142 4     4 1 8 my ($self, $other_object) = @_;
143 4 50       12 return 0 if $self == $other_object; # Does not collide with itself
144 4         12 my ($minx1, $miny1, $length1, $height1, $maxx1, $maxy1) = @$self;
145 4         8 my ($minx2, $miny2, $length2, $height2, $maxx2, $maxy2) = @$other_object;
146              
147 4   66     52 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       38 return $self if ! defined $self->parent;
157            
158 1         3 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 54 my ($self, $new_node) = @_;
171              
172 34         52 my @nodes_to_check = ($self);
173 34         63 while( @nodes_to_check ) {
174 72         91 my $check_node = shift @nodes_to_check;
175 72 100       104 return $check_node if ! $check_node->is_branch_node;
176              
177 39         75 my $left_node = $check_node->left_node;
178 39         58 my $right_node = $check_node->right_node;
179              
180 39 100       90 if(! defined $left_node ) {
    100          
181 1 50       3 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         3 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       3 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 37         93 my (undef, undef, $left_length, $left_height)
220             = $self->_calculate_bounding_box_for_nodes( $left_node, $new_node );
221 37         58 my (undef, undef, $right_length, $right_height)
222             = $self->_calculate_bounding_box_for_nodes( $right_node, $new_node);
223              
224 37         59 my $left_surface = $left_length * $left_height;
225 37         37 my $right_surface = $right_length * $right_height;
226 37 100       90 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 259     259 1 308 my ($self) = @_;
238 259   100     1003 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 22417 my ($self, $args) = @_;
268 2   50     20 my $add_x = $args->{add_x} // 0;
269 2   100     11 my $add_y = $args->{add_y} // 0;
270              
271 2         6 $self->[_X] = $self->[_X] + $add_x;
272 2         4 $self->[_Y] = $self->[_Y] + $add_y;
273 2         3 $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         1212 $self->_reinsert;
283 2         146 return;
284             }
285              
286             sub insert_new_aabb
287             {
288 32     32 1 51 my ($self, $new_node) = @_;
289 32         59 my $best_sibling = $self->find_best_sibling_node( $new_node );
290              
291 32         59 my $min_x = List::Util::min( $new_node->x, $best_sibling->x );
292 32         55 my $min_y = List::Util::min( $new_node->y, $best_sibling->y );
293              
294 32         108 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         80 my $old_parent = $best_sibling->parent;
302 32         65 $new_branch->set_left_node( $new_node );
303 32         60 $new_branch->set_right_node( $best_sibling );
304              
305 32         33 my $new_root;
306 32 100       67 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         16 $new_root = $new_branch;
310             }
311             else {
312 22 100       56 my $set_method = $best_sibling == $old_parent->left_node
313             ? "set_left_node"
314             : "set_right_node";
315 22         51 $old_parent->$set_method( $new_branch );
316             }
317              
318 32         70 $new_branch->resize_all_parents;
319 32         74 return $new_root;
320             }
321              
322             sub suggested_rotation
323             {
324 3     3 1 14 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         12 my $difference = abs( $left_depth - $right_depth );
328              
329 3 50       17 return $difference <= 1 ? 0 :
    100          
330             ($left_depth > $right_depth) ? -1 :
331             1;
332             }
333              
334             sub remove
335             {
336 5     5 1 28 my ($self) = @_;
337 5 100       17 confess "Can only remove leaf nodes" if $self->is_branch_node;
338              
339 4         16 my $parent = $self->parent;
340 4         17 $self->set_parent( undef );
341             # If there's only one node in the system, there will be no parent
342 4 100       16 $parent->_prune if defined $parent;
343              
344 4         7 return;
345             }
346              
347              
348             sub _prune
349             {
350 3     3   7 my ($self) = @_;
351 3 50       7 return unless $self->is_branch_node;
352 3         12 my $current_left = $self->left_node;
353 3         11 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 3 100 66     14 if(
      33        
359             (defined $self->[_LEFT_NODE])
360             && (
361             (! defined $self->[_LEFT_NODE]->parent)
362             || ($self->[_LEFT_NODE]->parent != $self)
363             )
364             ){
365 2         5 $self->[_LEFT_NODE][_PARENT_NODE] = undef;
366 2         4 $self->[_LEFT_NODE] = undef;
367             }
368 3 100 66     13 if(
      66        
369             (defined $self->[_RIGHT_NODE])
370             && (
371             (! defined $self->[_RIGHT_NODE]->parent)
372             || ($self->[_RIGHT_NODE]->parent != $self)
373             )
374             ){
375 1         2 $self->[_RIGHT_NODE][_PARENT_NODE] = undef;
376 1         2 $self->[_RIGHT_NODE] = undef;
377             }
378              
379             # Don't need to continue if we're the root node
380 3 100       8 return if ! defined $self->parent;
381 1 50 33     2 if( (! defined $self->left_node) && (defined $self->right_node) ) {
    0 0        
382             # Have right node but no left. Attach right node to our parent.
383 1 50       3 if( $self->parent->right_node == $self ) {
384 1         3 $self->parent->set_right_node( $current_right );
385             }
386             else {
387 0         0 $self->parent->set_left_node( $current_right );
388             }
389             }
390             elsif( (! defined $self->right_node) && (defined $self->left_node) ) {
391             # Have left node but no right. Attach left node to our parent.
392 0 0       0 if( $self->parent->right_node == $self ) {
393 0         0 $self->parent->set_right_node( $current_left );
394             }
395             else {
396 0         0 $self->parent->set_left_node( $current_right );
397             }
398             }
399              
400 1         3 $self->[_PARENT_NODE] = undef;
401 1         2 return;
402             }
403              
404             sub _depth
405             {
406 12     12   19 my ($self, $depth_so_far) = @_;
407             # TODO reimplement in iterative way
408 12 100       22 return $depth_so_far + 1 if ! $self->is_branch_node;
409 3         6 my $left_depth = $self->left_node->_depth( $depth_so_far + 1 );
410 3         7 my $right_depth = $self->right_node->_depth( $depth_so_far + 1 );
411              
412 3 100       11 return $left_depth > $right_depth
413             ? $left_depth
414             : $right_depth;
415             }
416              
417             sub _reinsert
418             {
419 3     3   11 my ($self) = @_;
420 3         40 my $current_parent = $self->parent;
421 3 100       11 return if ! defined $current_parent;
422 2         7 $self->_detach_from_parent;
423              
424 2         6 while( defined( my $possible_root = $current_parent->parent ) ) {
425 2         8 $current_parent = $possible_root;
426             }
427              
428             # $current_parent will now be the root of the tree
429 2         7 $current_parent->insert_new_aabb( $self );
430 2         4 return;
431             }
432              
433             sub _detach_from_parent
434             {
435 4     4   10 my ($self) = @_;
436 4         7 my $current_parent = $self->parent;
437 4 50       10 return unless defined $current_parent;
438              
439 4         7 my $current_grandparent = $current_parent->parent;
440 4         9 my $is_left = ($current_parent->left_node() == $self);
441 4 100       11 if(! defined $current_grandparent ) {
442             # Parent must have been root. Just detach ourselves.
443 2 100       7 if( $is_left ) {
444 1         2 $current_parent->set_left_node( undef );
445             }
446             else {
447 1         5 $current_parent->set_right_node( undef );
448             }
449             }
450             else {
451             # Our parent is removed, and our sibling takes its place in the
452             # grandparent
453 2 50       7 my $sibling = $is_left
454             ? $current_parent->right_node
455             : $current_parent->left_node;
456 2         5 my $is_parent_left
457             = ($current_grandparent->left_node == $current_parent);
458              
459 2 50       4 if( $is_parent_left ) {
460 2         4 $current_grandparent->set_left_node( $sibling );
461             }
462             else {
463 0         0 $current_grandparent->set_right_node( $sibling );
464             }
465             }
466            
467 4         10 $self->set_parent( undef );
468 4         5 return;
469             }
470              
471              
472              
473             sub _set_node
474             {
475 134     134   189 my ($self, $node, $index) = @_;
476 134 100       285 Scalar::Util::unweaken( $self->[$index] )
477             if defined $self->[$index];
478 134         150 $self->[$index] = $node;
479 134         289 Scalar::Util::weaken( $self->[$index] );
480 134 100       271 my $former_parent = defined $node
481             ? $node->set_parent( $self )
482             : undef;
483 134         206 return $former_parent;
484             }
485              
486             sub _resize_to_fit_children
487             {
488 69     69   84 my ($self) = @_;
489 69 50       128 return if ! $self->is_branch_node;
490 69         165 my ($x, $y, $length, $height) = $self->_calculate_bounding_box_for_nodes(
491             $self->[_LEFT_NODE],
492             $self->[_RIGHT_NODE],
493             );
494              
495 69         78 $self->[_X] = $x;
496 69         68 $self->[_Y] = $y;
497 69         78 $self->[_LENGTH] = $length;
498 69         63 $self->[_HEIGHT] = $height;
499 69         81 $self->[_MAX_X] = $x + $length;
500 69         72 $self->[_MAX_Y] = $y + $height;
501              
502 69         120 return;
503             }
504              
505             sub _calculate_bounding_box_for_nodes
506             {
507 143     143   176 my ($self, $node1, $node2) = @_;
508 143 100       192 return @$node1[_X, _Y, _LENGTH, _HEIGHT] if ! defined $node2;
509 142 50       167 return @$node2[_X, _Y, _LENGTH, _HEIGHT] if ! defined $node1;
510              
511 142         188 my $min_x = List::Util::min( $node1->x, $node2->x );
512 142         237 my $min_y = List::Util::min( $node1->y, $node2->y );
513 142         193 my $max_x = List::Util::max(
514             $node1->length + $node1->x,
515             $node2->length + $node2->x,
516             );
517 142         194 my $max_y = List::Util::max(
518             $node1->height + $node1->y,
519             $node2->height + $node2->y,
520             );
521              
522 142         176 my $length = $max_x - $min_x;
523 142         141 my $height = $max_y - $min_y;
524 142         245 return ($min_x, $min_y, $length, $height);
525             }
526              
527              
528             1;
529             __END__