File Coverage

blib/lib/Game/Collisions/AABB.pm
Criterion Covered Total %
statement 226 245 92.2
branch 58 84 69.0
condition 18 33 54.5
subroutine 48 49 97.9
pod 23 24 95.8
total 373 435 85.7


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.3';
26 19     19   11639 use utf8;
  19         265  
  19         94  
27 19     19   612 use v5.14;
  19         62  
28 19     19   91 use warnings;
  19         35  
  19         423  
29 19     19   96 use List::Util ();
  19         32  
  19         272  
30 19     19   85 use Scalar::Util ();
  19         27  
  19         336  
31 19     19   86 use Carp 'confess';
  19         37  
  19         1018  
32              
33 19     19   111 use constant _X => 0;
  19         25  
  19         1711  
34 19     19   113 use constant _Y => 1;
  19         41  
  19         981  
35 19     19   108 use constant _LENGTH => 2;
  19         48  
  19         896  
36 19     19   102 use constant _HEIGHT => 3;
  19         32  
  19         874  
37 19     19   114 use constant _MAX_X => 4;
  19         44  
  19         912  
38 19     19   102 use constant _MAX_Y => 5;
  19         44  
  19         903  
39 19     19   103 use constant _PARENT_NODE => 6;
  19         30  
  19         911  
40 19     19   101 use constant _LEFT_NODE => 7;
  19         29  
  19         906  
41 19     19   107 use constant _RIGHT_NODE => 8;
  19         29  
  19         983  
42 19     19   108 use constant _USER_DATA => 9;
  19         33  
  19         902  
43 19     19   99 use constant _DO_CALL_USER_DATA => 10;
  19         36  
  19         52935  
