diff --git a/Assets/Scripts/Teleportation.meta b/Assets/Scripts/Teleportation.meta new file mode 100644 index 00000000..589e4c78 --- /dev/null +++ b/Assets/Scripts/Teleportation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e1e704e47ac357c43a1c55af30b5e432 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Teleportation/TeleportManager.cs b/Assets/Scripts/Teleportation/TeleportManager.cs new file mode 100644 index 00000000..17a28475 --- /dev/null +++ b/Assets/Scripts/Teleportation/TeleportManager.cs @@ -0,0 +1,104 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AI; + +public class TeleportManager : MonoBehaviour +{ + public static TeleportManager Instance { get; private set; } + + [Header("Optional fade (CanvasGroup with RaycastBlock)")] + public TeleportScreenFader screenFader; // drag a CanvasGroup-based fader (below) + [Range(0f, 2f)] public float fadeDuration = 0.25f; + + // Prevent immediate re-triggering after arrival + private readonly Dictionary _entityCooldownUntil = new(); + private readonly Dictionary _entityLastPortalId = new(); + + private void Awake() + { + if (Instance != null && Instance != this) { Destroy(gameObject); return; } + Instance = this; + DontDestroyOnLoad(gameObject); + } + + public bool CanUsePortal(GameObject entity, string portalId, float cooldownSeconds) + { + if (cooldownSeconds <= 0f) return true; + + var now = Time.time; + if (_entityCooldownUntil.TryGetValue(entity, out float until) && now < until) + return false; + + // If just came out of this portal pair, block one frame of re-trigger + if (_entityLastPortalId.TryGetValue(entity, out string lastId) && lastId == portalId) + return false; + + return true; + } + + public void MarkUsed(GameObject entity, string portalId, float cooldownSeconds) + { + if (cooldownSeconds > 0f) + _entityCooldownUntil[entity] = Time.time + cooldownSeconds; + + _entityLastPortalId[entity] = portalId; + } + + /// + /// Teleport entity to destination. Handles CharacterController, Rigidbody, and NavMeshAgent safely. + /// + public void Teleport(GameObject entity, Transform destination, bool matchRotation = true) + { + if (entity == null || destination == null) return; + StartCoroutine(TeleportRoutine(entity, destination, matchRotation)); + } + + private IEnumerator TeleportRoutine(GameObject entity, Transform destination, bool matchRotation) + { + // Fade out (optional) + if (screenFader != null && fadeDuration > 0f) + yield return screenFader.FadeTo(1f, fadeDuration); + + // Disable common movers safely + var cc = entity.GetComponent(); + var rb = entity.GetComponent(); + var agent = entity.GetComponent(); + + bool ccWasEnabled = false; + bool agentWasEnabled = false; + + if (cc != null) { ccWasEnabled = cc.enabled; cc.enabled = false; } + if (agent != null) { agentWasEnabled = agent.enabled; agent.enabled = false; } + if (rb != null) + { + rb.isKinematic = true; // pause physics for an instant + rb.velocity = Vector3.zero; + rb.angularVelocity = Vector3.zero; + } + + // Move + rotate + if (agent != null) + { + agent.Warp(destination.position); + if (matchRotation) entity.transform.rotation = destination.rotation; + } + else + { + entity.transform.position = destination.position; + if (matchRotation) entity.transform.rotation = destination.rotation; + } + + // Small frame delay to settle + yield return null; + + // Re-enable components + if (rb != null) rb.isKinematic = false; + if (cc != null) cc.enabled = ccWasEnabled; + if (agent != null) agent.enabled = agentWasEnabled; + + // Fade in (optional) + if (screenFader != null && fadeDuration > 0f) + yield return screenFader.FadeTo(0f, fadeDuration); + } +} diff --git a/Assets/Scripts/Teleportation/TeleportManager.cs.meta b/Assets/Scripts/Teleportation/TeleportManager.cs.meta new file mode 100644 index 00000000..1d2edf7e --- /dev/null +++ b/Assets/Scripts/Teleportation/TeleportManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5dce2a4efa47e2f4aaa40c0820a478ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Teleportation/TeleportPoint.cs b/Assets/Scripts/Teleportation/TeleportPoint.cs new file mode 100644 index 00000000..98fa3a04 --- /dev/null +++ b/Assets/Scripts/Teleportation/TeleportPoint.cs @@ -0,0 +1,62 @@ +using UnityEngine; + +[RequireComponent(typeof(Collider))] +public class TeleportPoint : MonoBehaviour +{ + [Header("Destination")] + public Transform destination; // set this to SkyCity_Spawn or any target + + [Header("Filtering")] + public string requiredTag = "Player"; // only teleport objects with this tag + + [Header("Behavior")] + public bool matchRotation = true; + [Tooltip("Shared ID for A<->B portals to avoid instant ping-pong.")] + public string portalId = "SkyCityGate"; + [Tooltip("Seconds the entity cannot re-trigger a portal after using one.")] + public float reentryCooldown = 0.5f; + + private void Reset() + { + var c = GetComponent(); + c.isTrigger = true; + } + + private void OnTriggerEnter(Collider other) + { + if (!IsAllowed(other.gameObject)) return; + + var tm = TeleportManager.Instance; + if (tm == null) { Debug.LogWarning("[TeleportPoint] No TeleportManager in scene."); return; } + + if (!tm.CanUsePortal(other.gameObject, portalId, reentryCooldown)) return; + + tm.MarkUsed(other.gameObject, portalId, reentryCooldown); + tm.Teleport(other.gameObject, destination, matchRotation); + } + + private bool IsAllowed(GameObject go) + { + if (!string.IsNullOrEmpty(requiredTag) && !go.CompareTag(requiredTag)) + return false; + + if (destination == null) + { + Debug.LogWarning($"[TeleportPoint] '{name}' has no destination assigned."); + return false; + } + + return true; + } + +#if UNITY_EDITOR + private void OnDrawGizmos() + { + if (destination == null) return; + Gizmos.color = Color.cyan; + Gizmos.DrawWireSphere(transform.position, 0.3f); + Gizmos.DrawLine(transform.position, destination.position); + Gizmos.DrawWireSphere(destination.position, 0.3f); + } +#endif +} \ No newline at end of file diff --git a/Assets/Scripts/Teleportation/TeleportPoint.cs.meta b/Assets/Scripts/Teleportation/TeleportPoint.cs.meta new file mode 100644 index 00000000..d97c321d --- /dev/null +++ b/Assets/Scripts/Teleportation/TeleportPoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6cd2ca85a22ffb642bb124e4b6d6b716 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Teleportation/TeleportScreenFader.cs b/Assets/Scripts/Teleportation/TeleportScreenFader.cs new file mode 100644 index 00000000..963c9a8e --- /dev/null +++ b/Assets/Scripts/Teleportation/TeleportScreenFader.cs @@ -0,0 +1,29 @@ +using System.Collections; +using UnityEngine; + +[RequireComponent(typeof(CanvasGroup))] +public class TeleportScreenFader : MonoBehaviour +{ + private CanvasGroup _cg; + + private void Awake() + { + _cg = GetComponent(); + } + + public IEnumerator FadeTo(float targetAlpha, float duration) + { + float start = _cg.alpha; + float t = 0f; + _cg.blocksRaycasts = targetAlpha > 0.01f; + + while (t < duration) + { + t += Time.unscaledDeltaTime; + _cg.alpha = Mathf.Lerp(start, targetAlpha, t / duration); + yield return null; + } + _cg.alpha = targetAlpha; + _cg.blocksRaycasts = targetAlpha > 0.01f; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Teleportation/TeleportScreenFader.cs.meta b/Assets/Scripts/Teleportation/TeleportScreenFader.cs.meta new file mode 100644 index 00000000..18c9985b --- /dev/null +++ b/Assets/Scripts/Teleportation/TeleportScreenFader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf0f714f4603da745855c8b18be4de59 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: