This commit is contained in:
Alex38Lyon
2025-06-03 12:00:47 +02:00
parent ed8041abcd
commit 878ea46cac
1300 changed files with 527178 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
using System.Linq;
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.AI
{
public class DetectionModule : MonoBehaviour
{
[Tooltip("The point representing the source of target-detection raycasts for the enemy AI")]
public Transform DetectionSourcePoint;
[Tooltip("The max distance at which the enemy can see targets")]
public float DetectionRange = 20f;
[Tooltip("The max distance at which the enemy can attack its target")]
public float AttackRange = 10f;
[Tooltip("Time before an enemy abandons a known target that it can't see anymore")]
public float KnownTargetTimeout = 4f;
[Tooltip("Optional animator for OnShoot animations")]
public Animator Animator;
public UnityAction onDetectedTarget;
public UnityAction onLostTarget;
public GameObject KnownDetectedTarget { get; private set; }
public bool IsTargetInAttackRange { get; private set; }
public bool IsSeeingTarget { get; private set; }
public bool HadKnownTarget { get; private set; }
protected float TimeLastSeenTarget = Mathf.NegativeInfinity;
ActorsManager m_ActorsManager;
const string k_AnimAttackParameter = "Attack";
const string k_AnimOnDamagedParameter = "OnDamaged";
protected virtual void Start()
{
m_ActorsManager = FindAnyObjectByType<ActorsManager>();
DebugUtility.HandleErrorIfNullFindObject<ActorsManager, DetectionModule>(m_ActorsManager, this);
}
public virtual void HandleTargetDetection(Actor actor, Collider[] selfColliders)
{
// Handle known target detection timeout
if (KnownDetectedTarget && !IsSeeingTarget && (Time.time - TimeLastSeenTarget) > KnownTargetTimeout)
{
KnownDetectedTarget = null;
}
// Find the closest visible hostile actor
float sqrDetectionRange = DetectionRange * DetectionRange;
IsSeeingTarget = false;
float closestSqrDistance = Mathf.Infinity;
foreach (Actor otherActor in m_ActorsManager.Actors)
{
if (otherActor.Affiliation != actor.Affiliation)
{
float sqrDistance = (otherActor.transform.position - DetectionSourcePoint.position).sqrMagnitude;
if (sqrDistance < sqrDetectionRange && sqrDistance < closestSqrDistance)
{
// Check for obstructions
RaycastHit[] hits = Physics.RaycastAll(DetectionSourcePoint.position,
(otherActor.AimPoint.position - DetectionSourcePoint.position).normalized, DetectionRange,
-1, QueryTriggerInteraction.Ignore);
RaycastHit closestValidHit = new RaycastHit();
closestValidHit.distance = Mathf.Infinity;
bool foundValidHit = false;
foreach (var hit in hits)
{
if (!selfColliders.Contains(hit.collider) && hit.distance < closestValidHit.distance)
{
closestValidHit = hit;
foundValidHit = true;
}
}
if (foundValidHit)
{
Actor hitActor = closestValidHit.collider.GetComponentInParent<Actor>();
if (hitActor == otherActor)
{
IsSeeingTarget = true;
closestSqrDistance = sqrDistance;
TimeLastSeenTarget = Time.time;
KnownDetectedTarget = otherActor.AimPoint.gameObject;
}
}
}
}
}
IsTargetInAttackRange = KnownDetectedTarget != null &&
Vector3.Distance(transform.position, KnownDetectedTarget.transform.position) <=
AttackRange;
// Detection events
if (!HadKnownTarget &&
KnownDetectedTarget != null)
{
OnDetect();
}
if (HadKnownTarget &&
KnownDetectedTarget == null)
{
OnLostTarget();
}
// Remember if we already knew a target (for next frame)
HadKnownTarget = KnownDetectedTarget != null;
}
public virtual void OnLostTarget() => onLostTarget?.Invoke();
public virtual void OnDetect() => onDetectedTarget?.Invoke();
public virtual void OnDamaged(GameObject damageSource)
{
TimeLastSeenTarget = Time.time;
KnownDetectedTarget = damageSource;
if (Animator)
{
Animator.SetTrigger(k_AnimOnDamagedParameter);
}
}
public virtual void OnAttack()
{
if (Animator)
{
Animator.SetTrigger(k_AnimAttackParameter);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 927ff0b4cb9a5dd4fb83c45166cffc50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+490
View File
@@ -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;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3de939d976a5bb0419ccd8dbd0b6836b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+38
View File
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
public class EnemyManager : MonoBehaviour
{
public List<EnemyController> Enemies { get; private set; }
public int NumberOfEnemiesTotal { get; private set; }
public int NumberOfEnemiesRemaining => Enemies.Count;
void Awake()
{
Enemies = new List<EnemyController>();
}
public void RegisterEnemy(EnemyController enemy)
{
Enemies.Add(enemy);
NumberOfEnemiesTotal++;
}
public void UnregisterEnemy(EnemyController enemyKilled)
{
int enemiesRemainingNotification = NumberOfEnemiesRemaining - 1;
EnemyKillEvent evt = Events.EnemyKillEvent;
evt.Enemy = enemyKilled.gameObject;
evt.RemainingEnemyCount = enemiesRemainingNotification;
EventManager.Broadcast(evt);
// removes the enemy from the list, so that we can keep track of how many are left on the map
Enemies.Remove(enemyKilled);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 888c7d8f8f69c6b42a086589b6f01bb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+185
View File
@@ -0,0 +1,185 @@
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
[RequireComponent(typeof(EnemyController))]
public class EnemyMobile : MonoBehaviour
{
public enum AIState
{
Patrol,
Follow,
Attack,
}
public Animator Animator;
[Tooltip("Fraction of the enemy's attack range at which it will stop moving towards target while attacking")]
[Range(0f, 1f)]
public float AttackStopDistanceRatio = 0.5f;
[Tooltip("The random hit damage effects")]
public ParticleSystem[] RandomHitSparks;
public ParticleSystem[] OnDetectVfx;
public AudioClip OnDetectSfx;
[Header("Sound")] public AudioClip MovementSound;
public MinMaxFloat PitchDistortionMovementSpeed;
public AIState AiState { get; private set; }
EnemyController m_EnemyController;
AudioSource m_AudioSource;
const string k_AnimMoveSpeedParameter = "MoveSpeed";
const string k_AnimAttackParameter = "Attack";
const string k_AnimAlertedParameter = "Alerted";
const string k_AnimOnDamagedParameter = "OnDamaged";
void Start()
{
m_EnemyController = GetComponent<EnemyController>();
DebugUtility.HandleErrorIfNullGetComponent<EnemyController, EnemyMobile>(m_EnemyController, this,
gameObject);
m_EnemyController.onAttack += OnAttack;
m_EnemyController.onDetectedTarget += OnDetectedTarget;
m_EnemyController.onLostTarget += OnLostTarget;
m_EnemyController.SetPathDestinationToClosestNode();
m_EnemyController.onDamaged += OnDamaged;
// Start patrolling
AiState = AIState.Patrol;
// adding a audio source to play the movement sound on it
m_AudioSource = GetComponent<AudioSource>();
DebugUtility.HandleErrorIfNullGetComponent<AudioSource, EnemyMobile>(m_AudioSource, this, gameObject);
m_AudioSource.clip = MovementSound;
m_AudioSource.Play();
}
void Update()
{
UpdateAiStateTransitions();
UpdateCurrentAiState();
float moveSpeed = m_EnemyController.NavMeshAgent.velocity.magnitude;
// Update animator speed parameter
Animator.SetFloat(k_AnimMoveSpeedParameter, moveSpeed);
// changing the pitch of the movement sound depending on the movement speed
m_AudioSource.pitch = Mathf.Lerp(PitchDistortionMovementSpeed.Min, PitchDistortionMovementSpeed.Max,
moveSpeed / m_EnemyController.NavMeshAgent.speed);
}
void UpdateAiStateTransitions()
{
// Handle transitions
switch (AiState)
{
case AIState.Follow:
// Transition to attack when there is a line of sight to the target
if (m_EnemyController.IsSeeingTarget && m_EnemyController.IsTargetInAttackRange)
{
AiState = AIState.Attack;
m_EnemyController.SetNavDestination(transform.position);
}
break;
case AIState.Attack:
// Transition to follow when no longer a target in attack range
if (!m_EnemyController.IsTargetInAttackRange)
{
AiState = AIState.Follow;
}
break;
}
}
void UpdateCurrentAiState()
{
// Handle logic
switch (AiState)
{
case AIState.Patrol:
m_EnemyController.UpdatePathDestination();
// m_EnemyController.SetNavDestination(m_EnemyController.GetDestinationOnPath());
break;
case AIState.Follow:
m_EnemyController.SetNavDestination(m_EnemyController.KnownDetectedTarget.transform.position);
m_EnemyController.OrientTowards(m_EnemyController.KnownDetectedTarget.transform.position);
m_EnemyController.OrientWeaponsTowards(m_EnemyController.KnownDetectedTarget.transform.position);
break;
case AIState.Attack:
if (Vector3.Distance(m_EnemyController.KnownDetectedTarget.transform.position,
m_EnemyController.DetectionModule.DetectionSourcePoint.position)
>= (AttackStopDistanceRatio * m_EnemyController.DetectionModule.AttackRange))
{
m_EnemyController.SetNavDestination(m_EnemyController.KnownDetectedTarget.transform.position);
}
else
{
m_EnemyController.SetNavDestination(transform.position);
}
m_EnemyController.OrientTowards(m_EnemyController.KnownDetectedTarget.transform.position);
m_EnemyController.TryAtack(m_EnemyController.KnownDetectedTarget.transform.position);
break;
}
}
void OnAttack()
{
Animator.SetTrigger(k_AnimAttackParameter);
}
void OnDetectedTarget()
{
if (AiState == AIState.Patrol)
{
AiState = AIState.Follow;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Play();
}
if (OnDetectSfx)
{
AudioUtility.CreateSFX(OnDetectSfx, transform.position, AudioUtility.AudioGroups.EnemyDetection, 1f);
}
Animator.SetBool(k_AnimAlertedParameter, true);
}
void OnLostTarget()
{
if (AiState == AIState.Follow || AiState == AIState.Attack)
{
AiState = AIState.Patrol;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Stop();
}
Animator.SetBool(k_AnimAlertedParameter, false);
}
void OnDamaged()
{
if (RandomHitSparks.Length > 0)
{
int n = Random.Range(0, RandomHitSparks.Length - 1);
RandomHitSparks[n].Play();
}
Animator.SetTrigger(k_AnimOnDamagedParameter);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1857b2df8de24b24190ce9e067d2e1e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+170
View File
@@ -0,0 +1,170 @@
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
[RequireComponent(typeof(EnemyController))]
public class EnemyTurret : MonoBehaviour
{
public enum AIState
{
Idle,
Attack,
}
public Transform TurretPivot;
public Transform TurretAimPoint;
public Animator Animator;
public float AimRotationSharpness = 5f;
public float LookAtRotationSharpness = 2.5f;
public float DetectionFireDelay = 1f;
public float AimingTransitionBlendTime = 1f;
[Tooltip("The random hit damage effects")]
public ParticleSystem[] RandomHitSparks;
public ParticleSystem[] OnDetectVfx;
public AudioClip OnDetectSfx;
public AIState AiState { get; private set; }
EnemyController m_EnemyController;
Health m_Health;
Quaternion m_RotationWeaponForwardToPivot;
float m_TimeStartedDetection;
float m_TimeLostDetection;
Quaternion m_PreviousPivotAimingRotation;
Quaternion m_PivotAimingRotation;
const string k_AnimOnDamagedParameter = "OnDamaged";
const string k_AnimIsActiveParameter = "IsActive";
void Start()
{
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, EnemyTurret>(m_Health, this, gameObject);
m_Health.OnDamaged += OnDamaged;
m_EnemyController = GetComponent<EnemyController>();
DebugUtility.HandleErrorIfNullGetComponent<EnemyController, EnemyTurret>(m_EnemyController, this,
gameObject);
m_EnemyController.onDetectedTarget += OnDetectedTarget;
m_EnemyController.onLostTarget += OnLostTarget;
// Remember the rotation offset between the pivot's forward and the weapon's forward
m_RotationWeaponForwardToPivot =
Quaternion.Inverse(m_EnemyController.GetCurrentWeapon().WeaponMuzzle.rotation) * TurretPivot.rotation;
// Start with idle
AiState = AIState.Idle;
m_TimeStartedDetection = Mathf.NegativeInfinity;
m_PreviousPivotAimingRotation = TurretPivot.rotation;
}
void Update()
{
UpdateCurrentAiState();
}
void LateUpdate()
{
UpdateTurretAiming();
}
void UpdateCurrentAiState()
{
// Handle logic
switch (AiState)
{
case AIState.Attack:
bool mustShoot = Time.time > m_TimeStartedDetection + DetectionFireDelay;
// Calculate the desired rotation of our turret (aim at target)
Vector3 directionToTarget =
(m_EnemyController.KnownDetectedTarget.transform.position - TurretAimPoint.position).normalized;
Quaternion offsettedTargetRotation =
Quaternion.LookRotation(directionToTarget) * m_RotationWeaponForwardToPivot;
m_PivotAimingRotation = Quaternion.Slerp(m_PreviousPivotAimingRotation, offsettedTargetRotation,
(mustShoot ? AimRotationSharpness : LookAtRotationSharpness) * Time.deltaTime);
// shoot
if (mustShoot)
{
Vector3 correctedDirectionToTarget =
(m_PivotAimingRotation * Quaternion.Inverse(m_RotationWeaponForwardToPivot)) *
Vector3.forward;
m_EnemyController.TryAtack(TurretAimPoint.position + correctedDirectionToTarget);
}
break;
}
}
void UpdateTurretAiming()
{
switch (AiState)
{
case AIState.Attack:
TurretPivot.rotation = m_PivotAimingRotation;
break;
default:
// Use the turret rotation of the animation
TurretPivot.rotation = Quaternion.Slerp(m_PivotAimingRotation, TurretPivot.rotation,
(Time.time - m_TimeLostDetection) / AimingTransitionBlendTime);
break;
}
m_PreviousPivotAimingRotation = TurretPivot.rotation;
}
void OnDamaged(float dmg, GameObject source)
{
if (RandomHitSparks.Length > 0)
{
int n = Random.Range(0, RandomHitSparks.Length - 1);
RandomHitSparks[n].Play();
}
Animator.SetTrigger(k_AnimOnDamagedParameter);
}
void OnDetectedTarget()
{
if (AiState == AIState.Idle)
{
AiState = AIState.Attack;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Play();
}
if (OnDetectSfx)
{
AudioUtility.CreateSFX(OnDetectSfx, transform.position, AudioUtility.AudioGroups.EnemyDetection, 1f);
}
Animator.SetBool(k_AnimIsActiveParameter, true);
m_TimeStartedDetection = Time.time;
}
void OnLostTarget()
{
if (AiState == AIState.Attack)
{
AiState = AIState.Idle;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Stop();
}
Animator.SetBool(k_AnimIsActiveParameter, false);
m_TimeLostDetection = Time.time;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4cb5371dba09ee46806cbe0c2aa2c9c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+30
View File
@@ -0,0 +1,30 @@
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
public class FollowPlayer : MonoBehaviour
{
Transform m_PlayerTransform;
Vector3 m_OriginalOffset;
void Start()
{
ActorsManager actorsManager = FindAnyObjectByType<ActorsManager>();
if (actorsManager != null)
m_PlayerTransform = actorsManager.Player.transform;
else
{
enabled = false;
return;
}
m_OriginalOffset = transform.position - m_PlayerTransform.position;
}
void LateUpdate()
{
transform.position = m_PlayerTransform.position + m_OriginalOffset;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3cb6842226810914cb7e746a8f872d68
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+18
View File
@@ -0,0 +1,18 @@
using UnityEngine;
namespace Unity.FPS.AI
{
// Component used to override values on start from the NavmeshAgent component in order to change
// how the agent is moving
public class NavigationModule : MonoBehaviour
{
[Header("Parameters")] [Tooltip("The maximum speed at which the enemy is moving (in world units per second).")]
public float MoveSpeed = 0f;
[Tooltip("The maximum speed at which the enemy is rotating (degrees per second).")]
public float AngularSpeed = 0f;
[Tooltip("The acceleration to reach the maximum speed (in world units per second squared).")]
public float Acceleration = 0f;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22d6b672a3f9d27468cfdd79b2119c7e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+59
View File
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.AI
{
public class PatrolPath : MonoBehaviour
{
[Tooltip("Enemies that will be assigned to this path on Start")]
public List<EnemyController> EnemiesToAssign = new List<EnemyController>();
[Tooltip("The Nodes making up the path")]
public List<Transform> PathNodes = new List<Transform>();
void Start()
{
foreach (var enemy in EnemiesToAssign)
{
enemy.PatrolPath = this;
}
}
public float GetDistanceToNode(Vector3 origin, int destinationNodeIndex)
{
if (destinationNodeIndex < 0 || destinationNodeIndex >= PathNodes.Count ||
PathNodes[destinationNodeIndex] == null)
{
return -1f;
}
return (PathNodes[destinationNodeIndex].position - origin).magnitude;
}
public Vector3 GetPositionOfPathNode(int nodeIndex)
{
if (nodeIndex < 0 || nodeIndex >= PathNodes.Count || PathNodes[nodeIndex] == null)
{
return Vector3.zero;
}
return PathNodes[nodeIndex].position;
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
for (int i = 0; i < PathNodes.Count; i++)
{
int nextIndex = i + 1;
if (nextIndex >= PathNodes.Count)
{
nextIndex -= PathNodes.Count;
}
Gizmos.DrawLine(PathNodes[i].position, PathNodes[nextIndex].position);
Gizmos.DrawSphere(PathNodes[i].position, 0.1f);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2ec2f49cb5488d0459ff53ecc714acb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
{
"name": "fps.AI",
"references": [
"GUID:9c5543eabb73a6249ac07b2c065ee8b4"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e9dc24b3d0971d64a9e95b6986d97af5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: