update
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user