File Coverage

blib/lib/Game/Entities.pm
Criterion Covered Total %
statement 293 295 99.3
branch 56 68 82.3
condition 29 44 65.9
subroutine 38 38 100.0
pod 12 12 100.0
total 428 457 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   214176 use strict;
  3         32  
  3         89  
5 3     3   15 use warnings;
  3         6  
  3         90  
6              
7 3     3   15 use Carp ();
  3         6  
  3         37  
8 3     3   2062 use Data::Dumper ();
  3         21907  
  3         116  
9 3     3   23 use List::Util ();
  3         6  
  3         48  
10 3     3   13 use Scalar::Util ();
  3         5  
  3         38  
11 3     3   1535 use Sub::Util ();
  3         896  
  3         85  
12              
13 3     3   1582 use experimental 'signatures';
  3         10992  
  3         23  
14              
15             our $VERSION = '0.010';
16              
17             # The main entity registry, inspired by https://github.com/skypjack/entt
18              
19             use constant {
20 3         8815 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   596 };
  3         6  
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 7     7 1 16225 sub new ( $class ) {
  7         15  
  7         10  
112 7         16 my $self = bless {}, $class;
113 7         22 $self->clear;
114 7         18 $self;
115             }
116              
117 2     2 1 7 sub created ($self) { scalar @{ $self->{entities} } - 1 }
  2         3  
  2         33  
  2         7  
  2         10  
