using UnityEngine; using System.Collections; using System.Collections.Generic; using BehaviorDesigner.Runtime.Tasks; using Tooltip = BehaviorDesigner.Runtime.Tasks.TooltipAttribute; using HelpURL = BehaviorDesigner.Runtime.Tasks.HelpURLAttribute; #if DEATHMATCH_AI_KIT_PRESENT using Opsive.DeathmatchAIKit; #endif namespace BehaviorDesigner.Runtime.Formations.Tasks { public abstract class FormationGroup : Action { [Tooltip("The leader to follow. If null then the current agent will lead")] public SharedGameObject leader; [Tooltip("Specifies the group index of the leader behavior tree. This is not necessary if there is only one behavior tree on the leader")] public SharedInt leaderGroupIndex; [Tooltip("The target destination")] public SharedTransform targetTransform; [Tooltip("The target destination Vector3 position. Used if targetTransform is null")] public SharedVector3 targetPosition; [Tooltip("The distance to look ahead to the destination. The higher the value the better the agent will avoid obstacles and less keep formation")] public SharedFloat lookAhead = 4; [Tooltip("The agent move speed after the group has formed")] public SharedFloat fullSpeed = 3.5f; [Tooltip("The agent move speed as the group is forming")] public SharedFloat formationSpeed = 2f; [Tooltip("The agent move speed if waiting for another agent to join")] public SharedFloat slowdownSpeed = 1; [Tooltip("The agent move speed if falling behind the formation")] public SharedFloat catchupSpeed = 3f; [Tooltip("The amount of time to wait until the group starts forming")] public SharedFloat waitTime = 0; [Tooltip("Should the agent wait to move when new agents are added?")] public SharedBool waitToMove = true; public enum MoveStatus { Wait, Full, Formation, Slowdown, Catchup, Last } private List formationTrees; private List pathStarted; private List moveStatus; private int formationIndex = -1; private bool formationStarted; private bool inFormation; private Vector3 prevTargetPosition; private TaskStatus runStatus; private bool sendListenerEvent; private GameObject prevLeader; private MoveStatus prevMoveStatus = MoveStatus.Last; private MoveStatus leaderMoveStatus; private Transform prevTargetTransform; protected List agents; protected List formationAgents; protected Behavior leaderTree; protected FormationAgent formationAgent; protected FormationAgent leaderAgent; /// /// Listen for any agents that want to join the group. /// public override void OnAwake() { Owner.RegisterEvent("StartListeningForOrders", StartListeningForOrders); Owner.RegisterEvent("StopListeningToOrders", StopListeningToOrders); Owner.RegisterEvent("FormationUpdated", FormationUpdated); Owner.RegisterEvent("AddAgentToGroup", AddAgentToGroup); Owner.RegisterEvent("UpdateTargetPosition", UpdateTargetPosition); Owner.RegisterEvent("UpdateTarget", UpdateTarget); Owner.RegisterEvent("UpdateMoveStatus", UpdateMoveStatus); Owner.RegisterEvent("OrdersFinished", OrdersFinished); } /// /// Start forming the group immediately on start or after a set amount of time. /// public override void OnStart() { UpdateLeader(); runStatus = TaskStatus.Running; } /// /// The leader has changed. Update the leader. /// private void UpdateLeader() { // If the leader is null then the current agent is the leader. if (leader.Value == null) { AddAgentToGroup(Owner, 0); #if DEATHMATCH_AI_KIT_PRESENT if (TeamManager.IsInstantiated) { TeamManager.SetLeader(gameObject, true); } #endif if (waitTime.Value == 0) { StartFormation(); } else { StartCoroutine(WaitForGroupFormation()); } } else { var leaderTrees = leader.Value.GetComponents(); if (leaderTrees.Length > 1) { for (int i = 0; i < leaderTrees.Length; ++i) { if (leaderTrees[i].Group == leaderGroupIndex.Value) { leaderTree = leaderTrees[i]; break; } } } else if (leaderTrees.Length == 1) { leaderTree = leaderTrees[0]; } if (leaderTree != null) { sendListenerEvent = true; } #if DEATHMATCH_AI_KIT_PRESENT if (TeamManager.IsInstantiated) { // If the leader tree is null then the leader is a player-controlled character. formationIndex = TeamManager.AddToFormation(leader.Value, Owner) + (leaderTree == null ? 1 : 0); } #endif } prevLeader = leader.Value; } /// /// Wait a small amount of time before the group is formed. /// private IEnumerator WaitForGroupFormation() { yield return new WaitForSeconds(waitTime.Value); StartFormation(); } /// /// An agent wants to join the formation. /// /// The agent that wants to join the group. private void StartListeningForOrders(Behavior agent) { // StartListeningForOrders is registered within OnAwake which could cause the callback to be executed when the task isn't active. if (runStatus != TaskStatus.Running) { return; } // If the leader has changed then reinitialize with the new leader. if (prevLeader != leader.Value) { EndFormation(); UpdateLeader(); } // Add the agent based on the distance to the closest position. var distance = float.MaxValue; var insertIndex = agents.Count; for (int i = 1; i < insertIndex + 1; ++i) { var targetPosition = TargetPosition(i, 0); var agentDistance = (agent.transform.position - targetPosition).sqrMagnitude; // The agent should occupy the slot if it's the closest slot and the slot is closer to the current agent compared to the existing agent occupying the slot. if (agentDistance < distance && (i == agents.Count || (agents[i] != null && (agents[i].position - targetPosition).sqrMagnitude > agentDistance))) { insertIndex = i; distance = agentDistance; } } AddAgentToGroup(agent, insertIndex); } /// /// The formation has changed. Update the formation index. /// /// The new formation index. private void FormationUpdated(int index) { formationIndex = index; } /// /// Adds the agent to the formation group. /// /// The agent to add. /// The index of the agent within the group. protected virtual void AddAgentToGroup(Behavior agent, int index) { if (leader.Value == null) { if (formationTrees == null) { formationTrees = new List(); formationAgents = new List(); pathStarted = new List(); moveStatus = new List(); } // Notify the current agent of the existing agents. for (int i = 0; i < formationTrees.Count; ++i) { agent.SendEvent("AddAgentToGroup", formationTrees[i], i); } // Insert the agent in the lists. formationTrees.Insert(index, agent); pathStarted.Insert(index, false); moveStatus.Insert(index, index == 0 ? MoveStatus.Wait : MoveStatus.Full); // Notify the agent of the target. if (targetTransform.Value != null) { prevTargetTransform = targetTransform.Value; formationTrees[index].SendEvent("UpdateTarget", targetTransform.Value); } else { prevTargetPosition = targetPosition.Value; formationTrees[index].SendEvent("UpdateTargetPosition", targetPosition.Value); } // Notify other agents that the current agent has joined the formation. for (int i = 1; i < formationTrees.Count; ++i) { formationTrees[i].SendEvent("FormationUpdated", i); formationTrees[i].SendEvent("AddAgentToGroup", formationTrees[index], index); } formationIndex = index; if (waitToMove.Value) { moveStatus[0] = MoveStatus.Wait; } } else { sendListenerEvent = false; formationAgent.Resume(); } // The agents array is maintained on both the leader and follower. if (agents == null) { agents = new List(); } agents.Insert(index, agent.transform); if (waitToMove.Value) { inFormation = false; leaderMoveStatus = MoveStatus.Wait; prevMoveStatus = MoveStatus.Last; } } /// /// Updates the target position on the following agent. /// /// The new target position. private void UpdateTargetPosition(Vector3 target) { // UpdateTargetPosition is registered within OnAwake which could cause the callback to be executed when the task isn't active. if (runStatus != TaskStatus.Running) { return; } targetPosition.Value = target; } /// /// Updates the target transform on the following agent. /// /// The new target transform. private void UpdateTarget(Transform target) { // UpdateTarget is registered within OnAwake which could cause the callback to be executed when the task isn't active. if (runStatus != TaskStatus.Running) { return; } targetTransform.Value = target; } /// /// Update the leader or follower move status. /// /// The index of the agent to update. /// The move status of the index private void UpdateMoveStatus(int index, MoveStatus status) { // UpdateMoveStatus is registered within OnAwake which could cause the callback to be executed when the task isn't active. if (runStatus != TaskStatus.Running) { return; } if (leader.Value == null) { moveStatus[index] = status; } else { leaderMoveStatus = status; } } /// /// Start forming the group. /// private void StartFormation() { formationStarted = true; } /// /// Move the agents in a formation. The TargetPosition method will retrieve the target position for the individual group member. /// public override TaskStatus OnUpdate() { // If the leader has changed then reinitialize with the new leader. if (prevLeader != leader.Value) { EndFormation(); UpdateLeader(); } if (leader.Value == null) { if (formationStarted) { // Notify following agents if the target position has updated. if (targetTransform.Value != null) { if (targetTransform.Value != prevTargetTransform) { prevTargetTransform = targetTransform.Value; for (int i = 1; i < formationTrees.Count; ++i) { formationTrees[i].SendEvent("UpdateTarget", targetTransform.Value); } } } else if (targetPosition.Value != prevTargetPosition) { prevTargetPosition = targetPosition.Value; for (int i = 1; i < formationTrees.Count; ++i) { formationTrees[i].SendEvent("UpdateTargetPosition", targetPosition.Value); } } // Wait until all of the agents are in position before moving to the target. var waitForAgent = false; for (int i = 1; i < formationAgents.Count; ++i) { if (!pathStarted[i]) { pathStarted[i] = formationAgents[i].HasPath; } if (!pathStarted[i] || (moveStatus[0] == MoveStatus.Wait && moveStatus[i] == MoveStatus.Full) || moveStatus[i] == MoveStatus.Catchup) { waitForAgent = true; } } // Send the updated move status to all of the followers. if (waitForAgent) { moveStatus[0] = inFormation ? MoveStatus.Formation : MoveStatus.Wait; } else { moveStatus[0] = MoveStatus.Full; } if (moveStatus[0] != prevMoveStatus) { for (int i = 0; i < formationAgents.Count; ++i) { formationTrees[i].SendEvent("UpdateMoveStatus", 0, moveStatus[0]); } prevMoveStatus = moveStatus[0]; } var target = (targetTransform.Value != null ? targetTransform.Value.position : targetPosition.Value); formationAgent.SetDestination(target); // Determine if all of the agents have arrived. var arrived = true; for (int i = 0; i < formationAgents.Count; ++i) { if (formationAgents[i].RemainingDistance > formationAgent.StoppingDistance || formationAgents[i].PathPending) { arrived = false; break; } else { formationAgents[i].Stop(); } } if (arrived) { runStatus = TaskStatus.Success; return runStatus; } // The leader can move if all agents are ready. if (!waitForAgent) { inFormation = true; } formationAgent.Speed = (!waitForAgent ? fullSpeed.Value : (inFormation ? formationSpeed.Value : 0)); } } else { // Send within OnUpdate to ensure the at least one leader behavior tree is active. If registered within OnStart there is a chance that the behavior tree // isn't active yet and will never receive the event. if (sendListenerEvent) { leaderTree.SendEvent("StartListeningForOrders", Owner); return runStatus; } // A following agent should never have a formation index of -1. If the index is 0 then the agent hasn't been registered with the leader yet. if (formationIndex == -1) { return runStatus; } // Move towards the starting position and look in the same direction as the leader when just getting started in the formation. var targetDistance = (transform.position - TargetPosition(formationIndex, 0)).magnitude; if (!inFormation) { if (targetDistance <= formationAgent.StoppingDistance + 0.001f) { inFormation = formationAgent.RotateTowards(leader.Value.transform.rotation) || leaderMoveStatus != MoveStatus.Wait; } } // If the destination is immediately in front of the agent then that agents stopping distance will take over and slow down the agent. This will make the agent // lag behind the leader. Prevent this from happening by adding a small look ahead distance. var leaderTarget = targetTransform.Value != null ? targetTransform.Value.position : targetPosition.Value; var leaderDistance = (leader.Value.transform.position - leaderTarget).magnitude; var zLookAhead = inFormation ? Mathf.Min(leaderDistance, lookAhead.Value) : 0; var target = TargetPosition(formationIndex, zLookAhead); // TargetPosition will be overridden to return the target position for the individual agent. formationAgent.SetDestination(target); #if UNITY_EDITOR Debug.DrawRay(TargetPosition(formationIndex, 0), Vector3.up); #endif // Determine the current move status. MoveStatus currentMoveStatus; if (inFormation) { if (formationAgent.RemainingDistance - zLookAhead < -formationAgent.Radius / 2) { currentMoveStatus = leaderAgent != null && leaderMoveStatus == MoveStatus.Wait ? MoveStatus.Wait : MoveStatus.Slowdown; } else { if (targetDistance < (formationAgent.Radius * 2 + formationAgent.StoppingDistance) && leaderAgent != null) { currentMoveStatus = leaderMoveStatus; } else { currentMoveStatus = MoveStatus.Catchup; } } } else { currentMoveStatus = (leaderMoveStatus != MoveStatus.Wait ? MoveStatus.Catchup : MoveStatus.Full); } // Set the speed according to the move status, and notify the leader. if (currentMoveStatus != prevMoveStatus) { switch (currentMoveStatus) { case MoveStatus.Wait: formationAgent.Speed = 0; break; case MoveStatus.Slowdown: formationAgent.Speed = slowdownSpeed.Value; break; case MoveStatus.Formation: formationAgent.Speed = formationSpeed.Value; break; case MoveStatus.Full: formationAgent.Speed = fullSpeed.Value; break; case MoveStatus.Catchup: formationAgent.Speed = catchupSpeed.Value; break; } // Prevent auto breaking from slowing the agent down if they are trying to catch up. formationAgent.AutoBreaking = (currentMoveStatus != MoveStatus.Catchup); if (leaderTree != null) { leaderTree.SendEvent("UpdateMoveStatus", formationIndex, currentMoveStatus); } prevMoveStatus = currentMoveStatus; } } return runStatus; } /// /// The task has completed its orders. /// /// The return status of the task. private void OrdersFinished(TaskStatus status) { runStatus = status; } /// /// An agent has dropped out of the group so it should be removed. /// /// The agent to remove. private void StopListeningToOrders(Behavior agent) { // StopListeningToOrders is registered within OnAwake which could cause the callback to be executed when the task isn't active. if (runStatus != TaskStatus.Running) { return; } RemoveAgentFromGroup(agent); } /// /// Removes the agent from the group. /// /// The agent to remove. /// The index of the agent removed from the group. protected virtual int RemoveAgentFromGroup(Behavior agent) { var agentTransform = agent.transform; for (int i = agents.Count - 1; i >= 0; --i) { if (agents[i] == agentTransform) { if (prevLeader == null) { formationTrees.RemoveAt(i); formationAgents.RemoveAt(i); pathStarted.RemoveAt(i); moveStatus.RemoveAt(i); for (int j = 1; j < formationTrees.Count; ++j) { formationTrees[j].SendEvent("StopListeningToOrders", agent); formationTrees[j].SendEvent("FormationUpdated", j); } } agents.RemoveAt(i); if (waitToMove.Value) { inFormation = false; prevMoveStatus = MoveStatus.Last; } return i; } } return -1; } /// /// Virtual method to allow the formation tasks to specify a target position. /// /// The index of the group member. /// The z distance to look ahead of the target position. /// The position to move to, in world space. protected virtual Vector3 TargetPosition(int index, float zLookAhead) { return Vector3.zero; } /// /// The task has ended. /// public override void OnEnd() { EndFormation(); } /// /// Ends the current formation. /// private void EndFormation() { if (formationTrees != null) { // If the status is running then the leader task ended early. Send a status of failure to the group. if (runStatus == TaskStatus.Running) { runStatus = TaskStatus.Failure; } for (int i = 0; i < formationTrees.Count; ++i) { formationTrees[i].SendEvent("OrdersFinished", runStatus); } formationAgents.Clear(); formationTrees.Clear(); pathStarted.Clear(); moveStatus.Clear(); prevMoveStatus = MoveStatus.Full; #if DEATHMATCH_AI_KIT_PRESENT if (TeamManager.IsInstantiated) { TeamManager.SetLeader(gameObject, false); } #endif } else { if (leaderTree != null) { leaderTree.SendEvent("StopListeningToOrders", Owner); } #if DEATHMATCH_AI_KIT_PRESENT if (TeamManager.IsInstantiated) { TeamManager.RemoveFromFormation(prevLeader, Owner); } #endif } formationIndex = -1; formationAgent.Stop(); inFormation = false; if (agents != null) { agents.Clear(); } } /// /// The behavior tree is complete so the task should stop listening for the events. /// public override void OnBehaviorComplete() { Owner.UnregisterEvent("StartListeningForOrders", StartListeningForOrders); Owner.UnregisterEvent("StopListeningToOrders", StopListeningToOrders); Owner.UnregisterEvent("FormationUpdated", FormationUpdated); Owner.UnregisterEvent("AddAgentToGroup", AddAgentToGroup); Owner.UnregisterEvent("UpdateTargetPosition", UpdateTargetPosition); Owner.UnregisterEvent("UpdateTarget", UpdateTarget); Owner.UnregisterEvent("UpdateMoveStatus", UpdateMoveStatus); Owner.UnregisterEvent("OrdersFinished", OrdersFinished); } /// /// Reset the public variables back to their defaults. /// public override void OnReset() { targetTransform = null; targetPosition = Vector3.zero; lookAhead = 1; fullSpeed = 3.5f; formationSpeed = 2f; slowdownSpeed = 1f; catchupSpeed = 3f; waitTime = 0; waitToMove = true; } } }