250 lines
11 KiB
C#
250 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
/// <summary>Fixed concentric server rings, facing the initial meadow centre. Only the shader bends them.</summary>
|
|
[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(200)]
|
|
public sealed class PortalServerGrid : MonoBehaviour
|
|
{
|
|
[SerializeField] private GameObject serverPrefab;
|
|
[SerializeField] private Shader curvedServerShader;
|
|
[Tooltip("X: minimum chord distance between neighbours. Y: distance between rings.")]
|
|
[SerializeField] private Vector2 spacing = new Vector2(5, 6);
|
|
[Tooltip("Empty border inside the terrain on X and Z.")]
|
|
[SerializeField] private Vector2 edgeMargin = new Vector2(1, 1);
|
|
[SerializeField, Min(1)] private float firstRingRadius = 8;
|
|
[SerializeField, Range(0, 360)] private float angleOffset;
|
|
[Tooltip("Pivot adjustment along each server's facing direction (positive = towards the centre).")]
|
|
[SerializeField] private float offsetZ;
|
|
[Tooltip("Additional height above the ground, after automatically aligning the model base.")]
|
|
[SerializeField] private float heightOffset;
|
|
[SerializeField] private Vector3 rotation = new Vector3(0, 180, 0);
|
|
[Tooltip("Multiplier applied to the prefab's original scale.")]
|
|
[SerializeField, Min(0.001f)] private float scaleMultiplier = 1;
|
|
[Tooltip("Optional entrance (e.g. the portal): no server is placed in a circle just past it, along its forward axis.")]
|
|
[SerializeField] private Transform keepClear;
|
|
[SerializeField, Min(0)] private float keepClearRadius = 5;
|
|
|
|
private PortalMeadow meadow;
|
|
private GameObject generated;
|
|
private Vector2 lastSize;
|
|
private Vector3 lastCenter;
|
|
private bool rebuild;
|
|
private readonly Dictionary<Material, Material> curvedMaterials = new Dictionary<Material, Material>();
|
|
private readonly List<Transform> servers = new List<Transform>();
|
|
private float wrapExtent;
|
|
// Distance between the centres of two repeated server clusters.
|
|
public float RepetitionPeriod => wrapExtent * 2;
|
|
private bool lastEndless;
|
|
|
|
private void OnEnable()
|
|
{
|
|
meadow = GetComponent<PortalMeadow>();
|
|
rebuild = true;
|
|
RenderPipelineManager.beginCameraRendering += BeforeCameraRendering;
|
|
}
|
|
private void BeforeCameraRendering(ScriptableRenderContext context, Camera camera)
|
|
{
|
|
// Match the terrain's current material, including changes made after Update.
|
|
if (meadow) SyncBend();
|
|
}
|
|
private void OnValidate()
|
|
{
|
|
spacing.x = Mathf.Max(0.1f, spacing.x);
|
|
spacing.y = Mathf.Max(0.1f, spacing.y);
|
|
edgeMargin.x = Mathf.Max(0, edgeMargin.x);
|
|
edgeMargin.y = Mathf.Max(0, edgeMargin.y);
|
|
scaleMultiplier = Mathf.Max(0.001f, scaleMultiplier);
|
|
firstRingRadius = Mathf.Max(1, firstRingRadius);
|
|
rebuild = true;
|
|
}
|
|
private void Update()
|
|
{
|
|
if (!meadow) meadow = GetComponent<PortalMeadow>();
|
|
if (!meadow) return;
|
|
SyncBend();
|
|
if (!rebuild && lastSize == meadow.Size && lastCenter == meadow.Center && lastEndless == meadow.Endless) return;
|
|
rebuild = false;
|
|
lastSize = meadow.Size;
|
|
lastCenter = meadow.Center;
|
|
lastEndless = meadow.Endless;
|
|
Build();
|
|
}
|
|
private void OnDisable()
|
|
{
|
|
RenderPipelineManager.beginCameraRendering -= BeforeCameraRendering;
|
|
Clear();
|
|
}
|
|
private void LateUpdate()
|
|
{
|
|
if (!meadow || !meadow.Following || wrapExtent <= 0) return;
|
|
Vector3 centre = meadow.SurfaceCenter;
|
|
float period = wrapExtent * 2;
|
|
foreach (Transform server in servers)
|
|
{
|
|
Vector3 p = server.localPosition;
|
|
// Recycle only outside the visible disk. Nearby transforms/rotations stay fixed.
|
|
p.x += Mathf.Floor((centre.x - p.x + wrapExtent) / period) * period;
|
|
p.z += Mathf.Floor((centre.z - p.z + wrapExtent) / period) * period;
|
|
server.localPosition = p;
|
|
}
|
|
}
|
|
[ContextMenu("Regenerate server rings")]
|
|
private void Regenerate() { rebuild = true; }
|
|
|
|
private static void Release(Object value)
|
|
{
|
|
if (Application.isPlaying) Destroy(value);
|
|
else DestroyImmediate(value);
|
|
}
|
|
private void Clear()
|
|
{
|
|
if (generated) { generated.SetActive(false); Release(generated); }
|
|
generated = null;
|
|
foreach (Material material in curvedMaterials.Values) if (material) Release(material);
|
|
curvedMaterials.Clear();
|
|
servers.Clear();
|
|
}
|
|
private GameObject CreateServer()
|
|
{
|
|
var instance = Instantiate(serverPrefab, generated.transform, false);
|
|
instance.transform.localPosition = Vector3.zero;
|
|
instance.transform.localRotation = Quaternion.Euler(rotation) * serverPrefab.transform.localRotation;
|
|
instance.transform.localScale = serverPrefab.transform.localScale * scaleMultiplier;
|
|
foreach (Transform child in instance.GetComponentsInChildren<Transform>(true))
|
|
child.gameObject.hideFlags = HideFlags.DontSave;
|
|
return instance;
|
|
}
|
|
|
|
private void Build()
|
|
{
|
|
Clear();
|
|
if (!serverPrefab) return;
|
|
// Inactive during setup: avoid briefly enabling all instances at the origin.
|
|
generated = new GameObject("Server Rings (generated)") { hideFlags = HideFlags.DontSave };
|
|
generated.SetActive(false);
|
|
generated.transform.SetParent(transform, false);
|
|
var prototype = CreateServer();
|
|
if (!TryGetLocalBounds(prototype, out Bounds bounds))
|
|
{
|
|
Debug.LogWarning("Server grid: the prefab needs a Renderer to calculate its footprint.", this);
|
|
Clear();
|
|
return;
|
|
}
|
|
|
|
float footprint = new Vector2(bounds.size.x, bounds.size.z).magnitude;
|
|
wrapExtent = meadow.Radius + footprint + 1;
|
|
float pitch = Mathf.Max(spacing.x, footprint, 0.1f);
|
|
float ringStep = Mathf.Max(spacing.y, footprint, 0.1f);
|
|
float maxRadius = Mathf.Min(lastSize.x * 0.5f - edgeMargin.x,
|
|
lastSize.y * 0.5f - edgeMargin.y) - footprint * 0.5f - Mathf.Abs(offsetZ);
|
|
float startRadius = Mathf.Max(firstRingRadius, footprint * 0.5f + Mathf.Abs(offsetZ));
|
|
var poses = new List<Pose>();
|
|
// The circle touches the entrance and extends in front of it, so the player never lands in a server.
|
|
Vector3 clearCentre = keepClear
|
|
? transform.InverseTransformPoint(keepClear.position + keepClear.forward * keepClearRadius * 0.5f) : Vector3.zero;
|
|
int ring = 0;
|
|
for (float radius = startRadius; radius <= maxRadius; radius += ringStep, ring++)
|
|
{
|
|
// Use chord length, not arc length, so neighbours cannot overlap on small rings.
|
|
float placementRadius = radius - offsetZ;
|
|
int around = pitch <= 2 * placementRadius
|
|
? Mathf.Max(1, Mathf.FloorToInt(Mathf.PI / Mathf.Asin(pitch / (2 * placementRadius)))) : 1;
|
|
if (poses.Count + around > 4096)
|
|
{
|
|
Debug.LogWarning("Server rings: more than 4096 instances requested. Increase Spacing.", this);
|
|
Clear();
|
|
return;
|
|
}
|
|
for (int i = 0; i < around; i++)
|
|
{
|
|
float angle = angleOffset * Mathf.Deg2Rad + (i + (ring % 2) * 0.5f) * Mathf.PI * 2 / around;
|
|
Vector3 radial = new Vector3(Mathf.Sin(angle), 0, Mathf.Cos(angle));
|
|
Vector3 ringPoint = lastCenter + radial * radius;
|
|
if (keepClear && new Vector2(ringPoint.x - clearCentre.x, ringPoint.z - clearCentre.z).magnitude
|
|
< keepClearRadius + footprint * 0.5f) continue;
|
|
Quaternion facing = Quaternion.LookRotation(-radial, Vector3.up);
|
|
Vector3 position = lastCenter + radial * radius + facing * Vector3.forward * offsetZ;
|
|
// Bounds already include the prefab rotation, scale and user rotation correction.
|
|
position -= facing * new Vector3(bounds.center.x, bounds.min.y, bounds.center.z);
|
|
position.y += heightOffset;
|
|
poses.Add(new Pose(position, facing));
|
|
}
|
|
}
|
|
int count = 0;
|
|
foreach (Pose pose in poses)
|
|
{
|
|
GameObject server = count == 0 ? prototype : CreateServer();
|
|
server.transform.localPosition = pose.position;
|
|
server.transform.localRotation = pose.rotation * Quaternion.Euler(rotation) * serverPrefab.transform.localRotation;
|
|
server.name = $"Server {++count:000}";
|
|
ApplyCurvedMaterials(server);
|
|
servers.Add(server.transform);
|
|
}
|
|
if (count == 0) { Clear(); return; }
|
|
SyncBend();
|
|
generated.SetActive(true);
|
|
}
|
|
|
|
private void ApplyCurvedMaterials(GameObject server)
|
|
{
|
|
if (!curvedServerShader) return;
|
|
foreach (Renderer renderer in server.GetComponentsInChildren<Renderer>(true))
|
|
{
|
|
Material[] materials = renderer.sharedMaterials;
|
|
for (int i = 0; i < materials.Length; i++)
|
|
{
|
|
Material source = materials[i];
|
|
if (!source) continue;
|
|
if (!curvedMaterials.TryGetValue(source, out Material curved))
|
|
{
|
|
curved = new Material(source) { shader = curvedServerShader,
|
|
name = source.name + " (Curved)", hideFlags = HideFlags.DontSave };
|
|
curved.shaderKeywords = source.shaderKeywords;
|
|
curvedMaterials.Add(source, curved);
|
|
}
|
|
materials[i] = curved;
|
|
}
|
|
renderer.sharedMaterials = materials;
|
|
renderer.renderingLayerMask = meadow.RenderingLayers;
|
|
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
|
renderer.allowOcclusionWhenDynamic = false;
|
|
Bounds expanded = renderer.localBounds;
|
|
Vector3 scale = renderer.transform.lossyScale;
|
|
float minScale = Mathf.Max(0.001f, Mathf.Min(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)));
|
|
expanded.Expand(meadow.BoundsPadding * 2 / minScale);
|
|
renderer.localBounds = expanded;
|
|
}
|
|
}
|
|
|
|
private void SyncBend()
|
|
{
|
|
if (!meadow.MeadowMaterial) return;
|
|
foreach (Material material in curvedMaterials.Values)
|
|
{
|
|
meadow.ApplyBend(material);
|
|
meadow.ConfigureRegion(material);
|
|
}
|
|
}
|
|
|
|
private bool TryGetLocalBounds(GameObject instance, out Bounds bounds)
|
|
{
|
|
bounds = default;
|
|
bool found = false;
|
|
foreach (Renderer renderer in instance.GetComponentsInChildren<Renderer>(true))
|
|
{
|
|
Bounds local = renderer.localBounds;
|
|
Matrix4x4 matrix = generated.transform.worldToLocalMatrix * renderer.transform.localToWorldMatrix;
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
Vector3 corner = local.center + Vector3.Scale(local.extents,
|
|
new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1));
|
|
Vector3 point = matrix.MultiplyPoint3x4(corner);
|
|
if (!found) { bounds = new Bounds(point, Vector3.zero); found = true; }
|
|
else bounds.Encapsulate(point);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
}
|