Skip to content

Instantly share code, notes, and snippets.

@sabresaurus
Last active June 18, 2026 16:47
Show Gist options
  • Select an option

  • Save sabresaurus/42c5205b45222c95f139497738e5de0f to your computer and use it in GitHub Desktop.

Select an option

Save sabresaurus/42c5205b45222c95f139497738e5de0f to your computer and use it in GitHub Desktop.
Cover Node from Revengard
// MIT License
//
// Copyright (c) 2026 Sabresaurus
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using UnityEngine;
using System.Collections.Generic;
using System;
public enum CoverType
{
None, // Just in empty space
Partial, // Gives some cover (maybe up to waist height)
Full // Gives full cover for the agent (i.e. obscuring it completely in a direction)
};
[Serializable]
public class CoverNode
{
#if UNITY_EDITOR
Dictionary<EnemyAgent, float> displayWeight = new Dictionary<EnemyAgent, float>();
#endif
[NonSerialized]
List<EnemyAgent> agentsAtNode = new List<EnemyAgent>();
[SerializeField] private Vector3 position;
[SerializeField] private Vector3 forwardDirection;
[SerializeField]
CoverType coverType = CoverType.Partial;
public CoverType CoverType => coverType;
public Vector3 Position => position;
public Vector3 ForwardDirection => forwardDirection;
public int AgentsCount => agentsAtNode.Count;
#if UNITY_EDITOR
public Dictionary<EnemyAgent, float> DisplayWeight => displayWeight;
#endif
public CoverNode()
{
agentsAtNode = new List<EnemyAgent>();
}
public CoverNode(Vector3 coverPointPosition, Vector3? coverPointDirection)
{
position = coverPointPosition;
if (coverPointDirection.HasValue)
{
coverType = CoverType.Partial;
forwardDirection = -coverPointDirection.Value; // Cover away from a direction
}
else
{
coverType = CoverType.None;
}
agentsAtNode = new List<EnemyAgent>();
}
public void AddAgent(EnemyAgent agent)
{
if(!agentsAtNode.Contains(agent))
{
agentsAtNode.Add(agent);
}
}
public void RemoveAgent(EnemyAgent agent)
{
agentsAtNode.Remove(agent);
}
public bool ContainsAgent(EnemyAgent agent)
{
return agentsAtNode.Contains(agent);
}
public float CalculateWeightAndCache(CoverQuery query, EnemyAgent currentAgent, Squad squad)
{
float weight = CalculateWeight(query, currentAgent, squad);
#if UNITY_EDITOR
displayWeight[currentAgent] = weight;
#endif
return weight;
}
float CalculateWeight(CoverQuery query, EnemyAgent currentAgent, Squad squad)
{
// Disregard a cover node that is currently occupied by another agent
if(AgentsCount >= 1 && !ContainsAgent(currentAgent))
{
return -1;
}
Vector3 currentPosition = currentAgent.transform.position;
Vector3 worldNormal = forwardDirection;
if(coverType == CoverType.None)
{
worldNormal = Vector3.up; // Neutral direction
}
Vector3 playerPosition = AgentManager.GetPlayer().transform.position;
Vector3 enemyDirection = (currentPosition - playerPosition).normalized;
Vector3 squadCentroid = squad.Centroid;
Vector3 intendedDirection;
if (query.IntendedDestination == AgentDestination.PlayerFlank)
{
intendedDirection = Quaternion.Euler(0, 90, 0) * enemyDirection;
if (Vector3.Dot(intendedDirection, squadCentroid - currentPosition) > 0)
{
intendedDirection = Quaternion.Euler(0, -90, 0) * enemyDirection;
}
}
else if(query.IntendedDestination == AgentDestination.PlayerPosition)
{
intendedDirection = (playerPosition - currentPosition).normalized;
}
else if(query.IntendedDestination == AgentDestination.SquadPatrolTarget)
{
Debug.LogError("OBSOLETE - USE PLAYERPOSITION INSTEAD");
intendedDirection = (playerPosition - currentPosition).normalized;
}
else
{
throw new System.NotImplementedException("Logic for " + query.IntendedDestination + " destination has not been implemented");
}
Vector3 delta = position - currentPosition;
if (query.FilterOutsideSquad)
{
float squareDistance = (squadCentroid - position).sqrMagnitude;
if(squareDistance > (query.OutsideSquadDistance * query.OutsideSquadDistance))
{
return -1;
}
}
float intendedDot = Vector3.Dot(delta, intendedDirection);
float enemyDot = Vector3.Dot(worldNormal, enemyDirection);
if ((intendedDot > 0 || !query.FilterAwayFromIntended)
&& (enemyDot > 0 || !query.FilterAwayFromEnemy))
{
float weight = 1;
float directnessDot = Vector3.Dot(delta.normalized, intendedDirection);
if (query.DirectnessInfluence > 0)
{
weight *= Mathf.Lerp(1, directnessDot, query.DirectnessInfluence);
}
if (query.IdealDistanceToCoverInfluence > 0)
{
float distance = Vector3.Distance(position, currentPosition);
float distanceFromIdeal = Mathf.Abs(distance - query.IdealDistanceToCover);
float distanceWeight = Mathf.InverseLerp(25, 0, distanceFromIdeal);
weight *= Mathf.Lerp(1, distanceWeight, query.IdealDistanceToCoverInfluence);
}
if (query.IdealDistanceToPlayerInfluence > 0)
{
float distance = Vector3.Distance(position, playerPosition);
float distanceFromIdeal = Mathf.Abs(distance - query.IdealDistanceToPlayer);
float distanceWeight = Mathf.InverseLerp(25, 0, distanceFromIdeal);
weight *= Mathf.Lerp(1, distanceWeight, query.IdealDistanceToPlayerInfluence);
}
// Weight can only be in the 0 to 1 range, clamp to that range
return Mathf.Clamp01(weight);
}
else
{
return -1;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment