Files
Alex38Lyon 878ea46cac update
2025-06-03 12:00:47 +02:00

490 lines
18 KiB
C#

using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Events;
namespace Unity.FPS.AI
{
[RequireComponent(typeof(Health), typeof(Actor), typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{
[System.Serializable]
public struct RendererIndexData
{
public Renderer Renderer;
public int MaterialIndex;
public RendererIndexData(Renderer renderer, int index)
{
Renderer = renderer;
MaterialIndex = index;
}
}
[Header("Parameters")]
[Tooltip("The Y height at which the enemy will be automatically killed (if it falls off of the level)")]
public float SelfDestructYHeight = -20f;
[Tooltip("The distance at which the enemy considers that it has reached its current path destination point")]
public float PathReachingRadius = 2f;
[Tooltip("The speed at which the enemy rotates")]
public float OrientationSpeed = 10f;
[Tooltip("Delay after death where the GameObject is destroyed (to allow for animation)")]
public float DeathDuration = 0f;
[Header("Weapons Parameters")] [Tooltip("Allow weapon swapping for this enemy")]
public bool SwapToNextWeapon = false;
[Tooltip("Time delay between a weapon swap and the next attack")]
public float DelayAfterWeaponSwap = 0f;
[Header("Eye color")] [Tooltip("Material for the eye color")]
public Material EyeColorMaterial;
[Tooltip("The default color of the bot's eye")] [ColorUsageAttribute(true, true)]
public Color DefaultEyeColor;
[Tooltip("The attack color of the bot's eye")] [ColorUsageAttribute(true, true)]
public Color AttackEyeColor;
[Header("Flash on hit")] [Tooltip("The material used for the body of the hoverbot")]
public Material BodyMaterial;
[Tooltip("The gradient representing the color of the flash on hit")] [GradientUsageAttribute(true)]
public Gradient OnHitBodyGradient;
[Tooltip("The duration of the flash on hit")]
public float FlashOnHitDuration = 0.5f;
[Header("Sounds")] [Tooltip("Sound played when recieving damages")]
public AudioClip DamageTick;
[Header("VFX")] [Tooltip("The VFX prefab spawned when the enemy dies")]
public GameObject DeathVfx;
[Tooltip("The point at which the death VFX is spawned")]
public Transform DeathVfxSpawnPoint;
[Header("Loot")] [Tooltip("The object this enemy can drop when dying")]
public GameObject LootPrefab;
[Tooltip("The chance the object has to drop")] [Range(0, 1)]
public float DropRate = 1f;
[Header("Debug Display")] [Tooltip("Color of the sphere gizmo representing the path reaching range")]
public Color PathReachingRangeColor = Color.yellow;
[Tooltip("Color of the sphere gizmo representing the attack range")]
public Color AttackRangeColor = Color.red;
[Tooltip("Color of the sphere gizmo representing the detection range")]
public Color DetectionRangeColor = Color.blue;
public UnityAction onAttack;
public UnityAction onDetectedTarget;
public UnityAction onLostTarget;
public UnityAction onDamaged;
List<RendererIndexData> m_BodyRenderers = new List<RendererIndexData>();
MaterialPropertyBlock m_BodyFlashMaterialPropertyBlock;
float m_LastTimeDamaged = float.NegativeInfinity;
RendererIndexData m_EyeRendererData;
MaterialPropertyBlock m_EyeColorMaterialPropertyBlock;
public PatrolPath PatrolPath { get; set; }
public GameObject KnownDetectedTarget => DetectionModule.KnownDetectedTarget;
public bool IsTargetInAttackRange => DetectionModule.IsTargetInAttackRange;
public bool IsSeeingTarget => DetectionModule.IsSeeingTarget;
public bool HadKnownTarget => DetectionModule.HadKnownTarget;
public NavMeshAgent NavMeshAgent { get; private set; }
public DetectionModule DetectionModule { get; private set; }
int m_PathDestinationNodeIndex;
EnemyManager m_EnemyManager;
ActorsManager m_ActorsManager;
Health m_Health;
Actor m_Actor;
Collider[] m_SelfColliders;
GameFlowManager m_GameFlowManager;
bool m_WasDamagedThisFrame;
float m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;
int m_CurrentWeaponIndex;
WeaponController m_CurrentWeapon;
WeaponController[] m_Weapons;
NavigationModule m_NavigationModule;
void Start()
{
m_EnemyManager = FindAnyObjectByType<EnemyManager>();
DebugUtility.HandleErrorIfNullFindObject<EnemyManager, EnemyController>(m_EnemyManager, this);
m_ActorsManager = FindAnyObjectByType<ActorsManager>();
DebugUtility.HandleErrorIfNullFindObject<ActorsManager, EnemyController>(m_ActorsManager, this);
m_EnemyManager.RegisterEnemy(this);
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, EnemyController>(m_Health, this, gameObject);
m_Actor = GetComponent<Actor>();
DebugUtility.HandleErrorIfNullGetComponent<Actor, EnemyController>(m_Actor, this, gameObject);
NavMeshAgent = GetComponent<NavMeshAgent>();
m_SelfColliders = GetComponentsInChildren<Collider>();
m_GameFlowManager = FindAnyObjectByType<GameFlowManager>();
DebugUtility.HandleErrorIfNullFindObject<GameFlowManager, EnemyController>(m_GameFlowManager, this);
// Subscribe to damage & death actions
m_Health.OnDie += OnDie;
m_Health.OnDamaged += OnDamaged;
// Find and initialize all weapons
FindAndInitializeAllWeapons();
var weapon = GetCurrentWeapon();
weapon.ShowWeapon(true);
var detectionModules = GetComponentsInChildren<DetectionModule>();
DebugUtility.HandleErrorIfNoComponentFound<DetectionModule, EnemyController>(detectionModules.Length, this,
gameObject);
DebugUtility.HandleWarningIfDuplicateObjects<DetectionModule, EnemyController>(detectionModules.Length,
this, gameObject);
// Initialize detection module
DetectionModule = detectionModules[0];
DetectionModule.onDetectedTarget += OnDetectedTarget;
DetectionModule.onLostTarget += OnLostTarget;
onAttack += DetectionModule.OnAttack;
var navigationModules = GetComponentsInChildren<NavigationModule>();
DebugUtility.HandleWarningIfDuplicateObjects<DetectionModule, EnemyController>(detectionModules.Length,
this, gameObject);
// Override navmesh agent data
if (navigationModules.Length > 0)
{
m_NavigationModule = navigationModules[0];
NavMeshAgent.speed = m_NavigationModule.MoveSpeed;
NavMeshAgent.angularSpeed = m_NavigationModule.AngularSpeed;
NavMeshAgent.acceleration = m_NavigationModule.Acceleration;
}
foreach (var renderer in GetComponentsInChildren<Renderer>(true))
{
for (int i = 0; i < renderer.sharedMaterials.Length; i++)
{
if (renderer.sharedMaterials[i] == EyeColorMaterial)
{
m_EyeRendererData = new RendererIndexData(renderer, i);
}
if (renderer.sharedMaterials[i] == BodyMaterial)
{
m_BodyRenderers.Add(new RendererIndexData(renderer, i));
}
}
}
m_BodyFlashMaterialPropertyBlock = new MaterialPropertyBlock();
// Check if we have an eye renderer for this enemy
if (m_EyeRendererData.Renderer != null)
{
m_EyeColorMaterialPropertyBlock = new MaterialPropertyBlock();
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", DefaultEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);
}
}
void Update()
{
EnsureIsWithinLevelBounds();
DetectionModule.HandleTargetDetection(m_Actor, m_SelfColliders);
Color currentColor = OnHitBodyGradient.Evaluate((Time.time - m_LastTimeDamaged) / FlashOnHitDuration);
m_BodyFlashMaterialPropertyBlock.SetColor("_EmissionColor", currentColor);
foreach (var data in m_BodyRenderers)
{
data.Renderer.SetPropertyBlock(m_BodyFlashMaterialPropertyBlock, data.MaterialIndex);
}
m_WasDamagedThisFrame = false;
}
void EnsureIsWithinLevelBounds()
{
// at every frame, this tests for conditions to kill the enemy
if (transform.position.y < SelfDestructYHeight)
{
Destroy(gameObject);
return;
}
}
void OnLostTarget()
{
onLostTarget.Invoke();
// Set the eye attack color and property block if the eye renderer is set
if (m_EyeRendererData.Renderer != null)
{
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", DefaultEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);
}
}
void OnDetectedTarget()
{
onDetectedTarget.Invoke();
// Set the eye default color and property block if the eye renderer is set
if (m_EyeRendererData.Renderer != null)
{
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", AttackEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);
}
}
public void OrientTowards(Vector3 lookPosition)
{
Vector3 lookDirection = Vector3.ProjectOnPlane(lookPosition - transform.position, Vector3.up).normalized;
if (lookDirection.sqrMagnitude != 0f)
{
Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
transform.rotation =
Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * OrientationSpeed);
}
}
bool IsPathValid()
{
return PatrolPath && PatrolPath.PathNodes.Count > 0;
}
public void ResetPathDestination()
{
m_PathDestinationNodeIndex = 0;
}
public void SetPathDestinationToClosestNode()
{
if (IsPathValid())
{
int closestPathNodeIndex = 0;
for (int i = 0; i < PatrolPath.PathNodes.Count; i++)
{
float distanceToPathNode = PatrolPath.GetDistanceToNode(transform.position, i);
if (distanceToPathNode < PatrolPath.GetDistanceToNode(transform.position, closestPathNodeIndex))
{
closestPathNodeIndex = i;
}
}
m_PathDestinationNodeIndex = closestPathNodeIndex;
}
else
{
m_PathDestinationNodeIndex = 0;
}
}
public Vector3 GetDestinationOnPath()
{
if (IsPathValid())
{
return PatrolPath.GetPositionOfPathNode(m_PathDestinationNodeIndex);
}
else
{
return transform.position;
}
}
public void SetNavDestination(Vector3 destination)
{
if (NavMeshAgent)
{
NavMeshAgent.SetDestination(destination);
}
}
public void UpdatePathDestination(bool inverseOrder = false)
{
if (IsPathValid())
{
// Check if reached the path destination
if ((transform.position - GetDestinationOnPath()).magnitude <= PathReachingRadius)
{
// increment path destination index
m_PathDestinationNodeIndex =
inverseOrder ? (m_PathDestinationNodeIndex - 1) : (m_PathDestinationNodeIndex + 1);
if (m_PathDestinationNodeIndex < 0)
{
m_PathDestinationNodeIndex += PatrolPath.PathNodes.Count;
}
if (m_PathDestinationNodeIndex >= PatrolPath.PathNodes.Count)
{
m_PathDestinationNodeIndex -= PatrolPath.PathNodes.Count;
}
}
}
}
void OnDamaged(float damage, GameObject damageSource)
{
// test if the damage source is the player
if (damageSource && !damageSource.GetComponent<EnemyController>())
{
// pursue the player
DetectionModule.OnDamaged(damageSource);
onDamaged?.Invoke();
m_LastTimeDamaged = Time.time;
// play the damage tick sound
if (DamageTick && !m_WasDamagedThisFrame)
AudioUtility.CreateSFX(DamageTick, transform.position, AudioUtility.AudioGroups.DamageTick, 0f);
m_WasDamagedThisFrame = true;
}
}
void OnDie()
{
// spawn a particle system when dying
var vfx = Instantiate(DeathVfx, DeathVfxSpawnPoint.position, Quaternion.identity);
Destroy(vfx, 5f);
// tells the game flow manager to handle the enemy destuction
m_EnemyManager.UnregisterEnemy(this);
// loot an object
if (TryDropItem())
{
Instantiate(LootPrefab, transform.position, Quaternion.identity);
}
// this will call the OnDestroy function
Destroy(gameObject, DeathDuration);
}
void OnDrawGizmosSelected()
{
// Path reaching range
Gizmos.color = PathReachingRangeColor;
Gizmos.DrawWireSphere(transform.position, PathReachingRadius);
if (DetectionModule != null)
{
// Detection range
Gizmos.color = DetectionRangeColor;
Gizmos.DrawWireSphere(transform.position, DetectionModule.DetectionRange);
// Attack range
Gizmos.color = AttackRangeColor;
Gizmos.DrawWireSphere(transform.position, DetectionModule.AttackRange);
}
}
public void OrientWeaponsTowards(Vector3 lookPosition)
{
for (int i = 0; i < m_Weapons.Length; i++)
{
// orient weapon towards player
Vector3 weaponForward = (lookPosition - m_Weapons[i].WeaponRoot.transform.position).normalized;
m_Weapons[i].transform.forward = weaponForward;
}
}
public bool TryAtack(Vector3 enemyPosition)
{
if (m_GameFlowManager.GameIsEnding)
return false;
OrientWeaponsTowards(enemyPosition);
if ((m_LastTimeWeaponSwapped + DelayAfterWeaponSwap) >= Time.time)
return false;
// Shoot the weapon
bool didFire = GetCurrentWeapon().HandleShootInputs(false, true, false);
if (didFire && onAttack != null)
{
onAttack.Invoke();
if (SwapToNextWeapon && m_Weapons.Length > 1)
{
int nextWeaponIndex = (m_CurrentWeaponIndex + 1) % m_Weapons.Length;
SetCurrentWeapon(nextWeaponIndex);
}
}
return didFire;
}
public bool TryDropItem()
{
if (DropRate == 0 || LootPrefab == null)
return false;
else if (DropRate == 1)
return true;
else
return (Random.value <= DropRate);
}
void FindAndInitializeAllWeapons()
{
// Check if we already found and initialized the weapons
if (m_Weapons == null)
{
m_Weapons = GetComponentsInChildren<WeaponController>();
DebugUtility.HandleErrorIfNoComponentFound<WeaponController, EnemyController>(m_Weapons.Length, this,
gameObject);
for (int i = 0; i < m_Weapons.Length; i++)
{
m_Weapons[i].Owner = gameObject;
}
}
}
public WeaponController GetCurrentWeapon()
{
FindAndInitializeAllWeapons();
// Check if no weapon is currently selected
if (m_CurrentWeapon == null)
{
// Set the first weapon of the weapons list as the current weapon
SetCurrentWeapon(0);
}
DebugUtility.HandleErrorIfNullGetComponent<WeaponController, EnemyController>(m_CurrentWeapon, this,
gameObject);
return m_CurrentWeapon;
}
void SetCurrentWeapon(int index)
{
m_CurrentWeaponIndex = index;
m_CurrentWeapon = m_Weapons[m_CurrentWeaponIndex];
if (SwapToNextWeapon)
{
m_LastTimeWeaponSwapped = Time.time;
}
else
{
m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;
}
}
}
}