Creating the deck: an introduction to ECS systems, part 3
With our 54 card entities created, now we need a way to group them together to form the game's dungeon deck. In the same spirit as the previous lectures, we create a system named DeckCreationSystem to do so, which is defined as follows:
This system introduces several new concepts:
Entities.ForEach: The most convenient way to iterate over our entities is via the ECS's Entities.ForEach abstraction. It is similar to a regular, C# foreach loop, but carefully crafted to generate the most optimal code to traverse a given set of entities. In this particular case, we use the .WithAll<> filter to specify we are only interested in the entities that have an associated CardData component. The foreach lambda always takes the entity as its first parameter, which we use to cache the card into our Deck native list.
The Run invocation executes the loop on the main thread. There are alternative invocations that allow the loop to run on an independent thread, but in most of the code in this course there will not be a need to do that because the code will be simple enough that it would actually be more expensive to create a new thread for it than just running it on the main thread.
Native collections: We store the card entities in a native list, which is a native collection provided by the ECS. Native collections provide a safe way for jobs to access some data from the main thread directly, without needing a copy. We initialize it with our deck size (54 cards) and using the persistent memory allocator, because we need the deck's data to persist for the entire lifetime of the game. We use the OnCreate and OnDestroy entry points to initialize and dispose the list (note that you always need to explicitly dispose a native collection; should you forget to do so, you will get a runtime exception informing you of the resulting memory leak). You can find the official documentation for native collections here.
System barriers: Note the use of a system barrier via the ecbSystem variable. When the BarrierSystem updates, the command buffers playback on the main thread in the order they were created. This extra step is required so that memory management can be centralized and determinism of the generated entities and components can be guaranteed. A command buffer is represented by the EntityCommandBuffer type.
The deck sorting is performed using the well-known Fisher-Yates shuffle algorithm, which is particularly convenient for card-based games.
We use the retrievedDeck boolean to make sure we only add the 54 cards to the deck the first time the system runs; otherwise, we would keep adding them every frame ad infinitum.
Event-like entities: This system creates an event-like entity, which will be picked up by a different system. This entity has the following component attached to it:
The Amount field indicates the number of cards to be dealt from the deck (in this case, when the game starts). This component will be processed by a different system that we will cover later.