feat: add RetroMainMenuUI script and associated meta file for main menu functionality

This commit is contained in:
Thibault Pouch
2026-03-13 19:28:19 +01:00
parent 64b2d63799
commit 53fd617abe
3 changed files with 373 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using TMPro;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem.UI;
#endif
public class RetroMainMenuUI : MonoBehaviour
{
private Canvas m_MenuCanvas;
private bool m_MenuActive;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
if (Object.FindFirstObjectByType<RetroMainMenuUI>() != null)
{
return;
}
GameObject root = new("RetroMainMenuUI");
root.AddComponent<RetroMainMenuUI>();
}
private void Awake()
{
m_MenuActive = true;
Time.timeScale = 0f;
ApplyMenuCursorState();
BuildMenu();
EnsureEventSystem();
}
private void LateUpdate()
{
if (!m_MenuActive)
{
return;
}
// Some gameplay scripts lock the cursor during Start/Update.
// Force menu cursor state while the menu is active.
ApplyMenuCursorState();
}
private void BuildMenu()
{
Color bgColor = HexToColor("001e26");
Color panelColor = HexToColor("517567");
Color titleColor = HexToColor("f3d58d");
Color TextNormalColor = HexToColor("eb9843");
Color textWarningColor = HexToColor("c12204");
Color shadowColor = HexToColor("520805");
GameObject canvasObject = new("MainMenuCanvas");
Canvas canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 10000;
m_MenuCanvas = canvas;
CanvasScaler scaler = canvasObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0.5f;
canvasObject.AddComponent<GraphicRaycaster>();
// Background
GameObject background = CreateImage("Background", canvasObject.transform, bgColor);
StretchToFull(background.GetComponent<RectTransform>());
// Decorative horizontal lines (scanline aesthetic)
CreateLine("TopLine", background.transform, new Rect(0, -60, 0, 4), panelColor, AnchorPreset.TopStretch);
CreateLine("BotLine", background.transform, new Rect(0, 60, 0, 4), panelColor, AnchorPreset.BottomStretch);
// --- LEFT PANEL ---
GameObject leftPanel = new GameObject("LeftPanel", typeof(RectTransform));
leftPanel.transform.SetParent(canvasObject.transform, false);
RectTransform leftRect = leftPanel.GetComponent<RectTransform>();
leftRect.anchorMin = new Vector2(0.08f, 0.1f);
leftRect.anchorMax = new Vector2(0.45f, 0.9f);
leftRect.offsetMin = Vector2.zero;
leftRect.offsetMax = Vector2.zero;
// Title
TextMeshProUGUI titleText = CreateTMP("Title", leftPanel.transform, "HEADLESS HAZARD", titleColor, 72, TextAlignmentOptions.BottomLeft);
RectTransform titleRect = titleText.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0f, 0.85f);
titleRect.anchorMax = new Vector2(1f, 1f);
titleRect.offsetMin = Vector2.zero;
titleRect.offsetMax = Vector2.zero;
titleText.fontStyle = FontStyles.Bold;
// Title Shadow
TextMeshProUGUI titleShadow = CreateTMP("TitleShadow", leftPanel.transform, "HEADLESS HAZARD", shadowColor, 72, TextAlignmentOptions.BottomLeft);
RectTransform shadowRect = titleShadow.GetComponent<RectTransform>();
shadowRect.anchorMin = new Vector2(0f, 0.85f);
shadowRect.anchorMax = new Vector2(1f, 1f);
shadowRect.offsetMin = new Vector2(4f, -4f); // apply drop shadow offset
shadowRect.offsetMax = new Vector2(4f, -4f);
titleShadow.fontStyle = FontStyles.Bold;
titleShadow.transform.SetSiblingIndex(0); // push behind title
// Subtitle / Decorative Status
TextMeshProUGUI subText = CreateTMP("Subtitle", leftPanel.transform, "SYSTEM_BOOT // OS.ACTIVE_ ", panelColor, 20, TextAlignmentOptions.TopLeft);
RectTransform subRect = subText.GetComponent<RectTransform>();
subRect.anchorMin = new Vector2(0f, 0.80f);
subRect.anchorMax = new Vector2(1f, 0.85f);
subRect.offsetMin = Vector2.zero;
subRect.offsetMax = Vector2.zero;
// Button Group
GameObject buttonGroup = new("ButtonGroup", typeof(RectTransform), typeof(VerticalLayoutGroup));
buttonGroup.transform.SetParent(leftPanel.transform, false);
RectTransform groupRect = buttonGroup.GetComponent<RectTransform>();
groupRect.anchorMin = new Vector2(0f, 0f);
groupRect.anchorMax = new Vector2(1f, 0.65f);
groupRect.offsetMin = Vector2.zero;
groupRect.offsetMax = Vector2.zero;
VerticalLayoutGroup layout = buttonGroup.GetComponent<VerticalLayoutGroup>();
layout.childAlignment = TextAnchor.UpperLeft;
layout.spacing = 16f;
layout.childControlWidth = true;
layout.childControlHeight = false;
CreateTextButton(buttonGroup.transform, "> INITIALIZE_PLAY", TextNormalColor, titleColor, () =>
{
Debug.Log("Play clicked.");
OnPlayClicked();
});
CreateTextButton(buttonGroup.transform, "> CONFIGURE_PARAMS", TextNormalColor, titleColor, () =>
{
Debug.Log("Options clicked.");
});
CreateTextButton(buttonGroup.transform, "> TERMINATE_PROCESS", TextNormalColor, textWarningColor, () =>
{
Debug.Log("Quit clicked.");
Application.Quit();
});
// --- RIGHT PANEL (Level Info) ---
GameObject rightPanel = new GameObject("RightPanel", typeof(RectTransform));
rightPanel.transform.SetParent(canvasObject.transform, false);
RectTransform rightRect = rightPanel.GetComponent<RectTransform>();
rightRect.anchorMin = new Vector2(0.55f, 0.4f);
rightRect.anchorMax = new Vector2(0.92f, 0.82f);
rightRect.offsetMin = Vector2.zero;
rightRect.offsetMax = Vector2.zero;
// Right side Border lines
CreateLine("R_Top", rightPanel.transform, new Rect(0, 0, 0, 2), panelColor, AnchorPreset.TopStretch);
CreateLine("R_Bot", rightPanel.transform, new Rect(0, 0, 0, 2), panelColor, AnchorPreset.BottomStretch);
CreateLine("R_Left", rightPanel.transform, new Rect(0, 0, 2, 0), panelColor, AnchorPreset.LeftStretch);
CreateLine("R_Right", rightPanel.transform, new Rect(0, 0, 2, 0), panelColor, AnchorPreset.RightStretch);
// Right Panel Headers
TextMeshProUGUI headerText = CreateTMP("LevelHeader", rightPanel.transform, "CURRENT_SECTOR", panelColor, 24, TextAlignmentOptions.TopLeft);
headerText.GetComponent<RectTransform>().anchorMin = new Vector2(0f, 1f);
headerText.GetComponent<RectTransform>().anchorMax = new Vector2(1f, 1f);
headerText.GetComponent<RectTransform>().anchoredPosition = new Vector2(20f, -20f);
// Big Level Text
TextMeshProUGUI levelText = CreateTMP("LevelNumber", rightPanel.transform, "LEVEL 01", textWarningColor, 140, TextAlignmentOptions.Center);
StretchToFull(levelText.GetComponent<RectTransform>());
levelText.fontStyle = FontStyles.Bold;
// Decorative status
TextMeshProUGUI statusText = CreateTMP("LevelStatus", rightPanel.transform, "[ STATUS: OPTIMAL ]", panelColor, 24, TextAlignmentOptions.BottomRight);
statusText.GetComponent<RectTransform>().anchorMin = new Vector2(0f, 0f);
statusText.GetComponent<RectTransform>().anchorMax = new Vector2(1f, 0f);
statusText.GetComponent<RectTransform>().anchoredPosition = new Vector2(-20f, 20f);
}
private void OnPlayClicked()
{
m_MenuActive = false;
Time.timeScale = 1f;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
if (m_MenuCanvas != null)
{
Destroy(m_MenuCanvas.gameObject);
}
Destroy(gameObject);
}
private static GameObject CreateTextButton(
Transform parent,
string label,
Color normalColor,
Color highlightColor,
UnityEngine.Events.UnityAction clickAction)
{
GameObject buttonObject = new(label, typeof(RectTransform), typeof(TextMeshProUGUI), typeof(Button));
buttonObject.transform.SetParent(parent, false);
RectTransform rect = buttonObject.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(0f, 60f); // Height 60, width auto-controlled by LayoutGroup
TextMeshProUGUI text = buttonObject.GetComponent<TextMeshProUGUI>();
text.text = label;
text.fontSize = 38;
text.alignment = TextAlignmentOptions.Left;
text.color = Color.white; // Button tint applies on top of white
text.textWrappingMode = TextWrappingModes.NoWrap;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = text;
button.transition = Selectable.Transition.ColorTint;
ColorBlock colors = button.colors;
colors.normalColor = normalColor;
colors.highlightedColor = highlightColor;
colors.pressedColor = highlightColor;
colors.selectedColor = highlightColor;
colors.disabledColor = Color.gray;
colors.colorMultiplier = 1f;
colors.fadeDuration = 0.1f;
button.colors = colors;
button.onClick.AddListener(clickAction);
return buttonObject;
}
private static TextMeshProUGUI CreateTMP(string name, Transform parent, string textStr, Color color, float size, TextAlignmentOptions align)
{
GameObject go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
go.transform.SetParent(parent, false);
TextMeshProUGUI tmp = go.GetComponent<TextMeshProUGUI>();
tmp.text = textStr;
tmp.color = color;
tmp.fontSize = size;
tmp.alignment = align;
tmp.textWrappingMode = TextWrappingModes.NoWrap;
return tmp;
}
enum AnchorPreset { TopStretch, BottomStretch, LeftStretch, RightStretch }
private static GameObject CreateLine(string name, Transform parent, Rect details, Color color, AnchorPreset preset)
{
GameObject line = CreateImage(name, parent, color);
RectTransform rect = line.GetComponent<RectTransform>();
switch (preset)
{
case AnchorPreset.TopStretch:
rect.anchorMin = new Vector2(0, 1);
rect.anchorMax = new Vector2(1, 1);
rect.sizeDelta = new Vector2(details.width, details.height);
rect.anchoredPosition = new Vector2(details.x, details.y);
break;
case AnchorPreset.BottomStretch:
rect.anchorMin = new Vector2(0, 0);
rect.anchorMax = new Vector2(1, 0);
rect.sizeDelta = new Vector2(details.width, details.height);
rect.anchoredPosition = new Vector2(details.x, details.y);
break;
case AnchorPreset.LeftStretch:
rect.anchorMin = new Vector2(0, 0);
rect.anchorMax = new Vector2(0, 1);
rect.sizeDelta = new Vector2(details.width, details.height);
rect.anchoredPosition = new Vector2(details.x, details.y);
break;
case AnchorPreset.RightStretch:
rect.anchorMin = new Vector2(1, 0);
rect.anchorMax = new Vector2(1, 1);
rect.sizeDelta = new Vector2(details.width, details.height);
rect.anchoredPosition = new Vector2(details.x, details.y);
break;
}
return line;
}
private static GameObject CreateImage(string name, Transform parent, Color color)
{
GameObject imageObject = new(name, typeof(RectTransform), typeof(Image));
imageObject.transform.SetParent(parent, false);
Image image = imageObject.GetComponent<Image>();
image.color = color;
return imageObject;
}
private static void StretchToFull(RectTransform rect)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
}
private static Color HexToColor(string hex)
{
if (ColorUtility.TryParseHtmlString("#" + hex, out Color color))
{
return color;
}
return Color.magenta;
}
private static void ApplyMenuCursorState()
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
private static void EnsureEventSystem()
{
EventSystem[] allEventSystems = Object.FindObjectsByType<EventSystem>(FindObjectsSortMode.None);
EventSystem eventSystem;
if (allEventSystems.Length == 0)
{
GameObject eventSystemObject = new("EventSystem", typeof(EventSystem));
eventSystem = eventSystemObject.GetComponent<EventSystem>();
}
else
{
eventSystem = allEventSystems[0];
for (int i = 1; i < allEventSystems.Length; i++)
{
if (allEventSystems[i] != null)
{
Destroy(allEventSystems[i].gameObject);
}
}
}
EnsureCompatibleInputModule(eventSystem.gameObject);
}
private static void EnsureCompatibleInputModule(GameObject eventSystemObject)
{
#if ENABLE_INPUT_SYSTEM
StandaloneInputModule standaloneModule = eventSystemObject.GetComponent<StandaloneInputModule>();
if (standaloneModule != null)
{
Destroy(standaloneModule);
}
if (eventSystemObject.GetComponent<InputSystemUIInputModule>() == null)
{
eventSystemObject.AddComponent<InputSystemUIInputModule>();
}
#else
if (eventSystemObject.GetComponent<StandaloneInputModule>() == null)
{
eventSystemObject.AddComponent<StandaloneInputModule>();
}
#endif
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fa612753289203548aa7ed790c42847e