using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine.Profiling; namespace Pathfinding.Examples.RTS { using Pathfinding; using Pathfinding.RVO; using Pathfinding.Util; [HelpURL("https://arongranberg.com/astar/documentation/stable/rtsunit.html")] public class RTSUnit : VersionedMonoBehaviour { public GameObject selectionIndicator; public GameObject deathEffect; public int team; public float detectionRange; public float maxHealth; public enum Type { Infantry, Heavy, Worker, Harvester, HarvesterDropoff = 100, HarvesterDropoffQueue, ResourceCrystal = 200, } public Type type; [System.NonSerialized] public float health; IAstarAI ai; RVOController rvo; MovementMode movementMode; Vector3 lastDestination; RTSUnit attackTarget; RTSWeapon weapon; float lastSeenAttackTarget = float.NegativeInfinity; bool reachedDestination; public int storedCrystals; public RTSUnit reservedBy; public bool locked; new Transform transform; /// Position at the start of the current frame protected Vector3 position; public System.Action onMakeActiveUnit; public RTSPlayer owner { get { return RTSManager.instance.GetPlayer(team); } } public bool selectionIndicatorEnabled { get { if (selectionIndicator == null) return false; return selectionIndicator.activeSelf; } set { if (selectionIndicator != null) selectionIndicator.SetActive(value); } } public RTSHarvestableResource resource { get { return GetComponent(); } } public float radius { get { return rvo != null ? rvo.radius : 1f; } } bool mSelected; public bool selected { get { return mSelected; } set { mSelected = value; selectionIndicatorEnabled = value; if (value) { RTSManager.instance.units.OnSelected(this); } else { RTSManager.instance.units.OnDeselected(this); } } } public void OnMakeActiveUnit (bool active) { if (onMakeActiveUnit != null) onMakeActiveUnit(active); } public void SetDestination (Vector3 destination, MovementMode mode) { if (ai != null && this) { reachedDestination = false; movementMode = mode; ai.destination = lastDestination = destination; (ai as AIBase).rvoDensityBehavior.ClearDestinationReached(); ai.SearchPath(); if (mode == MovementMode.Move) { attackTarget = null; } } } protected override void Awake () { base.Awake(); transform = (this as MonoBehaviour).transform; ai = GetComponent(); rvo = GetComponent(); weapon = GetComponent(); } static System.Action OnUpdateDelegate; void OnEnable () { if (OnUpdateDelegate == null) OnUpdateDelegate = OnUpdate; RTSManager.instance.units.AddUnit(this); selected = false; health = maxHealth; movementMode = MovementMode.AttackMove; reachedDestination = true; if (ai != null) lastDestination = ai.destination; BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdateDelegate); } void OnDisable () { BatchedEvents.Remove(this); if (RTSManager.instance != null) RTSManager.instance.units.RemoveUnit(this); } static void OnUpdate (RTSUnit[] units, int count) { // Get some lists and arrays from an object pool List[] unitsByOwner = ArrayPool >.ClaimWithExactLength(RTSManager.instance.PlayerCount); for (int i = 0; i < unitsByOwner.Length; i++) { unitsByOwner[i] = ListPool.Claim(); } for (int i = 0; i < count; i++) { units[i].position = units[i].transform.position; unitsByOwner[units[i].owner.index].Add(units[i]); } for (int i = 0; i < count; i++) { units[i].OnUpdate(unitsByOwner); } // Release allocated lists back to a pool for (int i = 0; i < unitsByOwner.Length; i++) { ListPool.Release(ref unitsByOwner[i]); } ArrayPool >.Release(ref unitsByOwner, true); } // Update is called once per frame protected virtual void OnUpdate (List[] unitsByOwner) { if (ai == null) { // Stationary unit if (weapon != null) { float minDist = detectionRange*detectionRange; for (int player = 0; player < unitsByOwner.Length; player++) { if (!owner.IsHostile(RTSManager.instance.GetPlayer(player))) continue; for (int i = 0; i < unitsByOwner[player].Count; i++) { var unit = unitsByOwner[player][i]; var dist = (unit.position - position).sqrMagnitude; if (dist < minDist) { attackTarget = unit; minDist = dist; } } } if (attackTarget != null) { if (!weapon.InRangeOf(attackTarget.position)) { attackTarget = null; } else { if (weapon.Aim(attackTarget)) { weapon.Attack(attackTarget); } } } } if (attackTarget != null) { lastSeenAttackTarget = Time.time; } } else { rvo.locked = false | locked; // this.reachedDestination will be true once the AI has reached its destination // and it will stay true until the next time SetDestination is called. reachedDestination |= (ai as AIBase).rvoDensityBehavior.reachedDestination; if (weapon != null) { bool canAttack = movementMode == MovementMode.AttackMove; // This takes into account path calculations as well as if the AI stops far away from the destination due to being part of a large group canAttack |= reachedDestination && movementMode == MovementMode.Move; if (canAttack) { float minDist = detectionRange*detectionRange; Profiler.BeginSample("Distance"); var pos = position; for (int player = 0; player < unitsByOwner.Length; player++) { if (!owner.IsHostile(RTSManager.instance.GetPlayer(player))) continue; var enemies = unitsByOwner[player]; for (int i = 0; i < enemies.Count; i++) { var enemy = enemies[i]; var dist = (enemy.position - pos).sqrMagnitude; if (dist < minDist) { attackTarget = enemy; minDist = dist; } } } Profiler.EndSample(); float rangeFuzz = 1.1f; if (attackTarget != null && (attackTarget.position - position).magnitude > detectionRange*rangeFuzz) { attackTarget = null; } bool wantsToAttack = false; if (attackTarget != null) { if (!weapon.InRangeOf(attackTarget.position)) { ai.destination = attackTarget.position; } else { wantsToAttack = true; if (weapon.Aim(attackTarget)) { weapon.Attack(attackTarget); } } } if (attackTarget != null) { lastSeenAttackTarget = Time.time; } if (!weapon.canMoveWhileAttacking && (wantsToAttack || weapon.isAttacking)) { rvo.locked = true; } } } // Move back to original destination in case we followed an enemy for some time if (Time.time - lastSeenAttackTarget > 2) { ai.destination = lastDestination; } } } public void Die () { StartCoroutine(DieCoroutine()); } IEnumerator DieCoroutine () { yield return new WaitForEndOfFrame(); if (deathEffect != null) GameObject.Instantiate(deathEffect, transform.position, transform.rotation); GameObject.Destroy(gameObject); } public void ApplyDamage (float damage) { health = Mathf.Clamp(health - damage, 0, maxHealth); if (health <= 0) { Die(); } } } }