Spawn Bullets and Destroy Player
Full workflows and code to spawn bullets and self-destruct Player

What you'll develop on this page

Player is able to spawn bullets and self-destruct
We will spawn bullets and self-destruct the player from user inputs.

Spawning a bullet

    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 could simply "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:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
4
public struct BulletSpawnOffsetComponent : IComponentData
5
{
6
public float3 Value;
7
}
Copied!
    This is the component that will be added (via 'Add Component') to the player entity to store the Bullet Spawn offset
Creating BulletSpawn GameObject and BulletSpawnOffsetComponent
    Occasionally in our testing we found that BulletSpawnOffSetComponent causes errors (like in the gif above)
      If it causes you errors, go to "Assets" and choose "Reimport All"
    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 ito SetBulletSpawnOffset.cs:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using UnityEngine;
4
5
public class SetBulletSpawnOffset : UnityEngine.MonoBehaviour, IConvertGameObjectToEntity
6
{
7
public GameObject bulletSpawn;
8
9
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
10
{
11
var bulletOffset = default(BulletSpawnOffsetComponent);
12
13
var offsetVector = bulletSpawn.transform.position;
14
bulletOffset.Value = new float3(offsetVector.x, offsetVector.y, offsetVector.z);
15
16
dstManager.AddComponentData(entity, bulletOffset);
17
}
18
}
Copied!
Create SetBulletSpawnOffset and check out the Entity Debugger to see player entity
    If there is an error, reimport assets
    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 Entity Debugger to check out the player entity by finding the entity archetype with the BulletSpawnOffsetComponent and its Value field set to (0.5, 0, 1)
      We will use this value in InputSpawnSystem to spawn our bullet
Add SetBulletSpawnOffset to the Player prefab and seeing the
    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:
1
using Unity.Entities;
2
3
[GenerateAuthoringComponent]
4
public struct BulletTag : IComponentData
5
{
6
}
Copied!
Create Bullet prefab and add BulletTag
    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:
1
using Unity.Entities;
2
3
[GenerateAuthoringComponent]
4
public struct BulletAgeComponent : IComponentData
5
{
6
public BulletAgeComponent(float maxAge)
7
{
8
this.maxAge = maxAge;
9
age = 0;
10
}
11
12
public float age;
13
public float maxAge;
14
15
}
Copied!
    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
Create the BulletAgeComponent and Add it to the Bullet prefab with a maxAge of 5
    Next we'll create BulletAuthoringComponent to be put on the PrefabCollection in ConvertedSubScene
    Paste the below code snippet into BulletAuthoringComponent.cs:
1
using Unity.Entities;
2
3
[GenerateAuthoringComponent]
4
public struct BulletAuthoringComponent : IComponentData
5
{
6
public Entity Prefab;
7
}
Copied!
    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
Create the BulletAuthoringComponent and add it to our PrefabCollection GameObject
    Next, we add the VelocityComponent to to the Bullet so it can travel
Adding the VelocityComponent to the Bullet Prefab
    Now we we are ready to update InputSpawnSystem to spawn a Bullet
    Paste the below code snippet into InputSpawnSystem.cs:
