using System.Collections; using System.Collections.Generic; using Pathfinding.ECS; using UnityEngine; namespace Pathfinding.Examples { /// /// Example script for handling interactable objects in the example scenes. /// /// It implements a very simple and lightweight state machine. /// /// Note: This is an example script intended for the A* Pathfinding Project's example scenes. /// If you need a proper state machine for your game, you may be better served by other state machine solutions on the Unity Asset Store. /// /// It works by keeping a linear list of states, each with an associated action. /// When an agent iteracts with this object, it immediately does the first action in the list. /// Once that action is done, it will do the next action and so on. /// /// Some actions may cancel the whole interaction. For example the MoveTo action will cancel the interaction if the agent /// suddenly had its destination to something else. Presumably because the agent was interrupted by something. /// /// If this component is added to the same GameObject as a component, the interactable will automatically trigger when the agent traverses the link. /// Some components behave differently when used during an off-mesh link component. /// For example the will move the agent without taking the navmesh into account (becoming a thin wrapper for ). /// [HelpURL("https://arongranberg.com/astar/documentation/stable/interactable.html")] public class Interactable : VersionedMonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine { public enum CoroutineAction { Tick, Cancel, } [System.Serializable] public abstract class InteractableAction { public virtual IEnumerator Execute (IAstarAI ai) { return Execute(); } #if MODULE_ENTITIES public virtual IEnumerator Execute (Pathfinding.ECS.AgentOffMeshLinkTraversalContext context) { return Execute(); } #endif public virtual IEnumerator Execute () { throw new System.NotImplementedException("This action has no implementation"); } } [System.Serializable] public class AnimatorPlay : InteractableAction { public string stateName; public float normalizedTime = 0; public Animator animator; public override IEnumerator Execute () { animator.Play(stateName, -1, normalizedTime); yield break; } } [System.Serializable] public class AnimatorSetBoolAction : InteractableAction { public string propertyName; public bool value; public Animator animator; public override IEnumerator Execute () { animator.SetBool(propertyName, value); yield break; } } [System.Serializable] public class ActivateParticleSystem : InteractableAction { public ParticleSystem particleSystem; public override IEnumerator Execute () { particleSystem.Play(); yield break; } } [System.Serializable] public class DelayAction : InteractableAction { public float delay; public override IEnumerator Execute () { float time = Time.time + delay; while (Time.time < time) yield return CoroutineAction.Tick; yield break; } } [System.Serializable] public class SetObjectActiveAction : InteractableAction { public GameObject target; public bool active; public override IEnumerator Execute () { target.SetActive(active); yield break; } } [System.Serializable] public class InstantiatePrefab : InteractableAction { public GameObject prefab; public Transform position; public override IEnumerator Execute () { if (prefab != null && position != null) { GameObject.Instantiate(prefab, position.position, position.rotation); } yield break; } } [System.Serializable] public class CallFunction : InteractableAction { public UnityEngine.Events.UnityEvent function; public override IEnumerator Execute () { function.Invoke(); yield break; } } [System.Serializable] public class TeleportAgentAction : InteractableAction { public Transform destination; public override IEnumerator Execute (IAstarAI ai) { ai.Teleport(destination.position); yield break; } #if MODULE_ENTITIES public override IEnumerator Execute (AgentOffMeshLinkTraversalContext context) { context.Teleport(destination.position); yield break; } #endif } [System.Serializable] public class TeleportAgentOnLinkAction : InteractableAction { public enum Destination { /// The side of the link that the agent starts traversing it from RelativeStartOfLink, /// The side of the link that is opposite the one the agent starts traversing it from RelativeEndOfLink, } public Destination destination = Destination.RelativeEndOfLink; public override IEnumerator Execute() => throw new System.NotImplementedException("This action only works for agents traversing off-mesh links."); #if MODULE_ENTITIES public override IEnumerator Execute (AgentOffMeshLinkTraversalContext context) { context.Teleport(destination == Destination.RelativeStartOfLink ? context.link.relativeStart : context.link.relativeEnd); yield break; } #endif } [System.Serializable] public class SetTransformAction : InteractableAction { public Transform transform; public Transform source; public bool setPosition = true; public bool setRotation; public bool setScale; public override IEnumerator Execute () { if (setPosition) transform.position = source.position; if (setRotation) transform.rotation = source.rotation; if (setScale) transform.localScale = source.localScale; yield break; } } [System.Serializable] public class MoveToAction : InteractableAction { public Transform destination; public bool useRotation; public bool waitUntilReached; public override IEnumerator Execute (IAstarAI ai) { var dest = destination.position; #if MODULE_ENTITIES if (useRotation && ai is FollowerEntity follower) { follower.SetDestination(dest, destination.rotation * Vector3.forward); } else #endif { if (useRotation) Debug.LogError("useRotation is only supported for FollowerEntity agents", ai as MonoBehaviour); ai.destination = dest; } if (waitUntilReached) { if (ai is AIBase || ai is AILerp) { // Only the FollowerEntity component is good enough to set the reachedDestination property to false immediately. // The other movement scripts need to wait for the new path to be available, which is somewhat annoying. ai.SearchPath(); while (ai.pathPending) yield return CoroutineAction.Tick; } while (!ai.reachedDestination) { if (ai.destination != dest) { // Something else must have changed the destination yield return CoroutineAction.Cancel; } if (ai.reachedEndOfPath) { // We have reached the end of the path, but not the destination // This must mean that we cannot get any closer // TODO: More accurate 'cannot move forwards' check yield return CoroutineAction.Cancel; } yield return CoroutineAction.Tick; } } } #if MODULE_ENTITIES public override IEnumerator Execute (AgentOffMeshLinkTraversalContext context) { while (!context.MoveTowards(destination.position, destination.rotation, true, true).reached) { yield return CoroutineAction.Tick; } yield break; } #endif } [System.Serializable] public class InteractAction : InteractableAction { public Interactable interactable; public override IEnumerator Execute (IAstarAI ai) { var it = interactable.InteractCoroutine(ai); while (it.MoveNext()) { yield return it.Current; } } } [SerializeReference] public List actions; public void Interact (IAstarAI ai) { StartCoroutine(InteractCoroutine(ai)); } #if MODULE_ENTITIES IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this; IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext context) { var it = InteractCoroutine(context); while (it.MoveNext()) { yield return null; } } public IEnumerator InteractCoroutine (Pathfinding.ECS.AgentOffMeshLinkTraversalContext context) { if (actions.Count == 0) { Debug.LogWarning("No actions have been set up for this interactable", this); yield break; } var actionIndex = 0; while (actionIndex < actions.Count) { var action = actions[actionIndex]; if (action == null) { actionIndex++; continue; } var enumerator = action.Execute(context); while (enumerator.MoveNext()) { yield return enumerator.Current; if (enumerator.Current == CoroutineAction.Cancel) yield break; } actionIndex++; } } #endif public IEnumerator InteractCoroutine (IAstarAI ai) { if (actions.Count == 0) { Debug.LogWarning("No actions have been set up for this interactable", this); yield break; } var actionIndex = 0; while (actionIndex < actions.Count) { var action = actions[actionIndex]; if (action == null) { actionIndex++; continue; } var enumerator = action.Execute(ai); while (enumerator.MoveNext()) { yield return enumerator.Current; if (enumerator.Current == CoroutineAction.Cancel) yield break; } actionIndex++; } } void OnEnable () { // Allow the interactable to be triggered by an agent traversing an off-mesh link if (TryGetComponent(out var link)) link.onTraverseOffMeshLink = this; } void OnDisable () { if (TryGetComponent(out var link) && link.onTraverseOffMeshLink == (IOffMeshLinkHandler)this) link.onTraverseOffMeshLink = null; } } }