Weapon handling #1

Hi there! If you haven’t seen the previous post (detecting collisions), then here it is, or here are the very beginning.

Before we shoot

Before we start to shooting, we need some preparation, for example creating projectiles, but first, let's settle what we want to achieve.

In this example, the player is stationary, holding a weapon, which can be rotated by mouse or touch, and shooting will be done with mouse button 0 down or touch. The projectiles come from the other end of the weapon.
Probably we want to shoot a lot, so we want to make something like a BulletPool.

According to these, the player in the scene will look like this:
- Player (ConvertToEntity, PlayerProxy, MeshRenderer. If you want to collide: PhysicsShape and PhysicsBody)
   - WeaponPivot (WeaponProxy. This is the axis of rotation)
      - Weapon (The visual)
         - WeaponTip (an empty GameObject on the end of the weapon)

And we need two more GameObject for the projectiles:
- ProjectileSpawner (ConvertToEntity, ProjectileSpawnProxy)
- ProjectileHandler (ConvertToEntity, ProjectileHandlerProxy)

Before we move forward, let's create the projectiles.

Spawning

ComponentData


// Assets/Scripts/Projectiles/ProjectileSpawnData.cs

using Unity.Entities;

public struct ProjectileSpawnData : IComponentData
{
  public Entity Prefab;
  public int Count;
}

ComponentData, that holds the projectile's prefab reference and the count, how much we want to create.

Proxy


// Assets/Scripts/Projectiles/ProjectileSpawnProxy.cs

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

public class ProjectileSpawnProxy : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
#pragma warning disable 0649
  [Header("Projectile spawn data")]
  [SerializeField] private GameObject _projectilePrefab;
  [SerializeField] private int _projectileCount;
#pragma warning restore 0649

  public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
  {
    referencedPrefabs.Add(_projectilePrefab);
  }

  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    dstManager.AddComponentData(entity, new ProjectileSpawnData
    {
      Prefab = conversionSystem.GetPrimaryEntity(_projectilePrefab),
      Count = _projectileCount
    });
  }
}

The same as we learned before.

System


// Assets/Scripts/Projectiles/ProjectileSpawnSystem.cs

using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

[UpdateInGroup(typeof(SimulationSystemGroup))]
public class ProjectileSpawnSystem : JobComponentSystem
{
  private BeginInitializationEntityCommandBufferSystem _entityCommandBuffer;

  protected override void OnCreate()
  {
    _entityCommandBuffer = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
  }

  [BurstCompile]
  private struct ProjectileSpawnJob : IJobForEachWithEntity<ProjectileSpawnData>
  {
    public EntityCommandBuffer.Concurrent CommandBuffer;

    public void Execute(Entity entity, int index, ref ProjectileSpawnData spawnData)
    {
      for (int i = 0; i < spawnData.Count; i++)
      {
        Entity instance = CommandBuffer.Instantiate(index, spawnData.Prefab);
        CommandBuffer.SetComponent(index, instance, new Translation { Value = new float3(.0f, 100.0f, .0f) });
      }

      CommandBuffer.DestroyEntity(index, entity);
    }
  }

  protected override JobHandle OnUpdate(JobHandle inputDepts)
  {
    JobHandle spawnJob = new ProjectileSpawnJob
    {
      CommandBuffer = _entityCommandBuffer.CreateCommandBuffer().ToConcurrent()
    }.ScheduleSingle(this, inputDepts);

    _entityCommandBuffer.AddJobHandleForProducer(spawnJob);

    return spawnJob;
  }
}

Just as the one before.

Projectiles

ComponentDatas


// Assets/Scripts/Projectiles/ProjectilesData.cs

using Unity.Entities;

public struct ProjectilesData : IComponentData
{
  public float Speed;
  public float LifeTime;
  public float Damage;
}

The Projectiles(!)Data holds the information what affect all the projectiles (of course if you want to use more type of projectiles, than some of or all could go to the ProjectileData). I'm just wanted to try it out this way.

Speed and Damage are speak for themselfs. Lifetime is a counter, if the projectile doesn't hit something before it's expires, then it'll be "pooled".

And if we talked about ProjectileData:


// Assets/Scripts/Projectiles/ProjectileData.cs

using Unity.Entities;
using Unity.Transforms;

public enum ProjectileStatus
{
  Disabled,
  Created,
  Moving
}

public struct ProjectileData : IComponentData
{
  public ProjectileStatus Status;
  public LocalToWorld WeaponTipPos;
  public float CurrentLifeTime;
}

