In order to spawn a bullet, we need to know where exactly we want it spawned
We are going to update our Player prefab with a BulletSpawn GameObject
First, open the Player prefab and add an empty GameObject to the Hierarchy named Bullet Spawn
Change the position to (0.5, 0 , 1)
We couldsimply "remember" the offset (0.5, 0, 1) by implementing it in our bullet spawn system (via writing it into the script). The issue with this approach is that every time the Player prefab is updated, you'd need to remember to go back and update this code in the bullet spawn system (easy to remember on a small simple project like this, but harder to do on complex projects). Instead, we want to link the GameObject position to component data on the player entity. This way if the Player prefab is updated and the Bullet Spawn GameObject is moved around, the code does not have to change.
Let's create the BulletSpawnOffsetComponent and paste the below code snippet into BulletSpawnOffsetComponent.cs:
using Unity.Entities;
using Unity.Mathematics;
public struct BulletSpawnOffsetComponent : IComponentData
{
public float3 Value;
}
This is the component that will be added (via 'Add Component') to the player entity to store the Bullet Spawn offset
Next, we need to use IConvertGameObjectToEntity to run a process that takes in the BulletSpawn GameObject Transform and sets BulletSpawnOffsetComponent to that value
Let's create the SetBulletSpawnOffset and paste the below code snippet to SetBulletSpawnOffset.cs:
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
public class SetBulletSpawnOffset : UnityEngine.MonoBehaviour, IConvertGameObjectToEntity
{
public GameObject bulletSpawn;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var bulletOffset = default(BulletSpawnOffsetComponent);
var offsetVector = bulletSpawn.transform.position;
bulletOffset.Value = new float3(offsetVector.x, offsetVector.y, offsetVector.z);
dstManager.AddComponentData(entity, bulletOffset);
}
}
Next we open our Player prefab, put SetBulletSpawnOffset on our Player prefab (via Add Component), then drag the Bullet Spawn GameObject in Hierachy into the bulletSpawn field in the Inspector window when Player is selected
Hit "play" and then navigate to the DOTS Windows to check out the Player archetype by finding the Archetype that contains the BulletSpawnOffsetComponent
Now let's find the Player entity in the DOTS Hierarchy tab and check out the component values in the inspector
You will see the Bullet Spawn Offset Component and the values of .5, 0, 1
We will use this value in InputSpawnSystem to spawn our bullet
Now we must create a Bullet prefab
We will follow the same process as we did for asteroids and players
Create Bullet prefab
In the SampleScene Hierarchy, Create a 3D object > Sphere GameObject named "Bullet," drag it into the Scripts and Prefabs project folder
Once Bullet has been dragged into the folder, delete the Bullet GameObject from the Hierarchy
Open the Bullet prefab and change the scale to (0.1, 0.1, 0.1)
Change the material to "Black"
Remove the Sphere Collider component
Create a BulletTag component and add it to the Bullet prefab
paste the below code snippet into BulletTag.cs:
using Unity.Entities;
[GenerateAuthoringComponent]
public struct BulletTag : IComponentData
{
}
Now we are going to create a new Authoring component we did not have for asteroids or players, named BulletAgeComponent, and put it on the Bullet prefab by clicking "Add Component" while Bullet is selected in Hierarchy
This component will be used to delete bullets after a specific amount of time
Why the time limit? Not sure about you, but we don't want bullets to live forever in the game and use up resources
Paste the code snippet below into BulletAgeComponent.cs:
using Unity.Entities;
[GenerateAuthoringComponent]
public struct BulletAgeComponent : IComponentData
{
public BulletAgeComponent(float maxAge)
{
this.maxAge = maxAge;
age = 0;
}
public float age;
public float maxAge;
}
Add the BulletAgeComponent to the Bullet prefab and make maxAge = 5 by typing in 5 to the Max Age field
This will set the lifetime of a bullet to 5 seconds
Next we'll create BulletAuthoringComponent to be put on the PrefabCollection in ConvertedSubScene
Paste the below code snippet into BulletAuthoringComponent.cs:
using Unity.Entities;
[GenerateAuthoringComponent]
public struct BulletAuthoringComponent : IComponentData
{
public Entity Prefab;
}
Navigate to ConvertedSubScene, add the BulletAuthoringComponent to our PrefabCollection GameObject by clicking "Add Component" in Inspector
Drag the Bullet Prefab into the "Prefab" field in the Bullet Authoring Component in Inspector
save, and navigate to SampleScene, and reimport ConvertedSubScene
Next, we add the VelocityComponent to to the Bullet so it can travel
Now we we are ready to update InputSpawnSystem to spawn a Bullet
Paste the below code snippet into InputSpawnSystem.cs:
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using Unity.Burst;
public partial class InputSpawnSystem : SystemBase
{
//This will be our query for Players
private EntityQuery m_PlayerQuery;
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimECB;
//This will save our Player prefab to be used to spawn Players
private Entity m_PlayerPrefab;
//This will save our Bullet prefab to be used to spawn Bullets
private Entity m_BulletPrefab;
//We are going to use this to rate limit bullets per second
//We could have included this in the game settings, no "ECS reason" not to
private float m_PerSecond = 10f;
private float m_NextTime = 0;
protected override void OnCreate()
{
//This is an EntityQuery for our Players, they must have an PlayerTag
m_PlayerQuery = GetEntityQuery(ComponentType.ReadWrite<PlayerTag>());
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We need the GameSettingsComponent to grab the bullet velocity
//When there is only 1 instance of a component (like GamesSettingsComponent) we can use "RequireSingletonForUpdate"
RequireSingletonForUpdate<GameSettingsComponent>();
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_PlayerPrefab == Entity.Null || m_BulletPrefab == Entity.Null)
{
//We grab the converted PrefabCollection Entity's PlayerAuthoringCOmponent
//and set m_PlayerPrefab to its Prefab value
m_PlayerPrefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
m_BulletPrefab = GetSingleton<BulletAuthoringComponent>().Prefab;
//we must "return" after setting this prefab because if we were to continue into the Job
//we would run into errors because the variable was JUST set (ECS funny business)
//comment out return and see the error
return;
}
byte shoot;
shoot = 0;
var playerCount = m_PlayerQuery.CalculateEntityCountWithoutFiltering();
if (Input.GetKey("space"))
{
shoot = 1;
}
//If we have pressed the space bar and there is less than 1 player, create a new player
//This will be false after we create our first player
if (shoot == 1 && playerCount < 1)
{
var entity = EntityManager.Instantiate(m_PlayerPrefab);
return;
}
var commandBuffer = m_BeginSimECB.CreateCommandBuffer().AsParallelWriter();
//We must declare our local variables before the .ForEach()
var gameSettings = GetSingleton<GameSettingsComponent>();
var bulletPrefab = m_BulletPrefab;
//we are going to implement rate limiting for shooting
var canShoot = false;
if (UnityEngine.Time.time >= m_NextTime)
{
canShoot = true;
m_NextTime += (1/m_PerSecond);
}
Entities
.WithAll<PlayerTag>()
.ForEach((Entity entity, int entityInQueryIndex, in Translation position, in Rotation rotation,
in VelocityComponent velocity, in BulletSpawnOffsetComponent bulletOffset) =>
{
//If we don't have space bar pressed we don't have anything to do
if (shoot != 1 || !canShoot)
{
return;
}
// We create the bullet here
var bulletEntity = commandBuffer.Instantiate(entityInQueryIndex, bulletPrefab);
//we set the bullets position as the player's position + the bullet spawn offset
//math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
//think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
commandBuffer.SetComponent(entityInQueryIndex, bulletEntity, newPosition);
// bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
// adding to the players physics Velocity makes sure that it takes into account the already existing player velocity (so if shoot backwards while moving forwards it stays in place)
var vel = new VelocityComponent {Value = (gameSettings.bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocity.Value};
commandBuffer.SetComponent(entityInQueryIndex, bulletEntity, vel);
}).ScheduleParallel();
m_BeginSimECB.AddJobHandleForProducer(Dependency);
}
}
We just updated the rate in InputSpawnSystem to limit bullet generation to 10 per second
Otherwise bullets will be generated as fast as the .ForEach() can be run
Let's hit "play", spawn our player and shoot some bullets and check it out
Again, because our bullets have a Translation and VelocityComponent, our MovementSystem acts on them as well
The bullets are traveling too fast to make out
Navigate to ConvertedSubScene, and go to GameSettings to change the BulletVelocity to 20 in Inspector
Save, return to SampleScene, reimport the ConvertedSubScene, hit play, spawn your player, and shoot around again
Much better, but we still need to add a system that destroys bullets when they pass their max age value we set in the BulletAgeComponent
Paste the code snippet below into BulletAgeSystem.cs:
using Unity.Entities;
public partial class BulletAgeSystem : SystemBase
{
//We will be using the BeginSimulationEntityCommandBuffer to record our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
protected override void OnCreate()
{
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
//We create our CommandBuffer and add .AsParallelWriter() because we will be scheduling parallel jobs
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
//We must declare local variables before using them in the job below
var deltaTime = Time.DeltaTime;
//Our query writes to the BulletAgeComponent
//The reason we don't need to add .WithAll<BulletTag>() here is because referencing the BulletAgeComponent
//requires the Entities to have a BulletAgeComponent and only Bullets have those
Entities.ForEach((Entity entity, int entityInQueryIndex, ref BulletAgeComponent age) =>
{
age.age += deltaTime;
if (age.age > age.maxAge)
commandBuffer.DestroyEntity(entityInQueryIndex, entity);
}).ScheduleParallel();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
After creating BulletAgeSystem, hit "play", spawn a player, spawn bullets, and checkout the BulletAgeSystem at work
We can now spawn a bullet from user input
We created our bullet prefab and added it to PrefabCollection
We added a BulletTag, VelocityComponent, and BulletAge component to the bullet prefab
We created a BulletAgeSystem to destroy bullets at the end of their life
We added to the InputSystem to spawn bullet prefabs
Player self-destruction
We are now going going to add the ability for the player to self-destruct if the "p" button is pressed
Because the player is floating in space it is easy to get lost and so this will provide a way to bail and return to the origin
First we need to update InputSpawnSystem to add a DestroyTag to the player when "p" is pressed
Update InputSpawnSystem.cs with the code below:
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using Unity.Burst;
public partial class InputSpawnSystem : SystemBase
{
//This will be our query for Players
private EntityQuery m_PlayerQuery;
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimECB;
//This will save our Player prefab to be used to spawn Players
private Entity m_PlayerPrefab;
//This will save our Bullet prefab to be used to spawn Players
private Entity m_BulletPrefab;
//We are going to use this to rate limit bullets per second
//We could have included this in the game settings, no "ECS reason" not to
private float m_PerSecond = 10f;
private float m_NextTime = 0;
protected override void OnCreate()
{
//This is an EntityQuery for our Players, they must have an PlayerTag
m_PlayerQuery = GetEntityQuery(ComponentType.ReadWrite<PlayerTag>());
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We need the GameSettingsComponent to grab the bullet velocity
//When there is only 1 instance of a component (like GamesSettingsComponent) we can use "RequireSingletonForUpdate"
RequireSingletonForUpdate<GameSettingsComponent>();
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_PlayerPrefab == Entity.Null || m_BulletPrefab == Entity.Null)
{
//We grab the converted PrefabCollection Entity's PlayerAuthoringCOmponent
//and set m_PlayerPrefab to its Prefab value
m_PlayerPrefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
m_BulletPrefab = GetSingleton<BulletAuthoringComponent>().Prefab;
//we must "return" after setting this prefab because if we were to continue into the Job
//we would run into errors because the variable was JUST set (ECS funny business)
//comment out return and see the error
return;
}
byte shoot, selfDestruct;
shoot = selfDestruct = 0;
var playerCount = m_PlayerQuery.CalculateEntityCountWithoutFiltering();
if (Input.GetKey("space"))
{
shoot = 1;
}
if (Input.GetKey("p"))
{
selfDestruct = 1;
}
//If we have pressed the space bar and there is less than 1 player, create a new player
//This will be false after we create our first player
if (shoot == 1 && playerCount < 1)
{
var entity = EntityManager.Instantiate(m_PlayerPrefab);
return;
}
var commandBuffer = m_BeginSimECB.CreateCommandBuffer().AsParallelWriter();
//We must declare our local variables before the .ForEach()
var gameSettings = GetSingleton<GameSettingsComponent>();
var bulletPrefab = m_BulletPrefab;
//we are going to implement rate limiting for shooting
var canShoot = false;
if (UnityEngine.Time.time >= m_NextTime)
{
canShoot = true;
m_NextTime += (1/m_PerSecond);
}
Entities
.WithAll<PlayerTag>()
.ForEach((Entity entity, int entityInQueryIndex, in Translation position, in Rotation rotation,
in VelocityComponent velocity, in BulletSpawnOffsetComponent bulletOffset) =>
{
//If self-destruct was pressed we will add a DestroyTag to the player entity
if(selfDestruct == 1)
{
commandBuffer.AddComponent(entityInQueryIndex, entity, new DestroyTag {});
}
//If we don't have space bar pressed we don't have anything to do
if (shoot != 1 || !canShoot)
{
return;
}
// We create the bullet here
var bulletEntity = commandBuffer.Instantiate(entityInQueryIndex, bulletPrefab);
//we set the bullets position as the player's position + the bullet spawn offset
//math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
//think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
commandBuffer.SetComponent(entityInQueryIndex, bulletEntity, newPosition);
// bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
// adding to the players physics Velocity makes sure that it takes into account the already existing player velocity (so if shoot backwards while moving forwards it stays in place)
var vel = new VelocityComponent {Value = (gameSettings.bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocity.Value};
commandBuffer.SetComponent(entityInQueryIndex, bulletEntity, vel);
}).ScheduleParallel();
m_BeginSimECB.AddJobHandleForProducer(Dependency);
}
}
Now we need a PlayerDestructionSystem that will destroy the player entities
Create PlayerDestructionSystem and paste the code snippet below into PlayerDestructionSystem.cs:
using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;
//We are going to update LATE once all other systems are complete
//because we don't want to destroy the Entity before other systems have
//had a chance to interact with it if they need to
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
public partial class PlayerDestructionSystem : SystemBase
{
private EndSimulationEntityCommandBufferSystem m_EndSimEcb;
protected override void OnCreate()
{
//We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
//We add "AsParallelWriter" when we create our command buffer because we want
//to run our jobs in parallel
var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();
//We now any entities with a DestroyTag and an PlayerTag
//We could just query for a DestroyTag, but we might want to run different processes
//if different entities are destroyed, so we made this one specifically for Players
Entities
.WithAll<DestroyTag, PlayerTag>()
.ForEach((Entity entity, int entityInQueryIndex) =>
{
commandBuffer.DestroyEntity(entityInQueryIndex, entity);
}).WithBurst().ScheduleParallel();
//We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
//that will be playing back the structural changes recorded in this sytem
m_EndSimEcb.AddJobHandleForProducer(Dependency);
}
}
Notice that the PlayerDestructionSystem is nearly identical to the AsteroidDestructionSystem
If they are so similar, then why not just make a "DestructionSystem" that destroys anything with a destroy tag?
The reason is because this gitbook has a NetCode section where Player destruction needs to follow a player-specific process, so that's why we set it up this way here
Then why not make a "GeneralDestructionSystem" that has .WithNone<PlayerTag>() and .WithAll<DestroyTag>() to destroy all entities that need to be destroyed that aren't player entities?
First off, the purpose of this gitbook is not to illustrate excellent game architecture, but to show the "how" of putting different Unity technologies together. But even so, a single destruction system is bad software engineering for ECS. Instead, it's better to learn how to make tight, focused Systems that touch exactly as much as they're supposed to. Building a mega-huge Destruction System would make it hard to compartmentalize.
Hit "play", spawn your player, move around, then hit "p" to self-destruct
We now can hit "p" to self-destruct
We updated InputSpawnSystem to add a DestroyTag when "p" is pressed
We created PlayerDestructionSystem to destroy our player entity
Because this gitbook is a DOTS-based project we can enable "Enter Play Mode Settings Options"
First, hit "play" and notice how long it takes to load the game
Now navigate to "File", choose "Build Settings", then "Player Settings...", then "Editor", then scroll to the bottom to "Play Mode Settings" and enable "Enter Play Mode Settings Options"
Save and then navigate back to SampleScene and hit "play"
Holy moly! Start up is less than a second!
Why didn't we make this clear at the beginning of the gitbook?
To learn it, you must earn it 🙃
(actually because we needed to understand how DOTS worked to make sense of Joachim's post and not just assume it is a magic toggle)
Fast enter play-mode in Unity is not the default because most projects are game object based and game object based projects have a tendency to use tons of static variables for state. And doing that by default makes it so that a domain reload is required to reset all the static state before entering playmode.
By design, everything we do in DOTS avoids this pattern. Multiple worlds, per world singletons etc.
They exist so that it becomes trivial to turn on the faster enter play mode option.
This is the recommended setting in a DOTS based project: