diff --git a/.idea/.idea.HALLUCINATE/.idea/workspace.xml b/.idea/.idea.HALLUCINATE/.idea/workspace.xml
index b2192ac9..82314ef0 100644
--- a/.idea/.idea.HALLUCINATE/.idea/workspace.xml
+++ b/.idea/.idea.HALLUCINATE/.idea/workspace.xml
@@ -5,62 +5,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -203,7 +150,7 @@
-
+
diff --git a/Assets/Scripts/UI/UIManager.cs b/Assets/Scripts/UI/UIManager.cs
index 95894ec9..ac6c37dc 100644
--- a/Assets/Scripts/UI/UIManager.cs
+++ b/Assets/Scripts/UI/UIManager.cs
@@ -37,12 +37,11 @@ namespace Hallucinate.UI
[SerializeField] private Sprite cursorSprite;
[SerializeField] private Sprite cursorTrailSprite;
[SerializeField, Range(10f, 150f)] private float cursorSize = 40f;
- [SerializeField, Range(5, 50)] private int trailLength = 20;
- [SerializeField, Range(1, 10)] private int trailSpacing = 2;
+ [SerializeField, Range(2, 50)] private float trailDistanceThreshold = 10f;
[SerializeField] private bool enableRipples = true;
- [SerializeField] private Color rippleColor = new Color(1, 1, 1, 0.4f);
+ [SerializeField] private Color rippleColor = new Color(0, 1, 0.8f, 0.4f);
- [Header("UI Templates")]
+ [Header("UI Templates & Global Styles")]
[SerializeField] private VisualTreeAsset loginTemplate;
[SerializeField] private VisualTreeAsset mainMenuTemplate;
[SerializeField] private VisualTreeAsset lobbyTemplate;
@@ -50,24 +49,24 @@ namespace Hallucinate.UI
[SerializeField] private VisualTreeAsset profileTemplate;
[SerializeField] private VisualTreeAsset settingsTemplate;
[SerializeField] private VisualTreeAsset hudTemplate;
+ [SerializeField] private StyleSheet globalStyleSheet;
private LoginController _loginController;
private MainMenuController _mainMenuController;
private LobbyController _lobbyController;
private SettingsController _settingsController;
- private List _trailSegments = new List();
- private List _posHistory = new List();
- private Vector2 _lastMousePos;
- private float _trailOpacity = 1f;
+ // Osu Trail Pooling
+ private const int MAX_TRAIL_PARTICLES = 60;
+ private readonly List _trailPool = new List();
+ private int _currentTrailIndex = 0;
+
+ private Vector2 _lastTrailSpawnPos;
private bool _isSettingsOpen = false;
public bool IsSettingsOpen => _isSettingsOpen;
private const string UI_SCALE_KEY = "UIScale";
- [Header("Development Settings")]
- [SerializeField] private bool allowMultipleInstances = true;
-
#if UNITY_EDITOR
private void OnValidate()
{
@@ -85,11 +84,7 @@ namespace Hallucinate.UI
private void Awake()
{
- if (Instance != null && Instance != this)
- {
- Destroy(gameObject);
- return;
- }
+ if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
@@ -104,21 +99,23 @@ namespace Hallucinate.UI
{
cursorSize = PlayerPrefs.GetFloat("CursorSize", 40f);
enableRipples = PlayerPrefs.GetInt("CursorRipples", 1) == 1;
- bool trailEnabled = PlayerPrefs.GetInt("CursorTrail", 1) == 1;
- if (!trailEnabled) trailLength = 0;
}
public void SetCursorSize(float size)
{
cursorSize = size;
PlayerPrefs.SetFloat("CursorSize", size);
- SetupVirtualCursor();
+ if (_mainCursor != null)
+ {
+ _mainCursor.style.width = cursorSize;
+ _mainCursor.style.height = cursorSize;
+ }
+ foreach(var p in _trailPool) { p.style.width = cursorSize; p.style.height = cursorSize; }
}
public void SetCursorTrail(bool enabled)
{
PlayerPrefs.SetInt("CursorTrail", enabled ? 1 : 0);
- SetupVirtualCursor();
}
public void SetCursorRipples(bool enabled)
@@ -155,20 +152,34 @@ namespace Hallucinate.UI
float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f);
SetUIScale(savedScale);
}
+
private void Start()
{
if (_uiDocument == null) _uiDocument = GetComponent();
_rootElement = _uiDocument.rootVisualElement;
- _cursorLayer = new VisualElement();
- _cursorLayer.name = "CursorLayer";
+ if (globalStyleSheet != null)
+ _rootElement.panel.visualTree.styleSheets.Add(globalStyleSheet);
+
+ _cursorLayer = new VisualElement { 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);
- SetupVirtualCursor();
+ InitializeTrailPool();
+
+ _mainCursor = new VisualElement { name = "MainCursor" };
+ _mainCursor.style.position = Position.Absolute;
+ _mainCursor.style.width = cursorSize;
+ _mainCursor.style.height = cursorSize;
+ _mainCursor.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorSprite));
+
+ // Căn giữa sprite hình tròn bằng translate
+ _mainCursor.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
+ _mainCursor.pickingMode = PickingMode.Ignore;
+ _cursorLayer.Add(_mainCursor);
_rootElement.RegisterCallback(OnGlobalClick, TrickleDown.TrickleDown);
@@ -182,6 +193,24 @@ namespace Hallucinate.UI
CheckLoginStatus();
}
+ private void InitializeTrailPool()
+ {
+ for (int i = 0; i < MAX_TRAIL_PARTICLES; i++)
+ {
+ var particle = new VisualElement();
+ particle.style.position = Position.Absolute;
+ particle.style.width = cursorSize;
+ particle.style.height = cursorSize;
+ particle.style.backgroundImage = new StyleBackground(Background.FromSprite(cursorTrailSprite));
+ particle.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
+ particle.style.opacity = 0;
+ particle.style.display = DisplayStyle.None;
+ particle.pickingMode = PickingMode.Ignore;
+ _cursorLayer.Add(particle);
+ _trailPool.Add(particle);
+ }
+ }
+
private void CheckLoginStatus()
{
string savedName = PlayerPrefs.GetString("Username", "");
@@ -209,7 +238,7 @@ namespace Hallucinate.UI
{
_isSettingsOpen = true;
_settingsController.Root.BringToFront();
- _cursorLayer.BringToFront();
+ if (_cursorLayer != null) _cursorLayer.BringToFront();
await _settingsController.PlayTransitionIn();
}
else
@@ -222,47 +251,55 @@ namespace Hallucinate.UI
private void Update()
{
if (_history.Count > 0) _history.Peek().Update();
- UpdateCursorAndTrail();
+ UpdateCursorInput();
}
- private void SetupVirtualCursor()
+ private void UpdateCursorInput()
{
- if (_cursorLayer == null) return;
- _cursorLayer.Clear();
- _trailSegments.Clear();
- _posHistory.Clear();
+ if (!Application.isFocused || _cursorLayer == null) return;
- if (cursorTrailSprite != null)
+ // Dùng cách tính tọa độ thủ công để tránh offset khi pivot ở giữa
+ Vector2 mousePos = Input.mousePosition;
+ float scale = GetCurrentScale();
+ Vector2 uiPos = new Vector2(mousePos.x / scale, (Screen.height - mousePos.y) / scale);
+
+ if (_mainCursor != null)
{
- 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);
- segment.style.opacity = 0f;
- 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);
- }
+ _mainCursor.style.left = uiPos.x;
+ _mainCursor.style.top = uiPos.y;
}
- _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);
+ bool trailEnabled = PlayerPrefs.GetInt("CursorTrail", 1) == 1;
+ if (trailEnabled && cursorTrailSprite != null)
+ {
+ float dist = Vector2.Distance(uiPos, _lastTrailSpawnPos);
+ if (dist > trailDistanceThreshold)
+ {
+ SpawnPooledTrail(uiPos);
+ _lastTrailSpawnPos = uiPos;
+ }
+ }
+ }
- Vector2 startPos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y);
- _lastMousePos = startPos;
- for (int i = 0; i < trailLength * trailSpacing + 1; i++) _posHistory.Add(startPos);
+ private void SpawnPooledTrail(Vector2 pos)
+ {
+ var particle = _trailPool[_currentTrailIndex];
+ _currentTrailIndex = (_currentTrailIndex + 1) % MAX_TRAIL_PARTICLES;
+
+ Tween.StopAll(particle);
+ particle.style.display = DisplayStyle.Flex;
+ particle.style.left = pos.x;
+ particle.style.top = pos.y;
+ particle.style.opacity = 0f;
+ particle.style.scale = Vector3.one;
+
+ Sequence.Create()
+ .Group(Tween.Custom(0f, 0.6f, duration: 0.05f, onValueChange: val => particle.style.opacity = val))
+ .Chain(Tween.Custom(0.6f, 0f, duration: 0.35f, onValueChange: val => particle.style.opacity = val))
+ .Group(Tween.Custom(1f, 0.2f, duration: 0.4f, onValueChange: val => {
+ particle.style.scale = new StyleScale(new Scale(new Vector3(val, val, 1f)));
+ }))
+ .OnComplete(() => particle.style.display = DisplayStyle.None);
}
private float GetCurrentScale() => (_uiDocument != null && _uiDocument.panelSettings != null) ? _uiDocument.panelSettings.scale : 1.0f;
@@ -270,57 +307,29 @@ namespace Hallucinate.UI
private void OnGlobalClick(PointerDownEvent evt)
{
if (!enableRipples || _cursorLayer == null) return;
+
var ripple = new VisualElement();
ripple.style.position = Position.Absolute;
ripple.style.width = cursorSize;
ripple.style.height = cursorSize;
ripple.style.translate = new StyleTranslate(new Translate(Length.Percent(-50), Length.Percent(-50)));
+ ripple.style.left = evt.localPosition.x;
+ ripple.style.top = evt.localPosition.y;
+
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;
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.left = evt.localPosition.x; ripple.style.top = evt.localPosition.y;
ripple.pickingMode = PickingMode.Ignore;
+
_cursorLayer.Add(ripple);
+
Tween.Custom(Vector3.one, Vector3.one * 2.5f, duration: 0.4f, onValueChange: val => ripple.style.scale = new StyleScale(new Scale(val)), ease: Ease.OutQuad);
Tween.Custom(1f, 0f, duration: 0.4f, onValueChange: val => ripple.style.opacity = val).OnComplete(() => ripple.RemoveFromHierarchy());
}
- private void UpdateCursorAndTrail()
- {
- if (!Application.isFocused || _cursorLayer == null)
- {
- 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) { _cursorLayer.style.display = DisplayStyle.None; return; }
- _cursorLayer.style.display = DisplayStyle.Flex;
- float scale = GetCurrentScale();
- Vector2 uiPos = new Vector2(mousePos.x / scale, (Screen.height - mousePos.y) / scale);
- 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);
- _posHistory.Insert(0, uiPos);
- if (_posHistory.Count > trailLength * trailSpacing + 1) _posHistory.RemoveAt(_posHistory.Count - 1);
- if (_mainCursor != null) { _mainCursor.style.left = uiPos.x; _mainCursor.style.top = uiPos.y; }
- for (int i = 0; i < _trailSegments.Count; i++)
- {
- int historyIndex = (i + 1) * trailSpacing;
- if (historyIndex < _posHistory.Count)
- {
- _trailSegments[i].style.left = _posHistory[historyIndex].x;
- _trailSegments[i].style.top = _posHistory[historyIndex].y;
- float baseRatio = 1f - ((float)i / trailLength);
- _trailSegments[i].style.opacity = baseRatio * 0.5f * _trailOpacity;
- }
- }
- }
-
private void InitializeControllers()
{
try
@@ -346,7 +355,7 @@ namespace Hallucinate.UI
instance.style.width = Length.Percent(100); instance.style.height = Length.Percent(100);
instance.style.display = DisplayStyle.None;
_rootElement.Add(instance);
- if (_cursorLayer != null) _cursorLayer.BringToFront(); // Giữ cursor layer trên cùng của root
+ if (_cursorLayer != null) _cursorLayer.BringToFront();
var controller = ScriptableObject.CreateInstance();
controller.Initialize(instance, this);
_controllers[typeof(T)] = controller;
diff --git a/Assets/UI/Global.uss b/Assets/UI/Global.uss
index 4f86708b..7c55308b 100644
--- a/Assets/UI/Global.uss
+++ b/Assets/UI/Global.uss
@@ -1,7 +1,7 @@
/* Global Styles for Hallucinate UI */
/* ============================================================
- DESIGN TOKENS
+ DESIGN TOKENS & UNITY THEME OVERRIDES
============================================================ */
:root {
--primary-color: #ffffff;
@@ -14,6 +14,12 @@
--radius-md: 12px;
--radius-lg: 24px;
--radius-pill: 999px;
+
+ /* ÉP UNITY DÙNG MÀU CỦA GAME CHO CÁC THÀNH PHẦN NỘI BỘ (DROPDOWN POPUP) */
+ --unity-colors-surface-background: #0a0a0a;
+ --unity-colors-surface-border: #00ffcc;
+ --unity-colors-surface-text: #ffffff;
+ --unity-colors-highlight-background: rgba(0, 255, 204, 0.2);
}
/* ============================================================
@@ -143,18 +149,19 @@
/* ============================================================
DROPDOWN & FIELDS (THE BIG FIX)
============================================================ */
-.unity-dropdown-field {
+DropdownField, .unity-dropdown-field {
margin-bottom: 12px;
+ transition: 200ms;
}
-.unity-dropdown-field__label {
+DropdownField .unity-dropdown-field__label {
width: 35%;
color: #aaaaaa;
font-size: 14px;
-unity-font-style: bold;
}
-.unity-dropdown-field__input {
+DropdownField .unity-base-field__input {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 10px;
border-width: 1px;
@@ -165,50 +172,150 @@
transition: 200ms;
}
-.unity-dropdown-field__input:hover {
- background-color: rgba(255, 255, 255, 0.1);
+DropdownField:hover .unity-base-field__input {
+ background-color: rgba(255, 255, 255, 0.08);
border-color: #00ffcc;
}
/* NHẮM VÀO CÁI MENU XỔ XUỐNG (POPUP) */
-/* Cần selector cực kỳ mạnh để đè style mặc định của Unity */
+/* Cần selector cực kỳ mạnh và ghi đè toàn bộ phân cấp */
+
+/* Container chính của Popup - Lớp phủ toàn màn hình */
.unity-base-dropdown {
+ background-color: rgba(0, 0, 0, 0) !important; /* Phải để trong suốt để không tràn màn hình */
+ border-width: 0 !important;
+}
+
+/* Phần nền nội bộ của cái menu box thực tế */
+.unity-base-dropdown__container-inner {
background-color: #0a0a0a !important;
border-width: 2px !important;
border-color: #00ffcc !important;
border-radius: 12px !important;
margin-top: 10px !important;
+ padding: 5px !important;
}
-/* Đè phân cấp nội bộ của Popup */
-.unity-base-dropdown__container-inner {
- background-color: #0a0a0a !important;
- border-width: 0 !important;
-}
-
+/* Từng dòng Item */
.unity-base-dropdown__item {
padding: 12px 15px !important;
background-color: transparent !important;
color: #eeeeee !important;
}
+/* Chữ trong Item */
+.unity-base-dropdown__label {
+ color: #eeeeee !important;
+}
+
/* Đè hiệu ứng hover mặc định */
.unity-base-dropdown__item:hover {
background-color: rgba(0, 255, 204, 0.15) !important;
color: #00ffcc !important;
}
+.unity-base-dropdown__item:hover .unity-base-dropdown__label {
+ color: #00ffcc !important;
+}
+
/* Item được chọn */
.unity-base-dropdown__item--selected {
background-color: rgba(0, 255, 204, 0.1) !important;
color: #00ffcc !important;
- -unity-font-style: bold;
}
.unity-base-dropdown__checkmark {
-unity-background-image-tint-color: #00ffcc !important;
}
+/* ============================================================
+ SMART SIDEBAR (OSU STYLE OVERLAY)
+ ============================================================ */
+.sidebar-tabs-container {
+ background-color: rgba(5, 5, 5, 0.95);
+ padding-top: 60px;
+ flex-shrink: 0;
+ border-right-width: 1px;
+ border-right-color: rgba(0, 255, 204, 0.1);
+ transition: width 0.3s ease-out-quad;
+ overflow: hidden;
+ height: 100%;
+}
+
+.sidebar-collapsed {
+ width: 80px;
+}
+
+.sidebar-expanded {
+ width: 240px;
+ position: absolute;
+ z-index: 100;
+ border-right-color: #00ffcc;
+ border-right-width: 2px;
+}
+
+.sidebar-tab {
+ height: 60px;
+ background-color: transparent;
+ border-width: 0;
+ padding: 0;
+ flex-direction: row;
+ align-items: center;
+ border-left-width: 4px;
+ border-left-color: transparent;
+ margin: 2px 0;
+}
+
+.tab-icon-box {
+ width: 80px;
+ height: 60px;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.tab-icon {
+ width: 32px;
+ height: 32px;
+ background-color: #ffffff; /* Placeholder color */
+ -unity-background-image-tint-color: #ffffff;
+}
+
+.active-tab .tab-icon {
+ -unity-background-image-tint-color: #00ffcc;
+ background-color: #00ffcc;
+}
+
+.tab-label {
+ font-size: 16px;
+ -unity-font-style: bold;
+ color: #888888;
+ margin-left: 10px;
+ white-space: nowrap;
+ opacity: 0;
+ transition: opacity 0.2s;
+}
+
+.sidebar-expanded .tab-label {
+ opacity: 1;
+ color: #ffffff;
+}
+
+/* ICON-ONLY BACK BUTTON */
+.btn-icon-only {
+ width: 44px;
+ height: 44px;
+ border-radius: 22px;
+ padding: 0;
+ justify-content: center;
+ align-items: center;
+}
+
+.btn-icon-only .tab-icon {
+ width: 24px;
+ height: 24px;
+}
+
/* ============================================================
PANELS
============================================================ */