using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.Rendering; /// Reachable landmarks at the gaps between four repeated server clusters. [ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(205)] public sealed class PortalExitMarker : MonoBehaviour { [SerializeField] private Material exitMaterial; [SerializeField] private float rotationY = 45; [Tooltip("If set, every exit turns its face toward this point (e.g. where the player arrives) instead of using Rotation Y.")] [SerializeField] private Transform faceTowards; [SerializeField] private Vector3 dimensions = new Vector3(1.5f, 3.2f, 0.2f); [Tooltip("Invoked once, in play mode, when the camera walks through one of the exits.")] [SerializeField] private UnityEvent onPlayerEnter; [Tooltip("Extra thickness (meters) on each side of an exit counted as walking through it.")] [SerializeField, Min(0)] private float enterMargin = 0.35f; private PortalMeadow meadow; private readonly List markers = new List(); private readonly List lastLocalEye = new List(); private bool entered; private PortalServerGrid grid; private float period; private Material runtimeMaterial; private bool rebuild; private Vector3 initialCentre; private Material lastMaterial; private void OnEnable() { meadow = GetComponent(); grid = GetComponent(); rebuild = true; RenderPipelineManager.beginCameraRendering += Sync; } private void OnValidate() { rebuild = true; } private void Update() { if (!meadow || !exitMaterial || !grid || !grid.enabled || grid.RepetitionPeriod <= 0) { Clear(); return; } DetectPlayer(); if (!rebuild && markers.Count > 0 && initialCentre == meadow.Center && lastMaterial == exitMaterial && period == grid.RepetitionPeriod) return; Clear(); rebuild = false; initialCentre = meadow.Center; lastMaterial = exitMaterial; period = grid.RepetitionPeriod; runtimeMaterial = new Material(exitMaterial) { hideFlags = HideFlags.DontSave, name = "Distant Exit (runtime)" }; for (int z = -1; z <= 1; z++) for (int x = -1; x <= 1; x++) { var marker = GameObject.CreatePrimitive(PrimitiveType.Cube); marker.name = "Luminous Gap Exit (pooled)"; marker.hideFlags = HideFlags.DontSave; marker.transform.SetParent(transform, false); marker.transform.localPosition = initialCentre + new Vector3((x + 0.5f) * period, 0, (z + 0.5f) * period) + Vector3.up * Mathf.Max(0.1f, dimensions.y) * 0.5f; marker.transform.localRotation = Quaternion.Euler(0, rotationY, 0); marker.transform.localScale = new Vector3(Mathf.Max(0.1f, dimensions.x), Mathf.Max(0.1f, dimensions.y), Mathf.Max(0.01f, dimensions.z)); marker.GetComponent().isTrigger = true; var renderer = marker.GetComponent(); renderer.sharedMaterial = runtimeMaterial; renderer.renderingLayerMask = meadow.RenderingLayers; renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.allowOcclusionWhenDynamic = false; Bounds bounds = renderer.localBounds; float smallest = Mathf.Min(marker.transform.localScale.x, marker.transform.localScale.y, marker.transform.localScale.z); bounds.Expand(meadow.BoundsPadding * 2 / smallest); renderer.localBounds = bounds; markers.Add(marker.transform); } } private void LateUpdate() { if (!meadow || period <= 0) return; if (meadow.Endless) { float poolPeriod = period * 3; foreach (Transform marker in markers) { Vector3 p = marker.localPosition; p.x += Mathf.Floor((meadow.SurfaceCenter.x - p.x + poolPeriod * 0.5f) / poolPeriod) * poolPeriod; p.z += Mathf.Floor((meadow.SurfaceCenter.z - p.z + poolPeriod * 0.5f) / poolPeriod) * poolPeriod; marker.localPosition = p; } } if (faceTowards) foreach (Transform marker in markers) { Vector3 toTarget = faceTowards.position - marker.position; toTarget.y = 0; if (toTarget.sqrMagnitude > 1e-4f) marker.rotation = Quaternion.LookRotation(toTarget); } } // The exits are thin pooled cubes: the camera counts as entering when it is within an exit's // width/height and either inside its (margin-padded) thickness or crossed its plane since last frame. private void DetectPlayer() { if (!Application.isPlaying || entered || markers.Count == 0) return; Camera cam = Camera.main; if (!cam) return; Vector3 eye = cam.transform.position; while (lastLocalEye.Count < markers.Count) lastLocalEye.Add(new Vector3(float.PositiveInfinity, 0, 0)); for (int i = 0; i < markers.Count; i++) { Transform marker = markers[i]; if (!marker) continue; Vector3 local = marker.InverseTransformPoint(eye); Vector3 previous = lastLocalEye[i]; lastLocalEye[i] = local; if (Mathf.Abs(local.x) > 0.5f || Mathf.Abs(local.y) > 0.5f) continue; float halfThickness = 0.5f + enterMargin / Mathf.Max(0.01f, marker.localScale.z); bool crossed = Mathf.Abs(previous.x) <= 0.5f && Mathf.Sign(previous.z) != Mathf.Sign(local.z); if (Mathf.Abs(local.z) > halfThickness && !crossed) continue; entered = true; onPlayerEnter?.Invoke(); return; } } private void Sync(ScriptableRenderContext context, Camera camera) { if (!runtimeMaterial || !meadow || !meadow.MeadowMaterial) return; runtimeMaterial.CopyPropertiesFromMaterial(exitMaterial); meadow.ApplyBend(runtimeMaterial); meadow.ConfigureRegion(runtimeMaterial); } private void OnDisable() { RenderPipelineManager.beginCameraRendering -= Sync; Clear(); } private void Clear() { foreach (Transform marker in markers) if (marker) { marker.gameObject.SetActive(false); Release(marker.gameObject); } markers.Clear(); lastLocalEye.Clear(); if (runtimeMaterial) Release(runtimeMaterial); runtimeMaterial = null; } private static void Release(Object value) { if (Application.isPlaying) Destroy(value); else DestroyImmediate(value); } }