using System.Collections.Generic;using UnityEngine;namespace Unity.FPS.AI
{publicclassPatrolPath:MonoBehaviour{[Tooltip("Enemies that will be assigned to this path on Start")]public List<EnemyController> EnemiesToAssign =newList<EnemyController>();[Tooltip("The Nodes making up the path")]public List<Transform> PathNodes =newList<Transform>();voidStart(){foreach(var enemy in EnemiesToAssign){
enemy.PatrolPath =this;}}publicfloatGetDistanceToNode(Vector3 origin,int destinationNodeIndex){if(destinationNodeIndex <0|| destinationNodeIndex >= PathNodes.Count ||
PathNodes[destinationNodeIndex]==null){return-1f;}return(PathNodes[destinationNodeIndex].position - origin).magnitude;}publicVector3GetPositionOfPathNode(int nodeIndex){if(nodeIndex <0|| nodeIndex >= PathNodes.Count || PathNodes[nodeIndex]==null){return Vector3.zero;}return PathNodes[nodeIndex].position;}voidOnDrawGizmosSelected(){
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);}}}}
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 movingpublicclassNavigationModule:MonoBehaviour{[Header("Parameters")][Tooltip("The maximum speed at which the enemy is moving (in world units per second).")]publicfloat MoveSpeed =0f;[Tooltip("The maximum speed at which the enemy is rotating (degrees per second).")]publicfloat AngularSpeed =0f;[Tooltip("The acceleration to reach the maximum speed (in world units per second squared).")]publicfloat Acceleration =0f;}}
7. 控制Navigation赋值函数:
using Unity.FPS.Game;using UnityEngine;namespace Unity.FPS.AI
{[RequireComponent(typeof(EnemyController))]publicclassEnemyMobile:MonoBehaviour{publicenum AIState
{
Patrol,
Follow,
Attack,}publicAnimator Animator;[Tooltip("Fraction of the enemy's attack range at which it will stop moving towards target while attacking")][Range(0f,1f)]publicfloat AttackStopDistanceRatio =0.5f;[Tooltip("The random hit damage effects")]public ParticleSystem[] RandomHitSparks;public ParticleSystem[] OnDetectVfx;publicAudioClip OnDetectSfx;[Header("Sound")]publicAudioClip MovementSound;publicMinMaxFloat PitchDistortionMovementSpeed;publicAIState AiState {get;privateset;}EnemyController m_EnemyController;AudioSource m_AudioSource;conststring k_AnimMoveSpeedParameter ="MoveSpeed";conststring k_AnimAttackParameter ="Attack";conststring k_AnimAlertedParameter ="Alerted";conststring k_AnimOnDamagedParameter ="OnDamaged";voidStart(){
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();}voidUpdate(){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);}voidUpdateAiStateTransitions(){// Handle transitions switch(AiState){case AIState.Follow:// Transition to attack when there is a line of sight to the targetif(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 rangeif(!m_EnemyController.IsTargetInAttackRange){
AiState = AIState.Follow;}break;}}voidUpdateCurrentAiState(){// 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;}}voidOnAttack(){
Animator.SetTrigger(k_AnimAttackParameter);}voidOnDetectedTarget(){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);}voidOnLostTarget(){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);}voidOnDamaged(){if(RandomHitSparks.Length >0){int n = Random.Range(0, RandomHitSparks.Length -1);
RandomHitSparks[n].Play();}
Animator.SetTrigger(k_AnimOnDamagedParameter);}}}
8. 附EnemyController完整代码
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))]publicclassEnemyController:MonoBehaviour{[System.Serializable]publicstruct RendererIndexData
{publicRenderer Renderer;publicint MaterialIndex;publicRendererIndexData(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)")]publicfloat SelfDestructYHeight =-20f;[Tooltip("The distance at which the enemy considers that it has reached its current path destination point")]publicfloat PathReachingRadius =2f;[Tooltip("The speed at which the enemy rotates")]publicfloat OrientationSpeed =10f;[Tooltip("Delay after death where the GameObject is destroyed (to allow for animation)")]publicfloat DeathDuration =0f;[Header("Weapons Parameters")][Tooltip("Allow weapon swapping for this enemy")]publicbool SwapToNextWeapon =false;[Tooltip("Time delay between a weapon swap and the next attack")]publicfloat DelayAfterWeaponSwap =0f;[Header("Eye color")][Tooltip("Material for the eye color")]publicMaterial EyeColorMaterial;[Tooltip("The default color of the bot's eye")][ColorUsageAttribute(true,true)]publicColor DefaultEyeColor;[Tooltip("The attack color of the bot's eye")][ColorUsageAttribute(true,true)]publicColor AttackEyeColor;[Header("Flash on hit")][Tooltip("The material used for the body of the hoverbot")]publicMaterial BodyMaterial;[Tooltip("The gradient representing the color of the flash on hit")][GradientUsageAttribute(true)]publicGradient OnHitBodyGradient;[Tooltip("The duration of the flash on hit")]publicfloat FlashOnHitDuration =0.5f;[Header("Sounds")][Tooltip("Sound played when recieving damages")]publicAudioClip DamageTick;[Header("VFX")][Tooltip("The VFX prefab spawned when the enemy dies")]publicGameObject DeathVfx;[Tooltip("The point at which the death VFX is spawned")]publicTransform DeathVfxSpawnPoint;[Header("Loot")][Tooltip("The object this enemy can drop when dying")]publicGameObject LootPrefab;[Tooltip("The chance the object has to drop")][Range(0,1)]publicfloat DropRate =1f;[Header("Debug Display")][Tooltip("Color of the sphere gizmo representing the path reaching range")]publicColor PathReachingRangeColor = Color.yellow;[Tooltip("Color of the sphere gizmo representing the attack range")]publicColor AttackRangeColor = Color.red;[Tooltip("Color of the sphere gizmo representing the detection range")]publicColor DetectionRangeColor = Color.blue;publicUnityAction onAttack;publicUnityAction onDetectedTarget;publicUnityAction onLostTarget;publicUnityAction onDamaged;
List<RendererIndexData> m_BodyRenderers =newList<RendererIndexData>();MaterialPropertyBlock m_BodyFlashMaterialPropertyBlock;float m_LastTimeDamaged =float.NegativeInfinity;RendererIndexData m_EyeRendererData;MaterialPropertyBlock m_EyeColorMaterialPropertyBlock;publicPatrolPath PatrolPath {get;set;}publicGameObject KnownDetectedTarget => DetectionModule.KnownDetectedTarget;publicbool IsTargetInAttackRange => DetectionModule.IsTargetInAttackRange;publicbool IsSeeingTarget => DetectionModule.IsSeeingTarget;publicbool HadKnownTarget => DetectionModule.HadKnownTarget;publicNavMeshAgent NavMeshAgent {get;privateset;}publicDetectionModule DetectionModule {get;privateset;}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;voidStart(){
m_EnemyManager =FindObjectOfType<EnemyManager>();
DebugUtility.HandleErrorIfNullFindObject<EnemyManager,EnemyController>(m_EnemyManager,this);
m_ActorsManager =FindObjectOfType<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 =FindObjectOfType<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 weaponsFindAndInitializeAllWeapons();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 dataif(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 inGetComponentsInChildren<Renderer>(true)){for(int i =0; i < renderer.sharedMaterials.Length; i++){if(renderer.sharedMaterials[i]== EyeColorMaterial){
m_EyeRendererData =newRendererIndexData(renderer, i);}if(renderer.sharedMaterials[i]== BodyMaterial){
m_BodyRenderers.Add(newRendererIndexData(renderer, i));}}}
m_BodyFlashMaterialPropertyBlock =newMaterialPropertyBlock();// Check if we have an eye renderer for this enemyif(m_EyeRendererData.Renderer !=null){
m_EyeColorMaterialPropertyBlock =newMaterialPropertyBlock();
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", DefaultEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);}}voidUpdate(){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;}voidEnsureIsWithinLevelBounds(){// at every frame, this tests for conditions to kill the enemyif(transform.position.y < SelfDestructYHeight){Destroy(gameObject);return;}}voidOnLostTarget(){
onLostTarget.Invoke();// Set the eye attack color and property block if the eye renderer is setif(m_EyeRendererData.Renderer !=null){
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", DefaultEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);}}voidOnDetectedTarget(){
onDetectedTarget.Invoke();// Set the eye default color and property block if the eye renderer is setif(m_EyeRendererData.Renderer !=null){
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", AttackEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);}}publicvoidOrientTowards(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);}}boolIsPathValid(){return PatrolPath && PatrolPath.PathNodes.Count >0;}publicvoidResetPathDestination(){
m_PathDestinationNodeIndex =0;}publicvoidSetPathDestinationToClosestNode(){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;}}publicVector3GetDestinationOnPath(){if(IsPathValid()){return PatrolPath.GetPositionOfPathNode(m_PathDestinationNodeIndex);}else{return transform.position;}}publicvoidSetNavDestination(Vector3 destination){if(NavMeshAgent){
NavMeshAgent.SetDestination(destination);}}publicvoidUpdatePathDestination(bool inverseOrder =false){if(IsPathValid()){// Check if reached the path destinationif((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;}}}}voidOnDamaged(float damage,GameObject damageSource){// test if the damage source is the playerif(damageSource &&!damageSource.GetComponent<EnemyController>()){// pursue the player
DetectionModule.OnDamaged(damageSource);
onDamaged?.Invoke();
m_LastTimeDamaged = Time.time;// play the damage tick soundif(DamageTick &&!m_WasDamagedThisFrame)
AudioUtility.CreateSFX(DamageTick, transform.position, AudioUtility.AudioGroups.DamageTick,0f);
m_WasDamagedThisFrame =true;}}voidOnDie(){// spawn a particle system when dyingvar 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 objectif(TryDropItem()){Instantiate(LootPrefab, transform.position, Quaternion.identity);}// this will call the OnDestroy functionDestroy(gameObject, DeathDuration);}voidOnDrawGizmosSelected(){// 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);}}publicvoidOrientWeaponsTowards(Vector3 lookPosition){for(int i =0; i < m_Weapons.Length; i++){// orient weapon towards playerVector3 weaponForward =(lookPosition - m_Weapons[i].WeaponRoot.transform.position).normalized;
m_Weapons[i].transform.forward = weaponForward;}}publicboolTryAtack(Vector3 enemyPosition){if(m_GameFlowManager.GameIsEnding)returnfalse;OrientWeaponsTowards(enemyPosition);if((m_LastTimeWeaponSwapped + DelayAfterWeaponSwap)>= Time.time)returnfalse;// Shoot the weaponbool 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;}publicboolTryDropItem(){if(DropRate ==0|| LootPrefab ==null)returnfalse;elseif(DropRate ==1)returntrue;elsereturn(Random.value<= DropRate);}voidFindAndInitializeAllWeapons(){// Check if we already found and initialized the weaponsif(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;}}}publicWeaponControllerGetCurrentWeapon(){FindAndInitializeAllWeapons();// Check if no weapon is currently selectedif(m_CurrentWeapon ==null){// Set the first weapon of the weapons list as the current weaponSetCurrentWeapon(0);}
DebugUtility.HandleErrorIfNullGetComponent<WeaponController,EnemyController>(m_CurrentWeapon,this,
gameObject);return m_CurrentWeapon;}voidSetCurrentWeapon(int index){
m_CurrentWeaponIndex = index;
m_CurrentWeapon = m_Weapons[m_CurrentWeaponIndex];if(SwapToNextWeapon){
m_LastTimeWeaponSwapped = Time.time;}else{
m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;}}}}