File Coverage

blib/lib/Game/Entities.pm
Criterion Covered Total %
statement 295 295 100.0
branch 60 72 83.3
condition 31 46 67.3
subroutine 38 38 100.0
pod 12 12 100.0
total 436 463 94.1


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