Spawn Bullets and Destroy Player

Full workflows and code to spawn bullets and self-destruct Player

What you'll develop on this page

We will spawn bullets and self-destruct the player from user inputs.

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Shooting-and-Destroying-Player

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:

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

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Spawning-Updating-and-Destroying-Asteroids

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...

From Joachim (the founder of Unity)

  • 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)

Last updated