1
using Unity.Entities;
2
using Unity.Collections;
3
using Unity.Jobs;
4
using Unity.Mathematics;
5
using Unity.Transforms;
6
using UnityEngine;
7
using Unity.Burst;
8
9
public class InputSpawnSystem : SystemBase
10
{
11
//This will be our query for Players
12
private EntityQuery m_PlayerQuery;
13
14
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
15
private BeginSimulationEntityCommandBufferSystem m_BeginSimECB;
16
17
//This will save our Player prefab to be used to spawn Players
18
private Entity m_PlayerPrefab;
19
20
//This will save our Bullet prefab to be used to spawn Bullets
21
private Entity m_BulletPrefab;
22
23
//We are going to use this to rate limit bullets per second
24
//We could have included this in the game settings, no "ECS reason" not to
25
private float m_PerSecond = 10f;
26
private float m_NextTime = 0;
27
28
protected override void OnCreate()
29
{
30
//This is an EntityQuery for our Players, they must have an PlayerTag
31
m_PlayerQuery = GetEntityQuery(ComponentType.ReadWrite<PlayerTag>());
32
33
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
34
m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
35
36
//We need the GameSettingsComponent to grab the bullet velocity
37
//When there is only 1 instance of a component (like GamesSettingsComponent) we can use "RequireSingletonForUpdate"
38
RequireSingletonForUpdate<GameSettingsComponent>();
39
}
40
41
protected override void OnUpdate()
42
{
43
//Here we set the prefab we will use
44
if (m_PlayerPrefab == Entity.Null || m_BulletPrefab == Entity.Null)
45
{
46
//We grab the converted PrefabCollection Entity's PlayerAuthoringCOmponent
47
//and set m_PlayerPrefab to its Prefab value
48
m_PlayerPrefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
49
m_BulletPrefab = GetSingleton<BulletAuthoringComponent>().Prefab;
50
51
//we must "return" after setting this prefab because if we were to continue into the Job
52
//we would run into errors because the variable was JUST set (ECS funny business)
53
//comment out return and see the error
54
return;
55
}
56
57
byte shoot;
58
shoot = 0;
59
var playerCount = m_PlayerQuery.CalculateEntityCountWithoutFiltering();
60
61
if (Input.GetKey("space"))
62
{
63
shoot = 1;
64
}
65
66
//If we have pressed the space bar and there is less than 1 player, create a new player
67
//This will be false after we create our first player
68
if (shoot == 1 && playerCount < 1)
69
{
70
var entity = EntityManager.Instantiate(m_PlayerPrefab);
71
return;
72
}
73
74
var commandBuffer = m_BeginSimECB.CreateCommandBuffer().AsParallelWriter();
75
//We must declare our local variables before the .ForEach()
76
var gameSettings = GetSingleton<GameSettingsComponent>();
77
var bulletPrefab = m_BulletPrefab;
78
79
//we are going to implement rate limiting for shooting
80
var canShoot = false;
81
if (UnityEngine.Time.time >= m_NextTime)
82
{
83
canShoot = true;
84
m_NextTime += (1/m_PerSecond);
85
}
86
87
Entities
88
.WithAll<PlayerTag>()
89
.ForEach((Entity entity, int nativeThreadIndex, in Translation position, in Rotation rotation,
90
in VelocityComponent velocity, in BulletSpawnOffsetComponent bulletOffset) =>
91
{
92
//If we don't have space bar pressed we don't have anything to do
93
if (shoot != 1 || !canShoot)
94
{
95
return;
96
}
97
98
// We create the bullet here
99
var bulletEntity = commandBuffer.Instantiate(nativeThreadIndex, bulletPrefab);
100
101
//we set the bullets position as the player's position + the bullet spawn offset
102
//math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
103
//think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
104
var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
105
commandBuffer.SetComponent(nativeThreadIndex, bulletEntity, newPosition);
106
107
108
// bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
109
// 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)
110
var vel = new VelocityComponent {Value = (gameSettings.bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocity.Value};
111
112
commandBuffer.SetComponent(nativeThreadIndex, bulletEntity, vel);
113
114
}).ScheduleParallel();
115
116
m_BeginSimECB.AddJobHandleForProducer(Dependency);
117
}
118
}
Copied!
    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
      Hitting errors? Try Reimport All
After updating InputSpawnSystem we are able to spawn Bullet prefabs
    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
Change Bullet Velocity to 200 in GameSettings
    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:
1
using Unity.Entities;
2
3
public class BulletAgeSystem : SystemBase
4
{
5
//We will be using the BeginSimulationEntityCommandBuffer to record our structural changes
6
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
7
8
protected override void OnCreate()
9
{
10
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
11
}
12
13
protected override void OnUpdate()
14
{
15
//We create our CommandBuffer and add .AsParallelWriter() because we will be scheduling parallel jobs
16
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
17
18
//We must declare local variables before using them in the job below
19
var deltaTime = Time.DeltaTime;
20
21
//Our query writes to the BulletAgeComponent
22
//The reason we don't need to add .WithAll<BulletTag>() here is because referencing the BulletAgeComponent
23
//requires the Entities to have a BulletAgeComponent and only Bullets have those
24
Entities.ForEach((Entity entity, int nativeThreadIndex, ref BulletAgeComponent age) =>
25
{
26
age.age += deltaTime;
27
if (age.age > age.maxAge)
28
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
29
30
}).ScheduleParallel();
31
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
32
}
33
}
Copied!
    After creating BulletAgeSystem, hit "play", spawn a player, spawn bullets, and checkout the BulletAgeSystem at work
BulletAgeSystem works and destroys bullets after their max age
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:
1
using Unity.Entities;
2
using Unity.Collections;
3
using Unity.Jobs;
4
using Unity.Mathematics;
5
using Unity.Transforms;
6
using UnityEngine;
7
using Unity.Burst;
8
9
public class InputSpawnSystem : SystemBase
10
{
11
//This will be our query for Players
12
private EntityQuery m_PlayerQuery;
13
14
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
15
private BeginSimulationEntityCommandBufferSystem m_BeginSimECB;
16
17
//This will save our Player prefab to be used to spawn Players
18
private Entity m_PlayerPrefab;
19
20
//This will save our Bullet prefab to be used to spawn Players
21
private Entity m_BulletPrefab;
22
23
//We are going to use this to rate limit bullets per second
24
//We could have included this in the game settings, no "ECS reason" not to
25
private float m_PerSecond = 10f;
26
private float m_NextTime = 0;
27
28
protected override void OnCreate()
29
{
30
//This is an EntityQuery for our Players, they must have an PlayerTag
31
m_PlayerQuery = GetEntityQuery(ComponentType.ReadWrite<PlayerTag>());
32
33
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
34
m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
35
36
//We need the GameSettingsComponent to grab the bullet velocity
37
//When there is only 1 instance of a component (like GamesSettingsComponent) we can use "RequireSingletonForUpdate"
38
RequireSingletonForUpdate<GameSettingsComponent>();
39
}
40
41
protected override void OnUpdate()
42
{
43
//Here we set the prefab we will use
44
if (m_PlayerPrefab == Entity.Null || m_BulletPrefab == Entity.Null)
45
{
46
//We grab the converted PrefabCollection Entity's PlayerAuthoringCOmponent
47
//and set m_PlayerPrefab to its Prefab value
48
m_PlayerPrefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
49
m_BulletPrefab = GetSingleton<BulletAuthoringComponent>().Prefab;
50
51
//we must "return" after setting this prefab because if we were to continue into the Job
52
//we would run into errors because the variable was JUST set (ECS funny business)
53
//comment out return and see the error
54
return;
55
}
56
57
byte shoot, selfDestruct;
58
shoot = selfDestruct = 0;
59
var playerCount = m_PlayerQuery.CalculateEntityCountWithoutFiltering();
60
61
if (Input.GetKey("space"))
62
{
63
shoot = 1;
64
}
65
if (Input.GetKey("p"))
66
{
67
selfDestruct = 1;
68
}
69
70
//If we have pressed the space bar and there is less than 1 player, create a new player
71
//This will be false after we create our first player
72
if (shoot == 1 && playerCount < 1)
73
{
74
var entity = EntityManager.Instantiate(m_PlayerPrefab);
75
return;
76
}
77
78
var commandBuffer = m_BeginSimECB.CreateCommandBuffer().AsParallelWriter();
79
//We must declare our local variables before the .ForEach()
80
var gameSettings = GetSingleton<GameSettingsComponent>();
81
var bulletPrefab = m_BulletPrefab;
82
83
//we are going to implement rate limiting for shooting
84
var canShoot = false;
85
if (UnityEngine.Time.time >= m_NextTime)
86
{
87
canShoot = true;
88
m_NextTime += (1/m_PerSecond);
89
}
90
91
Entities
92
.WithAll<PlayerTag>()
93
.ForEach((Entity entity, int nativeThreadIndex, in Translation position, in Rotation rotation,
94
in VelocityComponent velocity, in BulletSpawnOffsetComponent bulletOffset) =>
95
{
96
//If self-destruct was pressed we will add a DestroyTag to the player entity
97
if(selfDestruct == 1)
98
{
99
commandBuffer.AddComponent(nativeThreadIndex, entity, new DestroyTag {});
100
}
101
//If we don't have space bar pressed we don't have anything to do
102
if (shoot != 1 || !canShoot)
103
{
104
return;
105
}
106
107
// We create the bullet here
108
var bulletEntity = commandBuffer.Instantiate(nativeThreadIndex, bulletPrefab);
109
110
//we set the bullets position as the player's position + the bullet spawn offset
111
//math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
112
//think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
113
var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
114
commandBuffer.SetComponent(nativeThreadIndex, bulletEntity, newPosition);
115
116
117
// bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
118
// 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)
119
var vel = new VelocityComponent {Value = (gameSettings.bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocity.Value};
120
121
commandBuffer.SetComponent(nativeThreadIndex, bulletEntity, vel);
122
123
}).ScheduleParallel();
124
125
m_BeginSimECB.AddJobHandleForProducer(Dependency);
126
}
127
}
Copied!
    Now we need a PlayerDestructionSystem that will destroy the player entities
    Create PlayerDestructionSystem and paste the code snippet below into PlayerDestructionSystem.cs:
1
using Unity.Burst;
2
using Unity.Entities;
3
using Unity.Collections;
4
using Unity.Mathematics;
5
using Unity.Jobs;
6
using Unity.Transforms;
7
using UnityEngine;
8
9
//We are going to update LATE once all other systems are complete
10
//because we don't want to destroy the Entity before other systems have
11
//had a chance to interact with it if they need to
12
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
13
public class PlayerDestructionSystem : SystemBase
14
{
15
private EndSimulationEntityCommandBufferSystem m_EndSimEcb;
16
17
protected override void OnCreate()
18
{
19
//We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
20
m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
21
}
22
23
protected override void OnUpdate()
24
{
25
//We add "AsParallelWriter" when we create our command buffer because we want
26
//to run our jobs in parallel
27
var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();
28
29
//We now any entities with a DestroyTag and an PlayerTag
30
//We could just query for a DestroyTag, but we might want to run different processes
31
//if different entities are destroyed, so we made this one specifically for Players
32
Entities
33
.WithAll<DestroyTag, PlayerTag>()
34
.ForEach((Entity entity, int nativeThreadIndex) =>
35
{
36
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
37
38
}).WithBurst().ScheduleParallel();
39
40
//We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
41
//that will be playing back the structural changes recorded in this sytem
42
m_EndSimEcb.AddJobHandleForProducer(Dependency);
43
44
}
45
}
46
Copied!
    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 exact 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
Update InputSpawnSystem and add PlayerDestructionSystem
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
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Shooting-and-Destroying-Player'

One more (extremely rad that shows off DOTS) thing...

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:
    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" (may have 'Experimental' after it)
    Save and then navigate back to SampleScene and hit "play"
Enable "Enter Play Mode Settings Options" to speed up load of "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)
Last modified 10d ago