This commit is contained in:
2026-05-01 16:17:12 +07:00
parent d6d88a5b0e
commit fe01a55dcb
3 changed files with 223 additions and 160 deletions

View File

@@ -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<VisualElement> _trailSegments = new List<VisualElement>();
private List<Vector2> _posHistory = new List<Vector2>();
private Vector2 _lastMousePos;
private float _trailOpacity = 1f;
// Osu Trail Pooling
private const int MAX_TRAIL_PARTICLES = 60;
private readonly List<VisualElement> _trailPool = new List<VisualElement>();
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<UIDocument>();
_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<PointerDownEvent>(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<T>();
controller.Initialize(instance, this);
_controllers[typeof(T)] = controller;