118              
119             # Get the number of created entities that are still valid; that is, that have
120             # not been deleted.
121 7     7 1 2321 sub alive ($self) {
  7         11  
  7         10  
122 7         10 my $size = @{ $self->{entities} } - 1;
  7         14  
123 7         10 my $current = $self->{available};
124              
125 7         19 until ( $current->$is_null ) {
126 10         12 $size--;
127 10         19 $current = $self->{entities}[ $current->$entity ];
128             }
129              
130 7         33 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 12     12 1 666 sub clear ($self) {
  12         23  
  12         16  
136 12         30 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 12         25 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 12         29 $self->{entities} = [ undef ];
145 12         19 $self->{available} = NULL_ENTITY;
146              
147 12         22 return;
148             }
149              
150             # Create a new entity
151 81     81 1 7957 sub create ( $self, @components ) {
  81         114  
  81         104  
  81         98  
152             Carp::croak 'Component must be a reference'
153 81 100   34   347 if List::Util::any { !ref } @components;
  34         390  
154              
155 79 100       241 my $guid = $self->{available}->$is_null
156             ? $self->$generate_guid : $self->$recycle_guid;
157              
158 79         196 $self->add( $guid, @components );
159              
160 79         181 return $guid;
161             }
162              
163 151     151 1 1321 sub check ( $self, $guid, $type ) {
  151         194  
  151         186  
  151         191  
  151         178  
164 151 50       253 Carp::croak 'GUID must be defined' unless defined $guid;
165 151 100 100     600 Carp::croak 'Component name must be defined and not a reference'
166             if ! defined $type || ref $type;
167              
168 149         223 my $index = $guid->$entity;
169 149         259 my $set = $self->{components}{"$type"};
170              
171 149         244 $set->$contains( $index );
172             }
173              
174             # Add or replace a component for an entity
175 147     147 1 525 sub add ( $self, $guid, @components ) {
  147         185  
  147         179  
  147         186  
  147         181  
176 147 50       254 Carp::croak 'GUID must be defined' unless defined $guid;
177              
178 147         214 my $index = $guid->$entity;
179 147         238 for my $component (@components) {
180 100   66     406 my $name = ref($component) || Carp::croak 'Component must be a reference';
181              
182             # SPARSE DENSE COMPONENTS
183             # \ | /
184 98   100     260 for ( $self->{components}{$name} //= [ [], [], [] ] ) {
185             # Replace component
186 98 100       175 if ( $self->check( $guid => $name ) ) {
187 1         9 $_->[COMPONENTS][ $_->[SPARSE][$index] ] = $component;
188             }
189              
190             # Add component
191             else {
192 97         122 push @{ $_->[COMPONENTS] }, $component;
  97         179  
193 97         138 push @{ $_->[DENSE ] }, $index;
  97         138  
194              
195 97         126 $_->[SPARSE][$index] = $#{ $_->[DENSE] };
  97         208  
196             }
197             }
198              
199             # Adding a component invalidates any cached view that uses it
200 98         134 delete $self->{view_cache}{$_} for
201 0         0 grep { index( $_, "|$name|" ) != -1 }
202 98         235 keys %{ $self->{view_cache} },
203             }
204              
205 145         263 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 2604 sub get ( $self, $guid, @types ) {
  30         45  
  30         39  
  30         42  
  30         40  
212 30 50       57 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   132 if List::Util::any { !defined || ref } @types;
  33 100       296  
216              
217 28         88 $self->$get( 0, $guid, @types );
218             }
219              
220 17     17 1 710 sub delete ( $self, $guid, @types ) {
  17         22  
  17         22  
  17         21  
  17         33  
221 17 50       40 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       20 if ( my @all = keys %{ $self->{components} } ) {
  13         33  
226 2         12 $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         24 my $ent = $guid->$entity;
234 13         24 my $ver = $guid->$version + 1;
235              
236 13         26 $self->{entities}[$ent] = $self->{available} | ( $ver << ENTITY_SHIFT );
237 13         18 $self->{available} = $ent;
238              
239 13         27 return;
240             }
241              
242             Carp::croak 'Component name must not be a reference'
243 4 50   6   20 if List::Util::any { ref } @types;
  6         14  
244              
245 4         15 for my $name (@types) {
246 6 100       15 next unless $self->check( $guid, $name );
247              
248 5         13 my $e = $guid->$entity;
249              
250 5         13 for ( $self->{components}{$name} ) {
251 5         9 my ( $i, $j ) = ( $_->[SPARSE][$e], $#{ $_->[DENSE] } );
  5         25  
252              
253 5         11 for ( $_->[DENSE], $_->[COMPONENTS] ) {
254 10         15 @{ $_ }[ $i, $j ] = @{ $_ }[ $j, $i ];
  10         16  
  10         16  
255 10         23 pop @$_;
256             }
257              
258 5   100     13 $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         9 delete $self->{view_cache}{$_} for
265 1         8 grep { index( $_, "|$name|" ) != -1 }
266 5         14 keys %{ $self->{view_cache} },
267             }
268              
269 4         20 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 909 sub valid ( $self, $guid ) {
  20         28  
  20         28  
  20         22  
275 20 50       38 Carp::croak 'GUID must be defined' unless defined $guid;
276              
277 20         36 my $pos = $guid->$entity;
278 20         127 $pos < @{ $self->{entities} }
279 20 100 66     24 && ( $self->{entities}[$pos] // $guid + 1 ) == $guid;
280             }
281              
282 3     3 1 22 sub sort ( $self, $name, $comparator ) {
  3         5  
  3         5  
  3         5  
  3         4  
283 3   33     8 my $set = $self->{components}{$name}
284             // Carp::croak "Cannot sort $name: no such component in registry";
285              
286 3         5 my $sparse = $set->[SPARSE];
287 3         6 my $dense = $set->[DENSE];
288 3         4 my $comps = $set->[COMPONENTS];
289              
290             # Sorting a component invalidates any cached view that uses it
291 3         4 delete $self->{view_cache}{$_} for
292 0         0 grep { index( $_, "|$name|" ) != -1 }
293 3         9 keys %{ $self->{view_cache} };
294              
295 3 100       9 if ( ! ref $comparator ) {
296 2   33     6 my $other = $self->{components}{$comparator}
297             // Carp::croak "Cannot sort according to $comparator: no such component in registry";
298              
299 2         2 my $j = 0;
300 2         3 for my $i ( 0 .. $#{ $other->[DENSE] } ) {
  2         6  
301 26 100       33 last if $i > $#{ $dense };
  26         43  
302              
303 25   50     47 my $this = $dense->[$j] // die "Undefined in set";
304 25   50     40 my $that = $other->[DENSE][$i] // die 'Undefined in other';
305              
306 25 100       39 next unless $set->$contains($that);
307              
308 17 50       49 $set->$swap( $this, $that ) unless $this == $that;
309 17         29 $j++;
310             }
311              
312 2         8 return;
313             }
314              
315             # See https://skypjack.github.io/2019-09-25-ecs-baf-part-5/
316 1         3 my $caller = caller;
317             {
318 3     3   84 no strict 'refs';
  3         8  
  3         734  
  1         2  
319             @$dense = sort {
320 1         4 local ${ $caller . '::a' } = $comps->[ $sparse->[ $a ] ];
  40         105  
  40         67  
321 40         48 local ${ $caller . '::b' } = $comps->[ $sparse->[ $b ] ];
  40         67  
322 40         52 $comparator->();
323             } @$dense;
324             }
325              
326 1         8 for my $curr ( 0 .. $#$dense ) {
327 15         18 my $next = $sparse->[ $dense->[ $curr ] ];
328              
329 15         30 while ( $next != $curr ) {
330 14         22 $set->$swap_components( @{ $dense }[ $curr, $next ] );
  14         30  
331              
332 14         24 $sparse->[ $dense->[ $curr ] ] = $curr;
333 14         17 $curr = $next;
334 14         40 $next = $sparse->[ $dense->[ $curr ] ];
335             }
336             }
337              
338 1         35 return;
339             }
340              
341             package
342             Game::Entities::View {
343 3     3   23 no overloading;
  3         15  
  3         371  
344              
345             use overload
346 16     16   165 bool => sub { 1 },
347 2     2   3 '@{}' => sub ($self, @) {
  2         3  
  2         4  
348 2         25 [ List::Util::pairs @$self ];
349 3     3   3794 };
  3         2994  
  3         30  
350              
351 12     12   19 sub new ( $class, @view ) { bless \@view, $class }
  12         18  
  12         35  
  12         18  
  12         138  
352              
353 7     7   11 sub each ( $self, $code ) {
  7         7  
  7         10  
  7         9  
354 7         48 $code->( $_->[0], @{ $_->[1] } ) for List::Util::pairs @$self
  26         202  
355             }
356              
357 2     2   4 sub first ( $self, $code ) {
  2         4  
  2         4  
  2         2  
358 2     8   25 my $res = List::Util::first { $code->( $_->[0], @{ $_->[1] } ) } List::Util::pairs @$self;
  8         54  
  8         16  
359 2 100       38 return $res ? ( $res->[0], @{ $res->[1] } ) : ();
  1         4  
360             }
361              
362 3     3   6 sub entities ($self) { ( List::Util::pairkeys @$self ) }
  3         4  
  3         5  
  3         34  
363 5     5   9 sub components ($self) { ( List::Util::pairvalues @$self ) }
  5         8  
  5         7  
  5         31  
364             }
365              
366 19     19 1 12656 sub view ( $self, @types ) {
  19         30  
  19         36  
  19         24  
367             # Return a view for all entities
368             # The view of all entities is never cached
369 19 100       53 unless (@types) {
370             return Game::Entities::View->new(
371 13         30 map {; $self->$add_version( $_->$entity ) => [] }
372             grep $self->valid( $_ ),
373 3         5 @{ $self->{entities} }[ 1 .. $#{ $self->{entities} } ]
  3         8  
  3         12  
374             )
375             }
376              
377             # Return a view for a single component
378 16 100       41 if ( @types == 1 ) {
379 10         21 my ($name) = @types;
380              
381 10   66     44 return $self->{view_cache}{"|$name|"} //= do {
382 6         14 my $set = $self->{components}{$name};
383 6         12 my $comps = $set->[COMPONENTS];
384              
385             Game::Entities::View->new(
386             map {
387 83         135 my ( $i, $e ) = ( $_, $set->[DENSE][$_] );
388 83         134 $self->$add_version($e) => [ $comps->[$i] ];
389 6         13 } 0 .. $#{ $set->[DENSE] }
  6         18  
390             )
391             };
392             }
393              
394             # Return a view for entities that have the specified set of components
395 6   66     29 return $self->{view_cache}{'|' . join( '|', @types ) . '|' } //= do {
396 3         5 my $map = $self->{components};
397              
398             my ( $short, @rest ) = sort {
399 3   50     11 @{ $map->{$a}[DENSE] // [] } <=> @{ $map->{$b}[DENSE] // [] }
  3   50     6  
  3         9  
  3         14  
400             } @types;
401              
402 3         7 my $set = $self->{components}{$short};
403 3         5 my $comps = $set->[COMPONENTS];
404              
405 3         5 my @view;
406 3         5 while ( my ( $i, $e ) = each @{ $set->[DENSE] } ) {
  14         47  
407 11         20 my $guid = $self->$add_version($e);
408              
409 11 100   11   39 next unless List::Util::all { $self->check( $guid => $_ ) } @rest;
  11         24  
410              
411             push @view, $guid => [
412             map {
413 9 100       24 $_ eq $short
  18         42  
414             ? $comps->[$i]
415             : $self->$get( 1, $guid, $_ )
416             } @types
417             ];
418             }
419              
420 3         10 Game::Entities::View->new(@view);
421             };
422             }
423              
424 2     2   4 sub _dump_entities ( $self, @types ) {
  2         3  
  2         4  
  2         3  
425 2         3 local $Data::Dumper::Terse = 1;
426 2         5 local $Data::Dumper::Indent = 0;
427              
428 2         5 my @names = @types;
429 2 50       6 @names = sort keys %{ $self->{components} } unless @types;
  2         10  
430              
431 2         7 my $print = ! defined wantarray;
432 1 50   1   10 open my $fh, '>', \my $out or $print = 1;
  1         2  
  1         9  
  2         87  
433 2 100       1023 $fh = *STDOUT if $print;
434              
435 2         10 my $index;
436 2         6 for (@names) {
437 4 50       20 next unless my $set = $self->{components}{$_};
438 4 50 50     6 next unless @{ $set->[SPARSE] // [] };
  4         18  
439              
440 4 50 33     87 print $fh "# [$_]\n" if !@types || @names > 1;
441 4         27 print $fh "# SPARSE DENSE WHERE COMPONENT\n";
442              
443 4         11 for ( 0 .. $#{ $set->[SPARSE] } ) {
  4         15  
444 18         748 my $component = $set->[COMPONENTS][$_];
445              
446 18 100 100     159 print $fh sprintf "# %6s %5s %12X %s\n",
      100        
      100        
447             $set->[SPARSE][$_] // '---',
448             $set->[DENSE][$_] // '---',
449             Scalar::Util::refaddr($component) // 0,
450             defined $component
451             ? Data::Dumper::Dumper($component) =~ s/[\n\r]//gr : '---';
452             }
453              
454 4 100       27 print $fh "#\n" if $index++ < $#names;
455             }
456              
457 2 100       21 $out unless $print;
458             }
459              
460             # Clean our namespace
461             delete $Game::Entities::{$_} for qw(
462             COMPONENTS
463             DENSE
464             ENTITY_MASK
465             ENTITY_SHIFT
466             NULL_ENTITY
467             SPARSE
468             VERSION_MASK
469             );
470              
471             1;