The ProjectileStatus enum tells what can or need to be done with the current projectile. This is our "BulletPool". If disabled, we can use for the next shoot, if created, we can move it to the WeaponTip, and if it's moving, then we move it.

LocalToWorld is a minimal version of the "ordinary" Transform. It holds the entity's transform information in world space.

Proxies


// Assets/Scripts/Projectiles/ProjectileProxy.cs

using Unity.Entities;
using UnityEngine;

public class ProjectileProxy : MonoBehaviour, IConvertGameObjectToEntity
{
  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    dstManager.AddComponentData(entity, new ProjectileData { Status = ProjectileStatus.Disabled });
  }
}

Nothing special here, however here comes a little new stuff:


// Assets/Scripts/Projectiles/ProjectileHandlerProxy.cs

using Unity.Entities;
using UnityEngine;

public class ProjectileHandlerProxy : MonoBehaviour, IConvertGameObjectToEntity
{
#pragma warning disable 0649
  [Header("Projectiles data")]
  [SerializeField] private float _speed;
  [SerializeField] private float _lifeTime;
  [SerializeField] private float _damage;
#pragma warning restore 0649

  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    ProjectilesData pd = new ProjectilesData
    {
      Speed = _speed,
      LifeTime = _lifeTime,
      Damage = _damage
    };
    dstManager.AddComponentData(entity, pd);

    World.Active.GetOrCreateSystem<ProjectileMovementSystem>().SetSingleton(pd);
  }
}

In this example, we need only one instance of the ProjectilesData, and it'll be used by the ProjectileMovementSystem, so here after we created the ComponentData with the parameters from the inspector, we get the reference of the system from the active world, and set the ComponentData as a singleton.

We arrived to the movement system, where the magic happens:

System


// Assets/Scripts/Projectiles/ProjectileMovementSystem.cs

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Transforms;
using UnityEngine;

[UpdateInGroup(typeof(PhysicsMovementSystemGroup))]
public class ProjectileMovementSystem : JobComponentSystem
{
  [BurstCompile]
  private struct ProjectileMovementJob : IJobForEach<ProjectileData, Translation, PhysicsVelocity>
  {
    [ReadOnly] public float DeltaTime;
    [ReadOnly] public ProjectilesData ProjectilesData;

    public void Execute(ref ProjectileData projectileData, ref Translation position, ref PhysicsVelocity velocity)
    {
      if (projectileData.Status == ProjectileStatus.Disabled) return;
      if (projectileData.Status == ProjectileStatus.Created)
      {
        position.Value = projectileData.WeaponTipPos.Position;

        projectileData.CurrentLifeTime = ProjectilesData.LifeTime;
        projectileData.Status = ProjectileStatus.Moving;
      }

      if ((projectileData.CurrentLifeTime -= DeltaTime) <= .0f)
      {
        projectileData.Status = ProjectileStatus.Disabled;

        velocity.Linear = float3.zero;
        position.Value = new float3(.0f, 100.0f, .0f);
        return;
      }

      velocity.Linear = projectileData.WeaponTipPos.Forward * DeltaTime * ProjectilesData.Speed;
      velocity.Angular = float3.zero;
    }
  }

  protected override JobHandle OnUpdate(JobHandle inputDepts)
  {
    JobHandle projectileMovementJob = new ProjectileMovementJob
    {
      DeltaTime = Time.fixedDeltaTime,
      ProjectilesData = GetSingleton<ProjectilesData>()
    }.Schedule(this, inputDepts);

    return projectileMovementJob;
  }
}

In our OnUpdate, we create the ProjectileMovementJob as we used to, but we don't get the ProjectilesData's reference with the help of a query or something else. In the ProjectileHandlerProxy we add it to the system as a singleton, so we can get it inside the system with the GetSingleton<T> method, simple is that.

In the job, we checking the projectiles status to decide what we'll do with it. If it's disabled, we return, than next please. If it's created, then it means that the projectile is shooted in this frame, so we set it's position to the WeaponTip's position.

After that we assume that the projectile is moving, so we check if the lifetime counter is zero or less, than we "move it to the pool", which means we set the status to disabled, reset the forces on it's PhysicsVelocity and place it a far place where we can't see, and we terminate the method with a return.

If the projectile should still move, than we set it's force.

Now we created a bunch of projectiles, which are moving, after that we will shoot them. The next article is about that.