File Coverage

blib/lib/Game/Entities.pm
Criterion Covered Total %
statement 293 295 99.3
branch 56 68 82.3
condition 31 46 67.3
subroutine 38 38 100.0
pod 12 12 100.0
total 430 459 93.6


line stmt bran cond sub pod time code
1             # ABSTRACT: A simple entity registry for ECS designs
2             package Game::Entities;
3              
4 3     3   221333 use strict;
  3         43  
  3         104  
5 3     3   17 use warnings;
  3         6  
  3         86  
6              
7 3     3   16 use Carp ();
  3         6  
  3         40  
8 3     3   1933 use Data::Dumper ();
  3         22172  
  3         111  
9 3     3   30 use List::Util ();
  3         6  
  3         46  
10 3     3   18 use Scalar::Util ();
  3         11  
  3         39  
11 3     3   1432 use Sub::Util ();
  3         987  
  3         83  
12              
13 3     3   2543 use experimental 'signatures';
  3         11135  
  3         20  
14              
15             our $VERSION = '0.100';
16              
17             # The main entity registry, inspired by https://github.com/skypjack/entt
18              
19             use constant {
20 3         9052 SPARSE => 0,
21             DENSE => 1,
22             COMPONENTS => 2,
23              
24             # Entity GUIDs are 32 bit integers:
25             # * 12 bits used for the entity version (used for recycing entities)
26             # * 20 bits used for the entity number
27             ENTITY_MASK => 0xFFFFF, # Used to convert GUIDs to entity numbers
28             VERSION_MASK => 0xFFF, # Used to convert GUIDs to entity versions
29             ENTITY_SHIFT => 20, # The size of the entity number within a GUID
30             NULL_ENTITY => 0, # The null entity
31 3     3   641 };
  3         7  
