Files
LowPolyBattleSim/Assets/Scripts/UnitController.cs

675 lines
22 KiB
C#
Raw Normal View History

2025-06-27 23:27:49 +01:00
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
}