Files
Cielonos/Assets/Opsive/BehaviorDesigner/Add-Ons/Shared/Runtime/FormationsManager.cs
SoulliesOfficial ef7b479712 initial
2025-11-25 08:19:33 -05:00

363 lines
14 KiB
C#

/// ---------------------------------------------
/// Formations Pack for Behavior Designer Pro
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.BehaviorDesigner.AddOns.Shared.Runtime
{
using Opsive.BehaviorDesigner.AddOns.Shared.Runtime.Tasks;
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// Manages formation groups and their agents.
/// </summary>
public class FormationsManager : MonoBehaviour
{
public static FormationsManager Instance
{
get {
if (s_Instance == null) {
var formationManager = new GameObject("Formations Manager");
s_Instance = formationManager.AddComponent<FormationsManager>();
}
return s_Instance;
}
}
private static FormationsManager s_Instance;
[Tooltip("The amount of time before the formation group starts to move after registering. This allows all agents to register themselves without the formation starting too soon.")]
[SerializeField] protected float m_StartDelay = 0.1f;
public static float StartDelay => Instance.m_StartDelay;
private Dictionary<int, FormationGroup> m_FormationGroups;
/// <summary>
/// The current state of the formation.
/// </summary>
public enum FormationState
{
Initialized, // The formation has been initialized.
MoveToFormation, // The members are moving into formation.
MoveToTarget, // The members are moving to the target set by the leader.
Arrived, // The members have arrived at the target.
Failure // The formation has failed.
}
/// <summary>
/// Stores the current state of a formation and its members.
/// </summary>
public class FormationGroup
{
[Tooltip("The leader of this formation group.")]
protected FormationsBase m_Leader;
[Tooltip("The list of agents in this formation group.")]
protected List<FormationsBase> m_Members;
[Tooltip("The current state of the formation.")]
protected FormationState m_State;
[Tooltip("The target position for the formation.")]
protected Vector3 m_TargetPosition;
[Tooltip("The forward direction the formation should face.")]
protected Vector3 m_Direction;
[Tooltip("The time when the formation group was created.")]
protected float m_StartTime;
public FormationsBase Leader { get => m_Leader; set => m_Leader = value; }
public List<FormationsBase> Members { get => m_Members; set => m_Members = value; }
public FormationState State { get => m_State; set => m_State = value; }
public Vector3 TargetPosition { get => m_TargetPosition; set => m_TargetPosition = value; }
public Vector3 Direction { get => m_Direction; set => m_Direction = value; }
public float StartTime { get => m_StartTime; set => m_StartTime = value; }
/// <summary>
/// Default constructor.
/// </summary>
public FormationGroup()
{
m_Members = new List<FormationsBase>();
m_Leader = null;
m_TargetPosition = Vector3.zero;
m_Direction = Vector3.forward;
m_State = FormationState.Initialized;
}
}
/// <summary>
/// Initialize any default objects.
/// </summary>
private void Awake()
{
m_FormationGroups = new Dictionary<int, FormationGroup>();
}
/// <summary>
/// The object has been enabled.
/// </summary>
private void OnEnable()
{
s_Instance = this;
}
/// <summary>
/// Gets or creates a new formation group with the specified ID.
/// </summary>
/// <param name="groupID">The ID of the group to create.</param>
/// <returns>The new group.</returns>
private FormationGroup GetOrCreateFormationGroup(int groupID)
{
var group = GetFormationGroup(groupID);
if (group != null) {
return group;
}
group = new FormationGroup();
m_FormationGroups[groupID] = group;
return group;
}
/// <summary>
/// Gets the formation group from the specified ID.
/// </summary>
/// <param name="groupID">The ID of the group.</param>
/// <returns>The formation group from the specified ID (can be null).</returns>
public static FormationGroup GetFormationGroup(int groupID)
{
return Instance.GetFormationGroupInternal(groupID);
}
/// <summary>
/// Internal method which gets the formation group from the specified ID.
/// </summary>
/// <param name="groupID">The ID of the group.</param>
/// <returns>The formation group from the specified ID (can be null).</returns>
private FormationGroup GetFormationGroupInternal(int groupID)
{
if (m_FormationGroups.TryGetValue(groupID, out var group)) {
return group;
}
return null;
}
/// <summary>
/// Add a formation task to a group. If an agent is already within a group that agent will be removed from the old group.
/// </summary>
/// <param name="groupID">The ID of the group to add to.</param>
/// <param name="task">The formation task to add.</param>
/// <returns>True if the task was added to the group.</returns>
public static bool AddTaskToGroup(int groupID, FormationsBase task)
{
return Instance.AddTaskToGroupInternal(groupID, task);
}
/// <summary>
/// Internal method which adds a formation task to a group. If an agent is already within a group that agent will be removed from the old group.
/// </summary>
/// <param name="groupID">The ID of the group to add to.</param>
/// <param name="task">The formation task to add.</param>
/// <returns>True if the task was added to the group.</returns>
private bool AddTaskToGroupInternal(int groupID, FormationsBase task)
{
if (task == null) {
return false;
}
RemoveTaskFromExistingGroup(task);
// Don't allow new agents to join if the group is in a terminal state.
var group = GetOrCreateFormationGroup(groupID);
if (group.State == FormationState.Arrived || group.State == FormationState.Failure) {
return false;
}
if (task.ForceLeader && (group.Leader == null || !group.Leader.ForceLeader)) {
// The leader should always be the first member.
group.Members.Insert(0, task);
group.Leader = task;
if (group.Members.Count == 1) {
group.StartTime = Time.time;
}
} else {
group.Members.Add(task);
// The first task should be the leader if no leader exists.
if (group.Members.Count == 1) {
group.Leader = task;
group.StartTime = Time.time;
}
#if UNITY_EDITOR
else {
// 2D sanity check.
if (group.Members[0].Is2D != task.Is2D) {
Debug.LogWarning("Warning: The added task does not use the same perspective as the group leader.");
}
}
#endif
}
// If the group is already in progress, assign the last index to the new agent and update the positions.
if (group.State != FormationState.Initialized) {
task.Group = group;
task.UpdateFormationIndex(group.Members.Count - 1);
for (int i = 0; i < group.Members.Count; ++i) {
group.Members[i].DesiredPosition = group.Members[i].UpdateFormationDestination();
}
}
return true;
}
/// <summary>
/// Assigns formation indices.
/// </summary>
/// <param name="group">The formation group to assign indices for.</param>
public static void AssignIndices(int groupID)
{
Instance.AssignIndicesInternal(groupID);
}
/// <summary>
/// Internal method which assigns formation indices.
/// </summary>
/// <param name="group">The formation group to assign indices for.</param>
public void AssignIndicesInternal(int groupID)
{
var group = GetFormationGroup(groupID);
if (group == null) {
return;
}
if (!group.Leader.AssignOptimialIndicies) {
for (int i = 0; i < group.Members.Count; ++i) {
group.Members[i].UpdateFormationIndex(i);
}
return;
}
// If requested assign the indicies based on their proximity to target positions.
var formationPositions = new List<Vector3>();
for (int i = 0; i < group.Members.Count; ++i) {
formationPositions.Add(group.Leader.CalculateFormationPosition(i, group.Members.Count, group.TargetPosition, group.Direction, false));
}
// Create a list of available formation indices.
var availableIndices = new List<int>();
for (int i = 0; i < group.Members.Count; ++i) {
availableIndices.Add(i);
}
// For each agent, find the closest available formation position.
for (int i = 0; i < group.Members.Count; ++i) {
var agent = group.Members[i];
var minDistance = float.MaxValue;
var bestIndex = -1;
// Find the closest available formation position
for (int j = 0; j < availableIndices.Count; ++j) {
var formationIndex = availableIndices[j];
var distance = Vector3.SqrMagnitude(agent.Transform.position - formationPositions[formationIndex]);
if (distance < minDistance) {
minDistance = distance;
bestIndex = j;
}
}
// Assign the agent to the closest available position
if (bestIndex != -1) {
agent.UpdateFormationIndex(availableIndices[bestIndex], formationPositions[availableIndices[bestIndex]]);
availableIndices.RemoveAt(bestIndex);
}
}
}
/// <summary>
/// Remove a formation task from a group and update remaining indices.
/// </summary>
/// <param name="groupID">The ID of the group to remove from.</param>
/// <param name="task">The formation task to remove.</param>
public static void RemoveTaskFromGroup(int groupID, FormationsBase task)
{
if (s_Instance == null) {
return;
}
s_Instance.RemoveTaskFromGroupInternal(groupID, task);
}
/// <summary>
/// Remove a formation task from a group and update remaining indices.
/// </summary>
/// <param name="groupID">The ID of the group to remove from.</param>
/// <param name="task">The formation task to remove.</param>
private void RemoveTaskFromGroupInternal(int groupID, FormationsBase task)
{
if (task == null || !m_FormationGroups.TryGetValue(groupID, out var group)) {
return;
}
var index = group.Members.IndexOf(task);
if (index != -1) {
group.Members.RemoveAt(index);
// Remove the formation group if there are no more members.
if (group.Members.Count == 0) {
m_FormationGroups.Remove(groupID);
} else {
if (group.Leader.FailOnAgentRemoval) {
group.State = FormationState.Failure;
} else {
// Assign a new leader if the leader left.
if (group.Leader == task) {
if (group.Members.Count > 0) {
group.Leader = group.Members[0];
// Update the formation target based on the new leader's position
if (group.State == FormationState.MoveToTarget) {
group.Leader.UpdateFormationDestination();
}
} else {
group.Leader = null;
group.State = FormationState.Failure;
return;
}
}
// Update indices for remaining tasks to indicate their new position.
if (group.Leader.UpdateUnitLocationsOnAgentRemoval) {
for (int i = index; i < group.Members.Count; ++i) {
group.Members[i].UpdateFormationIndex(i);
}
}
}
}
}
}
/// <summary>
/// Remove a formation task from any group it's in.
/// </summary>
/// <param name="task">The formation task to remove.</param>
private void RemoveTaskFromExistingGroup(FormationsBase task)
{
foreach (var group in m_FormationGroups) {
if (group.Value.Members.Contains(task)) {
RemoveTaskFromGroup(group.Key, task);
break;
}
}
}
/// <summary>
/// The component has been destroyed.
/// </summary>
private void OnDestroy()
{
s_Instance = null;
}
/// <summary>
/// Reset the static variables for domain reloading.
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void DomainReset()
{
s_Instance = null;
}
}
}