This commit is contained in:
2026-04-28 10:44:22 +07:00
parent 0ac5256b40
commit f925c61277
7 changed files with 180 additions and 62 deletions

View File

@@ -16,9 +16,11 @@ namespace Hallucinate.UI
private float _lastInteractionTime;
private const float IDLE_TIMEOUT = 5.0f;
private bool _isFirstLoad = true; // Biến cờ để nhận biết lần đầu vào game
private Tween _pulseTween;
private Tween _rotationTween;
private Texture2D _currentIcon;
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
{
@@ -34,7 +36,6 @@ namespace Hallucinate.UI
return;
}
ResetLogoPosition();
_logo.RegisterCallback<ClickEvent>(OnLogoClicked);
// Bind Buttons
@@ -44,6 +45,7 @@ namespace Hallucinate.UI
root.Q<Button>("ProfileBtn").clicked += () => uiManager.Push<ProfileController>();
root.Q<Button>("ExitBtn").clicked += () => Application.Quit();
ResetLogoPosition();
StartPulse();
_lastInteractionTime = Time.time;
}
@@ -51,6 +53,7 @@ namespace Hallucinate.UI
private void ResetLogoPosition()
{
if (_logo == null) return;
_logo.style.translate = new StyleTranslate(new Translate(0, 0));
_logo.style.left = (Screen.width / 2f) - 100;
_logo.style.top = (Screen.height / 2f) - 100;
_logo.style.width = 200;
@@ -60,6 +63,7 @@ namespace Hallucinate.UI
public void SetGameIcon(Texture2D icon)
{
if (icon == null || _logo == null) return;
_currentIcon = icon;
_logo.style.backgroundImage = icon;
var radius = new StyleLength(new Length(50, LengthUnit.Percent));
@@ -72,28 +76,45 @@ namespace Hallucinate.UI
var label = _logo.Q<Label>();
if (label != null) label.style.display = DisplayStyle.None;
StartRotation();
}
private void StartRotation()
{
if (_currentIcon == null) return;
if (_rotationTween.isAlive) _rotationTween.Stop();
_rotationTween = Tween.Custom(0f, 360f, duration: 4f, cycles: -1, ease: Ease.Linear,
onValueChange: val => _logo.style.rotate = new StyleRotate(new Rotate(Angle.Degrees(val))));
}
public override async Task PlayTransitionIn()
{
// 1. Đưa về trạng thái Idle và reset vị trí Logo về giữa trước khi hiện
_lastInteractionTime = Time.time;
_currentState = MenuState.Idle;
ResetLogoPosition();
if (_ribbon != null)
{
_ribbon.style.display = DisplayStyle.None;
_ribbon.style.opacity = 0;
}
// 2. Chạy hiệu ứng bay vào từ bên phải của Base class
// Hàm này sẽ gọi Show() và chạy Tween di chuyển toàn bộ root
// Khởi động lại rotation nếu có icon
StartRotation();
UnityEngine.Cursor.visible = true;
await base.PlayTransitionIn();
// 3. Sau khi bay vào xong, tự động kích hoạt Ribbon
TransitionToRibbon();
// Nếu không phải lần đầu load (tức là quay lại bằng nút Back), tự động bung Ribbon
if (!_isFirstLoad)
{
TransitionToRibbon();
}
else
{
_isFirstLoad = false; // Đã xong lần đầu, các lần sau sẽ tự động bung
}
}
public override async Task PlayTransitionOut()
@@ -109,33 +130,34 @@ namespace Hallucinate.UI
else _ = uiManager.Push<LobbyController>();
}
private async void TransitionToRibbon()
private void TransitionToRibbon()
{
// Tránh chạy đè nếu đang ở Ribbon rồi
if (_currentState == MenuState.Ribbon) return;
if (_currentState == MenuState.Ribbon && _ribbon.resolvedStyle.display == DisplayStyle.Flex) return;
_currentState = MenuState.Ribbon;
_lastInteractionTime = Time.time;
// Hiện Ribbon
_ribbon.style.display = DisplayStyle.Flex;
Tween.Custom(0f, 1f, duration: 0.3f, onValueChange: val => _ribbon.style.opacity = val);
// Đợi UI Toolkit cập nhật layout
await Task.Yield();
_logoSpace.RegisterCallback<GeometryChangedEvent>(OnLogoSpaceReady);
}
if (_logoSpace == null) return;
private void OnLogoSpaceReady(GeometryChangedEvent evt)
{
_logoSpace.UnregisterCallback<GeometryChangedEvent>(OnLogoSpaceReady);
Rect targetBounds = _logoSpace.worldBound;
// Di chuyển Logo vào vị trí trong Ribbon
Tween.Custom(_logo.style.left.value.value, targetBounds.x, duration: 0.5f, ease: Ease.OutQuad,
if (targetBounds.width <= 0) return;
Tween.Custom(_logo.resolvedStyle.left, targetBounds.x, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.left = val);
Tween.Custom(_logo.style.top.value.value, targetBounds.y - 35, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.top, targetBounds.y - 35, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.top = val);
Tween.Custom(_logo.style.width.value.value, 120f, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.width, 120f, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.width = val);
Tween.Custom(_logo.style.height.value.value, 120f, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.height, 120f, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.height = val);
_lastInteractionTime = Time.time;
@@ -149,13 +171,13 @@ namespace Hallucinate.UI
float targetX = (Screen.width / 2f) - 100;
float targetY = (Screen.height / 2f) - 100;
Tween.Custom(_logo.style.left.value.value, targetX, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.left, targetX, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.left = val);
Tween.Custom(_logo.style.top.value.value, targetY, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.top, targetY, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.top = val);
Tween.Custom(_logo.style.width.value.value, 200f, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.width, 200f, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.width = val);
Tween.Custom(_logo.style.height.value.value, 200f, duration: 0.5f, ease: Ease.OutQuad,
Tween.Custom(_logo.resolvedStyle.height, 200f, duration: 0.5f, ease: Ease.OutQuad,
onValueChange: val => _logo.style.height = val);
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _ribbon.style.opacity = val)
@@ -164,6 +186,11 @@ namespace Hallucinate.UI
public void Update()
{
if (Input.GetAxis("Mouse X") != 0 || Input.GetAxis("Mouse Y") != 0 || Input.anyKey)
{
_lastInteractionTime = Time.time;
}
if (_currentState == MenuState.Ribbon && Time.time - _lastInteractionTime > IDLE_TIMEOUT)
{
TransitionToIdle();

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using PrimeTween;
#if UNITY_EDITOR
using UnityEditor;
#endif
@@ -16,6 +17,7 @@ namespace Hallucinate.UI
private UIDocument _uiDocument;
private VisualElement _rootElement;
private VisualElement _cursorLayer; // Lớp trên cùng chứa trail và ripples
private readonly Dictionary<Type, BaseUIController> _controllers = new Dictionary<Type, BaseUIController>();
private readonly Stack<BaseUIController> _history = new Stack<BaseUIController>();
@@ -23,6 +25,13 @@ namespace Hallucinate.UI
[Header("Game Metadata")]
[SerializeField] private Texture2D gameIcon;
[Header("Cursor & Effects Settings")]
[SerializeField] private Sprite cursorTrailSprite;
[SerializeField, Range(10f, 100f)] private float cursorSize = 30f;
[SerializeField, Range(5, 30)] private int trailLength = 15;
[SerializeField] private bool enableRipples = true;
[SerializeField] private Color rippleColor = new Color(1, 1, 1, 0.5f);
[Header("UI Templates")]
[SerializeField] private VisualTreeAsset mainMenuTemplate;
[SerializeField] private VisualTreeAsset lobbyTemplate;
@@ -31,6 +40,7 @@ namespace Hallucinate.UI
[SerializeField] private VisualTreeAsset hudTemplate;
private MainMenuController _mainMenuController;
private List<VisualElement> _trailSegments = new List<VisualElement>();
private void Awake()
{
@@ -48,6 +58,20 @@ namespace Hallucinate.UI
_uiDocument = GetComponent<UIDocument>();
_rootElement = _uiDocument.rootVisualElement;
// Tạo lớp Cursor trên cùng
_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);
SetupCursorTrail();
// Đăng ký ripple toàn cục
_rootElement.RegisterCallback<PointerDownEvent>(OnGlobalClick, TrickleDown.TrickleDown);
#if UNITY_EDITOR
if (gameIcon == null)
{
@@ -59,6 +83,91 @@ namespace Hallucinate.UI
InitializeControllers();
}
private void SetupCursorTrail()
{
if (cursorTrailSprite == null) return;
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(cursorTrailSprite);
segment.style.opacity = 1f - ((float)i / trailLength);
segment.style.scale = new StyleScale(new Vector2(1f - ((float)i / trailLength), 1f - ((float)i / trailLength)));
segment.pickingMode = PickingMode.Ignore;
_cursorLayer.Add(segment);
_trailSegments.Add(segment);
}
}
private void OnGlobalClick(PointerDownEvent evt)
{
if (!enableRipples) return;
var ripple = new VisualElement();
ripple.style.position = Position.Absolute;
ripple.style.width = cursorSize;
ripple.style.height = cursorSize;
ripple.style.borderTopLeftRadius = cursorSize;
ripple.style.borderTopRightRadius = cursorSize;
ripple.style.borderBottomLeftRadius = cursorSize;
ripple.style.borderBottomRightRadius = cursorSize;
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;
ripple.style.left = evt.localPosition.x - (cursorSize / 2);
ripple.style.top = evt.localPosition.y - (cursorSize / 2);
ripple.pickingMode = PickingMode.Ignore;
_cursorLayer.Add(ripple);
// Hiệu ứng Ripple: To ra và nhạt dần (Sửa lỗi dùng Vector3 thay vì float)
Tween.Scale(ripple.transform, Vector3.one * 3f, duration: 0.5f, ease: Ease.OutQuad);
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => ripple.style.opacity = val)
.OnComplete(() => ripple.RemoveFromHierarchy());
}
private void Update()
{
_mainMenuController?.Update();
UpdateTrail();
}
private void UpdateTrail()
{
if (_trailSegments.Count == 0) return;
Vector2 mousePos = Input.mousePosition;
Vector2 uiPos = new Vector2(mousePos.x, Screen.height - mousePos.y);
// Segment đầu tiên đi theo chuột
_trailSegments[0].style.left = uiPos.x - (cursorSize / 2);
_trailSegments[0].style.top = uiPos.y - (cursorSize / 2);
// Các segment sau đuổi theo segment trước
for (int i = 1; i < _trailSegments.Count; i++)
{
float targetX = _trailSegments[i - 1].resolvedStyle.left;
float targetY = _trailSegments[i - 1].resolvedStyle.top;
float currX = _trailSegments[i].resolvedStyle.left;
float currY = _trailSegments[i].resolvedStyle.top;
// Nội suy để mượt mà (Lerp)
_trailSegments[i].style.left = Mathf.Lerp(currX, targetX, Time.deltaTime * 20f);
_trailSegments[i].style.top = Mathf.Lerp(currY, targetY, Time.deltaTime * 20f);
}
}
private void InitializeControllers()
{
_mainMenuController = RegisterController<MainMenuController>(mainMenuTemplate);
@@ -72,7 +181,6 @@ namespace Hallucinate.UI
RegisterController<SettingsController>(settingsTemplate);
RegisterController<HUDController>(hudTemplate);
// Khởi động màn hình đầu tiên
_ = Push<MainMenuController>();
}
@@ -85,9 +193,12 @@ namespace Hallucinate.UI
instance.style.position = Position.Absolute;
instance.style.width = Length.Percent(100);
instance.style.height = Length.Percent(100);
instance.style.display = DisplayStyle.None; // Ẩn mặc định
instance.style.display = DisplayStyle.None;
_rootElement.Add(instance);
// Luôn đảm bảo CursorLayer nằm trên cùng sau khi add màn hình mới
_cursorLayer.BringToFront();
var controller = new T();
controller.Initialize(instance, this);
_controllers[typeof(T)] = controller;
@@ -95,41 +206,23 @@ namespace Hallucinate.UI
return controller;
}
private void Update()
{
_mainMenuController?.Update();
// Update các controller khác nếu cần
}
public async Task Push<T>() where T : BaseUIController
{
if (!_controllers.TryGetValue(typeof(T), out var newScreen)) return;
// Nếu màn hình mới chính là màn hình đang hiện, không làm gì cả
if (_history.Count > 0 && _history.Peek() == newScreen) return;
if (_history.Count > 0)
{
var currentScreen = _history.Peek();
await currentScreen.PlayTransitionOut();
}
if (_history.Count > 0) await _history.Peek().PlayTransitionOut();
_history.Push(newScreen);
await newScreen.PlayTransitionIn();
_cursorLayer.BringToFront(); // Giữ trail luôn trên cùng
}
public async Task Pop()
{
if (_history.Count <= 1) return;
var currentScreen = _history.Pop();
await currentScreen.PlayTransitionOut();
if (_history.Count > 0)
{
var previousScreen = _history.Peek();
await previousScreen.PlayTransitionIn();
}
await _history.Pop().PlayTransitionOut();
if (_history.Count > 0) await _history.Peek().PlayTransitionIn();
_cursorLayer.BringToFront();
}
}
}