32              
33             ## Entity "methods"
34              
35             my $base = __PACKAGE__ . '::';
36             my $version = Sub::Util::set_subname "${base}GUID::version" => sub ($e) { $e >> ENTITY_SHIFT };
37             my $entity = Sub::Util::set_subname "${base}GUID::entity" => sub ($e) { $e & ENTITY_MASK };
38             my $is_null = Sub::Util::set_subname "${base}GUID::is_null" => sub ($e) { $e->$entity == NULL_ENTITY };
39             my $format = Sub::Util::set_subname "${base}GUID::format" => sub ($e) { sprintf '%03X:%05X', $e->$version, $e->$entity };
40              
41             ## Sparse set "methods"
42              
43             my $swap_components = Sub::Util::set_subname "${base}Set::swap_components" => sub ( $set, $le, $re ) {
44             my ( $ld, $rd ) = @{ $set->[SPARSE] }[ $le, $re ];
45             @{ $set->[COMPONENTS] }[ $ld, $rd ] = @{ $set->[COMPONENTS] }[ $rd, $ld ];
46             };
47              
48             my $swap = Sub::Util::set_subname "${base}Set::swap" => sub ( $set, $le, $re ) {
49             my ( $ld, $rd ) = @{ $set->[SPARSE] }[ $le, $re ];
50             my ( $ls, $rs ) = @{ $set->[DENSE ] }[ $ld, $rd ];
51              
52             Carp::confess "Cannot swap $le and $re: they are not members of the set"
53             unless $ls == $le && $rs == $re;
54              
55             $set->$swap_components( $le, $re );
56             @{ $set->[DENSE ] }[ $ld, $rd ] = @{ $set->[DENSE ] }[ $rd, $ld ];
57             @{ $set->[SPARSE] }[ $ls, $rs ] = @{ $set->[SPARSE] }[ $rs, $ls ];
58             };
59              
60             my $contains = Sub::Util::set_subname "${base}Set::contains" => sub ( $set, $index ) {
61             my $sparse = $set->[SPARSE][$index] // return;
62             return ( $set->[DENSE][$sparse] // $index + 1 ) == $index;
63             };
64              
65             ## Private, hidden methods
66              
67             my $add_version = sub ($self, $index) {
68             $index | ( $self->{entities}[$index]->$version << ENTITY_SHIFT )
69             };
70              
71             my $generate_guid = sub ($self) {
72             Carp::croak 'Exceeded maximum number of entities'
73             if @{ $self->{entities} } >= ENTITY_MASK;
74              
75             my $guid = @{ $self->{entities} };
76             push @{ $self->{entities} }, $guid;
77              
78             return $guid;
79             };
80              
81             my $recycle_guid = sub ($self) {
82             my $next = $self->{available};
83              
84             Carp::croak 'Cannot recycle GUID if none has been released'
85             if $next->$is_null;
86              
87             my $ver = $self->{entities}[$next]->$version;
88              
89             $self->{available} = $self->{entities}[$next]->$entity;
90              
91             return $self->{entities}[$next] = $next | ( $ver << ENTITY_SHIFT );
92             };
93              
94             my $get = sub ( $self, $unsafe, $guid, @types ) {
95             my $index = $guid->$entity;
96              
97             my @got = map {
98             my $set = $self->{components}{"$_"};
99             my $sparse = $set->[SPARSE][$index];
100              
101             defined($sparse) && ( $unsafe || $self->check( $guid, $_ ) )
102             ? $set->[COMPONENTS][$sparse] : undef
103             } @types;
104              
105             return $got[0] if @types == 1;
106             return @got;
107             };
108              
109             ## Public methods
110              
111 8     8 1 19534 sub new ( $class ) {
  8         17  
  8         12  
112 8         20 my $self = bless {}, $class;
113 8         27 $self->clear;
114 8         16 $self;
115             }
116              
117 2     2 1 7 sub created ($self) { scalar @{ $self->{entities} } - 1 }
  2         4  
  2         3  
  2         3  
  2         9  
118              
119             # Get the number of created entities that are still valid; that is, that have
120             # not been deleted.
121 7     7 1 2273 sub alive ($self) {
  7         14  
  7         8  
122 7         10 my $size = @{ $self->{entities} } - 1;
  7         15  
123 7         13 my $current = $self->{available};
124              
125 7         18 until ( $current->$is_null ) {
126 10         16 $size--;
127 10         17 $current = $self->{entities}[ $current->$entity ];
128             }
129              
130 7         43 return $size;
131             }
132              
133             # Reset the registry internal storage. All entities will be deleted, and all
134             # entity IDs will be made available.
135 13     13 1 656 sub clear ($self) {
  13         21  
  13         16  
136 13         38 delete $self->{view_cache};
137              
138             # Keys in this hash are component type names (ie. the result of ref),
139             # and values are sparse sets of entities that "have" that component.
140 13         29 delete $self->{components};
141              
142             # Parameters used for recycling entity GUIDs
143             # See https://skypjack.github.io/2019-05-06-ecs-baf-part-3
144 13         32 $self->{entities} = [ undef ];
145 13         24 $self->{available} = NULL_ENTITY;
146              
147 13         24 return;
148             }
149              
150             # Create a new entity
151 111     111 1 8240 sub create ( $self, @components ) {
  111         156  
  111         137  
  111         127  
152             Carp::croak 'Component must be a reference'
153 111 100   34   489 if List::Util::any { !ref } @components;
  34         338  
154              
155 109 100       324 my $guid = $self->{available}->$is_null
156             ? $self->$generate_guid : $self->$recycle_guid;
157              
158 109         256 $self->add( $guid, @components );
159              
160 109         214 return $guid;
161             }
162              
163 206     206 1 1396 sub check ( $self, $guid, $type ) {
  206         257  
  206         253  
  206         248  
  206         237  
164 206 50       352 Carp::croak 'GUID must be defined' unless defined $guid;
165 206 100 100     721 Carp::croak 'Component name must be defined and not a reference'
166             if ! defined $type || ref $type;
167              
168 204         304 my $index = $guid->$entity;
169 204         376 my $set = $self->{components}{"$type"};
170              
171 204         332 $set->$contains( $index );
172             }
173              
174             # Add or replace a component for an entity
175 232     232 1 767 sub add ( $self, $guid, @components ) {
  232         279  
  232         288  
  232         306  
  232         309  
176 232 50       405 Carp::croak 'GUID must be defined' unless defined $guid;
177              
178 232         367 my $index = $guid->$entity;
179 232         374 for my $component (@components) {
180 155   66     451 my $name = ref($component) || Carp::croak 'Component must be a reference';
181              
182             # SPARSE DENSE COMPONENTS
183             # \ | /
184 153   100     386 for ( $self->{components}{$name} //= [ [], [], [] ] ) {
185             # Replace component
186 153 100       261 if ( $self->check( $guid => $name ) ) {
187 1         9 $_->[COMPONENTS][ $_->[SPARSE][$index] ] = $component;
188             }
189              
190             # Add component
191             else {
192 152         195 push @{ $_->[COMPONENTS] }, $component;
  152         302  
193 152         203 push @{ $_->[DENSE ] }, $index;
  152         230  
194              
195 152         192 $_->[SPARSE][$index] = $#{ $_->[DENSE] };
  152         330  
196             }
197             }
198              
199             # Adding a component invalidates any cached view that uses it
200 153         194 delete $self->{view_cache}{$_} for
201 0         0 grep { index( $_, "|$name|" ) != -1 }
202 153         346 keys %{ $self->{view_cache} },
203             }
204              
205 230         372 return;
206             }
207              
208             # Get a component for an entity
209             # The public version of this method forwards to the "safe" flavour of the
210             # private one
211 30     30 1 2565 sub get ( $self, $guid, @types ) {
  30         49  
  30         35  
  30         48  
  30         37  
212 30 50       58 Carp::croak 'GUID must be defined' unless defined $guid;
213              
214             Carp::croak 'Component name must be defined and not a reference'
215 30 100   33   111 if List::Util::any { !defined || ref } @types;
  33 100       289  
216              
217 28         92 $self->$get( 0, $guid, @types );
218             }
219              
220 17     17 1 709 sub delete ( $self, $guid, @types ) {
  17         21  
  17         23  
  17         25  
  17         33  
221 17 50       41 Carp::croak 'GUID must be defined' unless defined $guid;
222              
223 17 100       32 unless (@types) {
224             # Remove an entity and all its components
225 13 100       19 if ( my @all = keys %{ $self->{components} } ) {
  13         37  
226 2         10 $self->delete( $guid, @all );
227             }
228              
229             # We mark an entity as available by splitting the entity and the version
230             # and storing the incremented version only in the entities list, and the
231             # available entity ID in the 'available' slot
232              
233 13         25 my $ent = $guid->$entity;
234 13         26 my $ver = $guid->$version + 1;
235              
236 13         23 $self->{entities}[$ent] = $self->{available} | ( $ver << ENTITY_SHIFT );
237 13         31 $self->{available} = $ent;
238              
239 13         31 return;
240             }
241              
242             Carp::croak 'Component name must not be a reference'
243 4 50   6   25 if List::Util::any { ref } @types;
  6         14  
244              
245 4         16 for my $name (@types) {
246 6 100       14 next unless $self->check( $guid, $name );
247              
248 5         12 my $e = $guid->$entity;
249              
250 5         10 for ( $self->{components}{$name} ) {
251 5         10 my ( $i, $j ) = ( $_->[SPARSE][$e], $#{ $_->[DENSE] } );
  5         15  
252              
253 5         12 for ( $_->[DENSE], $_->[COMPONENTS] ) {
254 10         13 @{ $_ }[ $i, $j ] = @{ $_ }[ $j, $i ];
  10         18  
  10         21  
255 10         24 pop @$_;
256             }
257              
258 5   100     14 $j = $_->[DENSE][$i] // next;
259              
260 2         4 $_->[SPARSE][$j] = $i;
261             }
262              
263             # Deleting a component invalidates any cached view that uses it
264 5         22 delete $self->{view_cache}{$_} for
265 1         8 grep { index( $_, "|$name|" ) != -1 }
266 5         18 keys %{ $self->{view_cache} },
267             }
268              
269 4         12 return;
270             }
271              
272             # Checks if an entity identifier refers to a valid entity; that is, one that
273             # has been created and not deleted.
274 20     20 1 928 sub valid ( $self, $guid ) {
  20         25  
  20         25  
  20         26  
275 20 50       41 Carp::croak 'GUID must be defined' unless defined $guid;
276              
277 20         38 my $pos = $guid->$entity;
278 20         112 $pos < @{ $self->{entities} }
279 20 100 66     30 && ( $self->{entities}[$pos] // $guid + 1 ) == $guid;
280             }
281              
282 6     6 1 41 sub sort ( $self, $name, $comparator ) {
  6         9  
  6         11  
  6         8  
  6         8  
283 6   33     19 my $set = $self->{components}{$name}
284             // Carp::croak "Cannot sort $name: no such component in registry";
285              
286 6         9 my $sparse = $set->[SPARSE];
287 6         8 my $dense = $set->[DENSE];
288 6         10 my $comps = $set->[COMPONENTS];
289              
290             # Sorting a component invalidates any cached view that uses it
291 6         7 delete $self->{view_cache}{$_} for
292 0         0 grep { index( $_, "|$name|" ) != -1 }
293 6         16 keys %{ $self->{view_cache} };
294              
295 6 100       16 if ( ! ref $comparator ) {
296 4   33     12 my $other = $self->{components}{$comparator}
297             // Carp::croak "Cannot sort according to $comparator: no such component in registry";
298              
299 4         8 my $j = 0;
300 4         4 for my $i ( 0 .. $#{ $other->[DENSE] } ) {
  4         11  
301 60   50     113 my $this = $dense->[$j] // die "Undefined in set";
302 60   50     97 my $that = $other->[DENSE][$i] // die 'Undefined in other';
303              
304 60 100       85 next unless $set->$contains($that);
305              
306 40 50       104 $set->$swap( $this, $that ) unless $this == $that;
307 40         60 $j++;
308             }
309              
310 4         12 return;
311             }
312              
313             # See https://skypjack.github.io/2019-09-25-ecs-baf-part-5/
314 2 100 100     10 if ( ( prototype($comparator) // '' ) eq '$$' ) {
315             @$dense = sort {
316 1         5 $comparator->(
  40         158  
317             $comps->[ $sparse->[ $a ] ],
318             $comps->[ $sparse->[ $b ] ],
319             );
320             } @$dense;
321             }
322             else {
323 1         3 my $caller = caller;
324 3     3   34 no strict 'refs';
  3         7  
  3         700  
325             @$dense = sort {
326 1         5 local ${ $caller . '::a' } = $comps->[ $sparse->[ $a ] ];
  40         107  
  40         66  
327 40         54 local ${ $caller . '::b' } = $comps->[ $sparse->[ $b ] ];
  40         60  
328 40         56 $comparator->();
329             } @$dense;
330             }
331              
332 2         17 for my $curr ( 0 .. $#$dense ) {
333 30         41 my $next = $sparse->[ $dense->[ $curr ] ];
334              
335 30         50 while ( $next != $curr ) {
336 28         37 $set->$swap_components( @{ $dense }[ $curr, $next ] );
  28         58  
337              
338 28         41 $sparse->[ $dense->[ $curr ] ] = $curr;
339 28         33 $curr = $next;
340 28         52 $next = $sparse->[ $dense->[ $curr ] ];
341             }
342             }
343              
344 2         37 return;
345             }
346              
347             package
348             Game::Entities::View {
349 3     3   29 no overloading;
  3         6  
  3         357  
350              
351             use overload
352 19     19   141 bool => sub { 1 },
353 2     2   3 '@{}' => sub ($self, @) {
  2         5  
  2         3  
354 2         28 [ List::Util::pairs @$self ];
355 3     3   3881 };
  3         3001  
  3         37  
356              
357 15     15   28 sub new ( $class, @view ) { bless \@view, $class }
  15         23  
  15         42  
  15         19  
  15         154  
358              
359 7     7   10 sub each ( $self, $code ) {
  7         11  
  7         9  
  7         9  
360 7         65 $code->( $_->[0], @{ $_->[1] } ) for List::Util::pairs @$self
  26         200  
361             }
362              
363 2     2   4 sub first ( $self, $code ) {
  2         4  
  2         3  
  2         4  
364 2     8   23 my $res = List::Util::first { $code->( $_->[0], @{ $_->[1] } ) } List::Util::pairs @$self;
  8         46  
  8         19  
365 2 100       27 return $res ? ( $res->[0], @{ $res->[1] } ) : ();
  1         4  
366             }
367              
368 3     3   4 sub entities ($self) { ( List::Util::pairkeys @$self ) }
  3         6  
  3         4  
  3         23  
369 8     8   12 sub components ($self) { ( List::Util::pairvalues @$self ) }
  8         12  
  8         11  
  8         48  
370             }
371              
372 22     22 1 14178 sub view ( $self, @types ) {
  22         39  
  22         38  
  22         30  
373             # Return a view for all entities
374             # The view of all entities is never cached
375 22 100       55 unless (@types) {
376             return Game::Entities::View->new(
377 13         23 map {; $self->$add_version( $_->$entity ) => [] }
378             grep $self->valid( $_ ),
379 3         7 @{ $self->{entities} }[ 1 .. $#{ $self->{entities} } ]
  3         12  
  3         8  
380             )
381             }
382              
383             # Return a view for a single component
384 19 100       55 if ( @types == 1 ) {
385 13         27 my ($name) = @types;
386              
387 13   66     62 return $self->{view_cache}{"|$name|"} //= do {
388 9         19 my $set = $self->{components}{$name};
389 9         15 my $comps = $set->[COMPONENTS];
390              
391             Game::Entities::View->new(
392             map {
393 138         223 my ( $i, $e ) = ( $_, $set->[DENSE][$_] );
394 138         199 $self->$add_version($e) => [ $comps->[$i] ];
395 9         18 } 0 .. $#{ $set->[DENSE] }
  9         24  
396             )
397             };
398             }
399              
400             # Return a view for entities that have the specified set of components
401 6   66     33 return $self->{view_cache}{'|' . join( '|', @types ) . '|' } //= do {
402 3         7 my $map = $self->{components};
403              
404             my ( $short, @rest ) = sort {
405 3   50     12 @{ $map->{$a}[DENSE] // [] } <=> @{ $map->{$b}[DENSE] // [] }
  3   50     6  
  3         10  
  3         17  
406             } @types;
407              
408 3         7 my $set = $self->{components}{$short};
409 3         4 my $comps = $set->[COMPONENTS];
410              
411 3         6 my @view;
412 3         5 while ( my ( $i, $e ) = each @{ $set->[DENSE] } ) {
  14         45  
413 11         19 my $guid = $self->$add_version($e);
414              
415 11 100   11   38 next unless List::Util::all { $self->check( $guid => $_ ) } @rest;
  11         25  
416              
417             push @view, $guid => [
418             map {
419 9 100       24 $_ eq $short
  18         43  
420             ? $comps->[$i]
421             : $self->$get( 1, $guid, $_ )
422             } @types
423             ];
424             }
425              
426 3         21 Game::Entities::View->new(@view);
427             };
428             }
429              
430 2     2   6 sub _dump_entities ( $self, @types ) {
  2         3  
  2         4  
  2         4  
431 2         4 local $Data::Dumper::Terse = 1;
432 2         4 local $Data::Dumper::Indent = 0;
433              
434 2         5 my @names = @types;
435 2 50       5 @names = sort keys %{ $self->{components} } unless @types;
  2         12  
436              
437 2         6 my $print = ! defined wantarray;
438 1 50   1   12 open my $fh, '>', \my $out or $print = 1;
  1         2  
  1         8  
  2         76  
439 2 100       984 $fh = *STDOUT if $print;
440              
441 2         7 my $index;
442 2         6 for (@names) {
443 4 50       20 next unless my $set = $self->{components}{$_};
444 4 50 50     6 next unless @{ $set->[SPARSE] // [] };
  4         16  
445              
446 4 50 33     64 print $fh "# [$_]\n" if !@types || @names > 1;
447 4         27 print $fh "# SPARSE DENSE WHERE COMPONENT\n";
448              
449 4         13 for ( 0 .. $#{ $set->[SPARSE] } ) {
  4         15  
450 18         720 my $component = $set->[COMPONENTS][$_];
451              
452 18 100 100     166 print $fh sprintf "# %6s %5s %12X %s\n",
      100        
      100        
453             $set->[SPARSE][$_] // '---',
454             $set->[DENSE][$_] // '---',
455             Scalar::Util::refaddr($component) // 0,
456             defined $component
457             ? Data::Dumper::Dumper($component) =~ s/[\n\r]//gr : '---';
458             }
459              
460 4 100       27 print $fh "#\n" if $index++ < $#names;
461             }
462              
463 2 100       25 $out unless $print;
464             }
465              
466             # Clean our namespace
467             delete $Game::Entities::{$_} for qw(
468             COMPONENTS
469             DENSE
470             ENTITY_MASK
471             ENTITY_SHIFT
472             NULL_ENTITY
473             SPARSE
474             VERSION_MASK
475             );
476              
477             1;