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,340 @@
|
||||
using Procedural;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ProceduralEditor
|
||||
{
|
||||
public static class SetupMainMenuScene
|
||||
{
|
||||
const string ScenePath = "Assets/Scenes/Menu.unity";
|
||||
const string MaterialsFolder = "Assets/Materials";
|
||||
const string FaceMaterialPath = MaterialsFolder + "/LowPolyTriangleFace.mat";
|
||||
const string SettingsFolder = "Assets/Settings";
|
||||
const string PostProcessProfilePath = SettingsFolder + "/MenuPostProcessProfile.asset";
|
||||
|
||||
// Slightly deeper background and a wider field of view for a more open framing
|
||||
static readonly Color BackgroundColor = new Color(0.92f, 0.93f, 0.95f);
|
||||
static readonly Vector3 PanelCenter = new Vector3(0f, 0f, 10f);
|
||||
|
||||
[MenuItem("Tools/Scene/Setup Main Menu Scene")]
|
||||
public static void Setup()
|
||||
{
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
Debug.LogError("Setup Main Menu Scene: exit Play Mode first — EditorSceneManager.OpenScene() can't run during Play Mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
|
||||
var faceMaterial = GetOrCreateMaterial(FaceMaterialPath, "Universal Render Pipeline/Lit", mat =>
|
||||
{
|
||||
mat.SetColor("_BaseColor", new Color(0.08f, 0.09f, 0.12f));
|
||||
if (mat.HasProperty("_Smoothness")) mat.SetFloat("_Smoothness", 0.3f);
|
||||
if (mat.HasProperty("_Metallic")) mat.SetFloat("_Metallic", 0.05f);
|
||||
if (mat.HasProperty("_Cull")) mat.SetFloat("_Cull", 0f);
|
||||
});
|
||||
|
||||
SetupQuality();
|
||||
SetupCamera();
|
||||
SetupLight();
|
||||
SetupPanel(faceMaterial);
|
||||
SetupFog();
|
||||
SetupPostProcessVolume();
|
||||
SetupFadeCanvas();
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
Debug.Log("Main Menu scene set up. Review the Game view framing, then save the scene.");
|
||||
}
|
||||
|
||||
static void SetupCamera()
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam == null)
|
||||
{
|
||||
var go = new GameObject("Main Camera", typeof(Camera), typeof(AudioListener));
|
||||
go.tag = "MainCamera";
|
||||
cam = go.GetComponent<Camera>();
|
||||
}
|
||||
|
||||
// Position/rotation/FOV captured from the live scene after manual framing in the
|
||||
// Editor (approved there first, then copied back here) rather than computed — the
|
||||
// rotation includes a deliberate manual roll (Z) that a LookAt-based rotation
|
||||
// wouldn't produce, and the narrow FOV is a much tighter, more telephoto framing
|
||||
// than the original 60.
|
||||
cam.transform.position = new Vector3(-0.86f, 0.07f, 5.88f);
|
||||
cam.transform.rotation = Quaternion.Euler(-29.064f, -21.054f, -11.311f);
|
||||
// Increased FOV to give a more open, less telephoto composition
|
||||
cam.fieldOfView = 35f;
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = BackgroundColor;
|
||||
cam.nearClipPlane = 0.3f;
|
||||
cam.farClipPlane = 60f;
|
||||
// URP silently disables TAA when MSAA is active (they're mutually exclusive) — the
|
||||
// pipeline asset has MSAA on for other scenes (see SetupQuality), so it's turned off
|
||||
// per-camera here rather than touching that shared, project-wide asset.
|
||||
cam.allowMSAA = false;
|
||||
|
||||
// URP has no "Post Process Layer" component (that's Built-in RP's Post-Processing
|
||||
// Stack v2) — post-processing is a per-camera flag on UniversalAdditionalCameraData
|
||||
// plus the antialiasing mode below; that combination is the URP equivalent.
|
||||
//
|
||||
// Switched from SMAA to TAA (Temporal Anti-Aliasing) for higher quality — SMAA was
|
||||
// already maxed at "High" quality with no further headroom, while TAA accumulates
|
||||
// sub-pixel samples across frames and handles the specular sparkle far better.
|
||||
// Tradeoff: TAA can ghost/smear on continuously moving edges, and this panel never
|
||||
// stops moving (wave + tilt + sway), so varianceClampScale is pulled down from URP's
|
||||
// default (0.9 -> 0.65) to bias toward less ghosting, with contrast-adaptive
|
||||
// sharpening turned on to counter the softening TAA otherwise introduces. Watch for
|
||||
// trailing/streaking behind fast-moving edges — if it shows up, SMAA is the fallback.
|
||||
var camData = cam.GetComponent<UniversalAdditionalCameraData>();
|
||||
if (camData == null) camData = cam.gameObject.AddComponent<UniversalAdditionalCameraData>();
|
||||
camData.renderPostProcessing = true;
|
||||
camData.antialiasing = AntialiasingMode.TemporalAntiAliasing;
|
||||
ref var taa = ref camData.taaSettings;
|
||||
taa.quality = TemporalAAQuality.High;
|
||||
taa.varianceClampScale = 0.65f;
|
||||
taa.contrastAdaptiveSharpening = 0.6f;
|
||||
}
|
||||
|
||||
static void SetupLight()
|
||||
{
|
||||
Light light = Object.FindFirstObjectByType<Light>();
|
||||
GameObject go;
|
||||
if (light == null)
|
||||
{
|
||||
go = new GameObject("Directional Light", typeof(Light));
|
||||
light = go.GetComponent<Light>();
|
||||
}
|
||||
else
|
||||
{
|
||||
go = light.gameObject;
|
||||
}
|
||||
|
||||
light.type = LightType.Directional;
|
||||
go.transform.position = new Vector3(0f, 3f, 0f);
|
||||
// The panel's visible faces have world normal +Z, so the light needs a strong
|
||||
// -Z travel direction (yaw ~180) to actually illuminate them head-on instead of
|
||||
// grazing the wall edge-on the way the old floor-tuned angle did.
|
||||
go.transform.rotation = Quaternion.Euler(35f, 180f, 0f);
|
||||
light.color = new Color(1f, 0.96f, 0.86f);
|
||||
light.intensity = 1.9f;
|
||||
|
||||
RenderSettings.ambientMode = AmbientMode.Flat;
|
||||
RenderSettings.ambientLight = new Color(0.22f, 0.24f, 0.28f);
|
||||
|
||||
if (go.GetComponent<MenuLightAnimator>() == null)
|
||||
go.AddComponent<MenuLightAnimator>();
|
||||
}
|
||||
|
||||
static void SetupPanel(Material faceMaterial)
|
||||
{
|
||||
var existing = GameObject.Find("Triangle Wave Panel");
|
||||
if (existing != null) Object.DestroyImmediate(existing);
|
||||
|
||||
var go = new GameObject("Triangle Wave Panel");
|
||||
go.transform.position = PanelCenter;
|
||||
// Rotate the panel from its default horizontal (floor, XZ) orientation to vertical
|
||||
// (XY, facing the camera along Z). Local Y (the wave/thickness axis) becomes world Z,
|
||||
// so tiles ripple toward/away from the camera instead of bobbing up/down.
|
||||
go.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
|
||||
var panel = go.AddComponent<TriangleWavePanel>();
|
||||
// Slightly larger than the minimum frame-filling size, since the camera now views the
|
||||
// panel off-axis (see SetupCamera) rather than dead-on, which eats into the margin.
|
||||
panel.columns = 46;
|
||||
panel.rows = 30;
|
||||
panel.triangleSize = 1f;
|
||||
// Values below captured from the live scene after manual tweaking in the Inspector
|
||||
// (approved there first, then copied back here).
|
||||
panel.gapScale = 0.973f;
|
||||
panel.cornerRadius = 0f;
|
||||
panel.cornerSegments = 3;
|
||||
panel.thickness = 0.15f;
|
||||
panel.waveAmplitude = 0.45f;
|
||||
panel.waveFrequency = 0.18f;
|
||||
panel.waveSpeed = 0.8f;
|
||||
panel.tiltAmount = 1.4f;
|
||||
panel.swayAmplitude = 10f;
|
||||
panel.swaySpeed = 0.2f;
|
||||
panel.showBacker = false;
|
||||
panel.faceMaterial = faceMaterial;
|
||||
panel.backerMaterial = faceMaterial;
|
||||
|
||||
// AddComponent already triggered one Rebuild() using class defaults before the fields
|
||||
// above were set (see TriangleWavePanel.RebuildNow's doc comment) — force a real one
|
||||
// now that the panel is fully configured.
|
||||
panel.RebuildNow();
|
||||
}
|
||||
|
||||
static void SetupFog()
|
||||
{
|
||||
// A vertical, camera-facing wall has no receding horizon to hide, so fog stays off;
|
||||
// the grid is sized so its edges fall outside the frustum instead.
|
||||
RenderSettings.fog = false;
|
||||
}
|
||||
|
||||
static void SetupFadeCanvas()
|
||||
{
|
||||
var existing = GameObject.Find("Fade Canvas");
|
||||
if (existing != null) Object.DestroyImmediate(existing);
|
||||
|
||||
var canvasGo = new GameObject("Fade Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
var canvas = canvasGo.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
// Ensure fade canvas draws on top of other UI canvases
|
||||
canvas.overrideSorting = true;
|
||||
canvas.sortingOrder = 1000;
|
||||
var scaler = canvasGo.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
|
||||
var imageGo = new GameObject("Fade Image", typeof(Image), typeof(CanvasGroup));
|
||||
imageGo.transform.SetParent(canvasGo.transform, false);
|
||||
var image = imageGo.GetComponent<Image>();
|
||||
image.color = new Color(0.03f, 0.03f, 0.04f, 1f);
|
||||
var rect = image.rectTransform;
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
|
||||
var fadeIn = imageGo.AddComponent<MenuFadeIn>();
|
||||
fadeIn.delay = 0.3f;
|
||||
fadeIn.duration = 3.5f;
|
||||
// Disable automatic fade-on-start so the debug toggle controls opacity predictably.
|
||||
// If you prefer the automatic fade, remove this line.
|
||||
fadeIn.enabled = false;
|
||||
|
||||
// Make sure the image initially is fully opaque so the menu starts covered by the backdrop
|
||||
var initColor = image.color;
|
||||
initColor.a = 1f;
|
||||
image.color = initColor;
|
||||
|
||||
// DEBUG: add a small top-right button to toggle fade (full opacity <-> transparent)
|
||||
var buttonGo = new GameObject("Debug Toggle Fade Button", typeof(RectTransform), typeof(UnityEngine.UI.Button), typeof(Image));
|
||||
buttonGo.transform.SetParent(canvasGo.transform, false);
|
||||
var btnRect = buttonGo.GetComponent<RectTransform>();
|
||||
// anchor to top-right
|
||||
btnRect.anchorMin = new Vector2(1f, 1f);
|
||||
btnRect.anchorMax = new Vector2(1f, 1f);
|
||||
btnRect.pivot = new Vector2(1f, 1f);
|
||||
btnRect.sizeDelta = new Vector2(140f, 32f);
|
||||
btnRect.anchoredPosition = new Vector2(-10f, -10f);
|
||||
|
||||
var btnImage = buttonGo.GetComponent<Image>();
|
||||
btnImage.color = new Color(0.08f, 0.08f, 0.08f, 0.85f);
|
||||
|
||||
var button = buttonGo.GetComponent<UnityEngine.UI.Button>();
|
||||
var colors = button.colors;
|
||||
colors.normalColor = new Color(0.12f, 0.12f, 0.12f, 0.9f);
|
||||
colors.highlightedColor = new Color(0.18f, 0.18f, 0.18f, 0.95f);
|
||||
colors.pressedColor = new Color(0.06f, 0.06f, 0.06f, 0.9f);
|
||||
button.colors = colors;
|
||||
|
||||
// simple text label (legacy UI Text so no TMP dependency required)
|
||||
var textGo = new GameObject("Text", typeof(RectTransform), typeof(UnityEngine.UI.Text));
|
||||
textGo.transform.SetParent(buttonGo.transform, false);
|
||||
var txt = textGo.GetComponent<UnityEngine.UI.Text>();
|
||||
txt.text = "Toggle Fade";
|
||||
txt.alignment = TextAnchor.MiddleCenter;
|
||||
txt.color = Color.white;
|
||||
txt.fontSize = 14;
|
||||
var txtRect = textGo.GetComponent<RectTransform>();
|
||||
txtRect.anchorMin = Vector2.zero;
|
||||
txtRect.anchorMax = Vector2.one;
|
||||
txtRect.offsetMin = Vector2.zero;
|
||||
txtRect.offsetMax = Vector2.zero;
|
||||
|
||||
// add runtime helper (component created in new file DebugFadeToggle.cs)
|
||||
var debugToggle = buttonGo.AddComponent<ProceduralEditor.DebugFadeToggleBridge>();
|
||||
debugToggle.targetImage = image;
|
||||
debugToggle.duration = fadeIn.duration;
|
||||
}
|
||||
|
||||
static void SetupPostProcessVolume()
|
||||
{
|
||||
var existing = GameObject.Find("Post Process Volume");
|
||||
if (existing != null) Object.DestroyImmediate(existing);
|
||||
|
||||
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(PostProcessProfilePath);
|
||||
if (profile == null)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(SettingsFolder))
|
||||
AssetDatabase.CreateFolder("Assets", "Settings");
|
||||
profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
||||
AssetDatabase.CreateAsset(profile, PostProcessProfilePath);
|
||||
}
|
||||
|
||||
if (!profile.TryGet(out DepthOfField dof))
|
||||
dof = profile.Add<DepthOfField>();
|
||||
|
||||
// "Blur" here is depth-of-field rather than a flat uniform blur: at this close a
|
||||
// camera distance (see SetupCamera, Z=7.5 vs panel at Z=10) the panel's own width
|
||||
// creates a big apparent depth range across the frame purely from perspective, so
|
||||
// gaussianStart/End are tuned to keep roughly the center sharp and let the
|
||||
// corners/edges soften — a graduated blur rather than blurring everything equally.
|
||||
dof.active = true;
|
||||
dof.mode.overrideState = true;
|
||||
dof.mode.value = DepthOfFieldMode.Gaussian;
|
||||
dof.gaussianStart.overrideState = true;
|
||||
dof.gaussianStart.value = 2f;
|
||||
dof.gaussianEnd.overrideState = true;
|
||||
dof.gaussianEnd.value = 9f;
|
||||
dof.gaussianMaxRadius.overrideState = true;
|
||||
dof.gaussianMaxRadius.value = 1f;
|
||||
dof.highQualitySampling.overrideState = true;
|
||||
dof.highQualitySampling.value = true;
|
||||
|
||||
EditorUtility.SetDirty(profile);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var go = new GameObject("Post Process Volume", typeof(Volume));
|
||||
var volume = go.GetComponent<Volume>();
|
||||
volume.isGlobal = true;
|
||||
volume.priority = 0f;
|
||||
volume.weight = 1f;
|
||||
volume.sharedProfile = profile;
|
||||
}
|
||||
|
||||
static void SetupQuality()
|
||||
{
|
||||
// Sets MSAA on the shared pipeline asset for scenes/cameras that still use SMAA/FXAA
|
||||
// (which do benefit from stacking with MSAA). This menu's own camera now uses TAA
|
||||
// instead, which requires MSAA off, so it opts itself out via cam.allowMSAA in
|
||||
// SetupCamera() rather than leaving this project-wide asset at 1x for everyone.
|
||||
if (GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset urpAsset)
|
||||
{
|
||||
urpAsset.msaaSampleCount = 8;
|
||||
EditorUtility.SetDirty(urpAsset);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
|
||||
static Material GetOrCreateMaterial(string path, string shaderName, System.Action<Material> configure)
|
||||
{
|
||||
var existing = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (existing != null) return existing;
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(MaterialsFolder))
|
||||
AssetDatabase.CreateFolder("Assets", "Materials");
|
||||
|
||||
var shader = Shader.Find(shaderName);
|
||||
if (shader == null)
|
||||
{
|
||||
Debug.LogError($"SetupMainMenuScene: could not find shader '{shaderName}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var mat = new Material(shader);
|
||||
configure(mat);
|
||||
AssetDatabase.CreateAsset(mat, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user