Files
DeviantMobile-Rohan/Assets/Scripts/AI/Core/AIBrain.cs

353 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// AIBrain — Central AI orchestrator. Replaces CashSystemAI.
/// Runs perception → utility scoring → action execution on a configurable tick.
///
/// Features:
/// - AI LOD (distance-based tick scaling)
/// - Zero FindObjectOfType calls
/// - Modular scoring via UtilityScorer
/// - Delegates combat to existing CharacterAIController
/// - Delegates navigation to AINavigationHelper
///
/// Attach to all AI characters (both player-team AI and enemy-team AI).
/// </summary>
public class AIBrain : MonoBehaviour
{
[Header("AI Settings")]
[SerializeField] private UtilityScorer.AIRole role = UtilityScorer.AIRole.Balanced;
[SerializeField] private bool autoAssignRole = true;
[Header("Tick Rate (AI LOD)")]
[SerializeField] private float baseTick = 0.3f; // Tier 0: on-screen, close
[SerializeField] private float midTick = 0.5f; // Tier 1: on-screen, far
[SerializeField] private float farTick = 1.0f; // Tier 2: off-screen
[SerializeField] private float lodMidDistance = 15f;
[SerializeField] private float lodFarDistance = 30f;
[Header("Behavior")]
[SerializeField] private float minActionDuration = 1.5f; // Minimum time before switching actions
// ─── Components ──────────────────────────────────────────
private AIPerceptionSystem _perception;
private AINavigationHelper _navigation;
private CharacterAIController _aiController;
private CashCarrier _carrier;
private HealthNew _health;
private TeamMember _teamMember;
// ─── AI State ────────────────────────────────────────────
private AIBlackboard _blackboard;
private TeamBlackboard _teamBlackboard;
private UtilityScorer _scorer;
private AIAction _currentAction;
private float _lastTickTime;
private float _currentTick;
private Transform _mainCamera;
private bool _combatEnabled;
// ─── Properties ──────────────────────────────────────────
public AIBlackboard Blackboard => _blackboard;
public AIAction CurrentAction => _currentAction;
public UtilityScorer.AIRole Role => role;
public string CurrentActionName => _scorer?.CurrentActionName ?? "Initializing";
// ─── Lifecycle ───────────────────────────────────────────
private void Awake()
{
// Ensure components exist
_perception = GetComponent<AIPerceptionSystem>();
if (_perception == null) _perception = gameObject.AddComponent<AIPerceptionSystem>();
_navigation = GetComponent<AINavigationHelper>();
if (_navigation == null) _navigation = gameObject.AddComponent<AINavigationHelper>();
}
private void Start()
{
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
{
enabled = false;
return;
}
// Cache components
_aiController = GetComponent<CharacterAIController>();
_carrier = GetComponent<CashCarrier>();
_health = GetComponent<HealthNew>();
_teamMember = GetComponent<TeamMember>();
// Find camera for LOD
if (Camera.main != null)
_mainCamera = Camera.main.transform;
// Initialize blackboard
_blackboard = new AIBlackboard();
_blackboard.teamTag = gameObject.tag;
_blackboard.team = _teamMember != null ? _teamMember.CurrentTeam : TeamMember.Team.EnemyTeam;
// Initialize perception
_perception.Initialize(_blackboard.teamTag);
// Find vaults (one-time, cached)
FindVaults();
// Auto-assign role
if (autoAssignRole)
AssignRandomRole();
// Initialize scorer
_scorer = new UtilityScorer(role);
// Get team blackboard
_teamBlackboard = TeamBlackboard.Get(_blackboard.teamTag);
_currentTick = baseTick;
// Subscribe to events
GameEvents.OnCombatEnabled += OnCombatEnabled;
GameEvents.OnVaultUnderAttack += OnVaultAttacked;
GameEvents.OnMatchEnd += OnMatchEnd;
}
private void OnDestroy()
{
GameEvents.OnCombatEnabled -= OnCombatEnabled;
GameEvents.OnVaultUnderAttack -= OnVaultAttacked;
GameEvents.OnMatchEnd -= OnMatchEnd;
// Release claims
_teamBlackboard?.ReleaseBagClaim(this);
}
private void Update()
{
if (!_combatEnabled) return;
if (_health != null && _health.isDead) return;
// AI LOD — adjust tick rate based on distance to camera
UpdateAILOD();
// Tick check
if (Time.time - _lastTickTime < _currentTick) return;
_lastTickTime = Time.time;
// 1. Update self state on blackboard
UpdateSelfState();
// 2. Run perception
_perception.UpdatePerception(_blackboard);
// 3. Update team state
UpdateTeamState();
// 4. Score and select action
AIAction bestAction = _scorer.Evaluate(this, _blackboard, _teamBlackboard);
// 5. Execute action
if (bestAction != null)
{
_currentAction = bestAction;
_currentAction.Execute(this, _blackboard, _teamBlackboard);
}
}
// ─── State Updates ───────────────────────────────────────
private void UpdateSelfState()
{
if (_health != null)
{
_blackboard.currentHealth = _health.currentHealth;
_blackboard.maxHealth = _health.maxHealth;
_blackboard.healthPercent = _health.maxHealth > 0 ? _health.currentHealth / _health.maxHealth : 1f;
_blackboard.isDead = _health.isDead;
}
if (_carrier != null)
{
_blackboard.carriedBagCount = _carrier.BagCount;
_blackboard.totalCarriedValue = _carrier.TotalValue;
_blackboard.isCarrying = _carrier.IsCarrying;
}
}
private void UpdateTeamState()
{
if (_teamBlackboard != null)
{
_blackboard.myVaultUnderAttack = _teamBlackboard.vaultUnderAttack;
_blackboard.myTeamScore = _teamBlackboard.teamScore;
}
// Get enemy team score
string enemyTag = _blackboard.teamTag == "Player" ? "Enemy" : "Player";
TeamBlackboard enemyBoard = TeamBlackboard.Get(enemyTag);
if (enemyBoard != null)
{
_blackboard.enemyTeamScore = enemyBoard.teamScore;
}
_blackboard.scoreDifference = _blackboard.myTeamScore - _blackboard.enemyTeamScore;
}
private void UpdateAILOD()
{
if (_mainCamera == null)
{
_currentTick = baseTick;
return;
}
float distToCamera = Vector3.Distance(transform.position, _mainCamera.position);
if (distToCamera > lodFarDistance)
_currentTick = farTick;
else if (distToCamera > lodMidDistance)
_currentTick = midTick;
else
_currentTick = baseTick;
}
private void FindVaults()
{
var vaults = EntityRegistry<TeamVault>.All;
for (int i = 0; i < vaults.Count; i++)
{
if (vaults[i] == null) continue;
if (vaults[i].TeamTag == _blackboard.teamTag)
_blackboard.myVault = vaults[i];
else
_blackboard.enemyVault = vaults[i];
}
// Fallback to FindObjectsOfType (only at initialization)
if (_blackboard.myVault == null || _blackboard.enemyVault == null)
{
TeamVault[] allVaults = FindObjectsOfType<TeamVault>();
foreach (var vault in allVaults)
{
if (vault.TeamTag == _blackboard.teamTag)
_blackboard.myVault = vault;
else
_blackboard.enemyVault = vault;
}
}
}
private void AssignRandomRole()
{
float rand = Random.value;
if (rand < 0.35f) role = UtilityScorer.AIRole.Balanced;
else if (rand < 0.60f) role = UtilityScorer.AIRole.Collector;
else if (rand < 0.80f) role = UtilityScorer.AIRole.Defender;
else role = UtilityScorer.AIRole.Aggressor;
}
// ─── Public API (called by AIActions) ────────────────────
/// <summary>Navigate to a world position.</summary>
public void NavigateTo(Vector3 position)
{
_navigation.SetDestination(position);
}
/// <summary>Stop all navigation.</summary>
public void StopNavigation()
{
_navigation.Stop();
}
/// <summary>Has the AI reached its current navigation destination?</summary>
public bool HasReachedDestination()
{
return _navigation.HasReachedDestination();
}
/// <summary>Engage a target in combat (delegates to CharacterAIController).</summary>
public void EngageTarget(Transform target)
{
if (_aiController != null)
{
_aiController.SetTarget(target.gameObject);
_aiController.NavigateTo(target.position);
_aiController.attack = true;
}
else
{
// Fallback: just navigate toward
NavigateTo(target.position);
}
}
/// <summary>Disengage from combat.</summary>
public void DisengageTarget()
{
if (_aiController != null)
{
_aiController.Stop();
}
}
// ─── Event Handlers ──────────────────────────────────────
private void OnCombatEnabled(bool enabled)
{
_combatEnabled = enabled;
}
private void OnVaultAttacked(TeamVault vault)
{
if (vault.TeamTag == _blackboard.teamTag)
{
_blackboard.myVaultUnderAttack = true;
if (_teamBlackboard != null)
_teamBlackboard.vaultUnderAttack = true;
// Force immediate decision (bypass tick timer)
_lastTickTime = 0;
}
}
private void OnMatchEnd(string winnerTag)
{
_combatEnabled = false;
StopNavigation();
}
// ─── Debug ───────────────────────────────────────────────
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (_blackboard == null) return;
// Show current action
UnityEditor.Handles.Label(transform.position + Vector3.up * 2.5f,
$"AI: {CurrentActionName}\n" +
$"Role: {role}\n" +
$"HP: {_blackboard.healthPercent:P0}\n" +
$"Bags: {_blackboard.carriedBagCount}\n" +
$"Enemies: {_blackboard.nearbyEnemyCount}\n" +
$"Tick: {_currentTick:F2}s");
// Show target
if (_blackboard.currentTarget != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position + Vector3.up, _blackboard.currentTarget.position + Vector3.up);
}
// Show target bag
if (_blackboard.targetBag != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position + Vector3.up, _blackboard.targetBag.transform.position);
}
}
#endif
}