/// --------------------------------------------- /// 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; /// /// Manages formation groups and their agents. /// public class FormationsManager : MonoBehaviour { public static FormationsManager Instance { get { if (s_Instance == null) { var formationManager = new GameObject("Formations Manager"); s_Instance = formationManager.AddComponent(); } 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 m_FormationGroups; /// /// The current state of the formation. /// 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. } /// /// Stores the current state of a formation and its members. /// 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 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 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; } /// /// Default constructor. /// public FormationGroup() { m_Members = new List(); m_Leader = null; m_TargetPosition = Vector3.zero; m_Direction = Vector3.forward; m_State = FormationState.Initialized; } } /// /// Initialize any default objects. /// private void Awake() { m_FormationGroups = new Dictionary(); } /// /// The object has been enabled. /// private void OnEnable() { s_Instance = this; } /// /// Gets or creates a new formation group with the specified ID. /// /// The ID of the group to create. /// The new group. private FormationGroup GetOrCreateFormationGroup(int groupID) { var group = GetFormationGroup(groupID); if (group != null) { return group; } group = new FormationGroup(); m_FormationGroups[groupID] = group; return group; } /// /// Gets the formation group from the specified ID. /// /// The ID of the group. /// The formation group from the specified ID (can be null). public static FormationGroup GetFormationGroup(int groupID) { return Instance.GetFormationGroupInternal(groupID); } /// /// Internal method which gets the formation group from the specified ID. /// /// The ID of the group. /// The formation group from the specified ID (can be null). private FormationGroup GetFormationGroupInternal(int groupID) { if (m_FormationGroups.TryGetValue(groupID, out var group)) { return group; } return null; } /// /// Add a formation task to a group. If an agent is already within a group that agent will be removed from the old group. /// /// The ID of the group to add to. /// The formation task to add. /// True if the task was added to the group. public static bool AddTaskToGroup(int groupID, FormationsBase task) { return Instance.AddTaskToGroupInternal(groupID, task); } /// /// 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. /// /// The ID of the group to add to. /// The formation task to add. /// True if the task was added to the group. 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; } /// /// Assigns formation indices. /// /// The formation group to assign indices for. public static void AssignIndices(int groupID) { Instance.AssignIndicesInternal(groupID); } /// /// Internal method which assigns formation indices. /// /// The formation group to assign indices for. 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(); 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(); 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); } } } /// /// Remove a formation task from a group and update remaining indices. /// /// The ID of the group to remove from. /// The formation task to remove. public static void RemoveTaskFromGroup(int groupID, FormationsBase task) { if (s_Instance == null) { return; } s_Instance.RemoveTaskFromGroupInternal(groupID, task); } /// /// Remove a formation task from a group and update remaining indices. /// /// The ID of the group to remove from. /// The formation task to remove. 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); } } } } } } /// /// Remove a formation task from any group it's in. /// /// The formation task to remove. private void RemoveTaskFromExistingGroup(FormationsBase task) { foreach (var group in m_FormationGroups) { if (group.Value.Members.Contains(task)) { RemoveTaskFromGroup(group.Key, task); break; } } } /// /// The component has been destroyed. /// private void OnDestroy() { s_Instance = null; } /// /// Reset the static variables for domain reloading. /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void DomainReset() { s_Instance = null; } } }