Characters
This commit is contained in:
534
Game/Scripts/WeaponController.cs
Normal file
534
Game/Scripts/WeaponController.cs
Normal file
@@ -0,0 +1,534 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MegaKoop.Game.Combat;
|
||||
using MegaKoop.Game.WeaponSystem;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace MegaKoop.Game.WeaponSystem
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class WeaponController : MonoBehaviour
|
||||
{
|
||||
[Header("Ownership")]
|
||||
[SerializeField] private Team ownerTeam = Team.Heroes;
|
||||
[SerializeField] private Transform weaponSocket;
|
||||
|
||||
[Header("Targeting")]
|
||||
[SerializeField] private float acquisitionRadius = 30f;
|
||||
[SerializeField] private float retargetInterval = 0.25f;
|
||||
[SerializeField] private LayerMask targetMask = Physics.DefaultRaycastLayers;
|
||||
[SerializeField] private LayerMask lineOfSightMask = Physics.DefaultRaycastLayers;
|
||||
[SerializeField] private bool requireLineOfSight = true;
|
||||
|
||||
[Header("Loadout")]
|
||||
[SerializeField] private WeaponDefinition[] startingWeapons;
|
||||
[SerializeField] private bool autoFire = true;
|
||||
|
||||
private static readonly RaycastHit[] LineOfSightHitsBuffer = new RaycastHit[8];
|
||||
|
||||
private readonly List<WeaponRuntime> equippedWeapons = new();
|
||||
private readonly HashSet<Collider> ignoredOwnerColliders = new();
|
||||
|
||||
public event Action<ProjectileSpawnEvent> ProjectileSpawned;
|
||||
|
||||
private float maxWeaponRange;
|
||||
private float retargetTimer;
|
||||
private TargetInfo currentTarget;
|
||||
private bool hasTarget;
|
||||
private Collider[] ownerColliders;
|
||||
private Health ownerHealth;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ownerHealth = GetComponent<Health>();
|
||||
if (ownerHealth != null)
|
||||
{
|
||||
ownerTeam = ownerHealth.Team;
|
||||
}
|
||||
|
||||
ownerColliders = GetComponentsInChildren<Collider>();
|
||||
if (ownerColliders != null)
|
||||
{
|
||||
for (int i = 0; i < ownerColliders.Length; i++)
|
||||
{
|
||||
Collider ownerCollider = ownerColliders[i];
|
||||
if (ownerCollider == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ignoredOwnerColliders.Add(ownerCollider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (weaponSocket == null)
|
||||
{
|
||||
weaponSocket = transform;
|
||||
}
|
||||
|
||||
if (startingWeapons != null)
|
||||
{
|
||||
foreach (WeaponDefinition definition in startingWeapons)
|
||||
{
|
||||
Equip(definition, weaponSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (equippedWeapons.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoFire)
|
||||
{
|
||||
UpdateTargetSelection(Time.deltaTime);
|
||||
}
|
||||
|
||||
TargetInfo target = hasTarget ? currentTarget : default;
|
||||
|
||||
foreach (WeaponRuntime weapon in equippedWeapons)
|
||||
{
|
||||
weapon.Tick(Time.deltaTime, target, this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Equip(WeaponDefinition definition, Transform mount)
|
||||
{
|
||||
if (definition == null || mount == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int weaponIndex = equippedWeapons.Count;
|
||||
WeaponRuntime runtime = new WeaponRuntime(definition, mount, weaponIndex);
|
||||
equippedWeapons.Add(runtime);
|
||||
maxWeaponRange = Mathf.Max(maxWeaponRange, definition.Range);
|
||||
}
|
||||
|
||||
public void Unequip(WeaponDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = equippedWeapons.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (equippedWeapons[i].Definition == definition)
|
||||
{
|
||||
equippedWeapons[i].Dispose();
|
||||
equippedWeapons.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
RecalculateMaxRange();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
foreach (WeaponRuntime weapon in equippedWeapons)
|
||||
{
|
||||
weapon.Dispose();
|
||||
}
|
||||
|
||||
equippedWeapons.Clear();
|
||||
hasTarget = false;
|
||||
}
|
||||
|
||||
private void UpdateTargetSelection(float deltaTime)
|
||||
{
|
||||
retargetTimer -= deltaTime;
|
||||
|
||||
if (hasTarget)
|
||||
{
|
||||
if (!currentTarget.IsValid)
|
||||
{
|
||||
hasTarget = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
float distance = Vector3.Distance(transform.position, currentTarget.Transform.position);
|
||||
float allowedRange = Mathf.Max(acquisitionRadius, maxWeaponRange);
|
||||
if (distance > allowedRange * 1.1f)
|
||||
{
|
||||
hasTarget = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (retargetTimer > 0f && hasTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
retargetTimer = retargetInterval;
|
||||
AcquireTarget();
|
||||
}
|
||||
|
||||
private void AcquireTarget()
|
||||
{
|
||||
float searchRadius = Mathf.Max(acquisitionRadius, maxWeaponRange);
|
||||
if (searchRadius <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Collider[] hits = Physics.OverlapSphere(transform.position, searchRadius, targetMask, QueryTriggerInteraction.Collide);
|
||||
TargetInfo bestTarget = default;
|
||||
float bestScore = float.MaxValue;
|
||||
|
||||
foreach (Collider hit in hits)
|
||||
{
|
||||
if (hit == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredOwnerColliders.Contains(hit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IDamageable damageable = hit.GetComponentInParent<IDamageable>();
|
||||
if (damageable == null || !damageable.IsAlive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (damageable.Team == ownerTeam && ownerTeam != Team.Neutral)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (damageable is not Component component)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Transform targetTransform = component.transform;
|
||||
float sqrDistance = (targetTransform.position - transform.position).sqrMagnitude;
|
||||
if (sqrDistance > maxWeaponRange * maxWeaponRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float score = sqrDistance;
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestTarget = new TargetInfo(damageable, hit, targetTransform);
|
||||
}
|
||||
}
|
||||
|
||||
hasTarget = bestTarget.IsValid;
|
||||
currentTarget = bestTarget;
|
||||
}
|
||||
|
||||
private void RecalculateMaxRange()
|
||||
{
|
||||
maxWeaponRange = 0f;
|
||||
for (int i = 0; i < equippedWeapons.Count; i++)
|
||||
{
|
||||
WeaponRuntime weapon = equippedWeapons[i];
|
||||
weapon.SetIndex(i);
|
||||
maxWeaponRange = Mathf.Max(maxWeaponRange, weapon.Definition.Range);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetAimPoint(TargetInfo target)
|
||||
{
|
||||
if (!target.IsValid)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
if (target.Collider != null)
|
||||
{
|
||||
return target.Collider.bounds.center;
|
||||
}
|
||||
|
||||
return target.Transform.position;
|
||||
}
|
||||
|
||||
private bool HasLineOfSight(Transform muzzle, Vector3 aimPoint, TargetInfo target)
|
||||
{
|
||||
if (!requireLineOfSight)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector3 origin = muzzle.position;
|
||||
Vector3 toTarget = aimPoint - origin;
|
||||
float distance = toTarget.magnitude;
|
||||
if (distance <= 0.01f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector3 direction = toTarget.normalized;
|
||||
int hitCount = Physics.RaycastNonAlloc(origin, direction, LineOfSightHitsBuffer, distance, lineOfSightMask, QueryTriggerInteraction.Ignore);
|
||||
|
||||
if (hitCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float targetDistance = float.MaxValue;
|
||||
bool hasTargetHit = false;
|
||||
|
||||
for (int i = 0; i < hitCount; i++)
|
||||
{
|
||||
RaycastHit hit = LineOfSightHitsBuffer[i];
|
||||
Collider hitCollider = hit.collider;
|
||||
if (hitCollider == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredOwnerColliders.Contains(hitCollider))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.Collider != null && hitCollider == target.Collider)
|
||||
{
|
||||
targetDistance = hit.distance;
|
||||
hasTargetHit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hit.distance < targetDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasTargetHit;
|
||||
}
|
||||
|
||||
private void SpawnProjectile(WeaponRuntime weapon, Transform muzzle, Vector3 direction)
|
||||
{
|
||||
Projectile projectilePrefab = weapon.Definition.ProjectilePrefab;
|
||||
if (projectilePrefab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion rotation = direction.sqrMagnitude > 0f ? Quaternion.LookRotation(direction) : muzzle.rotation;
|
||||
Projectile projectile = Instantiate(projectilePrefab, muzzle.position, rotation);
|
||||
projectile.Initialize(direction, weapon.Definition.ProjectileSpeed, weapon.Definition.Damage, weapon.Definition.ProjectileLifetime, gameObject, ownerTeam, ownerColliders, weapon.Definition.HitMask);
|
||||
|
||||
ProjectileSpawned?.Invoke(new ProjectileSpawnEvent(weapon.Index, muzzle.position, direction, weapon.Definition.ProjectileSpeed, weapon.Definition.ProjectileLifetime, weapon.Definition.Damage));
|
||||
}
|
||||
|
||||
public void SpawnNetworkProjectile(int weaponIndex, Vector3 position, Vector3 direction, float speed, float lifetime, float damage)
|
||||
{
|
||||
if (weaponIndex < 0 || weaponIndex >= equippedWeapons.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WeaponRuntime weapon = equippedWeapons[weaponIndex];
|
||||
weapon.SpawnFromNetwork(position, direction, speed, lifetime, damage, ownerTeam, ownerColliders, gameObject);
|
||||
}
|
||||
|
||||
private void FireWeapon(WeaponRuntime weapon, TargetInfo target, Transform muzzle)
|
||||
{
|
||||
Vector3 aimPoint = GetAimPoint(target);
|
||||
Vector3 toTarget = aimPoint - muzzle.position;
|
||||
if (toTarget.sqrMagnitude <= 0.0001f)
|
||||
{
|
||||
toTarget = muzzle.forward;
|
||||
}
|
||||
|
||||
if (!HasLineOfSight(muzzle, aimPoint, target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 baseDirection = toTarget.normalized;
|
||||
int projectiles = weapon.Definition.ProjectilesPerShot;
|
||||
float spread = weapon.Definition.SpreadAngle;
|
||||
|
||||
for (int i = 0; i < projectiles; i++)
|
||||
{
|
||||
Vector3 direction = baseDirection;
|
||||
if (spread > 0f)
|
||||
{
|
||||
Vector2 offset = Random.insideUnitCircle * spread;
|
||||
Quaternion spreadRotation = Quaternion.AngleAxis(offset.x, muzzle.up) * Quaternion.AngleAxis(offset.y, muzzle.right);
|
||||
direction = spreadRotation * baseDirection;
|
||||
}
|
||||
|
||||
SpawnProjectile(weapon, muzzle, direction);
|
||||
}
|
||||
|
||||
weapon.ResetCooldown();
|
||||
}
|
||||
|
||||
private readonly struct TargetInfo
|
||||
{
|
||||
public readonly IDamageable Damageable;
|
||||
public readonly Collider Collider;
|
||||
public readonly Transform Transform;
|
||||
|
||||
public TargetInfo(IDamageable damageable, Collider collider, Transform transform)
|
||||
{
|
||||
Damageable = damageable;
|
||||
Collider = collider;
|
||||
Transform = transform;
|
||||
}
|
||||
|
||||
public bool IsValid => Damageable != null && Damageable.IsAlive && Transform != null;
|
||||
}
|
||||
|
||||
private sealed class WeaponRuntime
|
||||
{
|
||||
private readonly Transform mountPoint;
|
||||
private readonly Transform fallbackMuzzle;
|
||||
private readonly Transform[] muzzles;
|
||||
private int muzzleIndex;
|
||||
private float cooldown;
|
||||
|
||||
public WeaponDefinition Definition { get; }
|
||||
public WeaponView ViewInstance { get; }
|
||||
public int Index { get; private set; }
|
||||
|
||||
public WeaponRuntime(WeaponDefinition definition, Transform mount, int index)
|
||||
{
|
||||
Definition = definition ?? throw new ArgumentNullException(nameof(definition));
|
||||
mountPoint = mount;
|
||||
Index = index;
|
||||
|
||||
if (definition.ViewPrefab != null)
|
||||
{
|
||||
ViewInstance = UnityEngine.Object.Instantiate(definition.ViewPrefab, mountPoint);
|
||||
ViewInstance.transform.localPosition = Vector3.zero;
|
||||
ViewInstance.transform.localRotation = Quaternion.identity;
|
||||
ViewInstance.transform.localScale = Vector3.one;
|
||||
muzzles = ViewInstance.Muzzles;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewInstance = null;
|
||||
muzzles = Array.Empty<Transform>();
|
||||
}
|
||||
|
||||
fallbackMuzzle = mountPoint;
|
||||
muzzleIndex = 0;
|
||||
cooldown = 0f;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime, TargetInfo target, WeaponController controller)
|
||||
{
|
||||
cooldown = Mathf.Max(0f, cooldown - deltaTime);
|
||||
if (cooldown > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.IsValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform muzzle = GetMuzzle();
|
||||
if (muzzle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 aimPoint = controller.GetAimPoint(target);
|
||||
Vector3 toTarget = aimPoint - muzzle.position;
|
||||
float sqrDistance = toTarget.sqrMagnitude;
|
||||
float maxRange = Definition.Range;
|
||||
if (sqrDistance > maxRange * maxRange)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
controller.FireWeapon(this, target, muzzle);
|
||||
}
|
||||
|
||||
public Transform GetMuzzle()
|
||||
{
|
||||
if (muzzles != null && muzzles.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < muzzles.Length; i++)
|
||||
{
|
||||
int current = (muzzleIndex + i) % muzzles.Length;
|
||||
Transform muzzle = muzzles[current];
|
||||
if (muzzle != null)
|
||||
{
|
||||
muzzleIndex = (current + 1) % muzzles.Length;
|
||||
return muzzle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackMuzzle;
|
||||
}
|
||||
|
||||
public void ResetCooldown()
|
||||
{
|
||||
cooldown = Definition.FireInterval;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (ViewInstance != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(ViewInstance.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetIndex(int newIndex)
|
||||
{
|
||||
Index = newIndex;
|
||||
}
|
||||
|
||||
public void SpawnFromNetwork(Vector3 position, Vector3 direction, float speed, float lifetime, float damage, Team ownerTeam, Collider[] ownerColliders, GameObject owner)
|
||||
{
|
||||
Projectile projectilePrefab = Definition.ProjectilePrefab;
|
||||
if (projectilePrefab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion rotation = direction.sqrMagnitude > 0f ? Quaternion.LookRotation(direction) : fallbackMuzzle.rotation;
|
||||
Projectile projectile = UnityEngine.Object.Instantiate(projectilePrefab, position, rotation);
|
||||
float resolvedSpeed = speed > 0f ? speed : Definition.ProjectileSpeed;
|
||||
float resolvedLifetime = lifetime > 0f ? lifetime : Definition.ProjectileLifetime;
|
||||
float resolvedDamage = damage > 0f ? damage : Definition.Damage;
|
||||
projectile.Initialize(direction, resolvedSpeed, resolvedDamage, resolvedLifetime, owner, ownerTeam, ownerColliders, Definition.HitMask);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ProjectileSpawnEvent
|
||||
{
|
||||
public readonly int WeaponIndex;
|
||||
public readonly Vector3 Position;
|
||||
public readonly Vector3 Direction;
|
||||
public readonly float Speed;
|
||||
public readonly float Lifetime;
|
||||
public readonly float Damage;
|
||||
|
||||
public ProjectileSpawnEvent(int weaponIndex, Vector3 position, Vector3 direction, float speed, float lifetime, float damage)
|
||||
{
|
||||
WeaponIndex = weaponIndex;
|
||||
Position = position;
|
||||
Direction = direction;
|
||||
Speed = speed;
|
||||
Lifetime = lifetime;
|
||||
Damage = damage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user