217 lines
7.2 KiB
C#
217 lines
7.2 KiB
C#
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// UtilityScorer — Evaluates all AI actions and returns the highest-scoring one.
|
|
/// Stateless, zero-allocation. Called by AIBrain on each decision tick.
|
|
///
|
|
/// Uses Response Curves for nuanced decisions rather than hard thresholds.
|
|
/// Each AIAction.Score() returns 0-1 utility; UtilityScorer scales by role weights.
|
|
/// </summary>
|
|
public class UtilityScorer
|
|
{
|
|
// ─── Role Weight Profiles ────────────────────────────────
|
|
public enum AIRole
|
|
{
|
|
Balanced,
|
|
Collector,
|
|
Defender,
|
|
Aggressor
|
|
}
|
|
|
|
[System.Serializable]
|
|
public struct RoleWeights
|
|
{
|
|
public float collectWeight;
|
|
public float depositWeight;
|
|
public float attackWeight;
|
|
public float defendWeight;
|
|
public float retreatWeight;
|
|
public float escortWeight;
|
|
public float interceptWeight;
|
|
public float patrolWeight;
|
|
|
|
public static RoleWeights Balanced => new RoleWeights
|
|
{
|
|
collectWeight = 1.0f,
|
|
depositWeight = 1.0f,
|
|
attackWeight = 1.0f,
|
|
defendWeight = 1.2f,
|
|
retreatWeight = 1.0f,
|
|
escortWeight = 0.8f,
|
|
interceptWeight = 1.0f,
|
|
patrolWeight = 1.0f
|
|
};
|
|
|
|
public static RoleWeights Collector => new RoleWeights
|
|
{
|
|
collectWeight = 1.5f,
|
|
depositWeight = 1.3f,
|
|
attackWeight = 0.6f,
|
|
defendWeight = 0.8f,
|
|
retreatWeight = 1.2f,
|
|
escortWeight = 0.5f,
|
|
interceptWeight = 0.7f,
|
|
patrolWeight = 1.0f
|
|
};
|
|
|
|
public static RoleWeights Defender => new RoleWeights
|
|
{
|
|
collectWeight = 0.6f,
|
|
depositWeight = 0.8f,
|
|
attackWeight = 1.2f,
|
|
defendWeight = 2.0f,
|
|
retreatWeight = 0.8f,
|
|
escortWeight = 1.5f,
|
|
interceptWeight = 1.3f,
|
|
patrolWeight = 0.6f
|
|
};
|
|
|
|
public static RoleWeights Aggressor => new RoleWeights
|
|
{
|
|
collectWeight = 0.5f,
|
|
depositWeight = 0.7f,
|
|
attackWeight = 1.8f,
|
|
defendWeight = 0.8f,
|
|
retreatWeight = 0.6f,
|
|
escortWeight = 0.4f,
|
|
interceptWeight = 1.6f,
|
|
patrolWeight = 0.5f
|
|
};
|
|
|
|
public static RoleWeights ForRole(AIRole role)
|
|
{
|
|
switch (role)
|
|
{
|
|
case AIRole.Collector: return Collector;
|
|
case AIRole.Defender: return Defender;
|
|
case AIRole.Aggressor: return Aggressor;
|
|
default: return Balanced;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Pre-allocated action instances (no allocation per tick) ──
|
|
private readonly CollectBagAction _collectAction = new CollectBagAction();
|
|
private readonly DepositBagAction _depositAction = new DepositBagAction();
|
|
private readonly AttackEnemyAction _attackAction = new AttackEnemyAction();
|
|
private readonly DefendVaultAction _defendAction = new DefendVaultAction();
|
|
private readonly PatrolAction _patrolAction = new PatrolAction();
|
|
private readonly RetreatAction _retreatAction = new RetreatAction();
|
|
private readonly EscortCarrierAction _escortAction = new EscortCarrierAction();
|
|
private readonly InterceptCarrierAction _interceptAction = new InterceptCarrierAction();
|
|
|
|
private readonly AIAction[] _allActions;
|
|
private RoleWeights _weights;
|
|
|
|
// ─── Decision State ──────────────────────────────────────
|
|
private AIAction _currentAction;
|
|
private float _currentActionScore;
|
|
private float _minSwitchTime = 1.5f;
|
|
private float _actionStartTime;
|
|
|
|
// ─── Debug ───────────────────────────────────────────────
|
|
public AIAction CurrentAction => _currentAction;
|
|
public float CurrentActionScore => _currentActionScore;
|
|
public string CurrentActionName => _currentAction?.Name ?? "None";
|
|
|
|
// Expose for debug display
|
|
private readonly float[] _lastScores = new float[8];
|
|
public float[] LastScores => _lastScores;
|
|
|
|
public UtilityScorer(AIRole role = AIRole.Balanced)
|
|
{
|
|
_weights = RoleWeights.ForRole(role);
|
|
|
|
_allActions = new AIAction[]
|
|
{
|
|
_collectAction, // 0
|
|
_depositAction, // 1
|
|
_attackAction, // 2
|
|
_defendAction, // 3
|
|
_patrolAction, // 4
|
|
_retreatAction, // 5
|
|
_escortAction, // 6
|
|
_interceptAction // 7
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluate all actions and return the best one.
|
|
/// Zero allocation — all actions are pre-allocated.
|
|
/// </summary>
|
|
public AIAction Evaluate(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
float bestScore = float.MinValue;
|
|
AIAction bestAction = null;
|
|
|
|
for (int i = 0; i < _allActions.Length; i++)
|
|
{
|
|
float rawScore = _allActions[i].Score(bb, tb);
|
|
float weight = GetWeight(i);
|
|
float weightedScore = rawScore * weight;
|
|
|
|
_lastScores[i] = weightedScore; // Store for debug
|
|
|
|
if (weightedScore > bestScore)
|
|
{
|
|
bestScore = weightedScore;
|
|
bestAction = _allActions[i];
|
|
}
|
|
}
|
|
|
|
// Hysteresis: don't switch unless new action is significantly better
|
|
// (prevents oscillation between similar-scoring actions)
|
|
if (_currentAction != null && bestAction != _currentAction)
|
|
{
|
|
bool timeLocked = (Time.time - _actionStartTime) < _minSwitchTime;
|
|
bool canInterrupt = bestAction.CanInterrupt;
|
|
bool significantlyBetter = bestScore > _currentActionScore * 1.2f;
|
|
|
|
if (timeLocked && !canInterrupt && !significantlyBetter)
|
|
{
|
|
return _currentAction; // Stay with current action
|
|
}
|
|
}
|
|
|
|
// Switch action
|
|
if (bestAction != _currentAction)
|
|
{
|
|
_currentAction?.OnExit(brain, bb, tb);
|
|
_currentAction = bestAction;
|
|
_currentActionScore = bestScore;
|
|
_actionStartTime = Time.time;
|
|
_currentAction?.OnEnter(brain, bb, tb);
|
|
}
|
|
|
|
return _currentAction;
|
|
}
|
|
|
|
/// <summary>Force reset (e.g., on death/respawn).</summary>
|
|
public void Reset()
|
|
{
|
|
_currentAction = null;
|
|
_currentActionScore = 0f;
|
|
}
|
|
|
|
private float GetWeight(int actionIndex)
|
|
{
|
|
switch (actionIndex)
|
|
{
|
|
case 0: return _weights.collectWeight;
|
|
case 1: return _weights.depositWeight;
|
|
case 2: return _weights.attackWeight;
|
|
case 3: return _weights.defendWeight;
|
|
case 4: return _weights.patrolWeight;
|
|
case 5: return _weights.retreatWeight;
|
|
case 6: return _weights.escortWeight;
|
|
case 7: return _weights.interceptWeight;
|
|
default: return 1f;
|
|
}
|
|
}
|
|
|
|
public void SetRole(AIRole role)
|
|
{
|
|
_weights = RoleWeights.ForRole(role);
|
|
}
|
|
}
|