44              
45              
46             sub new
47             {
48 110     110 1 1174 my ($class, $args) = @_;
49             my $do_call_user_data = defined( $args->{user_data} )
50             && Scalar::Util::blessed( $args->{user_data} )
51 110   66     321 && $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 110         361 $do_call_user_data,
64             ];
65              
66 110         253 bless $self => $class;
67             }
68              
69              
70 651     651 1 1005 sub x { $_[0]->[_X] }
71 650     650 1 1021 sub y { $_[0]->[_Y] }
72 296     296 1 421 sub length { $_[0]->[_LENGTH] }
73 296     296 1 445 sub height { $_[0]->[_HEIGHT] }
74 145     145 1 890 sub left_node { $_[0]->[_LEFT_NODE] }
75 120     120 1 181 sub right_node { $_[0]->[_RIGHT_NODE] }
76 179     179 1 344 sub parent { $_[0]->[_PARENT_NODE] }
77 3     3 1 14 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 72     72 1 162 my ($self, $node) = @_;
84 72         146 return $self->_set_node( $node, _LEFT_NODE );
85             }
86              
87             sub set_right_node
88             {
89 59     59 1 128 my ($self, $node) = @_;
90 59         105 return $self->_set_node( $node, _RIGHT_NODE );
91             }
92              
93             sub set_parent
94             {
95 135     135 1 191 my ($self, $parent) = @_;
96 135         163 my $current_parent = $self->[_PARENT_NODE];
97 135         174 $self->[_PARENT_NODE] = $parent;
98 135         179 return $current_parent;
99             }
100              
101             sub set_user_data
102             {
103 1     1 1 3 my ($self, $data) = @_;
104 1   33     11 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         2 $self->[_DO_CALL_USER_DATA] = $do_call_user_data;
109 1         3 return;
110             }
111              
112             sub resize_all_parents
113             {
114 31     31 1 45 my ($self) = @_;
115              
116 31         53 my @nodes_to_resize = ($self);
117 31         78 while( @nodes_to_resize ) {
118 70         88 my $next_node = shift @nodes_to_resize;
119 70 100       108 push @nodes_to_resize, $next_node->parent
120             if defined $next_node->parent;
121 70         105 $next_node->_resize_to_fit_children;
122             }
123              
124 31         50 return;
125             }
126              
127             sub does_collide
128             {
129 122     122 1 161 my ($self, $other_object) = @_;
130 122 100       262 return 0 if $self == $other_object; # Does not collide with itself
131 102         180 my ($minx1, $miny1, $length1, $height1, $maxx1, $maxy1) = @$self;
132 102         161 my ($minx2, $miny2, $length2, $height2, $maxx2, $maxy2) = @$other_object;
133              
134 102   66     620 return $maxx1 >= $minx2
135             && $minx1 <= $maxx2
136             && $maxy1 >= $miny1
137             && $miny1 <= $maxy2;
138             }
139              
140             sub does_fully_enclose
141             {
142 4     4 1 10 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     49 return $maxx1 >= $maxx2
148             && $minx1 <= $minx2
149             && $maxy1 >= $maxy1
150             && $miny1 <= $miny2;
151             }
152              
153             sub root
154             {
155 1     1 0 5 my ($self) = @_;
156 1 50       35 return $self if ! defined $self->parent;
157            
158 1         3 my $current_parent = $self->parent;
159 1         2 while( defined $current_parent->parent ) {
160 2         3 $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 33     33 1 62 my ($self, $new_node) = @_;
171              
172 33         56 my @nodes_to_check = ($self);
173 33         76 while( @nodes_to_check ) {
174 73         100 my $check_node = shift @nodes_to_check;
175 73 100       126 return $check_node if ! $check_node->is_branch_node;
176              
177 41         81 my $left_node = $check_node->left_node;
178 41         88 my $right_node = $check_node->right_node;
179              
180 41 100       112 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       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 39         69 my (undef, undef, $left_length, $left_height)
220             = $self->_calculate_bounding_box_for_nodes( $left_node, $new_node );
221 39         81 my (undef, undef, $right_length, $right_height)
222             = $self->_calculate_bounding_box_for_nodes( $right_node, $new_node);
223              
224 39         60 my $left_surface = $left_length * $left_height;
225 39         47 my $right_surface = $right_length * $right_height;
226 39 100       101 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 260     260 1 325 my ($self) = @_;
238 260   100     1072 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 21517 my ($self, $args) = @_;
268 2   50     7 my $add_x = $args->{add_x} // 0;
269 2   100     10 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         5 $self->[_MAX_X] = $self->[_MAX_X] + $add_x;
274 2         3 $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         1101 $self->_reinsert;
283 2         137 return;
284             }
285              
286             sub insert_new_aabb
287             {
288 31     31 1 64 my ($self, $new_node) = @_;
289 31         67 my $best_sibling = $self->find_best_sibling_node( $new_node );
290              
291 31         69 my $min_x = List::Util::min( $new_node->x, $best_sibling->x );
292 31         54 my $min_y = List::Util::min( $new_node->y, $best_sibling->y );
293              
294 31         114 my $new_branch = Game::Collisions::AABB->new({
295             x => $min_x,
296             y => $min_y,
297             length => 1,
298             height => 1,
299             });
300              
301 31         80 my $old_parent = $best_sibling->parent;
302 31         72 $new_branch->set_left_node( $new_node );
303 31         65 $new_branch->set_right_node( $best_sibling );
304              
305 31         32 my $new_root;
306 31 100       68 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 9         15 $new_root = $new_branch;
310             }
311             else {
312 22 100       36 my $set_method = $best_sibling == $old_parent->left_node
313             ? "set_left_node"
314             : "set_right_node";
315 22         60 $old_parent->$set_method( $new_branch );
316             }
317              
318 31         92 $new_branch->resize_all_parents;
319 31         68 return $new_root;
320             }
321              
322             sub suggested_rotation
323             {
324 3     3 1 15 my ($self) = @_;
325 3         9 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       19 return $difference <= 1 ? 0 :
    100          
330             ($left_depth > $right_depth) ? -1 :
331             1;
332             }
333              
334             sub remove
335             {
336 3     3 1 15 my ($self) = @_;
337 3 100       11 confess "Can only remove leaf nodes" if $self->is_branch_node;
338              
339 2         9 my $parent = $self->parent;
340 2         8 $self->set_parent( undef );
341 2         8 $parent->_prune;
342              
343 2         6 return;
344             }
345              
346              
347             sub _prune
348             {
349 2     2   4 my ($self) = @_;
350 2 50       4 return unless $self->is_branch_node;
351 2         8 my $current_left = $self->left_node;
352 2         5 my $current_right = $self->right_node;
353              
354             # The main setters do things to try to keep things consistently
355             # connected, which isn't what we want here. Access internal strucutre
356             # directly.
357 2 100 66     6 if( (! defined $self->[_LEFT_NODE]->parent)
358             || ($self->[_LEFT_NODE]->parent != $self)
359             ){
360 1         3 $self->[_LEFT_NODE][_PARENT_NODE] = undef;
361 1         2 $self->[_LEFT_NODE] = undef;
362             }
363 2 100 66     7 if( (! defined $self->[_RIGHT_NODE]->parent)
364             || ($self->[_RIGHT_NODE]->parent != $self)
365             ){
366 1         3 $self->[_RIGHT_NODE][_PARENT_NODE] = undef;
367 1         3 $self->[_RIGHT_NODE] = undef;
368             }
369              
370             # Don't need to continue if we're the root node
371 2 100       6 return if ! defined $self->parent;
372 1 50 33     2 if( (! defined $self->left_node) && (defined $self->right_node) ) {
    0 0        
373             # Have right node but no left. Attach right node to our parent.
374 1 50       20 if( $self->parent->right_node == $self ) {
375 1         3 $self->parent->set_right_node( $current_right );
376             }
377             else {
378 0         0 $self->parent->set_left_node( $current_right );
379             }
380             }
381             elsif( (! defined $self->right_node) && (defined $self->left_node) ) {
382             # Have left node but no right. Attach left node to our parent.
383 0 0       0 if( $self->parent->right_node == $self ) {
384 0         0 $self->parent->set_right_node( $current_left );
385             }
386             else {
387 0         0 $self->parent->set_left_node( $current_right );
388             }
389             }
390              
391 1         3 $self->[_PARENT_NODE] = undef;
392 1         2 return;
393             }
394              
395             sub _depth
396             {
397 12     12   18 my ($self, $depth_so_far) = @_;
398             # TODO reimplement in iterative way
399 12 100       22 return $depth_so_far + 1 if ! $self->is_branch_node;
400 3         7 my $left_depth = $self->left_node->_depth( $depth_so_far + 1 );
401 3         7 my $right_depth = $self->right_node->_depth( $depth_so_far + 1 );
402              
403 3 100       11 return $left_depth > $right_depth
404             ? $left_depth
405             : $right_depth;
406             }
407              
408             sub _reinsert
409             {
410 3     3   12 my ($self) = @_;
411 3         40 my $current_parent = $self->parent;
412 3 100       22 return if ! defined $current_parent;
413 2         8 $self->_detach_from_parent;
414              
415 2         4 while( defined( my $possible_root = $current_parent->parent ) ) {
416 2         9 $current_parent = $possible_root;
417             }
418              
419             # $current_parent will now be the root of the tree
420 2         7 $current_parent->insert_new_aabb( $self );
421 2         4 return;
422             }
423              
424             sub _detach_from_parent
425             {
426 4     4   11 my ($self) = @_;
427 4         10 my $current_parent = $self->parent;
428 4 50       10 return unless defined $current_parent;
429              
430 4         7 my $current_grandparent = $current_parent->parent;
431 4         10 my $is_left = ($current_parent->left_node() == $self);
432 4 100       12 if(! defined $current_grandparent ) {
433             # Parent must have been root. Just detach ourselves.
434 2 100       7 if( $is_left ) {
435 1         2 $current_parent->set_left_node( undef );
436             }
437             else {
438 1         3 $current_parent->set_right_node( undef );
439             }
440             }
441             else {
442             # Our parent is removed, and our sibling takes its place in the
443             # grandparent
444 2 50       8 my $sibling = $is_left
445             ? $current_parent->right_node
446             : $current_parent->left_node;
447 2         5 my $is_parent_left
448             = ($current_grandparent->left_node == $current_parent);
449              
450 2 50       5 if( $is_parent_left ) {
451 2         6 $current_grandparent->set_left_node( $sibling );
452             }
453             else {
454 0         0 $current_grandparent->set_right_node( $sibling );
455             }
456             }
457            
458 4         13 $self->set_parent( undef );
459 4         8 return;
460             }
461              
462              
463              
464             sub _set_node
465             {
466 131     131   197 my ($self, $node, $index) = @_;
467 131 100       305 Scalar::Util::unweaken( $self->[$index] )
468             if defined $self->[$index];
469 131         176 $self->[$index] = $node;
470 131         302 Scalar::Util::weaken( $self->[$index] );
471 131 100       274 my $former_parent = defined $node
472             ? $node->set_parent( $self )
473             : undef;
474 131         224 return $former_parent;
475             }
476              
477             sub _resize_to_fit_children
478             {
479 70     70   95 my ($self) = @_;
480 70 50       94 return if ! $self->is_branch_node;
481 70         187 my ($x, $y, $length, $height) = $self->_calculate_bounding_box_for_nodes(
482             $self->[_LEFT_NODE],
483             $self->[_RIGHT_NODE],
484             );
485              
486 70         96 $self->[_X] = $x;
487 70         77 $self->[_Y] = $y;
488 70         75 $self->[_LENGTH] = $length;
489 70         74 $self->[_HEIGHT] = $height;
490 70         82 $self->[_MAX_X] = $x + $length;
491 70         82 $self->[_MAX_Y] = $y + $height;
492              
493 70         140 return;
494             }
495              
496             sub _calculate_bounding_box_for_nodes
497             {
498 148     148   199 my ($self, $node1, $node2) = @_;
499 148 100       223 return @$node1[_X, _Y, _LENGTH, _HEIGHT] if ! defined $node2;
500 147 50       203 return @$node2[_X, _Y, _LENGTH, _HEIGHT] if ! defined $node1;
501              
502 147         201 my $min_x = List::Util::min( $node1->x, $node2->x );
503 147         276 my $min_y = List::Util::min( $node1->y, $node2->y );
504 147         212 my $max_x = List::Util::max(
505             $node1->length + $node1->x,
506             $node2->length + $node2->x,
507             );
508 147         235 my $max_y = List::Util::max(
509             $node1->height + $node1->y,
510             $node2->height + $node2->y,
511             );
512              
513 147         195 my $length = $max_x - $min_x;
514 147         179 my $height = $max_y - $min_y;
515 147         257 return ($min_x, $min_y, $length, $height);
516             }
517              
518              
519             1;
520             __END__