chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
352
Assets/Scripts/AI/Core/AIBrain.cs
Normal file
352
Assets/Scripts/AI/Core/AIBrain.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user