using System; using System.Collections; using System.Collections.Generic; using System.Linq; using DG.Tweening; using Unity.Burst; using Unity.Collections; using Unity.Jobs; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Jobs; using Utils; using Weapons.Scripts; using Weapons.Spawning; using Random = UnityEngine.Random; [CreateAssetMenu] public class Weapon : ScriptableObject { [BurstCompile] private struct BulletMoveJob : IJobParallelForTransform { public float DeltaTime; public NativeArray Bullets; public void Execute(int index, TransformAccess transform) { if (Bullets[index].IsAlive) transform.position += Bullets[index].Direction * Bullets[index].Speed * DeltaTime; } } private struct Bullet { public Vector3 Direction; public float Speed; public float Lifetime; public bool IsAlive; public int Idx; } public event Action BulletCollision; [SerializeField] private float fireRate; [SerializeField] private ParticleSystem.MinMaxCurve bulletSpeed; [SerializeField] private ParticleSystem.MinMaxCurve bulletLifetime; [SerializeField] private ParticleSystem.MinMaxCurve bulletSize; [SerializeField] private float damage; [SerializeField] private LayerMask collidesWith; [SerializeField] private float accuracy; [SerializeField] private SpawnZone zone; [SerializeField] private bool manualFire; [SerializeField] private BaseBulletBehaviour behaviour; [SerializeField] private bool useDeltaTime; private List _bullets; private List _bulletTransforms; private static Collider[] _results = new Collider[32]; private ObjectPool _pool; private BulletMoveJob _job; private JobHandle _jobHandle; private Bullet _currentBullet; private Transform _currentTransform; private TransformAccessArray _accessArray; private float _currentCooldown; public void Init() { _job = new BulletMoveJob(); _bullets = new List(); _bulletTransforms = new List(); _currentCooldown = fireRate * Time.deltaTime; } public void Update() { _currentCooldown += Time.deltaTime; _job.DeltaTime = Time.deltaTime; _job.Bullets = _bullets.ToNativeArray(Allocator.Persistent); _accessArray = new TransformAccessArray(_bulletTransforms.ToArray()); _jobHandle = _job.Schedule(_accessArray); } public void LateUpdate() { _jobHandle.Complete(); _job.Bullets.Dispose(); _accessArray.Dispose(); // remove all the inactive bullets from our internal list so we don't accidentally update them multiple times for (var i = _bullets.Count - 1; i >= 0; i--) { _currentBullet = _bullets[i]; _currentBullet.Lifetime -= Time.deltaTime; if (_currentBullet.Lifetime <= 0) _currentBullet.IsAlive = false; _bullets[i] = _currentBullet; if (_bullets[i].IsAlive) continue; _bulletTransforms[i].gameObject.SetActive(false); _pool.ReturnObject(_bulletTransforms[i], _bullets[i].Idx); _bullets.RemoveAt(i); _bulletTransforms.RemoveAt(i); } } public void FixedUpdate() { // loop through the bullets. Backwards, because that is marginally quicker for (var i = _bullets.Count - 1; i >= 0; i--) { _currentBullet = _bullets[i]; _currentTransform = _bulletTransforms[i]; // if the bullet is inactive, continue. We don't care about it if (!_currentTransform.gameObject.activeSelf || !_currentBullet.IsAlive) continue; // if it has hit something if (CheckCollision(_currentTransform, _currentTransform.localScale.x, out var numHits)) { // send the event BulletCollision?.Invoke(_currentTransform.position); // damage any damageable entities it has hit for (int j = 0; j < numHits; j++) { var healthObject = _results[j].GetComponent(); healthObject?.Hit(damage); } // deactivate the bullet _currentBullet.IsAlive = false; } // apply the changes we made _bullets[i] = _currentBullet; } } private bool CheckCollision(Transform instance, float size, out int numHits) { numHits = 0; // if it's inactive, we haven't hit anything if (!instance.gameObject.activeSelf) return false; // use the non allocating version so we don't have to allocate memory for every bullet numHits = Physics.OverlapSphereNonAlloc(instance.position, size/2f, _results, collidesWith, QueryTriggerInteraction.Collide); return numHits > 0; } public bool Fire(ObjectPool pool, Transform position) { if (_pool == null) _pool = pool; if (!manualFire) { if (_currentCooldown > fireRate * (useDeltaTime ? Time.deltaTime : 1)) return false; _currentCooldown = 0; } SpawnBullets(position); return true; } /// /// Spawn the bullets. /// private void SpawnBullets(Transform transform) { // alter the direction based on accuracy var direction = Quaternion.Euler(0, Random.Range(-accuracy, accuracy), 0) * transform.forward; // for every bullet zone.GetPoint(transform, (point, dir) => { // get the object in the pool var (bullet, idx) = _pool.GetObject(); // if the pool returns no bullet, continue (it probably hasn't initialised yet) if (bullet == null) return; // enable the bullet bullet.gameObject.SetActive(true); var newPos = new Vector3(point.x, point.y); newPos = Vector3.Lerp(newPos, Vector3.zero, Random.Range(0, 1f)); bullet.position = transform.position + newPos; // point the bullet in the right direction bullet.forward = new Vector3(dir.x, dir.y); bullet.transform.localScale = Vector3.one * bulletSize.EvaluateMinMaxCurve(); behaviour.DoBehaviour(bullet, bulletSize.EvaluateMinMaxCurve(), bullet.position); if (zone.SpawnDir != SpawnDir.Spherised) { var y = bullet.eulerAngles.y; bullet.forward = Quaternion.Euler(0, y, 0) * direction; } if (zone.SpawnDir == SpawnDir.None) bullet.forward = direction; // add the bullet to the list we're returning _bulletTransforms.Add(bullet); _bullets.Add(new Bullet { Direction = bullet.forward, IsAlive = true, Speed = bulletSpeed.EvaluateMinMaxCurve(), Lifetime = bulletLifetime.EvaluateMinMaxCurve(), Idx = idx }); }); } /// /// Draw gizmos for the weapon /// public void DrawGizmos(Transform transform) { #if UNITY_EDITOR var color = Color.cyan; color.a = Selection.activeObject == this ? 1 : 0.05f; zone.DrawGizmos(color, transform); if (_bullets == null) return; foreach (var b in _bulletTransforms.Where(b => b != null)) { Gizmos.DrawWireSphere(b.position, b.localScale.x/2f * bulletSize.EvaluateMinMaxCurve()); } #endif } }