375 lines
14 KiB
C#
375 lines
14 KiB
C#
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();
|
|
|
|
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
}
|
|
|
|
private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
|
|
{
|
|
// Check again when the scene finishes loading to remove any baked-in duplicate EventSystems
|
|
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>(FindObjectsInactive.Include, 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
|
|
}
|
|
} |