 This ECS demo uses a hybrid of ECS and the conventional game objects and mono behaviors. The player character, the gun, the floor, and the lights are all just conventional game objects, but then the bullets and the enemies are modeled as entities and they're rendered using the hybrid.rendering package. Collision detection between the bullets and the enemies is all done in ECS and then also is the collision check between enemies and the player. And so the player is represented doubly not just as a game object, but also as an entity. When a bullet hits an enemy, the enemy is destroyed, and in its place, using the conventional game objects and mono behaviors, we play a sound and some particle effects. Because these bullet impacts get frequently created, we actually use a pool of bullet impact objects. Otherwise, we'd be creating garbage and also might get some stutters when the bullet impacts are created. So first, looking at the scene hierarchy, we have our player object, which is from a prefab, and it includes the gun as a child. The player itself has a few scripts attached to it, which we'll look at, including these entity conversion scripts, which create at scene start the entity representation of the player. Looking at the camera, it has some cinema machine components, which are responsible for the camera following the player as the player moves around. That's just some off-the-shelf camera logic. This post-processing volume adds a number of standard post-processing effects. The ground is just a flat plane with a mesh collider. I'm not sure why they use a mesh collider where a simple box collider would suffice. They probably made it a mesh collider simply out of convenience. The scene has two directional lights from up above. The main light casts shadows, and the other one is just four highlights as the name implies. This game settings object has a script that holds the main global state of the game. The enemy spawner script is what spawns enemies around the player at a regular interval. You can see there's a flag here to disable using ECS, so that the enemies are spawned as just conventional game objects. This is here to demonstrate the performance difference, but I'll just leave it checked. And lastly, the bullet impact pool script keeps a pool of bullet impact objects, which are created when bullets hit enemies. Looking back at the player in our player shooting script, we also have an option to disable ECS for spawning the bullets. So that's why in the bullet prefab, it includes a rigid body and a capsule collider, but these both are irrelevant when our bullets are entities. Same deal for the enemy prefab. It has a rigid body and a capsule collider, but when our enemies are entities, these have no bearing. Before looking at any of the system code, we'll look at the mono-behavior code. The settings mono-behavior holds a little bit of game state and configuration. It has the transform of where the player is currently, as well as the radius for the player and the enemies that we're going to use in collision detection. This script is treated as a singleton, so it has a static field of its own type, which is supposed to hold the only instance of this mono-behavior. It also includes a few helper methods. Like for example, when we spawn enemies, we're going to spawn them in a random point around the player. So this method returns a random point on the circle that is a radius around the player. The class also keeps track of when the player is dead, so when the player dies, we'll call player died, and we'll check for their current state with isPlayerDead. Player movement and look mono-behavior handles the input for moving our player. We can move the player horizontally and vertically, and for the player's facing, we cast array from the screen and where it collides with the ground that is the direction the player will face. The script also animates the player, given their current direction of movement. If the enemies are game objects rather than entities, then this checks for collision with those enemy objects. And when the player dies, we call this method to set its animation to the proper state. The bullet impact pool has a reference to our bullet hit prefab, and it keeps a pool of 100 game objects. And whether the enemies are game objects or entities, either way, we call this method played bullet impact, specifying where the impact occurs, and that puts a bullet impact object at that location. The enemy behavior script is attached to our enemy prefab, and when the enemies are game objects rather than entities, then this is the logic that handles their movement and collision detection. But when we use entities, this is all irrelevant, and all that matters is this convert method down at the bottom, which is the method for the iConvertGameObjectEntity interface. When this method is called, a new entity has already been created and gets passed in, along with the entity manager of that entity's world, and in the method we add and set the components to set up the entity equivalent of the game object to which this model behavior is attached. So here an enemy entity should have the enemy tag component, the moveForward component, which is also another tag component, though it doesn't have tag in the name, and then we set a moveSpeed component and a health component. So again, when we use entities, this is all that's really relevant. Now, you might be wondering what this GameObjectConversionSystem parameter is about. Well, as I explained in a video called Converting Scene Data to Dots, for various reasons, conversion of game objects into entities is done with a set of systems, these GameObjectConversionSystems. For our purposes here, it's not really relevant, so we can ignore it in this context. Anyway, so when is this convert method called? Well, in the enemy spawner model behavior. It has an enemy prefab gameObject field. And in the start method here, when we use ECS, when this flag is true, then we call this GameObjectConversionUtility.ConvertGameObjectHierarchy, passing in the enemy prefab and specifying which world we want to create it in. This creates an entity and calls the convert method we just saw, and the new entity is returned, which we assign to this other field enemy entity prefab. Using the entity debugger, we can inspect this entity at runtime, and because this is the second entity created after the game starts, it has entity ID 1. In the inspector, you can see that it has the health and move speed and enemy tag and forward tag, like we gave it explicitly in the convert method, but it also has a number of other stuff. For one thing, this is confusing because the indentation here, I'm pretty sure this is just a bug. It implies some kind of hierarchy, but for tag components that don't have any data, for whatever reason, it erroneously indents the next component under it when it shouldn't be indented at all. So just imagine this is a straight list. Anyway, where this other stuff comes from is if we look at the enemy prefab from which we converted this entity, it has, say, the mesh renderer component here, and it has a transform component, and that's why its entity conversion has the translation, rotation, local to world matrix, for instance, calling tag and render mesh components. And it also has this prefab tag component because it was converted from a prefab. The prefab component is a special tag that marks the entity as ignored by all systems, so this entity entity 1 is not going to actually render or anything, it's not actually going to be used by any system. We just want to create it so that in our entity spawner, we can create clones of it using the instantiate method. So that's what happens here in the update. Every frame we check if the spawn interval has elapsed, and if so, we get a random position in the radius around the player. If we're not using ECS, then we just instantiate the enemy prefab game object, creating a clone game object, but if we are using ECS, we create a clone of the entity, entity 1, and set its translation to this random position. Note that when we clone an entity with instantiate, the clone is a full copy, except it doesn't have the prefab tag, and so it will be an active entity used by the systems. The projectile behavior script is attached to the bullet prefab, and like the enemy behavior script, it has a bit of state, but also a rigid body component and some relevant methods, but these are only relevant when our bullets are game objects rather than entities. When our bullets are entities, all that matters is the convert method here, which from the game object instances is attached to, in this case the bullet prefab, we create the entity equivalent by giving it the right components. So for our bullets, we give them a move forward component, a move speed, and a time to live. As we'll see later in the system, time to live of every bullet is decremented each frame, and when this value goes below zero, the bullet is destroyed. The bullets are instantiated in the player shooting script, which is attached to the player object. It has a field to store the bullet prefab, and just like in the enemy spawner, we're calling game object conversion utility dot convert game object hierarchy, passing in this bullet prefab, this creates a new entity and passes it to the convert method we just saw, and the entity is returned, which we assigned to this field bullet entity prefab. In the update loop, if the player fires, then we spawn some bullets, and if we're using ECS and spreadshot as we saw in the demo, then we call this method here, passing in the rotation the angle the gun's pointing in, and that method down here creates a number of new bullet entities, creating them as clones of the bullet entity prefab we just created in the start method. For the sake of the spreadshot, we go through this loop that gives in different translations and rotations. I'll let you study that logic yourself, but that's how we create the bullets. Looking again in the entity debugger, the third entity created, entity two, is our bullet prefab entity. It has the components we explicitly gave it, which are move speed, time to live, and move forward, but it also has these other components derived from certain components of the prefab, and again note that it has the prefab tag component, so this entity itself is not actually active. We just need this entity so we can make clones when we shoot actual bullets. Now, lastly before getting to the system code, you'll notice the first entity created is this one called player. This is the entity representation of the player, and it's created by two scripts on the player prefab. Down here we have player to entity conversion and convert to entity script. Player to entity conversion is a simple model behavior that implements iConvertGameObject entity, as the convert method that's called when this entity is created, and it's this convert to entity script which is actually creating the entity and calling the convert method. This is a standard script in the entity's package, we didn't write it. It gives the entity the debugging name which is the same as the GameObject name, so that's why this first entity has an actual name rather than just entity zero. Now finally we can look at the system code and we'll go through it in their approximate order of execution. So first off is the removeDead system, which removes entities of dead enemies and also the player if they have died. At the top here it says the system updates in the initialization system group, which is the first of the three system groups. There's initialization, simulation, and then presentation. So this is done in the first group. And on each update for each entity with a health component and a translation component, we check if the health value is less than or equal to zero. Well if so, we check if the entity is the player entity by checking if it has the player tag component, and if so, we invoke player died of the setting's mono behavior. Otherwise, if the entity is an enemy as represented by the enemy tag, then we want to destroy the entity and create a bullet impact where the enemy was. PostUpdateCommands is a field of the system, and it's an entity command buffer that's going to be played back after this system finishes updating. So here we're just recording our intention to destroy the dead entity enemy, and then the entities are actually destroyed after this on update returns. The next system turned towards player is what makes the enemies always rotate to face the player. This time it's a job component system so that the work can be done in a job. The turn job that we define, note it's burst compiled, and it operates upon entities that have a translation, a rotation, but also an enemy tag, except we don't need to actually read or write the enemy tag, that's why it's specified through this attribute, rather than in the list with the other components. In the job, we have a float three defining the player position, the target to turn towards, and so for each entity, we're going to be reading the position and reading and modifying the rotation. By subtracting the position from the player position, we get a vector pointing towards the player. We zero out the Y component, because we always want our enemies rotated flat on the XZ plane, and to get a quaternion to set as a new rotation, the Unity Mathematics package has this quaternion.look rotation where we specify a facing direction and an up direction. So that's how the job is defined, and then in on update, first we check if the player is dead, because if so we don't want the enemies to rotate towards the player anymore, instead they should just keep going straight. But otherwise, we create a job, setting the player position from the settings class, and then scheduling the job. As I mentioned in a previous video, when you have an eye job for each, you pass in the job component system itself as the first argument, because the schedule method wants to register the query that gets built with the job system. There's a query implicit from how the job is defined, and the component system needs to keep track of that for the sake of managing the job dependencies. Next, we have the move forward system, which moves forward both the enemies and the bullets. So our move forward rotation job here operates upon anything that has a move forward tag, but also has a translation, rotation, and move speed component. The job needs to know how much time is advanced, so we have this dt delta time field, and in the execute here, all of the components are read only except for the translation. It's the translation that's getting updated. The vector of change is dt times our speed value multiplied by a forward vector derived from our rotation, and this change vector is then added to the current position, which we assign as the new position. In on update, we create the job and schedule it unconditionally, and we get dt from time delta time. Next, we have the player transform update system, which is responsible for setting the player entity position from the player game object position. If the player is dead, they're not going to move, so we don't need to update this position. Otherwise, for all entities with the player tag, which should just be one entity, we want to set the translation, so we assign it to pause here this new translation with the player position from settings. Next, we have the collision system, which is the most complicated system, so we'll come back to this. First, let's look at time destroy system, which is the last one. This system is what destroys our bullets when their time to live component is less than or equal to zero. Like in the remove system, we're going to be destroying entities, and so we'll need an entity command buffer, but we're going to be recording our commands in a job, which isn't necessarily going to be finished immediately after the on update here returns. And so we can't use post update commands. Instead, we want to get an entity command buffer from the end simulation entity command buffer system, which is the entity command buffer system that updates at the end of the simulation group. So an on create manager here, which actually should be on create on create managers deprecated upon creation of the system, we're getting the end simulation entity command buffer system from our world and assigning it to this field buffer, which is really not appropriately named because it itself is not the entity command buffer. It's an entity command buffer system, but close enough. And so down here, when we create our job, the calling job, we're going to need to call create command buffer and make it concurrent. And the reason it must be concurrent is because I job for each with entity here, the calls to execute maybe split up into separate sub jobs, and those sub jobs may run on several threads. So the entity command buffer might be used concurrently. That's why it must be concurrent. Anyway, this calling job, it operates upon everything with a time to live component. And in execute, it decrements the time to live by the delta time. And if the time to live then is less than or equal to zero, we destroy the entity using our concurrent entity command buffer. Note here the job index parameter we get an execute, we want to pass this into any method calls on our concurrent entity command buffer. For reasons I'm not entirely clear on, but that's what it's there for. And note also that when we schedule the job, we call this method to tell the entity command buffer that the job has to complete before the entity command buffer should play back. So this is how the bullets get destroyed when they time out. Lastly, looking back now on the collision system, if any enemy gets within radius of the player, then we want to document the player's health to below zero. And likewise, we want to check for collisions between any bullet and any enemy. And when there is such a collision, then the enemy health drops below zero. Note that because this collision system is updated after the remove dead system, the player and enemies don't actually die in the same frame where they get hit, they actually die in the next frame. As long as the frame rate isn't super low, then the user can't tell the difference. Whereas in the other systems, we used iJob for each. Here we're using iJobChunk. And when you schedule an iJobChunk, you have to pass in a query. So in onCreate, we create some queries. Player group should just match the player because it looks for the player tag. Enemy group is the same, except instead of player tag, it looks for the enemy tag. And bullet group matches bullets because only bullets have the time to live component. I suppose time to live here should be read only like translation, but it's actually not accessed at all in the job anyway, so it doesn't really matter. So in this collision job, the chunks are going to have a health component and a translation component. And so we need these archetypeChunk component type fields to get those arrays using getNativeArray down here. In the case of translation, we only need to read it rather than write, so we mark this read only. The job also needs this array of translations, which we'll just call trans to test against. And this too is read only, we're not going to modify any of the translations. And note here the deallocate on job completion. This special attribute makes sure that the native array here is disposed of once the job completes. The radius here is the radius of collision. If two translations are within radius distance of each other, then they're considered to be colliding. So anyway, for each chunk we have its array of health and its array of translations. For each entity, we're accumulating a damage number. Every time there's a collision, then we add more to the damage down here, add damage plus one. And if the damage is greater than zero, then we subtract it from the health of the entity. Then for each entity of this chunk, we perform an interloop through all of the trans to test against. And so we do a collision check between the position of the entity and each position here, pause two of the trans to test against. Now the collision test here simply takes into flow threes and through the Pythagorean theorem, we can see if these two positions are within radius distance of each other. The note here actually instead of radius, we're using radius squared because that just makes the calculation here cheaper. Because we only care about relative distance, we can get the distance squared between these two positions and compare that against the radius squared, which is cheaper than having to compute the actual distance between the two positions because that requires getting a square root, which is an expensive operation. So there's just a little trick to avoid that extra expense. Anyway, that's how we do our collision check. It is certainly not the optimal way to do collision detection between many objects. A proper collision detection algorithm would involve at the very least a so-called broad phase test that uses axes aligned bounding boxes. But anyway, this is meant to be a simple example. Just keep in mind this is certainly not the most efficient way to do collision detection. Anyway, that's how the collision check is performed. And when there's collision, we increment the damage, which is subtracted from the health. As current configured in our game, the health of the player and enemies is always one. So a single collision is an instant kill. But if we set the health to something greater, then it could take multiple bullets or multiple collisions to destroy enemies and to destroy the player. So in the update of this collision system, again, we're doing two sets of collision tests, collisions between the player and enemies, and collisions between enemies and the bullets. So we're creating two collision jobs, EVB as in enemies versus bullets, and PVE as in players versus enemies. So these jobs need the archetype trunk component type values for the health and translation. So we get those from settings. We get the radius collision value for the bullets to the enemies and for the enemies to the player. And note that for the job, we're passing in the square of these radii. And for the trans to test against, well, in this first job, it's the entities versus the bullets. When we schedule the job, we pass in the enemy group. So the chunks are going to be the enemy entities. And for each enemy, we want to test it against every bullet. So from our bullet query, we call two component data array translation to get an array, which is a copy of all the translations of all of our bullets. And then likewise down here for PVE, players versus enemies, it's the player query passed a schedule. So in this case, the chunk loop is always going to be iterating just through one entity. But for trans to test against, we get an array, which is a copy of all the translations of all the enemies. You may be concerned that copying these arrays every frame is expensive. And well, it's not the cheapest thing we might do. But because say here for each enemy, we have to iterate through every bullet. Well, then we're paying a big cost in terms of memory access anyway. Like say if we have a thousand entities, and so we have to iterate through this bullet translation array a thousand times. Well, the upfront copy has about the expense of reading through it once. So it's really not adding much to the workload. Accessing all this data a thousand one times is not that much more than a thousand times. So that's everything for the code of this example. You may be wondering how are these entities all rendered? Well, that's handled by the hybrid dot rendering package, which has systems that run in the presentation group. It's the render mesh system here that does the actual drawing of the bullets and our enemies. All we need to do in our code is make sure our entities have the right components, which they do. The details of this though I'll cover in later videos.