update
This commit is contained in:
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1857b2df8de24b24190ce9e067d2e1e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4cb5371dba09ee46806cbe0c2aa2c9c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ec2f49cb5488d0459ff53ecc714acb7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "fps.AI",
|
||||
"references": [
|
||||
"GUID:9c5543eabb73a6249ac07b2c065ee8b4"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9dc24b3d0971d64a9e95b6986d97af5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user