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   220764 use strict;
  3         34  
  3         88  
5 3     3   15 use warnings;
  3         6  
  3         69  
6              
7 3     3   13 use Carp ();
  3         6  
  3         36  
8 3     3   2074 use Data::Dumper ();
  3         21720  
  3         87  
9 3     3   20 use List::Util ();
  3         6  
  3         41  
10 3     3   12 use Scalar::Util ();
  3         6  
  3         37  
11 3     3   1507 use Sub::Util ();
  3         947  
  3         83  
12              
13 3     3   1793 use experimental 'signatures';
  3         11005  
  3         21  
14              
15             our $VERSION = '0.011';
16              
17             # The main entity registry, inspired by https://github.com/skypjack/entt
18              
19             use constant {
20 3         9122 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   974 };
  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 8     8 1 18904 sub new ( $class ) {
  8         17  
  8         13  
112 8         15 my $self = bless {}, $class;
113 8         23 $self->clear;
114 8         18 $self;
115             }
116              
117 2     2 1 7 sub created ($self) { scalar @{ $self->{entities} } - 1 }
  2         3  
  2         3  
  2         3  
  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 2291 sub alive ($self) {
  7         10  
  7         11  
122 7         10 my $size = @{ $self->{entities} } - 1;
  7         14  
123 7         16 my $current = $self->{available};
124              
125 7         14 until ( $current->$is_null ) {
126 10         12 $size--;
127 10         20 $current = $self->{entities}[ $current->$entity ];
128             }
129              
130 7         27 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 653 sub clear ($self) {
  13         17  
  13         18  
136 13         33 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         22 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         30 $self->{entities} = [ undef ];
145 13         73 $self->{available} = NULL_ENTITY;
146              
147 13         31 return;
148             }
149              
150             # Create a new entity
151 111     111 1 7817 sub create ( $self, @components ) {
  111         148  
  111         138  
  111         128  
152             Carp::croak 'Component must be a reference'
153 111 100   34   470 if List::Util::any { !ref } @components;
  34         322  
154              
155 109 100       325 my $guid = $self->{available}->$is_null
156             ? $self->$generate_guid : $self->$recycle_guid;
157              
158 109         263 $self->add( $guid, @components );
159              
160 109         215 return $guid;
161             }
162              
163 206     206 1 1405 sub check ( $self, $guid, $type ) {
  206         256  
  206         258  
  206         265  
  206         238  
164 206 50       348 Carp::croak 'GUID must be defined' unless defined $guid;
165 206 100 100     732 Carp::croak 'Component name must be defined and not a reference'
166             if ! defined $type || ref $type;
167              
168 204         295 my $index = $guid->$entity;
169 204         355 my $set = $self->{components}{"$type"};
170              
171 204         306 $set->$contains( $index );
172             }
173              
174             # Add or replace a component for an entity
175 232     232 1 789 sub add ( $self, $guid, @components ) {
  232         286  
  232         298  
  232         290  
  232         271  
176 232 50       408 Carp::croak 'GUID must be defined' unless defined $guid;
177              
178 232         322 my $index = $guid->$entity;
179 232         366 for my $component (@components) {
180 155   66     450 my $name = ref($component) || Carp::croak 'Component must be a reference';
181              
182             # SPARSE DENSE COMPONENTS
183             # \ | /
184 153   100     374 for ( $self->{components}{$name} //= [ [], [], [] ] ) {
185             # Replace component
186 153 100       254 if ( $self->check( $guid => $name ) ) {
187 1         8 $_->[COMPONENTS][ $_->[SPARSE][$index] ] = $component;
188             }
189              
190             # Add component
191             else {
192 152         195 push @{ $_->[COMPONENTS] }, $component;
  152         280  
193 152         191 push @{ $_->[DENSE ] }, $index;
  152         228  
194              
195 152         194 $_->[SPARSE][$index] = $#{ $_->[DENSE] };
  152         312  
196             }
197             }
198              
199             # Adding a component invalidates any cached view that uses it
200 153         201 delete $self->{view_cache}{$_} for
201 0         0 grep { index( $_, "|$name|" ) != -1 }
202 153         346 keys %{ $self->{view_cache} },
203             }
204              
205 230         364 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 2482 sub get ( $self, $guid, @types ) {
  30         41  
  30         41  
  30         43  
  30         38  
212 30 50       60 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   138 if List::Util::any { !defined || ref } @types;
  33 100       278  
216              
217 28         93 $self->$get( 0, $guid, @types );
218             }
219              
220 17     17 1 753 sub delete ( $self, $guid, @types ) {
  17         24  
  17         18  
  17         26  
  17         32  
221 17 50       50 Carp::croak 'GUID must be defined' unless defined $guid;
222              
223 17 100       35 unless (@types) {
224             # Remove an entity and all its components
225 13 100       20 if ( my @all = keys %{ $self->{components} } ) {
  13         40  
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         25 my $ent = $guid->$entity;
234 13         20 my $ver = $guid->$version + 1;
235              
236 13         26 $self->{entities}[$ent] = $self->{available} | ( $ver << ENTITY_SHIFT );
237 13         17 $self->{available} = $ent;
238              
239 13         28 return;
240             }
241              
242             Carp::croak 'Component name must not be a reference'
243 4 50   6   24 if List::Util::any { ref } @types;
  6         13  
244              
245 4         16 for my $name (@types) {
246 6 100       15 next unless $self->check( $guid, $name );
247              
248 5         10 my $e = $guid->$entity;
249              
250 5         11 for ( $self->{components}{$name} ) {
251 5         9 my ( $i, $j ) = ( $_->[SPARSE][$e], $#{ $_->[DENSE] } );
  5         12  
252              
253 5         11 for ( $_->[DENSE], $_->[COMPONENTS] ) {
254 10         16 @{ $_ }[ $i, $j ] = @{ $_ }[ $j, $i ];
  10         24  
  10         17  
255 10         30 pop @$_;
256             }
257              
258 5   100     13 $j = $_->[DENSE][$i] // next;
259              
260 2         5 $_->[SPARSE][$j] = $i;
261             }
262              
263             # Deleting a component invalidates any cached view that uses it
264 5         7 delete $self->{view_cache}{$_} for
265 1         6 grep { index( $_, "|$name|" ) != -1 }
266 5         13 keys %{ $self->{view_cache} },
267             }
268              
269 4         11 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 948 sub valid ( $self, $guid ) {
  20         25  
  20         25  
  20         24  
275 20 50       41 Carp::croak 'GUID must be defined' unless defined $guid;
276              
277 20         29 my $pos = $guid->$entity;
278 20         108 $pos < @{ $self->{entities} }
279 20 100 66     29 && ( $self->{entities}[$pos] // $guid + 1 ) == $guid;
280             }
281              
282 6     6 1 41 sub sort ( $self, $name, $comparator ) {
  6         8  
  6         10  
  6         9  
  6         9  
283 6   33     16 my $set = $self->{components}{$name}
284             // Carp::croak "Cannot sort $name: no such component in registry";
285              
286 6         10 my $sparse = $set->[SPARSE];
287 6         8 my $dense = $set->[DENSE];
288 6         7 my $comps = $set->[COMPONENTS];
289              
290             # Sorting a component invalidates any cached view that uses it
291 6         10 delete $self->{view_cache}{$_} for
292 0         0 grep { index( $_, "|$name|" ) != -1 }
293 6         15 keys %{ $self->{view_cache} };
294              
295 6 100       14 if ( ! ref $comparator ) {
296 4   33     11 my $other = $self->{components}{$comparator}
297             // Carp::croak "Cannot sort according to $comparator: no such component in registry";
298              
299 4         5 my $j = 0;
300 4         6 for my $i ( 0 .. $#{ $other->[DENSE] } ) {
  4         11  
301 60   50     139 my $this = $dense->[$j] // die "Undefined in set";
302 60   50     96 my $that = $other->[DENSE][$i] // die 'Undefined in other';
303              
304 60 100       83 next unless $set->$contains($that);
305              
306 40 50       111 $set->$swap( $this, $that ) unless $this == $that;
307 40         59 $j++;
308             }
309              
310 4         10 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         4 $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   28 no strict 'refs';
  3         7  
  3         714  
325             @$dense = sort {
326 1         4 local ${ $caller . '::a' } = $comps->[ $sparse->[ $a ] ];
  40         102  
  40         62  
327 40         53 local ${ $caller . '::b' } = $comps->[ $sparse->[ $b ] ];
  40         54  
328 40         59 $comparator->();
329             } @$dense;
330             }
331              
332 2         16 for my $curr ( 0 .. $#$dense ) {
333 30         39 my $next = $sparse->[ $dense->[ $curr ] ];
334              
335 30         53 while ( $next != $curr ) {
336 28         38 $set->$swap_components( @{ $dense }[ $curr, $next ] );
  28         58  
337              
338 28         43 $sparse->[ $dense->[ $curr ] ] = $curr;
339 28         48 $curr = $next;
340 28         49 $next = $sparse->[ $dense->[ $curr ] ];
341             }
342             }
343              
344 2         38 return;
345             }
346              
347             package
348             Game::Entities::View {
349 3     3   25 no overloading;
  3         6  
  3         372  
350              
351             use overload
352 19     19   152 bool => sub { 1 },
353 2     2   3 '@{}' => sub ($self, @) {
  2         4  
  2         3  
354 2         42 [ List::Util::pairs @$self ];
355 3     3   3818 };
  3         2975  
  3         31  
356              
357 15     15   24 sub new ( $class, @view ) { bless \@view, $class }
  15         23  
  15         53  
  15         21  
  15         157  
358              
359 7     7   10 sub each ( $self, $code ) {
  7         9  
  7         10  
  7         9  
360 7         59 $code->( $_->[0], @{ $_->[1] } ) for List::Util::pairs @$self
  26         233  
361             }
362              
363 2     2   9 sub first ( $self, $code ) {
  2         4  
  2         4  
  2         3  
364 2     8   24 my $res = List::Util::first { $code->( $_->[0], @{ $_->[1] } ) } List::Util::pairs @$self;
  8         45  
  8         19  
365 2 100       31 return $res ? ( $res->[0], @{ $res->[1] } ) : ();
  1         13  
366             }
367              
368 3     3   5 sub entities ($self) { ( List::Util::pairkeys @$self ) }
  3         5  
  3         6  
  3         24  
369 8     8   12 sub components ($self) { ( List::Util::pairvalues @$self ) }
  8         13  
  8         11  
  8         51  
370             }
371              
372 22     22 1 14139 sub view ( $self, @types ) {
  22         36  
  22         43  
  22         28  
373             # Return a view for all entities
374             # The view of all entities is never cached
375 22 100       68 unless (@types) {
376             return Game::Entities::View->new(
377 13         23 map {; $self->$add_version( $_->$entity ) => [] }
378             grep $self->valid( $_ ),
379 3         6 @{ $self->{entities} }[ 1 .. $#{ $self->{entities} } ]
  3         10  
  3         6  
380             )
381             }
382              
383             # Return a view for a single component
384 19 100       51 if ( @types == 1 ) {
385 13         23 my ($name) = @types;
386              
387 13   66     70 return $self->{view_cache}{"|$name|"} //= do {
388 9         18 my $set = $self->{components}{$name};
389 9         14 my $comps = $set->[COMPONENTS];
390              
391             Game::Entities::View->new(
392             map {
393 138         221 my ( $i, $e ) = ( $_, $set->[DENSE][$_] );
394 138         199 $self->$add_version($e) => [ $comps->[$i] ];
395 9         15 } 0 .. $#{ $set->[DENSE] }
  9         26  
396             )
397             };
398             }
399              
400             # Return a view for entities that have the specified set of components
401 6   66     34 return $self->{view_cache}{'|' . join( '|', @types ) . '|' } //= do {
402 3         6 my $map = $self->{components};
403              
404             my ( $short, @rest ) = sort {
405 3   50     12 @{ $map->{$a}[DENSE] // [] } <=> @{ $map->{$b}[DENSE] // [] }
  3   50     6  
  3         8  
  3         17  
406             } @types;
407              
408 3         7 my $set = $self->{components}{$short};
409 3         4 my $comps = $set->[COMPONENTS];
410              
411 3         5 my @view;
412 3         6 while ( my ( $i, $e ) = each @{ $set->[DENSE] } ) {
  14         48  
413 11         19 my $guid = $self->$add_version($e);
414              
415 11 100   11   47 next unless List::Util::all { $self->check( $guid => $_ ) } @rest;
  11         27  
416              
417             push @view, $guid => [
418             map {
419 9 100       26 $_ eq $short
  18         41  
420             ? $comps->[$i]
421             : $self->$get( 1, $guid, $_ )
422             } @types
423             ];
424             }
425              
426 3         10 Game::Entities::View->new(@view);
427             };
428             }
429              
430 2     2   3 sub _dump_entities ( $self, @types ) {
  2         5  
  2         3  
  2         4  
431 2         3 local $Data::Dumper::Terse = 1;
432 2         6 local $Data::Dumper::Indent = 0;
433              
434 2         3 my @names = @types;
435 2 50       7 @names = sort keys %{ $self->{components} } unless @types;
  2         9  
436              
437 2         6 my $print = ! defined wantarray;
438 1 50   1   7 open my $fh, '>', \my $out or $print = 1;
  1         2  
  1         5  
  2         47  
439 2 100       906 $fh = *STDOUT if $print;
440              
441 2         7 my $index;
442 2         6 for (@names) {
443 4 50       18 next unless my $set = $self->{components}{$_};
444 4 50 50     7 next unless @{ $set->[SPARSE] // [] };
  4         13  
445              
446 4 50 33     72 print $fh "# [$_]\n" if !@types || @names > 1;
447 4         29 print $fh "# SPARSE DENSE WHERE COMPONENT\n";
448              
449 4         11 for ( 0 .. $#{ $set->[SPARSE] } ) {
  4         16  
450 18         670 my $component = $set->[COMPONENTS][$_];
451              
452 18 100 100     159 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       33 print $fh "#\n" if $index++ < $#names;
461             }
462              
463 2 100       47 $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;