Files
BABA_YAGA/Assets/Scripts/UI/UIManager.cs

434 lines
16 KiB
C#
Raw Normal View History

2026-04-28 00:07:42 +07:00
using System;
2026-04-23 23:09:54 +07:00
using System.Collections.Generic;
2026-04-28 00:07:42 +07:00
using System.Threading.Tasks;
2026-04-23 23:09:54 +07:00
using UnityEngine;
using UnityEngine.UIElements;
2026-04-28 10:44:22 +07:00
using PrimeTween;
2026-04-28 18:49:05 +07:00
using OnlyScove.Scripts;
2026-04-28 10:11:28 +07:00
#if UNITY_EDITOR
using UnityEditor;
2026-04-29 01:04:28 +07:00
using UnityEditor.Build;
2026-04-28 10:11:28 +07:00
#endif
2026-04-23 23:09:54 +07:00
2026-04-28 00:07:42 +07:00
namespace Hallucinate.UI
2026-04-23 23:09:54 +07:00
{
2026-04-28 00:07:42 +07:00
[RequireComponent(typeof(UIDocument))]
2026-04-23 23:09:54 +07:00
public class UIManager : MonoBehaviour
{
2026-04-25 18:20:16 +07:00
public static UIManager Instance { get; private set; }
2026-04-28 00:07:42 +07:00
private UIDocument _uiDocument;
private VisualElement _rootElement;
2026-04-28 10:48:04 +07:00
private VisualElement _cursorLayer;
2026-04-28 11:03:57 +07:00
private VisualElement _mainCursor;
2026-04-23 23:09:54 +07:00
2026-04-28 00:07:42 +07:00
private readonly Dictionary<Type, BaseUIController> _controllers = new Dictionary<Type, BaseUIController>();
private readonly Stack<BaseUIController> _history = new Stack<BaseUIController>();
2026-04-25 18:20:16 +07:00
2026-04-28 11:35:49 +07:00
[Header("References")]
[SerializeField] private InputReader inputReader;
2026-04-29 01:04:28 +07:00
public InputReader InputReader => inputReader;
2026-04-28 11:35:49 +07:00
2026-04-28 10:11:28 +07:00
[Header("Game Metadata")]
[SerializeField] private Texture2D gameIcon;
2026-04-28 10:44:22 +07:00
[Header("Cursor & Effects Settings")]
2026-04-28 11:03:57 +07:00
[SerializeField] private Sprite cursorSprite;
2026-04-28 10:44:22 +07:00
[SerializeField] private Sprite cursorTrailSprite;
2026-04-28 11:03:57 +07:00
[SerializeField, Range(10f, 150f)] private float cursorSize = 40f;
[SerializeField, Range(5, 50)] private int trailLength = 20;
[SerializeField, Range(1, 10)] private int trailSpacing = 2;
2026-04-28 10:44:22 +07:00
[SerializeField] private bool enableRipples = true;
2026-04-28 11:03:57 +07:00
[SerializeField] private Color rippleColor = new Color(1, 1, 1, 0.4f);
2026-04-28 10:44:22 +07:00
2026-04-28 00:07:42 +07:00
[Header("UI Templates")]
2026-04-28 19:04:09 +07:00
[SerializeField] private VisualTreeAsset loginTemplate;
2026-04-28 00:07:42 +07:00
[SerializeField] private VisualTreeAsset mainMenuTemplate;
[SerializeField] private VisualTreeAsset lobbyTemplate;
2026-04-29 01:04:28 +07:00
[SerializeField] private VisualTreeAsset roomItemTemplate;
2026-04-28 00:07:42 +07:00
[SerializeField] private VisualTreeAsset profileTemplate;
[SerializeField] private VisualTreeAsset settingsTemplate;
[SerializeField] private VisualTreeAsset hudTemplate;
2026-04-28 11:35:49 +07:00
2026-04-28 18:49:05 +07:00
private LoginController _loginController;
2026-04-28 00:07:42 +07:00
private MainMenuController _mainMenuController;
2026-04-28 19:04:09 +07:00
private LobbyController _lobbyController;
2026-04-28 11:35:49 +07:00
private SettingsController _settingsController;
2026-04-28 10:44:22 +07:00
private List<VisualElement> _trailSegments = new List<VisualElement>();
2026-04-28 11:03:57 +07:00
private List<Vector2> _posHistory = new List<Vector2>();
2026-04-25 18:20:16 +07:00
2026-04-28 11:35:49 +07:00
private Vector2 _lastMousePos;
private float _trailOpacity = 1f;
private bool _isSettingsOpen = false;
2026-04-29 02:31:15 +07:00
private const string UI_SCALE_KEY = "UIScale";
2026-04-28 00:07:42 +07:00
private void Awake()
2026-04-23 23:09:54 +07:00
{
2026-04-29 13:10:00 +07:00
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
2026-04-28 11:03:57 +07:00
Instance = this;
2026-04-29 13:10:00 +07:00
DontDestroyOnLoad(gameObject);
2026-04-28 11:03:57 +07:00
_uiDocument = GetComponent<UIDocument>();
UnityEngine.Cursor.visible = false;
2026-04-29 02:31:15 +07:00
ApplySavedUIScale();
}
2026-04-29 13:10:00 +07:00
public void OnGameStarted()
{
_ = Push<HUDController>();
}
public void OnBackToMenu()
{
_ = Push<MainMenuController>();
}
2026-04-29 02:31:15 +07:00
public void SetUIScale(float scale)
{
if (_uiDocument == null || _uiDocument.panelSettings == null) return;
// Unity UI Toolkit dùng panelSettings để điều khiển tỉ lệ
// Chúng ta thay đổi scale multiplier
_uiDocument.panelSettings.scale = scale;
PlayerPrefs.SetFloat(UI_SCALE_KEY, scale);
PlayerPrefs.Save();
}
private void ApplySavedUIScale()
{
float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f);
SetUIScale(savedScale);
2026-04-28 11:03:57 +07:00
}
2026-04-25 18:20:16 +07:00
2026-04-28 11:03:57 +07:00
private void Start()
{
2026-04-29 01:04:28 +07:00
if (_uiDocument == null) _uiDocument = GetComponent<UIDocument>();
if (_uiDocument == null)
{
Debug.LogError("[UIManager] UIDocument component missing!");
return;
}
2026-04-28 11:03:57 +07:00
_rootElement = _uiDocument.rootVisualElement;
2026-04-29 01:04:28 +07:00
if (_rootElement == null)
{
Debug.LogError("[UIManager] Root VisualElement is null!");
return;
}
2026-04-28 10:11:28 +07:00
2026-04-28 11:03:57 +07:00
_cursorLayer = new VisualElement();
_cursorLayer.name = "CursorLayer";
_cursorLayer.style.position = Position.Absolute;
_cursorLayer.style.width = Length.Percent(100);
_cursorLayer.style.height = Length.Percent(100);
_cursorLayer.pickingMode = PickingMode.Ignore;
_rootElement.Add(_cursorLayer);
2026-04-28 10:44:22 +07:00
2026-04-28 11:03:57 +07:00
SetupVirtualCursor();
2026-04-28 10:44:22 +07:00
2026-04-28 11:03:57 +07:00
_rootElement.RegisterCallback<PointerDownEvent>(OnGlobalClick, TrickleDown.TrickleDown);
2026-04-28 10:44:22 +07:00
2026-04-28 11:35:49 +07:00
if (inputReader != null)
{
inputReader.OnToggleSettingsEvent += ToggleSettings;
inputReader.OnCancelEvent += HandleCancel;
}
2026-04-28 10:11:28 +07:00
#if UNITY_EDITOR
2026-04-28 11:03:57 +07:00
if (gameIcon == null)
2026-04-28 10:11:28 +07:00
{
2026-04-29 01:04:28 +07:00
var icons = PlayerSettings.GetIcons(NamedBuildTarget.Unknown, IconKind.Any);
2026-04-28 11:03:57 +07:00
if (icons != null && icons.Length > 0) gameIcon = icons[0];
2026-04-28 10:11:28 +07:00
}
2026-04-28 11:03:57 +07:00
#endif
InitializeControllers();
2026-04-28 18:49:05 +07:00
CheckLoginStatus();
}
private void CheckLoginStatus()
{
string savedName = PlayerPrefs.GetString("Username", "");
if (string.IsNullOrEmpty(savedName))
{
_ = Push<LoginController>();
}
else
{
Debug.Log($"[UIManager] Welcome back, {savedName}!");
_ = Push<MainMenuController>();
}
}
public void OnLoginSuccess()
{
_ = Push<MainMenuController>();
2026-04-28 00:07:42 +07:00
}
2026-04-26 04:39:59 +07:00
2026-04-28 11:35:49 +07:00
private void OnDestroy()
{
if (inputReader != null)
{
inputReader.OnToggleSettingsEvent -= ToggleSettings;
inputReader.OnCancelEvent -= HandleCancel;
}
}
private void HandleCancel()
{
if (_isSettingsOpen) ToggleSettings();
}
public async void ToggleSettings()
{
if (_settingsController == null) return;
if (!_isSettingsOpen)
{
_isSettingsOpen = true;
2026-04-28 18:49:05 +07:00
_settingsController.Root.BringToFront();
2026-04-28 11:35:49 +07:00
_cursorLayer.BringToFront();
await _settingsController.PlayTransitionIn();
}
else
{
_isSettingsOpen = false;
await _settingsController.PlayTransitionOut();
}
}
private void Update()
{
2026-04-29 13:10:00 +07:00
if (_history.Count > 0) _history.Peek().Update();
2026-04-28 11:35:49 +07:00
UpdateCursorAndTrail();
}
2026-04-28 11:03:57 +07:00
private void SetupVirtualCursor()
2026-04-28 10:44:22 +07:00
{
2026-04-29 01:04:28 +07:00
if (_cursorLayer == null) return;
2026-04-28 11:03:57 +07:00
_cursorLayer.Clear();
_trailSegments.Clear();
_posHistory.Clear();
2026-04-28 10:44:22 +07:00
2026-04-28 11:03:57 +07:00
if (cursorTrailSprite != null)
2026-04-28 10:44:22 +07:00
{
2026-04-28 11:03:57 +07:00
for (int i = 0; i < trailLength; i++)
{
var segment = new VisualElement();
segment.style.position = Position.Absolute;
segment.style.width = cursorSize;
segment.style.height = cursorSize;
segment.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorTrailSprite));
float ratio = 1f - ((float)i / trailLength);
2026-04-28 11:35:49 +07:00
segment.style.opacity = 0f;
2026-04-28 11:03:57 +07:00
segment.style.scale = new StyleScale(new Scale(new Vector3(ratio, ratio, 1f)));
segment.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
segment.pickingMode = PickingMode.Ignore;
_cursorLayer.Add(segment);
_trailSegments.Add(segment);
}
2026-04-28 10:44:22 +07:00
}
2026-04-28 11:03:57 +07:00
_mainCursor = new VisualElement();
_mainCursor.style.position = Position.Absolute;
_mainCursor.style.width = cursorSize;
_mainCursor.style.height = cursorSize;
_mainCursor.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorSprite != null ? cursorSprite : cursorTrailSprite));
_mainCursor.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
_mainCursor.pickingMode = PickingMode.Ignore;
_cursorLayer.Add(_mainCursor);
Vector2 startPos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y);
2026-04-28 11:35:49 +07:00
_lastMousePos = startPos;
2026-04-28 11:03:57 +07:00
for (int i = 0; i < trailLength * trailSpacing + 1; i++) _posHistory.Add(startPos);
2026-04-28 10:44:22 +07:00
}
2026-04-29 02:31:15 +07:00
private float GetCurrentScale()
{
if (_uiDocument != null && _uiDocument.panelSettings != null)
return _uiDocument.panelSettings.scale;
return 1.0f;
}
2026-04-28 10:44:22 +07:00
private void OnGlobalClick(PointerDownEvent evt)
{
2026-04-29 01:04:28 +07:00
if (!enableRipples || _cursorLayer == null) return;
2026-04-28 10:44:22 +07:00
var ripple = new VisualElement();
ripple.style.position = Position.Absolute;
ripple.style.width = cursorSize;
ripple.style.height = cursorSize;
2026-04-28 11:03:57 +07:00
ripple.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
2026-04-28 10:48:04 +07:00
var radius = new StyleLength(new Length(50, LengthUnit.Percent));
ripple.style.borderTopLeftRadius = radius;
ripple.style.borderTopRightRadius = radius;
ripple.style.borderBottomLeftRadius = radius;
ripple.style.borderBottomRightRadius = radius;
2026-04-28 10:44:22 +07:00
ripple.style.borderTopColor = rippleColor;
ripple.style.borderBottomColor = rippleColor;
ripple.style.borderLeftColor = rippleColor;
ripple.style.borderRightColor = rippleColor;
ripple.style.borderTopWidth = 2;
ripple.style.borderBottomWidth = 2;
ripple.style.borderLeftWidth = 2;
ripple.style.borderRightWidth = 2;
2026-04-29 02:31:15 +07:00
// PointerDownEvent.localPosition đã được Unity tự động scale theo Panel
2026-04-28 11:03:57 +07:00
ripple.style.left = evt.localPosition.x;
ripple.style.top = evt.localPosition.y;
2026-04-28 10:44:22 +07:00
ripple.pickingMode = PickingMode.Ignore;
_cursorLayer.Add(ripple);
2026-04-29 01:04:28 +07:00
// Correct Fluent API for PrimeTween
Tween.Custom(Vector3.one, Vector3.one * 2.5f, duration: 0.4f,
onValueChange: val => ripple.style.scale = new StyleScale(new Scale(val)),
ease: Ease.OutQuad);
2026-04-28 11:03:57 +07:00
Tween.Custom(1f, 0f, duration: 0.4f, onValueChange: val => ripple.style.opacity = val)
2026-04-28 10:44:22 +07:00
.OnComplete(() => ripple.RemoveFromHierarchy());
}
2026-04-28 11:03:57 +07:00
private void UpdateCursorAndTrail()
2026-04-28 10:44:22 +07:00
{
2026-04-29 01:04:28 +07:00
if (!Application.isFocused || _cursorLayer == null)
2026-04-28 11:03:57 +07:00
{
if (_cursorLayer != null) _cursorLayer.style.display = DisplayStyle.None;
return;
}
Vector3 mousePos = Input.mousePosition;
bool isMouseInWindow = mousePos.x >= 0 && mousePos.x <= Screen.width && mousePos.y >= 0 && mousePos.y <= Screen.height;
if (!isMouseInWindow)
{
2026-04-29 01:04:28 +07:00
_cursorLayer.style.display = DisplayStyle.None;
2026-04-28 11:03:57 +07:00
return;
}
2026-04-28 10:44:22 +07:00
2026-04-29 01:04:28 +07:00
_cursorLayer.style.display = DisplayStyle.Flex;
2026-04-29 02:31:15 +07:00
// QUAN TRỌNG: Chia tọa độ pixel cho scale của UI để có tọa độ local chính xác
float scale = GetCurrentScale();
Vector2 uiPos = new Vector2(mousePos.x / scale, (Screen.height - mousePos.y) / scale);
2026-04-28 10:44:22 +07:00
2026-04-28 11:35:49 +07:00
float mouseSpeed = Vector2.Distance(uiPos, _lastMousePos);
_lastMousePos = uiPos;
if (mouseSpeed > 0.1f) _trailOpacity = Mathf.MoveTowards(_trailOpacity, 1f, Time.deltaTime * 5f);
else _trailOpacity = Mathf.MoveTowards(_trailOpacity, 0f, Time.deltaTime * 3f);
2026-04-28 11:03:57 +07:00
_posHistory.Insert(0, uiPos);
if (_posHistory.Count > trailLength * trailSpacing + 1)
_posHistory.RemoveAt(_posHistory.Count - 1);
2026-04-28 10:44:22 +07:00
2026-04-28 11:03:57 +07:00
if (_mainCursor != null)
2026-04-28 10:44:22 +07:00
{
2026-04-28 11:03:57 +07:00
_mainCursor.style.left = uiPos.x;
_mainCursor.style.top = uiPos.y;
2026-04-28 10:48:04 +07:00
}
2026-04-28 10:44:22 +07:00
2026-04-28 10:48:04 +07:00
for (int i = 0; i < _trailSegments.Count; i++)
{
2026-04-28 11:03:57 +07:00
int historyIndex = (i + 1) * trailSpacing;
if (historyIndex < _posHistory.Count)
{
_trailSegments[i].style.left = _posHistory[historyIndex].x;
_trailSegments[i].style.top = _posHistory[historyIndex].y;
2026-04-28 11:35:49 +07:00
float baseRatio = 1f - ((float)i / trailLength);
_trailSegments[i].style.opacity = baseRatio * 0.5f * _trailOpacity;
2026-04-28 11:03:57 +07:00
}
2026-04-28 10:44:22 +07:00
}
}
2026-04-28 00:07:42 +07:00
private void InitializeControllers()
{
2026-04-29 01:04:28 +07:00
try
{
_mainMenuController = RegisterController<MainMenuController>(mainMenuTemplate);
if (_mainMenuController != null && gameIcon != null) _mainMenuController.SetGameIcon(gameIcon);
2026-04-28 10:11:28 +07:00
2026-04-29 01:04:28 +07:00
_lobbyController = RegisterController<LobbyController>(lobbyTemplate);
if (_lobbyController != null) _lobbyController.SetRoomTemplate(roomItemTemplate);
2026-04-28 19:04:09 +07:00
2026-04-29 01:04:28 +07:00
RegisterController<ProfileController>(profileTemplate);
_settingsController = RegisterController<SettingsController>(settingsTemplate);
RegisterController<HUDController>(hudTemplate);
2026-04-28 19:04:09 +07:00
2026-04-29 01:04:28 +07:00
_loginController = RegisterController<LoginController>(loginTemplate);
}
catch (Exception e)
{
Debug.LogError($"[UIManager] Failed to initialize controllers: {e}");
}
2026-04-26 04:39:59 +07:00
}
2026-04-25 18:20:16 +07:00
2026-04-29 01:04:28 +07:00
private T RegisterController<T>(VisualTreeAsset template) where T : BaseUIController
2026-04-26 04:39:59 +07:00
{
2026-04-29 01:04:28 +07:00
if (template == null)
{
Debug.LogWarning($"[UIManager] Template for {typeof(T).Name} is missing in Inspector.");
return null;
}
if (_rootElement == null) return null;
VisualElement instance = null;
try
{
instance = template.Instantiate();
}
catch (Exception e)
{
Debug.LogError($"[UIManager] Failed to instantiate template for {typeof(T).Name}: {e.Message}");
return null;
}
if (instance == null) return null;
2026-04-28 00:07:42 +07:00
instance.style.flexGrow = 1;
instance.style.position = Position.Absolute;
instance.style.width = Length.Percent(100);
instance.style.height = Length.Percent(100);
2026-04-28 10:44:22 +07:00
instance.style.display = DisplayStyle.None;
2026-04-29 01:04:28 +07:00
2026-04-28 00:07:42 +07:00
_rootElement.Add(instance);
2026-04-28 11:03:57 +07:00
if (_cursorLayer != null) _cursorLayer.BringToFront();
2026-04-28 10:44:22 +07:00
2026-04-29 01:04:28 +07:00
var controller = ScriptableObject.CreateInstance<T>();
2026-04-28 00:07:42 +07:00
controller.Initialize(instance, this);
_controllers[typeof(T)] = controller;
return controller;
2026-04-23 23:09:54 +07:00
}
2026-04-28 00:07:42 +07:00
public async Task Push<T>() where T : BaseUIController
2026-04-26 05:02:49 +07:00
{
2026-04-28 10:21:28 +07:00
if (!_controllers.TryGetValue(typeof(T), out var newScreen)) return;
if (_history.Count > 0 && _history.Peek() == newScreen) return;
2026-04-28 10:44:22 +07:00
if (_history.Count > 0) await _history.Peek().PlayTransitionOut();
2026-04-26 04:39:59 +07:00
2026-04-28 00:07:42 +07:00
_history.Push(newScreen);
await newScreen.PlayTransitionIn();
2026-04-28 11:03:57 +07:00
if (_cursorLayer != null) _cursorLayer.BringToFront();
2026-04-26 04:39:59 +07:00
}
2026-04-28 00:07:42 +07:00
public async Task Pop()
2026-04-26 04:39:59 +07:00
{
2026-04-28 00:07:42 +07:00
if (_history.Count <= 1) return;
2026-04-28 10:44:22 +07:00
await _history.Pop().PlayTransitionOut();
if (_history.Count > 0) await _history.Peek().PlayTransitionIn();
2026-04-28 11:03:57 +07:00
if (_cursorLayer != null) _cursorLayer.BringToFront();
2026-04-23 23:09:54 +07:00
}
}
}