Skip to content

Commit

Permalink
complete ai behaviour overhaul part 3
Browse files Browse the repository at this point in the history
  • Loading branch information
p-svacha committed Jan 2, 2025
1 parent 45895bc commit 242b8b2
Show file tree
Hide file tree
Showing 18 changed files with 511 additions and 184 deletions.
24 changes: 12 additions & 12 deletions Assets/Scenes/CaptureTheFlag.unity
Original file line number Diff line number Diff line change
Expand Up @@ -1768,8 +1768,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 64.259995, y: 0}
m_SizeDelta: {x: 59.26, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 1, y: 0}
--- !u!114 &481135219
MonoBehaviour:
Expand Down Expand Up @@ -7020,19 +7020,19 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchorMax.y
value: 1
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchorMin.y
value: 1
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_SizeDelta.x
value: 168
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_SizeDelta.y
Expand Down Expand Up @@ -7068,11 +7068,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchoredPosition.x
value: 89
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchoredPosition.y
value: -35
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
Expand Down Expand Up @@ -10537,19 +10537,19 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchorMax.y
value: 1
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchorMin.y
value: 1
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_SizeDelta.x
value: 168
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_SizeDelta.y
Expand Down Expand Up @@ -10585,11 +10585,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchoredPosition.x
value: 89
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_AnchoredPosition.y
value: -15
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3600956094793267932, guid: ea2cd62cbaf192f45b87b10951c6d6d6, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
Expand Down
83 changes: 71 additions & 12 deletions Assets/Scripts/CaptureTheFlag/AI/AICharacterJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public virtual void OnNextActionRequested() { }
protected Action_Movement GetSingleNodeMovementTo(BlockmapNode targetNode, NavigationPath targetPath = null)
{
// Get path to target
if (targetPath == null || !targetPath.Nodes.Contains(targetNode)) targetPath = GetPath(targetNode);
if (targetPath == null || !targetPath.Nodes.Contains(Character.OriginNode)) targetPath = GetPath(targetNode);
else targetPath.CutEverythingBefore(Character.OriginNode); // Adapt the given path so the start point is where the character is currently at

if (targetPath == null) // No path found
Expand All @@ -69,7 +69,7 @@ protected Action_Movement GetSingleNodeMovementTo(BlockmapNode targetNode, Navig

// Look for a possible move that corresponds to a single step in the target path.
// If the move exists but is blocked, try moves going over multiple nodes (up to 5) along the path.
List<Action_Movement> candidateMoves = Character.PossibleMoves.Values.Where(m => m.CanPerformNow() && !Opponent.Characters.Any(c => c.Node == m.Target)).ToList();
List<Action_Movement> candidateMoves = Character.PossibleMoves.Values.Where(m => m.CanPerformNow() && (Player.Territory.ContainsNode(m.Target) || !Opponent.Characters.Any(c => c.Node == m.Target))).ToList();
for (int i = 1; i < 5; i++)
{
if (targetPath.Nodes.Count < (i + 1)) break;
Expand All @@ -84,13 +84,60 @@ protected Action_Movement GetSingleNodeMovementTo(BlockmapNode targetNode, Navig
return null;
}

/// <summary>
/// Returns a new non-urgent job for this character that fits their role.
/// </summary>
/// <returns></returns>
protected AICharacterJob GetNewNonUrgentJob()
{
switch (Player.Roles[Character])
{
case AIPlayer.AICharacterRole.Defender:
return GetNewDefenderJob();

case AIPlayer.AICharacterRole.Attacker:
return GetNewAttackerJob();

default:
throw new System.Exception($"Role {Player.Roles[Character]} not handled.");
}
}

/// <summary>
/// Returns a job that a defender should no if there is no urgent thing to do.
/// </summary>
private AICharacterJob GetNewDefenderJob()
{
if (Random.value < AIPlayer.CHANCE_THAT_RANDOM_DEFENDER_JOB_IS_EXPLORE) return new AIJob_ExploreOwnTerritory(Character);
else return new AIJob_PatrolDefendFlag(Character);
}

/// <summary>
/// Returns a job that an attacker should no if there is no urgent thing to do.
/// </summary>
private AICharacterJob GetNewAttackerJob()
{
// If we know where enemy flag is => capture it
if (IsEnemyFlagExplored)
{
return new AIJob_CaptureOpponentFlag(Character);
}

// If we don't know where enemy flag is => search it
else
{
return new AIJob_SearchOpponentFlag(Character);
}
}

/// <summary>
/// Returns the fastest possible path to the given node without going through the own flag zone.
/// </summary>
protected NavigationPath GetPath(BlockmapNode targetNode)
{
return Pathfinder.GetPath(Character, Character.OriginNode, targetNode, considerUnexploredNodes: false, forbiddenNodes: Player.FlagZone.Nodes);
}
protected float GetPathCost(BlockmapNode targetNode) => GetPath(targetNode).GetCost(Character);

protected bool IsOnOrNear(BlockmapNode node)
{
Expand All @@ -101,7 +148,9 @@ protected bool IsAnyOpponentNearby(float maxCost)
{
foreach(CtfCharacter opp in VisibleOpponentCharactersNotInJail)
{
if (Character.IsInRange(opp.Node, maxCost, out _)) return true;
bool isInRange = (Character.IsInRange(opp.Node, maxCost, out float cost));
// Log($"IsAnyOpponentNearby: Is {opp.LabelCap} in Range? {isInRange} with cost = {cost}");
if (isInRange) return true;
}
return false;
}
Expand Down Expand Up @@ -136,12 +185,12 @@ protected bool CanTagCharacterDirectly(out CtfCharacter target)
/// If there is a visible opponent character or a position that is marked to be checked for search within the given search, returns true and the corresponding AIJob_ChaseToTagOpponent or AIJob_SearchOpponentInOwnTerritory job.
/// <br/>Else returns false.
/// </summary>
protected bool ShouldChaseOrSearchOpponent(float maxDistanceCost, out CtfCharacter target, out AICharacterJobId jobId)
protected bool ShouldChaseOrSearchOpponent(float maxDistanceCost, out CtfCharacter target, out AICharacterJobId jobId, out float costToTarget)
{
target = null;
jobId = AICharacterJobId.Error;

float lowestCost = float.MaxValue;
costToTarget = float.MaxValue;

// Look for visibile opponents nearby
foreach (CtfCharacter opp in VisibleOpponentCharactersNotInJail)
Expand All @@ -151,9 +200,9 @@ protected bool ShouldChaseOrSearchOpponent(float maxDistanceCost, out CtfCharact
{
Log($"Detected possibility to switch from {Id} to ChaseToTagOpponent because {opp.LabelCap} is nearby (distance = {cost}).");

if(cost < lowestCost)
if(cost < costToTarget)
{
lowestCost = cost;
costToTarget = cost;
target = opp;
jobId = AICharacterJobId.ChaseAndTagOpponent;
}
Expand All @@ -170,7 +219,7 @@ protected bool ShouldChaseOrSearchOpponent(float maxDistanceCost, out CtfCharact
{
Log($"Detected possibility to switch from {Id} to SearchOpponentInOwnTerritory because {opp.LabelCap} was seen nearby (distance = {cost}).");

if (cost < lowestCost)
if (cost < costToTarget)
{
target = opp;
jobId = AICharacterJobId.SearchOpponentInOwnTerritory;
Expand All @@ -188,17 +237,27 @@ protected bool ShouldChaseOrSearchOpponent(float maxDistanceCost, out CtfCharact
public bool ShouldFlee()
{
if (!Character.IsInOpponentTerritory) return false;
if (GetOpponentsToFleeFrom().Count == 0) return false;

return true;
}
protected List<CtfCharacter> GetOpponentsToFleeFrom()
{
List<CtfCharacter> relevantOpponents = new List<CtfCharacter>();
foreach (CtfCharacter opponentCharacter in VisibleOpponentCharactersNotInJail)
{
if (Character.IsVisibleBy(opponentCharacter)) return true;
if (opponentCharacter.IsInRange(Character.Node, AIPlayer.FLEE_DISTANCE, out float cost))
{
Log($"Should flee from {opponentCharacter.LabelCap} because distance is {cost}.");
relevantOpponents.Add(opponentCharacter);
}
}
return false;
return relevantOpponents;
}

protected void Log(string msg)
protected void Log(string msg, bool isWarning = false)
{
if (Match.DevMode) Player.Log(Character, msg);
if (Match.DevMode) Player.Log(Character, msg, isWarning);
}

#endregion
Expand Down
3 changes: 2 additions & 1 deletion Assets/Scripts/CaptureTheFlag/AI/AICharacterJobId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum AICharacterJobId
PatrolDefendFlag,
Flee,
LingerInNeutral,
SearchOpponentInOwnTerritory
SearchOpponentInOwnTerritory,
ExploreOwnTerritory
}
}
62 changes: 50 additions & 12 deletions Assets/Scripts/CaptureTheFlag/AI/AIPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ public class AIPlayer : Player

// AI Behaviour
private const float INVISIBLE_CHARACTER_SPEED = 25;
private const float CHANCE_THAT_ATTACKERS_TURN_INTO_DEFENDERS_AFTER_JAIL = 0.5f;
public const float CHANCE_THAT_RANDOM_DEFENDER_JOB_IS_EXPLORE = 0.25f;
public const float CHANCE_THAT_DEFENDER_SWITCHES_TO_ATTACKER_EACH_ACTION = 0.01f;

public const float MAX_STAMINA_FOR_REST_CHANCE = 0.4f; // If stamina is below is value (in %), there is a chance that characters with non-urgent jobs chose to rest
public const float NON_URGENT_REST_CHANCE_PER_ACTION = 0.06f;

public const float FLEE_DISTANCE = 16; // Path cost at which characters start fleeing from opponents

private const float DEFEND_PERIMETER_RADIUS = 60; // Transition cost

Expand Down Expand Up @@ -48,6 +56,8 @@ public class AIPlayer : Player

public AIPlayer(ClientInfo info) : base(info) { }

#region Game Loop

public override void OnMatchReady(CtfMatch game)
{
base.OnMatchReady(game);
Expand Down Expand Up @@ -105,12 +115,6 @@ public void StartTurn()
}
}

public void UnmarkOpponentCharactersLastPositionToBeChecked(CtfCharacter c)
{
Debug.Log($"[AI] Marking {c.LabelCap}'s last position to be no longer checked for search.");
OpponentPositionsToCheckForDefense[c] = null;
}

/// <summary>
/// Gets called every frame during the AI's turn.
/// </summary>
Expand Down Expand Up @@ -156,7 +160,6 @@ private void UpdateCharacterActions()
}
}
}

private void UpdateCameraFollow()
{
// Check if we should queue-follow an action
Expand Down Expand Up @@ -211,12 +214,37 @@ private void UpdateCameraFollow()
}
}

public string GetDevModeLabel(CtfCharacter c)
public override void OnCharacterGotSentToJail(CtfCharacter c)
{
return $"{c.LabelCap}: {Roles[c]} | {Jobs[c].DevmodeDisplayText}";
Jobs[c] = new AIJob_InitialJob(c);
}

public override void OnCharacterGotReleasedFromJail(CtfCharacter c)
{
// Chance that attackers that get released from jail turn into defenders
if(Roles[c] == AICharacterRole.Attacker)
{
if(Random.value < CHANCE_THAT_ATTACKERS_TURN_INTO_DEFENDERS_AFTER_JAIL)
{
Log(c, $"Changing role to Defender after being released from jail");
Roles[c] = AICharacterRole.Defender;
}
}
}

#region Private
#endregion



#region Logic



public void UnmarkOpponentCharactersLastPositionToBeChecked(CtfCharacter c)
{
Debug.Log($"[AI] Marking {c.LabelCap}'s last position to be no longer checked for search.");
OpponentPositionsToCheckForDefense[c] = null;
}

/// <summary>
/// Returns the action the given character will do next this turn.
Expand Down Expand Up @@ -264,9 +292,19 @@ private CharacterAction GetNextCharacterAction(CtfCharacter c)
public List<CtfCharacter> OpponentCharacters => Opponent.Characters;
public List<CtfCharacter> VisibleOpponentCharactersNotInJail => Opponent.Characters.Where(c => c.IsVisibleByOpponent && c.JailTime <= 1).ToList();

public void Log(CtfCharacter c, string msg)

public string GetDevModeLabel(CtfCharacter c)
{
if (Match.DevMode) Debug.Log($"[AI - {c.LabelCap}] {msg}");
return $"{c.LabelCap}: {Roles[c]} | {Jobs[c].DevmodeDisplayText}";
}
public void Log(CtfCharacter c, string msg, bool isWarning = false)
{
if (Match.DevMode)
{
string logMessage = $"[AI - {c.LabelCap}] {msg}";
if (isWarning) Debug.LogWarning(logMessage);
else Debug.Log(logMessage);
}
}

/// <summary>
Expand Down
Loading

0 comments on commit 242b8b2

Please sign in to comment.