675 lines
22 KiB
C#
675 lines
22 KiB
C#
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
|
|
public class UnitController : MonoBehaviour
|
|
{
|
|
[Header("Unit Configuration")]
|
|
public UnitData unitData;
|
|
public Team team;
|
|
public int unitId;
|
|
|
|
[Header("Current State")]
|
|
public float currentHealth;
|
|
public UnitState currentState = UnitState.Idle;
|
|
public BattleStrategy assignedStrategy;
|
|
|
|
[Header("Combat")]
|
|
public Transform target;
|
|
public float lastAttackTime;
|
|
public int remainingAmmo;
|
|
|
|
[Header("Movement & Formation")]
|
|
public Vector3 formationPosition;
|
|
public Vector3 rallyPoint;
|
|
public bool isInFormation = true;
|
|
|
|
// Private variables for AI behavior
|
|
private Rigidbody rb;
|
|
private UnityEngine.AI.NavMeshAgent navAgent;
|
|
private UnitSightSystem sightSystem;
|
|
private List<UnitController> nearbyAllies = new List<UnitController>();
|
|
private List<UnitController> nearbyEnemies = new List<UnitController>();
|
|
private float stateTimer = 0f;
|
|
private bool hasChargeReady = true;
|
|
private Vector3 lastKnownEnemyPosition;
|
|
private float lastScanTime = 0f;
|
|
private float scanInterval = 0.2f; // Fallback scanning if no sight system
|
|
|
|
// Events
|
|
public System.Action<UnitController> OnUnitDeath;
|
|
public System.Action<UnitController, float> OnUnitDamaged;
|
|
|
|
void Start()
|
|
{
|
|
InitializeUnit();
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (currentHealth <= 0) return;
|
|
|
|
// Check if battle has started (add this debug check)
|
|
BattleManager battleManager = FindFirstObjectByType<BattleManager>();
|
|
if (battleManager != null && !battleManager.battleStarted)
|
|
{
|
|
return; // Don't execute AI until battle starts
|
|
}
|
|
else if (battleManager == null)
|
|
{
|
|
if (Time.frameCount % 300 == 0) // Log every 5 seconds
|
|
Debug.LogWarning($"Unit {gameObject.name} cannot find BattleManager!");
|
|
}
|
|
|
|
UpdateAI();
|
|
UpdateState();
|
|
|
|
stateTimer += Time.deltaTime;
|
|
}
|
|
|
|
private void InitializeUnit()
|
|
{
|
|
if (unitData == null)
|
|
{
|
|
Debug.LogError("UnitData is null for " + gameObject.name);
|
|
return;
|
|
}
|
|
|
|
currentHealth = unitData.health;
|
|
remainingAmmo = unitData.maxAmmo;
|
|
rb = GetComponent<Rigidbody>();
|
|
|
|
// Initialize NavMesh Agent
|
|
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (navAgent != null)
|
|
{
|
|
navAgent.speed = unitData.moveSpeed;
|
|
navAgent.angularSpeed = unitData.rotationSpeed;
|
|
navAgent.acceleration = 8f;
|
|
navAgent.stoppingDistance = unitData.attackRange * 0.8f;
|
|
Debug.Log($"{gameObject.name}: NavMeshAgent initialized");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"{gameObject.name}: NavMeshAgent not found! Add one for proper pathfinding.");
|
|
}
|
|
|
|
// Initialize sight system
|
|
sightSystem = GetComponent<UnitSightSystem>();
|
|
if (sightSystem != null)
|
|
{
|
|
sightSystem.OnEnemyDetected += OnEnemyDetected;
|
|
sightSystem.OnEnemyLost += OnEnemyLost;
|
|
sightSystem.OnAllyDetected += OnAllyDetected;
|
|
sightSystem.OnAllyLost += OnAllyLost;
|
|
Debug.Log($"{gameObject.name}: UnitSightSystem found and initialized");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"UnitSightSystem not found on {gameObject.name}. Using fallback detection.");
|
|
}
|
|
|
|
// Set initial formation position
|
|
formationPosition = transform.position;
|
|
rallyPoint = transform.position;
|
|
|
|
// Initialize based on unit type
|
|
switch (unitData.unitType)
|
|
{
|
|
case UnitType.Cavalry:
|
|
hasChargeReady = true;
|
|
break;
|
|
case UnitType.Archer:
|
|
// Archers prefer to stay at range
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void UpdateAI()
|
|
{
|
|
ScanForUnits();
|
|
ExecuteStrategy();
|
|
}
|
|
|
|
private void ScanForUnits()
|
|
{
|
|
// Use RaycastPro sight system if available, otherwise fallback to sphere overlap
|
|
if (sightSystem != null)
|
|
{
|
|
nearbyEnemies = sightSystem.GetDetectedEnemies();
|
|
nearbyAllies = sightSystem.GetDetectedAllies();
|
|
|
|
if (Time.frameCount % 120 == 0) // Debug every 2 seconds
|
|
Debug.Log($"{gameObject.name}: Sight system found {nearbyEnemies.Count} enemies, {nearbyAllies.Count} allies");
|
|
}
|
|
else
|
|
{
|
|
// Fallback method using sphere overlap (only scan periodically for performance)
|
|
if (Time.time - lastScanTime > scanInterval)
|
|
{
|
|
ScanForUnitsManual();
|
|
lastScanTime = Time.time;
|
|
|
|
if (Time.frameCount % 120 == 0) // Debug every 2 seconds
|
|
Debug.Log($"{gameObject.name}: Manual scan found {nearbyEnemies.Count} enemies, {nearbyAllies.Count} allies");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ScanForUnitsManual()
|
|
{
|
|
nearbyAllies.Clear();
|
|
nearbyEnemies.Clear();
|
|
|
|
Collider[] nearbyUnits = Physics.OverlapSphere(transform.position, unitData.detectionRange);
|
|
|
|
foreach (Collider col in nearbyUnits)
|
|
{
|
|
UnitController unit = col.GetComponent<UnitController>();
|
|
if (unit != null && unit != this && unit.currentHealth > 0)
|
|
{
|
|
if (unit.team == this.team)
|
|
{
|
|
nearbyAllies.Add(unit);
|
|
}
|
|
else
|
|
{
|
|
nearbyEnemies.Add(unit);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event handlers for sight system
|
|
private void OnEnemyDetected(UnitController enemy)
|
|
{
|
|
if (!nearbyEnemies.Contains(enemy))
|
|
{
|
|
nearbyEnemies.Add(enemy);
|
|
}
|
|
|
|
// React to enemy detection based on unit type and strategy
|
|
ReactToEnemyDetection(enemy);
|
|
}
|
|
|
|
private void OnEnemyLost(UnitController enemy)
|
|
{
|
|
if (nearbyEnemies.Contains(enemy))
|
|
{
|
|
nearbyEnemies.Remove(enemy);
|
|
}
|
|
|
|
// Store last known position for pursuit
|
|
if (enemy != null)
|
|
{
|
|
lastKnownEnemyPosition = enemy.transform.position;
|
|
}
|
|
}
|
|
|
|
private void OnAllyDetected(UnitController ally)
|
|
{
|
|
if (!nearbyAllies.Contains(ally))
|
|
{
|
|
nearbyAllies.Add(ally);
|
|
}
|
|
}
|
|
|
|
private void OnAllyLost(UnitController ally)
|
|
{
|
|
if (nearbyAllies.Contains(ally))
|
|
{
|
|
nearbyAllies.Remove(ally);
|
|
}
|
|
}
|
|
|
|
public void OnEnemyInSight(UnitController enemy)
|
|
{
|
|
// Called continuously while enemy is in sight
|
|
// Update targeting information
|
|
if (target == null || Vector3.Distance(transform.position, enemy.transform.position) <
|
|
Vector3.Distance(transform.position, target.position))
|
|
{
|
|
target = enemy.transform;
|
|
}
|
|
}
|
|
|
|
private void ReactToEnemyDetection(UnitController enemy)
|
|
{
|
|
// Different reactions based on unit type and current strategy
|
|
switch (unitData.unitType)
|
|
{
|
|
case UnitType.Archer:
|
|
if (unitData.isRanged && remainingAmmo > 0)
|
|
{
|
|
// Archers should maintain distance and prepare to fire
|
|
currentState = UnitState.Waiting;
|
|
}
|
|
break;
|
|
|
|
case UnitType.Cavalry:
|
|
if (assignedStrategy != null && assignedStrategy.strategyType == StrategyType.Charge)
|
|
{
|
|
// Cavalry charges immediately on enemy detection
|
|
currentState = UnitState.Charging;
|
|
}
|
|
break;
|
|
|
|
case UnitType.Infantry:
|
|
// Infantry holds formation unless ordered otherwise
|
|
if (assignedStrategy != null && assignedStrategy.strategyType == StrategyType.HoldPosition)
|
|
{
|
|
currentState = UnitState.Defending;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ExecuteStrategy()
|
|
{
|
|
if (assignedStrategy == null)
|
|
{
|
|
if (Time.frameCount % 300 == 0) // Log every 5 seconds at 60 FPS
|
|
Debug.Log($"Unit {gameObject.name} has no assigned strategy!");
|
|
return;
|
|
}
|
|
|
|
switch (assignedStrategy.strategyType)
|
|
{
|
|
case StrategyType.HoldPosition:
|
|
ExecuteHoldPosition();
|
|
break;
|
|
case StrategyType.Flank:
|
|
ExecuteFlank();
|
|
break;
|
|
case StrategyType.Charge:
|
|
ExecuteCharge();
|
|
break;
|
|
case StrategyType.SpreadWide:
|
|
ExecuteSpreadWide();
|
|
break;
|
|
case StrategyType.WaitForClose:
|
|
ExecuteWaitForClose();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ExecuteHoldPosition()
|
|
{
|
|
// Stay in formation and defend, but be more aggressive when enemies are nearby
|
|
float distanceToFormation = Vector3.Distance(transform.position, formationPosition);
|
|
|
|
// If enemies are nearby, prioritize combat over formation
|
|
if (nearbyEnemies.Count > 0)
|
|
{
|
|
if (Time.frameCount % 180 == 0) // Debug every 3 seconds
|
|
Debug.Log($"{gameObject.name}: Prioritizing combat - {nearbyEnemies.Count} enemies nearby");
|
|
|
|
currentState = UnitState.Defending;
|
|
AttackNearestEnemy();
|
|
}
|
|
else if (distanceToFormation > 3f) // Only return to formation if no enemies
|
|
{
|
|
if (Time.frameCount % 60 == 0) // Debug every second at 60 FPS
|
|
Debug.Log($"{gameObject.name}: Returning to formation, distance: {distanceToFormation:F1}");
|
|
MoveTowards(formationPosition);
|
|
currentState = UnitState.Moving;
|
|
}
|
|
else
|
|
{
|
|
// Stop moving and hold position
|
|
if (navAgent != null && navAgent.hasPath)
|
|
{
|
|
navAgent.ResetPath();
|
|
}
|
|
|
|
if (Time.frameCount % 180 == 0) // Debug every 3 seconds
|
|
Debug.Log($"{gameObject.name}: Holding formation position");
|
|
currentState = UnitState.Defending;
|
|
}
|
|
}
|
|
|
|
private void ExecuteFlank()
|
|
{
|
|
// Move to flank enemies, especially effective for cavalry
|
|
if (nearbyEnemies.Count > 0)
|
|
{
|
|
Vector3 flankPosition = GetFlankPosition();
|
|
MoveTowards(flankPosition);
|
|
currentState = UnitState.Flanking;
|
|
|
|
if (Vector3.Distance(transform.position, flankPosition) < 3f)
|
|
{
|
|
AttackNearestEnemy();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MoveTowards(formationPosition);
|
|
currentState = UnitState.Moving;
|
|
}
|
|
}
|
|
|
|
private void ExecuteCharge()
|
|
{
|
|
// Aggressive charge towards enemies
|
|
if (nearbyEnemies.Count > 0 && hasChargeReady)
|
|
{
|
|
currentState = UnitState.Charging;
|
|
Transform closestEnemy = GetClosestEnemy();
|
|
if (closestEnemy != null)
|
|
{
|
|
MoveTowards(closestEnemy.position, unitData.moveSpeed * 1.5f); // Charge speed boost
|
|
|
|
if (Vector3.Distance(transform.position, closestEnemy.position) < unitData.attackRange)
|
|
{
|
|
AttackTarget(closestEnemy.GetComponent<UnitController>());
|
|
hasChargeReady = false; // Charge cooldown
|
|
Invoke(nameof(ResetCharge), 10f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ExecuteSpreadWide()
|
|
{
|
|
// Spread formation, good for archers
|
|
Vector3 spreadPosition = formationPosition + GetSpreadOffset();
|
|
MoveTowards(spreadPosition);
|
|
currentState = UnitState.Moving;
|
|
|
|
if (nearbyEnemies.Count > 0)
|
|
{
|
|
AttackNearestEnemy();
|
|
}
|
|
}
|
|
|
|
private void ExecuteWaitForClose()
|
|
{
|
|
// Wait until enemies are close before attacking (archers)
|
|
if (nearbyEnemies.Count > 0)
|
|
{
|
|
Transform closestEnemy = GetClosestEnemy();
|
|
if (closestEnemy != null)
|
|
{
|
|
float distance = Vector3.Distance(transform.position, closestEnemy.position);
|
|
|
|
if (distance <= assignedStrategy.engagementRange)
|
|
{
|
|
AttackTarget(closestEnemy.GetComponent<UnitController>());
|
|
currentState = UnitState.Attacking;
|
|
}
|
|
else
|
|
{
|
|
currentState = UnitState.Waiting;
|
|
// Face the enemy but don't attack yet
|
|
FaceTarget(closestEnemy.position);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateState()
|
|
{
|
|
// Handle state-specific behaviors
|
|
switch (currentState)
|
|
{
|
|
case UnitState.Retreating:
|
|
if (currentHealth > unitData.health * unitData.retreatHealthThreshold)
|
|
{
|
|
currentState = UnitState.Idle;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Check for retreat condition
|
|
if (currentHealth < unitData.health * unitData.retreatHealthThreshold && currentState != UnitState.Retreating)
|
|
{
|
|
currentState = UnitState.Retreating;
|
|
MoveTowards(rallyPoint);
|
|
}
|
|
}
|
|
|
|
private void AttackNearestEnemy()
|
|
{
|
|
Transform closestEnemy = GetClosestEnemy();
|
|
if (closestEnemy != null)
|
|
{
|
|
Debug.Log($"{gameObject.name}: Attempting to attack {closestEnemy.name}");
|
|
AttackTarget(closestEnemy.GetComponent<UnitController>());
|
|
}
|
|
else
|
|
{
|
|
if (Time.frameCount % 180 == 0) // Debug every 3 seconds
|
|
Debug.Log($"{gameObject.name}: No closest enemy found despite nearbyEnemies.Count = {nearbyEnemies.Count}");
|
|
}
|
|
}
|
|
|
|
private void AttackTarget(UnitController enemyUnit)
|
|
{
|
|
if (enemyUnit == null) return;
|
|
|
|
float distance = Vector3.Distance(transform.position, enemyUnit.transform.position);
|
|
|
|
// Check if target is in attack range
|
|
bool inAttackRange = (sightSystem != null) ?
|
|
sightSystem.IsTargetInAttackRange(enemyUnit.transform) :
|
|
distance <= unitData.attackRange;
|
|
|
|
// Check if we can see the target
|
|
bool canSeeTarget = (sightSystem != null) ?
|
|
sightSystem.CanSeeTarget(enemyUnit.transform) :
|
|
true; // Fallback assumes line of sight
|
|
|
|
if (Time.frameCount % 60 == 0) // Debug every second
|
|
Debug.Log($"{gameObject.name}: Attacking {enemyUnit.name} - Distance: {distance:F1}, InRange: {inAttackRange}, CanSee: {canSeeTarget}, AttackRange: {unitData.attackRange}");
|
|
|
|
if (inAttackRange && canSeeTarget && Time.time - lastAttackTime >= 1f / unitData.attackSpeed)
|
|
{
|
|
// Face the target
|
|
FaceTarget(enemyUnit.transform.position);
|
|
|
|
// Perform attack
|
|
if (unitData.isRanged && remainingAmmo > 0)
|
|
{
|
|
Debug.Log($"{gameObject.name}: Firing projectile at {enemyUnit.name}");
|
|
FireProjectile(enemyUnit);
|
|
remainingAmmo--;
|
|
}
|
|
else if (!unitData.isRanged)
|
|
{
|
|
// Melee attack
|
|
Debug.Log($"{gameObject.name}: Melee attack on {enemyUnit.name} for {unitData.damage} damage");
|
|
enemyUnit.TakeDamage(unitData.damage);
|
|
}
|
|
|
|
lastAttackTime = Time.time;
|
|
currentState = UnitState.Attacking;
|
|
}
|
|
else if (!inAttackRange && canSeeTarget)
|
|
{
|
|
// Move closer to attack - be more aggressive
|
|
if (Time.frameCount % 60 == 0) // Debug every second
|
|
Debug.Log($"{gameObject.name}: Aggressively pursuing {enemyUnit.name} (distance: {distance:F1})");
|
|
|
|
// Set higher speed when pursuing enemies
|
|
MoveTowards(enemyUnit.transform.position, 1.5f); // 50% speed boost
|
|
currentState = UnitState.Moving;
|
|
}
|
|
}
|
|
|
|
public void TakeDamage(float damage)
|
|
{
|
|
float actualDamage = Mathf.Max(0, damage - unitData.armor);
|
|
currentHealth -= actualDamage;
|
|
|
|
OnUnitDamaged?.Invoke(this, actualDamage);
|
|
|
|
if (currentHealth <= 0)
|
|
{
|
|
Die();
|
|
}
|
|
}
|
|
|
|
private void Die()
|
|
{
|
|
currentState = UnitState.Dead;
|
|
OnUnitDeath?.Invoke(this);
|
|
|
|
// Disable or destroy the unit
|
|
gameObject.SetActive(false);
|
|
}
|
|
|
|
private void MoveTowards(Vector3 targetPosition, float speedMultiplier = 1f)
|
|
{
|
|
if (navAgent != null)
|
|
{
|
|
// Use NavMesh Agent for pathfinding
|
|
navAgent.speed = unitData.moveSpeed * speedMultiplier;
|
|
navAgent.SetDestination(targetPosition);
|
|
|
|
if (Time.frameCount % 180 == 0) // Debug every 3 seconds
|
|
Debug.Log($"{gameObject.name}: NavAgent moving to {targetPosition}, distance: {Vector3.Distance(transform.position, targetPosition):F1}");
|
|
}
|
|
else
|
|
{
|
|
// Fallback to direct movement (less ideal)
|
|
Vector3 direction = (targetPosition - transform.position).normalized;
|
|
|
|
if (rb != null)
|
|
{
|
|
rb.MovePosition(transform.position + direction * unitData.moveSpeed * speedMultiplier * Time.deltaTime);
|
|
}
|
|
else
|
|
{
|
|
transform.position += direction * unitData.moveSpeed * speedMultiplier * Time.deltaTime;
|
|
}
|
|
|
|
FaceTarget(targetPosition);
|
|
}
|
|
}
|
|
|
|
private void FaceTarget(Vector3 targetPosition)
|
|
{
|
|
Vector3 direction = (targetPosition - transform.position).normalized;
|
|
if (direction != Vector3.zero)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(direction);
|
|
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, unitData.rotationSpeed * Time.deltaTime);
|
|
}
|
|
}
|
|
|
|
private Transform GetClosestEnemy()
|
|
{
|
|
// Use sight system if available for more accurate detection
|
|
if (sightSystem != null)
|
|
{
|
|
UnitController closestEnemy = sightSystem.GetClosestEnemy();
|
|
return closestEnemy != null ? closestEnemy.transform : null;
|
|
}
|
|
|
|
// Fallback to manual search
|
|
Transform closest = null;
|
|
float closestDistance = float.MaxValue;
|
|
|
|
foreach (UnitController enemy in nearbyEnemies)
|
|
{
|
|
if (enemy != null && enemy.currentHealth > 0)
|
|
{
|
|
float distance = Vector3.Distance(transform.position, enemy.transform.position);
|
|
if (distance < closestDistance)
|
|
{
|
|
closest = enemy.transform;
|
|
closestDistance = distance;
|
|
}
|
|
}
|
|
}
|
|
|
|
return closest;
|
|
}
|
|
|
|
private Vector3 GetFlankPosition()
|
|
{
|
|
if (nearbyEnemies.Count == 0) return formationPosition;
|
|
|
|
Vector3 enemyCenter = Vector3.zero;
|
|
foreach (UnitController enemy in nearbyEnemies)
|
|
{
|
|
enemyCenter += enemy.transform.position;
|
|
}
|
|
enemyCenter /= nearbyEnemies.Count;
|
|
|
|
// Move to side of enemy formation
|
|
Vector3 toEnemy = (enemyCenter - transform.position).normalized;
|
|
Vector3 flankDirection = Vector3.Cross(toEnemy, Vector3.up).normalized;
|
|
|
|
return enemyCenter + flankDirection * 10f;
|
|
}
|
|
|
|
private Vector3 GetSpreadOffset()
|
|
{
|
|
// Create spread formation based on unit ID
|
|
float angle = (unitId * 360f / 10f) * Mathf.Deg2Rad; // Spread 10 units in circle
|
|
float radius = unitData.formationSpacing * 2f;
|
|
|
|
return new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
|
|
}
|
|
|
|
private void FireProjectile(UnitController target)
|
|
{
|
|
// This would instantiate a projectile - for now just apply damage directly
|
|
// In a full implementation, you'd spawn a projectile GameObject
|
|
target.TakeDamage(unitData.damage);
|
|
}
|
|
|
|
private void ResetCharge()
|
|
{
|
|
hasChargeReady = true;
|
|
}
|
|
|
|
public void SetStrategy(BattleStrategy strategy)
|
|
{
|
|
assignedStrategy = strategy;
|
|
Debug.Log($"Unit {gameObject.name} received strategy: {(strategy != null ? strategy.strategyName : "NULL")}");
|
|
}
|
|
|
|
public void SetFormationPosition(Vector3 position)
|
|
{
|
|
formationPosition = position;
|
|
}
|
|
|
|
public void SetRallyPoint(Vector3 position)
|
|
{
|
|
rallyPoint = position;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
// Unsubscribe from sight system events
|
|
if (sightSystem != null)
|
|
{
|
|
sightSystem.OnEnemyDetected -= OnEnemyDetected;
|
|
sightSystem.OnEnemyLost -= OnEnemyLost;
|
|
sightSystem.OnAllyDetected -= OnAllyDetected;
|
|
sightSystem.OnAllyLost -= OnAllyLost;
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum Team
|
|
{
|
|
Player,
|
|
Enemy
|
|
}
|
|
|
|
public enum UnitState
|
|
{
|
|
Idle,
|
|
Moving,
|
|
Attacking,
|
|
Defending,
|
|
Charging,
|
|
Flanking,
|
|
Retreating,
|
|
Waiting,
|
|
Dead
|
|
}
|