chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
274
Assets/Scripts/AI/Core/AIBlackboard.cs
Normal file
274
Assets/Scripts/AI/Core/AIBlackboard.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// AIBlackboard — Per-AI knowledge store.
|
||||
/// Holds perception results, targets, and state without allocations.
|
||||
/// Updated by AIPerceptionSystem and read by AIActions/UtilityScorer.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class AIBlackboard
|
||||
{
|
||||
// ─── Identity ────────────────────────────────────────────
|
||||
public string teamTag;
|
||||
public TeamMember.Team team;
|
||||
|
||||
// ─── Current Targets ─────────────────────────────────────
|
||||
public Transform currentTarget;
|
||||
public CashBag targetBag;
|
||||
public TeamVault myVault;
|
||||
public TeamVault enemyVault;
|
||||
public CashCarrier nearestEnemyCarrier; // Priority target
|
||||
public Transform escortTarget; // Ally to escort
|
||||
|
||||
// ─── Self State ──────────────────────────────────────────
|
||||
public float currentHealth;
|
||||
public float maxHealth;
|
||||
public float healthPercent;
|
||||
public bool isDead;
|
||||
public int carriedBagCount;
|
||||
public float totalCarriedValue;
|
||||
public bool isCarrying;
|
||||
|
||||
// ─── Perception Results (updated by AIPerceptionSystem) ──
|
||||
public readonly List<PerceivedEntity> perceivedEnemies = new List<PerceivedEntity>(8);
|
||||
public readonly List<PerceivedEntity> perceivedAllies = new List<PerceivedEntity>(8);
|
||||
public readonly List<CashBag> nearbyAvailableBags = new List<CashBag>(8);
|
||||
public readonly List<CashBag> nearbyDroppedBags = new List<CashBag>(4);
|
||||
public CashCarrier nearestAllyCarrier;
|
||||
public int nearbyEnemyCount;
|
||||
public int nearbyAllyCount;
|
||||
public float nearestEnemyDistance;
|
||||
public float nearestBagDistance;
|
||||
|
||||
// ─── Team State (from TeamBlackboard) ────────────────────
|
||||
public float myTeamScore;
|
||||
public float enemyTeamScore;
|
||||
public bool myVaultUnderAttack;
|
||||
public float scoreDifference; // positive = winning
|
||||
|
||||
// ─── Navigation ──────────────────────────────────────────
|
||||
public Vector3 patrolDestination;
|
||||
public bool hasPatrolDestination;
|
||||
public float timeAtCurrentState;
|
||||
|
||||
/// <summary>Clear all perception data (called before perception update).</summary>
|
||||
public void ClearPerception()
|
||||
{
|
||||
perceivedEnemies.Clear();
|
||||
perceivedAllies.Clear();
|
||||
nearbyAvailableBags.Clear();
|
||||
nearbyDroppedBags.Clear();
|
||||
nearestEnemyCarrier = null;
|
||||
nearestAllyCarrier = null;
|
||||
nearbyEnemyCount = 0;
|
||||
nearbyAllyCount = 0;
|
||||
nearestEnemyDistance = float.MaxValue;
|
||||
nearestBagDistance = float.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data about a perceived entity (enemy or ally).
|
||||
/// Reused from pool to avoid allocation.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct PerceivedEntity
|
||||
{
|
||||
public Transform transform;
|
||||
public GameObject gameObject;
|
||||
public float distance;
|
||||
public float sqrDistance;
|
||||
public float healthPercent;
|
||||
public bool isCarryingBag;
|
||||
public int carriedBagCount;
|
||||
public bool isVisible; // In line of sight
|
||||
public Vector3 lastKnownPosition;
|
||||
public float timeSinceLastSeen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TeamBlackboard — Shared knowledge between all AI on a team.
|
||||
/// Prevents clustering and enables coordination.
|
||||
/// Singleton per team — accessed via TeamBlackboard.Get(teamTag).
|
||||
/// </summary>
|
||||
public class TeamBlackboard : MonoBehaviour
|
||||
{
|
||||
// ─── Static Access ───────────────────────────────────────
|
||||
private static TeamBlackboard _playerTeam;
|
||||
private static TeamBlackboard _enemyTeam;
|
||||
|
||||
public static TeamBlackboard Get(string teamTag)
|
||||
{
|
||||
if (teamTag == "Player")
|
||||
{
|
||||
if (_playerTeam == null)
|
||||
{
|
||||
var go = new GameObject("TeamBlackboard_Player");
|
||||
_playerTeam = go.AddComponent<TeamBlackboard>();
|
||||
_playerTeam._teamTag = "Player";
|
||||
}
|
||||
return _playerTeam;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_enemyTeam == null)
|
||||
{
|
||||
var go = new GameObject("TeamBlackboard_Enemy");
|
||||
_enemyTeam = go.AddComponent<TeamBlackboard>();
|
||||
_enemyTeam._teamTag = "Enemy";
|
||||
}
|
||||
return _enemyTeam;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearAll()
|
||||
{
|
||||
if (_playerTeam != null) { Destroy(_playerTeam.gameObject); _playerTeam = null; }
|
||||
if (_enemyTeam != null) { Destroy(_enemyTeam.gameObject); _enemyTeam = null; }
|
||||
}
|
||||
|
||||
// ─── Team Data ───────────────────────────────────────────
|
||||
[SerializeField] private string _teamTag;
|
||||
|
||||
/// <summary>Which bags are claimed by AI (prevents all AI chasing same bag).</summary>
|
||||
public readonly Dictionary<CashBag, AIBrain> claimedBags = new Dictionary<CashBag, AIBrain>(8);
|
||||
|
||||
/// <summary>Which enemies are being targeted (limit engagement per enemy).</summary>
|
||||
public readonly Dictionary<Transform, int> targetedEnemies = new Dictionary<Transform, int>(8);
|
||||
|
||||
/// <summary>Who on our team is carrying bags?</summary>
|
||||
public readonly List<CashCarrier> teamCarriers = new List<CashCarrier>(4);
|
||||
|
||||
/// <summary>Is our vault under attack?</summary>
|
||||
public bool vaultUnderAttack;
|
||||
|
||||
/// <summary>Our team's score.</summary>
|
||||
public float teamScore;
|
||||
|
||||
// ─── Claim System ────────────────────────────────────────
|
||||
|
||||
/// <summary>Try to claim a bag for this AI. Returns true if claimed successfully.</summary>
|
||||
public bool TryClaimBag(CashBag bag, AIBrain claimant)
|
||||
{
|
||||
if (bag == null) return false;
|
||||
|
||||
// Clean stale claims
|
||||
if (claimedBags.TryGetValue(bag, out AIBrain existing))
|
||||
{
|
||||
if (existing == null || !existing.gameObject.activeInHierarchy)
|
||||
{
|
||||
claimedBags[bag] = claimant;
|
||||
return true;
|
||||
}
|
||||
return existing == claimant; // Already claimed by us
|
||||
}
|
||||
|
||||
claimedBags[bag] = claimant;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Release claim on a bag.</summary>
|
||||
public void ReleaseBagClaim(AIBrain claimant)
|
||||
{
|
||||
CashBag toRemove = null;
|
||||
foreach (var kvp in claimedBags)
|
||||
{
|
||||
if (kvp.Value == claimant) { toRemove = kvp.Key; break; }
|
||||
}
|
||||
if (toRemove != null) claimedBags.Remove(toRemove);
|
||||
}
|
||||
|
||||
/// <summary>Is this bag already claimed by a teammate?</summary>
|
||||
public bool IsBagClaimed(CashBag bag, AIBrain excludeSelf = null)
|
||||
{
|
||||
if (!claimedBags.TryGetValue(bag, out AIBrain claimant)) return false;
|
||||
if (claimant == null || !claimant.gameObject.activeInHierarchy)
|
||||
{
|
||||
claimedBags.Remove(bag);
|
||||
return false;
|
||||
}
|
||||
return claimant != excludeSelf;
|
||||
}
|
||||
|
||||
/// <summary>Register an enemy as being targeted. Returns current count.</summary>
|
||||
public int AddEnemyTarget(Transform enemy)
|
||||
{
|
||||
if (enemy == null) return 0;
|
||||
if (!targetedEnemies.ContainsKey(enemy))
|
||||
targetedEnemies[enemy] = 0;
|
||||
targetedEnemies[enemy]++;
|
||||
return targetedEnemies[enemy];
|
||||
}
|
||||
|
||||
/// <summary>Unregister from targeting an enemy.</summary>
|
||||
public void RemoveEnemyTarget(Transform enemy)
|
||||
{
|
||||
if (enemy == null) return;
|
||||
if (targetedEnemies.ContainsKey(enemy))
|
||||
{
|
||||
targetedEnemies[enemy]--;
|
||||
if (targetedEnemies[enemy] <= 0)
|
||||
targetedEnemies.Remove(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>How many teammates are targeting this enemy?</summary>
|
||||
public int GetEnemyTargetCount(Transform enemy)
|
||||
{
|
||||
if (enemy == null) return 0;
|
||||
return targetedEnemies.TryGetValue(enemy, out int count) ? count : 0;
|
||||
}
|
||||
|
||||
/// <summary>Update team carriers list.</summary>
|
||||
public void RefreshCarriers()
|
||||
{
|
||||
teamCarriers.Clear();
|
||||
var allCarriers = EntityRegistry<CashCarrier>.All;
|
||||
for (int i = 0; i < allCarriers.Count; i++)
|
||||
{
|
||||
if (allCarriers[i] != null && allCarriers[i].TeamTag == _teamTag && allCarriers[i].IsCarrying)
|
||||
teamCarriers.Add(allCarriers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clean stale entries (call periodically).</summary>
|
||||
public void CleanStaleEntries()
|
||||
{
|
||||
// Clean bag claims
|
||||
var staleBags = new List<CashBag>();
|
||||
foreach (var kvp in claimedBags)
|
||||
{
|
||||
if (kvp.Key == null || kvp.Value == null || !kvp.Value.gameObject.activeInHierarchy)
|
||||
staleBags.Add(kvp.Key);
|
||||
}
|
||||
for (int i = 0; i < staleBags.Count; i++)
|
||||
claimedBags.Remove(staleBags[i]);
|
||||
|
||||
// Clean enemy targets
|
||||
var staleTargets = new List<Transform>();
|
||||
foreach (var kvp in targetedEnemies)
|
||||
{
|
||||
if (kvp.Key == null) staleTargets.Add(kvp.Key);
|
||||
}
|
||||
for (int i = 0; i < staleTargets.Count; i++)
|
||||
targetedEnemies.Remove(staleTargets[i]);
|
||||
}
|
||||
|
||||
private float _lastCleanTime;
|
||||
private void Update()
|
||||
{
|
||||
if (Time.time - _lastCleanTime > 3f)
|
||||
{
|
||||
CleanStaleEntries();
|
||||
RefreshCarriers();
|
||||
_lastCleanTime = Time.time;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_playerTeam == this) _playerTeam = null;
|
||||
if (_enemyTeam == this) _enemyTeam = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user