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