Add main menu scene with Rive UI, procedural background, and scene transitions
- Add Menu scene with Rive-driven login and navigation UI (RiveLoginController, RiveNavigationController) for entering a username and starting/navigating the client via Rive triggers - Add SceneTransitionController to fade out and load the Landscape gameplay scene when Play is selected - Add procedural low-poly triangle field background and animated wave panel (LowPolyTriangleField, TriangleWavePanel) with matching editor menus and materials - Add MenuLightAnimator for animated menu lighting and UI fade helpers (MenuFadeIn, DebugFadeToggle/Bridge) - Rename SampleScene to Landscape and wire ChatNetworkManager to hide the new Rive login canvas once gameplay starts - Bump Input System action codegen version and add Rive package references/project settings updates to support the new UI
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Procedural
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a panel out of individual equilateral-triangle GameObjects tiled edge-to-edge.
|
||||
/// Each tile is its own mesh/transform; a shared driver moves every tile's transform
|
||||
/// independently each frame so the panel reads as a wave, rather than deforming one mesh.
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
public class TriangleWavePanel : MonoBehaviour
|
||||
{
|
||||
[Header("Grid")]
|
||||
[Min(1)] public int columns = 14;
|
||||
[Min(1)] public int rows = 10;
|
||||
public float triangleSize = 1.2f;
|
||||
[Range(0.5f, 1f)] public float gapScale = 1f;
|
||||
|
||||
[Header("Volume & Corner Chamfer")]
|
||||
public float thickness = 0.18f;
|
||||
[Range(0f, 0.4f)] public float cornerRadius = 0f;
|
||||
[Range(1, 8)] public int cornerSegments = 1;
|
||||
|
||||
[Header("Wave")]
|
||||
public bool animate = true;
|
||||
public float waveAmplitude = 0.22f;
|
||||
public float waveFrequency = 0.35f;
|
||||
public float waveSpeed = 1.1f;
|
||||
[Range(0f, 20f)] public float tiltAmount = 5f;
|
||||
|
||||
[Header("Panel Sway (whole-panel rotation)")]
|
||||
[Range(0f, 45f)] public float swayAmplitude = 0f;
|
||||
public float swaySpeed = 0.2f;
|
||||
|
||||
[Header("Backer (fills the gaps)")]
|
||||
public bool showBacker = true;
|
||||
public float backerOffset = -0.4f;
|
||||
public float backerPadding = 1.5f;
|
||||
|
||||
[Header("Materials")]
|
||||
public Material faceMaterial;
|
||||
public Material backerMaterial;
|
||||
|
||||
Transform[] _tileTransforms;
|
||||
Vector3[] _tileBasePos;
|
||||
Quaternion _baseRotation;
|
||||
bool _dirty = true;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
_baseRotation = transform.localRotation;
|
||||
Rebuild();
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.update -= EditorTick;
|
||||
if (!Application.isPlaying)
|
||||
EditorApplication.update += EditorTick;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.update -= EditorTick;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces an immediate rebuild. Needed after configuring fields via code (e.g. from an
|
||||
/// editor script right after AddComponent), since plain field writes don't route through
|
||||
/// OnValidate the way Inspector edits do, so _dirty never flips back on by itself.
|
||||
/// </summary>
|
||||
public void RebuildNow()
|
||||
{
|
||||
_baseRotation = transform.localRotation;
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void EditorTick()
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
if (_dirty) Rebuild();
|
||||
if (animate) AnimateTiles((float)EditorApplication.timeSinceStartup);
|
||||
}
|
||||
#endif
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (_dirty) Rebuild();
|
||||
if (animate) AnimateTiles(Time.time);
|
||||
}
|
||||
|
||||
void Rebuild()
|
||||
{
|
||||
_dirty = false;
|
||||
ClearChildren();
|
||||
|
||||
float s = triangleSize;
|
||||
float h = s * Mathf.Sqrt(3f) * 0.5f;
|
||||
|
||||
Mesh upMesh = BuildRoundedTrianglePrism(
|
||||
new Vector2(-s * 0.5f, -h / 3f),
|
||||
new Vector2(s * 0.5f, -h / 3f),
|
||||
new Vector2(0f, 2f * h / 3f),
|
||||
gapScale, cornerRadius, cornerSegments, thickness, "TrianglePanel_Up");
|
||||
|
||||
Mesh downMesh = BuildRoundedTrianglePrism(
|
||||
new Vector2(-s * 0.5f, h / 3f),
|
||||
new Vector2(s * 0.5f, h / 3f),
|
||||
new Vector2(0f, -2f * h / 3f),
|
||||
gapScale, cornerRadius, cornerSegments, thickness, "TrianglePanel_Down");
|
||||
|
||||
float originX = -(columns * s) * 0.5f;
|
||||
float originZ = -(rows * h) * 0.5f;
|
||||
|
||||
var transforms = new List<Transform>(columns * rows * 2);
|
||||
var basePositions = new List<Vector3>(columns * rows * 2);
|
||||
|
||||
for (int r = 0; r < rows; r++)
|
||||
{
|
||||
for (int c = 0; c < columns; c++)
|
||||
{
|
||||
float rowZ = originZ + r * h;
|
||||
|
||||
Vector3 upPos = new Vector3(originX + c * s + s * 0.5f, 0f, rowZ + h / 3f);
|
||||
CreateTile($"Tile_{r}_{c}_Up", upMesh, upPos, transforms, basePositions);
|
||||
|
||||
Vector3 downPos = new Vector3(originX + c * s + s, 0f, rowZ + 2f * h / 3f);
|
||||
CreateTile($"Tile_{r}_{c}_Down", downMesh, downPos, transforms, basePositions);
|
||||
}
|
||||
}
|
||||
|
||||
_tileTransforms = transforms.ToArray();
|
||||
_tileBasePos = basePositions.ToArray();
|
||||
|
||||
if (showBacker)
|
||||
BuildBacker(originX, originZ, s, h);
|
||||
|
||||
AnimateTiles(0f);
|
||||
}
|
||||
|
||||
void CreateTile(string tileName, Mesh mesh, Vector3 localPos, List<Transform> transforms, List<Vector3> basePositions)
|
||||
{
|
||||
var go = new GameObject(tileName);
|
||||
go.transform.SetParent(transform, false);
|
||||
go.transform.localPosition = localPos;
|
||||
go.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||||
var mr = go.AddComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = faceMaterial;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
transforms.Add(go.transform);
|
||||
basePositions.Add(localPos);
|
||||
}
|
||||
|
||||
void BuildBacker(float originX, float originZ, float s, float h)
|
||||
{
|
||||
var go = new GameObject("Backer");
|
||||
go.transform.SetParent(transform, false);
|
||||
float width = columns * s + backerPadding * 2f;
|
||||
float depth = rows * h + backerPadding * 2f;
|
||||
float cx = originX + (columns * s) * 0.5f;
|
||||
float cz = originZ + (rows * h) * 0.5f;
|
||||
go.transform.localPosition = new Vector3(cx, backerOffset, cz);
|
||||
|
||||
var mesh = new Mesh { name = "TrianglePanel_Backer" };
|
||||
float hw = width * 0.5f;
|
||||
float hd = depth * 0.5f;
|
||||
mesh.vertices = new[]
|
||||
{
|
||||
new Vector3(-hw, 0f, -hd),
|
||||
new Vector3(hw, 0f, -hd),
|
||||
new Vector3(-hw, 0f, hd),
|
||||
new Vector3(hw, 0f, hd),
|
||||
};
|
||||
mesh.triangles = new[] { 0, 2, 1, 1, 2, 3 };
|
||||
mesh.normals = new[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
|
||||
mesh.RecalculateBounds();
|
||||
|
||||
go.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||||
var mr = go.AddComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = backerMaterial;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
}
|
||||
|
||||
static Mesh BuildRoundedTrianglePrism(Vector2 a, Vector2 b, Vector2 c, float gapScale, float requestedRadius,
|
||||
int cornerSegments, float thickness, string meshName)
|
||||
{
|
||||
Vector2[] pts = { a * gapScale, b * gapScale, c * gapScale };
|
||||
|
||||
float minEdge = Mathf.Min(
|
||||
Vector2.Distance(pts[0], pts[1]),
|
||||
Mathf.Min(Vector2.Distance(pts[1], pts[2]), Vector2.Distance(pts[2], pts[0])));
|
||||
float radius = Mathf.Clamp(requestedRadius, 0f, minEdge * 0.28f);
|
||||
|
||||
List<Vector2> outline = RoundedTriangleOutline(pts, radius, cornerSegments);
|
||||
int n = outline.Count;
|
||||
float halfT = thickness * 0.5f;
|
||||
|
||||
var vertices = new List<Vector3>(n * 4 + 2);
|
||||
var normals = new List<Vector3>(n * 4 + 2);
|
||||
var triangles = new List<int>(n * 12);
|
||||
|
||||
int topCenter = vertices.Count;
|
||||
vertices.Add(new Vector3(0f, halfT, 0f));
|
||||
normals.Add(Vector3.up);
|
||||
int topStart = vertices.Count;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
vertices.Add(new Vector3(outline[i].x, halfT, outline[i].y));
|
||||
normals.Add(Vector3.up);
|
||||
}
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
triangles.Add(topCenter);
|
||||
triangles.Add(topStart + i);
|
||||
triangles.Add(topStart + (i + 1) % n);
|
||||
}
|
||||
|
||||
int bottomCenter = vertices.Count;
|
||||
vertices.Add(new Vector3(0f, -halfT, 0f));
|
||||
normals.Add(Vector3.down);
|
||||
int bottomStart = vertices.Count;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
vertices.Add(new Vector3(outline[i].x, -halfT, outline[i].y));
|
||||
normals.Add(Vector3.down);
|
||||
}
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
triangles.Add(bottomCenter);
|
||||
triangles.Add(bottomStart + (i + 1) % n);
|
||||
triangles.Add(bottomStart + i);
|
||||
}
|
||||
|
||||
int sideTopStart = vertices.Count;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Vector2 p = outline[i];
|
||||
Vector3 outward = new Vector3(p.x, 0f, p.y);
|
||||
outward = outward.sqrMagnitude > 0.0001f ? outward.normalized : Vector3.forward;
|
||||
vertices.Add(new Vector3(p.x, halfT, p.y));
|
||||
normals.Add(outward);
|
||||
}
|
||||
int sideBottomStart = vertices.Count;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Vector2 p = outline[i];
|
||||
Vector3 outward = new Vector3(p.x, 0f, p.y);
|
||||
outward = outward.sqrMagnitude > 0.0001f ? outward.normalized : Vector3.forward;
|
||||
vertices.Add(new Vector3(p.x, -halfT, p.y));
|
||||
normals.Add(outward);
|
||||
}
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int next = (i + 1) % n;
|
||||
int t0 = sideTopStart + i;
|
||||
int t1 = sideTopStart + next;
|
||||
int b0 = sideBottomStart + i;
|
||||
int b1 = sideBottomStart + next;
|
||||
triangles.Add(t0); triangles.Add(b0); triangles.Add(t1);
|
||||
triangles.Add(t1); triangles.Add(b0); triangles.Add(b1);
|
||||
}
|
||||
|
||||
var mesh = new Mesh { name = meshName };
|
||||
mesh.SetVertices(vertices);
|
||||
mesh.SetNormals(normals);
|
||||
mesh.SetTriangles(triangles, 0);
|
||||
mesh.RecalculateBounds();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static List<Vector2> RoundedTriangleOutline(Vector2[] pts, float radius, int segments)
|
||||
{
|
||||
var outline = new List<Vector2>();
|
||||
|
||||
for (int k = 0; k < 3; k++)
|
||||
{
|
||||
Vector2 v = pts[k];
|
||||
|
||||
if (radius < 0.0001f)
|
||||
{
|
||||
outline.Add(v);
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 prev = pts[(k + 2) % 3];
|
||||
Vector2 next = pts[(k + 1) % 3];
|
||||
|
||||
Vector2 dirToPrev = (prev - v).normalized;
|
||||
Vector2 dirToNext = (next - v).normalized;
|
||||
|
||||
float dot = Mathf.Clamp(Vector2.Dot(dirToPrev, dirToNext), -1f, 1f);
|
||||
float angle = Mathf.Acos(dot);
|
||||
float halfAngle = angle * 0.5f;
|
||||
float tangentDist = radius / Mathf.Tan(halfAngle);
|
||||
float centerDist = radius / Mathf.Sin(halfAngle);
|
||||
|
||||
Vector2 bisector = dirToPrev + dirToNext;
|
||||
bisector = bisector.sqrMagnitude > 0.0001f ? bisector.normalized : dirToPrev;
|
||||
|
||||
Vector2 tangentOnPrevEdge = v + dirToPrev * tangentDist;
|
||||
Vector2 tangentOnNextEdge = v + dirToNext * tangentDist;
|
||||
Vector2 center = v + bisector * centerDist;
|
||||
|
||||
Vector2 startDir = (tangentOnPrevEdge - center).normalized;
|
||||
Vector2 endDir = (tangentOnNextEdge - center).normalized;
|
||||
|
||||
float cross = startDir.x * endDir.y - startDir.y * endDir.x;
|
||||
float sweepSign = cross >= 0f ? 1f : -1f;
|
||||
float sweepAngle = Mathf.Acos(Mathf.Clamp(Vector2.Dot(startDir, endDir), -1f, 1f));
|
||||
|
||||
int segs = Mathf.Max(1, segments);
|
||||
for (int seg = 0; seg <= segs; seg++)
|
||||
{
|
||||
float t = (float)seg / segs;
|
||||
float a = sweepSign * sweepAngle * t;
|
||||
float ca = Mathf.Cos(a), sa = Mathf.Sin(a);
|
||||
Vector2 rotated = new Vector2(
|
||||
startDir.x * ca - startDir.y * sa,
|
||||
startDir.x * sa + startDir.y * ca);
|
||||
outline.Add(center + rotated * radius);
|
||||
}
|
||||
}
|
||||
|
||||
return outline;
|
||||
}
|
||||
|
||||
void AnimateTiles(float time)
|
||||
{
|
||||
if (swayAmplitude > 0.001f && animate)
|
||||
{
|
||||
float sway = Mathf.Sin(time * swaySpeed) * swayAmplitude;
|
||||
// Rotate around the world Y axis on top of whatever base orientation the panel
|
||||
// was authored with (e.g. tipped vertical), rather than a local axis, so the sway
|
||||
// always reads as a gentle turn regardless of how the panel itself is oriented.
|
||||
transform.localRotation = Quaternion.AngleAxis(sway, Vector3.up) * _baseRotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.localRotation = _baseRotation;
|
||||
}
|
||||
|
||||
if (_tileTransforms == null) return;
|
||||
float t = time * waveSpeed;
|
||||
|
||||
for (int i = 0; i < _tileTransforms.Length; i++)
|
||||
{
|
||||
var tf = _tileTransforms[i];
|
||||
if (tf == null) continue;
|
||||
var b = _tileBasePos[i];
|
||||
|
||||
float sum = (b.x + b.z) * waveFrequency;
|
||||
float diff = (b.x - b.z) * waveFrequency * 0.7f;
|
||||
|
||||
float wave = animate
|
||||
? waveAmplitude * 0.5f * (Mathf.Sin(sum + t) + Mathf.Sin(diff + t * 1.3f))
|
||||
: 0f;
|
||||
|
||||
tf.localPosition = new Vector3(b.x, b.y + wave, b.z);
|
||||
|
||||
if (tiltAmount > 0f && animate)
|
||||
{
|
||||
float tiltX = tiltAmount * Mathf.Cos(diff + t * 1.3f);
|
||||
float tiltZ = tiltAmount * Mathf.Cos(sum + t);
|
||||
tf.localRotation = Quaternion.Euler(tiltX, 0f, -tiltZ);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf.localRotation = Quaternion.identity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClearChildren()
|
||||
{
|
||||
for (int i = transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = transform.GetChild(i).gameObject;
|
||||
if (Application.isPlaying) Destroy(child);
|
||||
else DestroyImmediate(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user