Update
This commit is contained in:
44
Assets/Scripts/UI/MouseMetricsHelper.cs
Normal file
44
Assets/Scripts/UI/MouseMetricsHelper.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Hallucinate.UI
|
||||
{
|
||||
public static class MouseMetricsHelper
|
||||
{
|
||||
private static int _reportCount = 0;
|
||||
private static float _timer = 0f;
|
||||
private static int _lastPollingRate = 0;
|
||||
private static float _lastLatency = 0f;
|
||||
private static Vector2 _lastMousePos;
|
||||
|
||||
public static (int pollingRate, float latencyMs) GetMetrics()
|
||||
{
|
||||
Update();
|
||||
return (_lastPollingRate, _lastLatency);
|
||||
}
|
||||
|
||||
private static void Update()
|
||||
{
|
||||
_timer += Time.unscaledDeltaTime;
|
||||
|
||||
// Count "reports" based on position changes (simulated polling rate)
|
||||
Vector2 currentPos = Input.mousePosition;
|
||||
if (currentPos != _lastMousePos)
|
||||
{
|
||||
_reportCount++;
|
||||
_lastMousePos = currentPos;
|
||||
}
|
||||
|
||||
if (_timer >= 1f)
|
||||
{
|
||||
_lastPollingRate = (int)(_reportCount / _timer);
|
||||
_reportCount = 0;
|
||||
_timer = 0f;
|
||||
}
|
||||
|
||||
// Dummy latency simulation (Input System doesn't expose raw hardware latency easily)
|
||||
// In a real scenario, this would involve timestamping HID reports
|
||||
_lastLatency = (1.0f / (Screen.currentResolution.refreshRateRatio.value > 0 ? (float)Screen.currentResolution.refreshRateRatio.value : 60f)) * 1000f;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/MouseMetricsHelper.cs.meta
Normal file
2
Assets/Scripts/UI/MouseMetricsHelper.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff50ca9a6ee819b48b273986e55f2772
|
||||
76
Assets/Scripts/UI/PerformanceOverlay.cs
Normal file
76
Assets/Scripts/UI/PerformanceOverlay.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Hallucinate.UI
|
||||
{
|
||||
public class PerformanceOverlay : MonoBehaviour
|
||||
{
|
||||
private static PerformanceOverlay _instance;
|
||||
private Label _fpsLabel;
|
||||
private UIDocument _uiDocument;
|
||||
private VisualElement _root;
|
||||
|
||||
private float _deltaTime = 0f;
|
||||
private bool _isVisible = false;
|
||||
|
||||
public static void SetVisible(bool visible)
|
||||
{
|
||||
if (_instance == null && visible)
|
||||
{
|
||||
var go = new GameObject("PerformanceOverlay");
|
||||
_instance = go.AddComponent<PerformanceOverlay>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
_instance._isVisible = visible;
|
||||
if (_instance._root != null)
|
||||
{
|
||||
_instance._root.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_uiDocument = gameObject.AddComponent<UIDocument>();
|
||||
// Use same panel settings as UIManager if possible, or default
|
||||
_uiDocument.panelSettings = Resources.Load<PanelSettings>("UI/PerformancePanelSettings");
|
||||
|
||||
_root = new VisualElement();
|
||||
_root.style.position = Position.Absolute;
|
||||
_root.style.bottom = 10;
|
||||
_root.style.left = 10;
|
||||
_root.pickingMode = PickingMode.Ignore;
|
||||
|
||||
_fpsLabel = new Label("0 FPS (0.0ms)");
|
||||
_fpsLabel.style.color = Color.white;
|
||||
_fpsLabel.style.fontSize = 14;
|
||||
_fpsLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
// Shadow effect
|
||||
_fpsLabel.style.textShadow = new TextShadow { offset = new Vector2(1, 1), blurRadius = 1, color = Color.black };
|
||||
|
||||
_root.Add(_fpsLabel);
|
||||
_uiDocument.rootVisualElement.Add(_root);
|
||||
|
||||
_root.style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_isVisible) return;
|
||||
|
||||
_deltaTime += (Time.unscaledDeltaTime - _deltaTime) * 0.1f;
|
||||
float fps = 1.0f / _deltaTime;
|
||||
float ms = _deltaTime * 1000.0f;
|
||||
|
||||
_fpsLabel.text = $"{Mathf.Ceil(fps)} FPS ({ms:F1}ms)";
|
||||
|
||||
// Color coding based on performance
|
||||
if (fps < 30) _fpsLabel.style.color = Color.red;
|
||||
else if (fps < 55) _fpsLabel.style.color = Color.yellow;
|
||||
else _fpsLabel.style.color = Color.green;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UI/PerformanceOverlay.cs.meta
Normal file
2
Assets/Scripts/UI/PerformanceOverlay.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e018e7d76dda004e89cd989a7413168
|
||||
@@ -1,12 +1,12 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.InputSystem;
|
||||
using PrimeTween;
|
||||
using System;
|
||||
using UnityEngine.Audio;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.SceneManagement;
|
||||
using OnlyScove.Scripts;
|
||||
using Hallucinate.Audio;
|
||||
|
||||
namespace Hallucinate.UI
|
||||
{
|
||||
@@ -15,11 +15,24 @@ namespace Hallucinate.UI
|
||||
private VisualElement _sidebar;
|
||||
private Label _tabTitle;
|
||||
private ScrollView _content;
|
||||
private InputActionAsset _inputActions;
|
||||
|
||||
private Dictionary<string, Button> _tabButtons = new Dictionary<string, Button>();
|
||||
private string _activeTab = "GENERAL";
|
||||
|
||||
// Advanced Mouse Metrics
|
||||
private Label _mouseMetricsLabel;
|
||||
|
||||
// FPS State
|
||||
private bool _fpsVisible;
|
||||
|
||||
// Hover Tracking for Arrow Key Slider Control
|
||||
private Slider _hoveredSlider;
|
||||
private float _sliderMin, _sliderMax;
|
||||
|
||||
// Osu-style Volume Overlay
|
||||
private VisualElement _volumeOverlay;
|
||||
private Label _masterVolLabel;
|
||||
private float _masterVol = 80f;
|
||||
|
||||
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
|
||||
{
|
||||
base.Initialize(uxmlRoot, manager);
|
||||
@@ -28,19 +41,17 @@ namespace Hallucinate.UI
|
||||
_tabTitle = root.Q<Label>("TabTitle");
|
||||
_content = root.Q<ScrollView>("SettingsContent");
|
||||
|
||||
_inputActions = uiManager.InputReader?.InputActions;
|
||||
if (_inputActions == null)
|
||||
{
|
||||
_inputActions = GameObject.FindAnyObjectByType<PlayerInput>()?.actions;
|
||||
}
|
||||
// Osu Volume Logic - Registering on Root for Global Wheel Catch
|
||||
root.RegisterCallback<WheelEvent>(OnMouseWheel);
|
||||
SetupVolumeOverlay();
|
||||
|
||||
root.RegisterCallback<PointerDownEvent>(evt => {
|
||||
if (evt.target == root)
|
||||
{
|
||||
uiManager.ToggleSettings();
|
||||
}
|
||||
if (evt.target == root) uiManager.ToggleSettings();
|
||||
});
|
||||
|
||||
// Keyboard navigation for sliders
|
||||
root.RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
|
||||
SetupTab("GeneralTab", "GENERAL");
|
||||
SetupTab("VideoTab", "VIDEO");
|
||||
SetupTab("SoundTab", "SOUND");
|
||||
@@ -49,9 +60,76 @@ namespace Hallucinate.UI
|
||||
var closeBtn = root.Q<Button>("CloseSettingsBtn");
|
||||
if (closeBtn != null) closeBtn.clicked += () => uiManager.ToggleSettings();
|
||||
|
||||
_masterVol = PlayerPrefs.GetFloat("MasterVolume", 80f);
|
||||
|
||||
SwitchTab("GENERAL");
|
||||
}
|
||||
|
||||
private void SetupVolumeOverlay()
|
||||
{
|
||||
_volumeOverlay = new VisualElement();
|
||||
_volumeOverlay.style.position = Position.Absolute;
|
||||
_volumeOverlay.style.right = 40;
|
||||
_volumeOverlay.style.top = Length.Percent(40);
|
||||
_volumeOverlay.style.width = 120;
|
||||
_volumeOverlay.style.height = 120;
|
||||
_volumeOverlay.style.backgroundColor = new Color(0, 0, 0, 0.8f);
|
||||
_volumeOverlay.style.borderTopLeftRadius = 60;
|
||||
_volumeOverlay.style.borderTopRightRadius = 60;
|
||||
_volumeOverlay.style.borderBottomLeftRadius = 60;
|
||||
_volumeOverlay.style.borderBottomRightRadius = 60;
|
||||
_volumeOverlay.style.borderTopWidth = 4;
|
||||
_volumeOverlay.style.borderBottomWidth = 4;
|
||||
_volumeOverlay.style.borderLeftWidth = 4;
|
||||
_volumeOverlay.style.borderRightWidth = 4;
|
||||
_volumeOverlay.style.borderTopColor = Color.cyan;
|
||||
_volumeOverlay.style.borderBottomColor = Color.cyan;
|
||||
_volumeOverlay.style.borderLeftColor = Color.cyan;
|
||||
_volumeOverlay.style.borderRightColor = Color.cyan;
|
||||
_volumeOverlay.style.justifyContent = Justify.Center;
|
||||
_volumeOverlay.style.alignItems = Align.Center;
|
||||
_volumeOverlay.style.display = DisplayStyle.None;
|
||||
_volumeOverlay.pickingMode = PickingMode.Ignore;
|
||||
|
||||
_masterVolLabel = new Label("80%");
|
||||
_masterVolLabel.style.color = Color.white;
|
||||
_masterVolLabel.style.fontSize = 24;
|
||||
_masterVolLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_volumeOverlay.Add(_masterVolLabel);
|
||||
|
||||
root.Add(_volumeOverlay);
|
||||
}
|
||||
|
||||
private void OnMouseWheel(WheelEvent evt)
|
||||
{
|
||||
// Osu style: Volume control with scroll wheel
|
||||
// Only control master if not hovering a specific sound slider
|
||||
if (_activeTab == "SOUND" && _hoveredSlider != null) return;
|
||||
|
||||
UpdateMasterVolume(-evt.delta.y * 2f);
|
||||
}
|
||||
|
||||
private void UpdateMasterVolume(float delta)
|
||||
{
|
||||
_masterVol = Mathf.Clamp(_masterVol + delta, 0f, 100f);
|
||||
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
|
||||
|
||||
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
|
||||
ShowVolumeOverlay();
|
||||
}
|
||||
|
||||
private async void ShowVolumeOverlay()
|
||||
{
|
||||
_volumeOverlay.style.display = DisplayStyle.Flex;
|
||||
_volumeOverlay.style.opacity = 1f;
|
||||
await Task.Delay(1500);
|
||||
if (_volumeOverlay.style.opacity == 1f)
|
||||
{
|
||||
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _volumeOverlay.style.opacity = val)
|
||||
.OnComplete(() => _volumeOverlay.style.display = DisplayStyle.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTab(string btnName, string tabId)
|
||||
{
|
||||
var btn = root.Q<Button>(btnName);
|
||||
@@ -74,135 +152,176 @@ namespace Hallucinate.UI
|
||||
}
|
||||
|
||||
_content.Clear();
|
||||
_hoveredSlider = null;
|
||||
|
||||
switch (tabId)
|
||||
{
|
||||
case "GENERAL":
|
||||
RenderGeneralSettings();
|
||||
break;
|
||||
case "CONTROL":
|
||||
RenderControlSettings();
|
||||
break;
|
||||
default:
|
||||
var comingSoon = new Label($"Settings for {tabId} coming soon...");
|
||||
comingSoon.AddToClassList("text-body");
|
||||
_content.Add(comingSoon);
|
||||
break;
|
||||
case "GENERAL": RenderGeneralTab(); break;
|
||||
case "VIDEO": RenderVideoTab(); break;
|
||||
case "SOUND": RenderSoundTab(); break;
|
||||
case "CONTROL": RenderControlTab(); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderGeneralSettings()
|
||||
#region GENERAL TAB
|
||||
private void RenderGeneralTab()
|
||||
{
|
||||
// --- ACCOUNT ---
|
||||
_content.Add(CreateSection(GetLoc("settings_general")));
|
||||
|
||||
var wipeBtn = new Button { text = "WIPE ALL USER DATA (TEST ONLY)" };
|
||||
wipeBtn.AddToClassList("button-spring");
|
||||
wipeBtn.AddToClassList("btn-exit");
|
||||
wipeBtn.style.marginTop = 10;
|
||||
wipeBtn.style.backgroundColor = new Color(0.8f, 0.2f, 0.2f, 0.8f);
|
||||
|
||||
wipeBtn.clicked += () => {
|
||||
bool confirm = true;
|
||||
#if UNITY_EDITOR
|
||||
confirm = UnityEditor.EditorUtility.DisplayDialog("Wipe Data", "This will delete your local username and restart the game. Proceed?", "Yes", "Cancel");
|
||||
#endif
|
||||
if (confirm)
|
||||
{
|
||||
PlayerPrefs.DeleteAll();
|
||||
PlayerPrefs.Save();
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
||||
}
|
||||
};
|
||||
_content.Add(wipeBtn);
|
||||
_content.Add(CreateSection("ACCOUNT"));
|
||||
string username = PlayerPrefs.GetString("Username", "Guest");
|
||||
var userRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 10 } };
|
||||
userRow.Add(new Label("Logged in as: ") { className = "text-body" });
|
||||
userRow.Add(new Label(username) { style = { color = Color.cyan, marginLeft = 5, unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
_content.Add(userRow);
|
||||
|
||||
// --- LANGUAGE ---
|
||||
_content.Add(CreateSection(GetLoc("label_language")));
|
||||
var langContainer = new VisualElement();
|
||||
langContainer.style.flexDirection = FlexDirection.Row;
|
||||
langContainer.style.alignItems = Align.Center;
|
||||
langContainer.style.marginTop = 10;
|
||||
|
||||
var langLabel = new Label(GetLoc("label_language"));
|
||||
langLabel.AddToClassList("text-body");
|
||||
langLabel.style.width = Length.Percent(40);
|
||||
|
||||
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" }, LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
|
||||
langDropdown.style.flexGrow = 1;
|
||||
_content.Add(CreateSection("LANGUAGE"));
|
||||
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" },
|
||||
LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
|
||||
langDropdown.AddToClassList("custom-dropdown");
|
||||
langDropdown.RegisterValueChangedCallback(evt => {
|
||||
string code = evt.newValue == "Tiếng Việt" ? "vi" : "en";
|
||||
LocalizationManager.Instance?.LoadLanguage(code);
|
||||
// Refresh current tab to update text
|
||||
LocalizationManager.Instance?.LoadLanguage(evt.newValue == "Tiếng Việt" ? "vi" : "en");
|
||||
SwitchTab("GENERAL");
|
||||
});
|
||||
_content.Add(langDropdown);
|
||||
|
||||
langContainer.Add(langLabel);
|
||||
langContainer.Add(langDropdown);
|
||||
_content.Add(langContainer);
|
||||
_content.Add(CreateSection("UPDATES"));
|
||||
var versionBox = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
||||
versionBox.Add(new Label($"Version: {Application.version}") { className = "text-body" });
|
||||
var checkBtn = new Button { text = "CHECK FOR UPDATES" };
|
||||
checkBtn.AddToClassList("button-spring");
|
||||
checkBtn.clicked += () => checkBtn.text = "UP TO DATE";
|
||||
versionBox.Add(checkBtn);
|
||||
_content.Add(versionBox);
|
||||
|
||||
// --- INTERFACE ---
|
||||
_content.Add(CreateSection("INTERFACE"));
|
||||
_content.Add(CreateSection("CURSOR & MOUSE"));
|
||||
_content.Add(CreateSliderWithInput("Cursor Size", 10, 150, PlayerPrefs.GetFloat("CursorSize", 40), val => PlayerPrefs.SetFloat("CursorSize", val)));
|
||||
|
||||
float currentScale = PlayerPrefs.GetFloat("UIScale", 1.0f);
|
||||
var scaleRow = CreateSliderWithInput("UI SCALE", 0.5f, 2.0f, currentScale, (val) => {
|
||||
uiManager.SetUIScale(val);
|
||||
var trailToggle = new Toggle("Enable Cursor Trail") { value = PlayerPrefs.GetInt("CursorTrail", 1) == 1 };
|
||||
trailToggle.RegisterValueChangedCallback(evt => PlayerPrefs.SetInt("CursorTrail", evt.newValue ? 1 : 0));
|
||||
_content.Add(trailToggle);
|
||||
|
||||
var rippleToggle = new Toggle("Enable Ripple Effects") { value = PlayerPrefs.GetInt("CursorRipples", 1) == 1 };
|
||||
rippleToggle.RegisterValueChangedCallback(evt => PlayerPrefs.SetInt("CursorRipples", evt.newValue ? 1 : 0));
|
||||
_content.Add(rippleToggle);
|
||||
|
||||
_content.Add(CreateSliderWithInput("Sensitivity", 0.1f, 5.0f, PlayerPrefs.GetFloat("MouseSensitivity", 1.0f), val => PlayerPrefs.SetFloat("MouseSensitivity", val)));
|
||||
|
||||
var rawInputToggle = new Toggle("Raw Input (Bypass Acceleration)") { value = true };
|
||||
_content.Add(rawInputToggle);
|
||||
|
||||
_mouseMetricsLabel = new Label("[(report: 0/sec latency: 0ms)]") { style = { fontSize = 11, color = Color.gray, marginTop = 5 } };
|
||||
_content.Add(_mouseMetricsLabel);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VIDEO TAB
|
||||
private void RenderVideoTab()
|
||||
{
|
||||
_content.Add(CreateSection("RENDERER"));
|
||||
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, 2);
|
||||
frameLimit.RegisterValueChangedCallback(evt => {
|
||||
switch (evt.newValue) {
|
||||
case "VSync": QualitySettings.vSyncCount = 1; Application.targetFrameRate = -1; break;
|
||||
case "Power Saving": QualitySettings.vSyncCount = 0; Application.targetFrameRate = 60; break;
|
||||
case "Optimal": QualitySettings.vSyncCount = 0; Application.targetFrameRate = 144; break;
|
||||
case "Unlimited": QualitySettings.vSyncCount = 0; Application.targetFrameRate = 999; break;
|
||||
}
|
||||
});
|
||||
_content.Add(scaleRow);
|
||||
_content.Add(frameLimit);
|
||||
|
||||
var fpsToggle = new Toggle("Show FPS Counter") { value = _fpsVisible };
|
||||
fpsToggle.RegisterValueChangedCallback(evt => { _fpsVisible = evt.newValue; PerformanceOverlay.SetVisible(_fpsVisible); });
|
||||
_content.Add(fpsToggle);
|
||||
|
||||
_content.Add(CreateSection("LAYOUT"));
|
||||
Resolution native = Screen.currentResolution;
|
||||
var resList = Screen.resolutions.Select(r => $"{r.width}x{r.height}").Distinct().Select(s => s == $"{native.width}x{native.height}" ? s + " (native)" : s).ToList();
|
||||
var resDropdown = new DropdownField("Resolution", resList, resList.FindIndex(s => s.Contains("native")));
|
||||
resDropdown.RegisterValueChangedCallback(evt => {
|
||||
string[] parts = evt.newValue.Split(' ')[0].Split('x');
|
||||
Screen.SetResolution(int.Parse(parts[0]), int.Parse(parts[1]), Screen.fullScreen);
|
||||
});
|
||||
_content.Add(resDropdown);
|
||||
|
||||
var fullToggle = new Toggle("Fullscreen Mode") { value = Screen.fullScreen };
|
||||
fullToggle.RegisterValueChangedCallback(evt => Screen.fullScreen = evt.newValue);
|
||||
_content.Add(fullToggle);
|
||||
|
||||
_content.Add(CreateSliderWithInput("Background Dim", 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => PlayerPrefs.SetFloat("BackgroundDim", val)));
|
||||
_content.Add(CreateSliderWithInput("UI Scale", 0.5f, 2.0f, PlayerPrefs.GetFloat("UIScale", 1.0f), val => uiManager.SetUIScale(val)));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SOUND TAB
|
||||
private void RenderSoundTab()
|
||||
{
|
||||
_content.Add(CreateSection("AUDIO VOLUMES"));
|
||||
_content.Add(CreateAudioSlider("Master", "MasterVolume"));
|
||||
_content.Add(CreateAudioSlider("Music", "MusicVolume"));
|
||||
_content.Add(CreateAudioSlider("VFX", "VFXVolume"));
|
||||
_content.Add(CreateAudioSlider("Player", "PlayerVolume"));
|
||||
_content.Add(CreateAudioSlider("UI", "UIVolume"));
|
||||
|
||||
_content.Add(new Label("Use Scroll Wheel to control volume.") { style = { marginTop = 20, color = Color.gray, fontSize = 12 } });
|
||||
}
|
||||
|
||||
private VisualElement CreateAudioSlider(string label, string prefKey)
|
||||
{
|
||||
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => PlayerPrefs.SetFloat(prefKey, val));
|
||||
|
||||
_content.Add(new Label("\nNote: Some elements may require restart to align perfectly.") {
|
||||
style = { fontSize = 12, color = new Color(0.6f, 0.6f, 0.6f), whiteSpace = WhiteSpace.Normal, marginTop = 20 }
|
||||
// Register wheel specifically on this row
|
||||
sliderRow.RegisterCallback<WheelEvent>(evt => {
|
||||
float current = PlayerPrefs.GetFloat(prefKey, 80f);
|
||||
float newVal = Mathf.Clamp(current - (evt.delta.y * 2f), 0f, 100f);
|
||||
PlayerPrefs.SetFloat(prefKey, newVal);
|
||||
|
||||
// Visual update only (to avoid heavy re-render of whole list)
|
||||
var slider = sliderRow.Q<Slider>();
|
||||
if (slider != null) slider.value = newVal;
|
||||
});
|
||||
|
||||
return sliderRow;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CONTROL TAB
|
||||
private void RenderControlTab()
|
||||
{
|
||||
_content.Add(CreateSection("KEY BINDINGS"));
|
||||
_content.Add(new Label("Controls Implementation Pending context.") { className = "text-body" });
|
||||
}
|
||||
#endregion
|
||||
|
||||
private VisualElement CreateSection(string title)
|
||||
{
|
||||
var label = new Label(title);
|
||||
label.AddToClassList("setting-section-header");
|
||||
label.style.marginTop = 20;
|
||||
return label;
|
||||
}
|
||||
|
||||
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.marginTop = 10;
|
||||
row.style.marginBottom = 10;
|
||||
|
||||
var label = new Label(labelText);
|
||||
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 5, marginBottom = 5 } };
|
||||
var label = new Label(labelText) { style = { width = Length.Percent(35) } };
|
||||
label.AddToClassList("text-body");
|
||||
label.style.width = Length.Percent(35);
|
||||
|
||||
var slider = new Slider(min, max);
|
||||
slider.style.flexGrow = 1;
|
||||
slider.value = startVal;
|
||||
|
||||
var input = new TextField();
|
||||
input.style.width = 60;
|
||||
input.style.marginLeft = 15;
|
||||
input.value = startVal.ToString("F1");
|
||||
var slider = new Slider(min, max) { value = startVal, style = { flexGrow = 1 } };
|
||||
var input = new TextField { value = startVal.ToString("F1"), style = { width = 50, marginLeft = 10 } };
|
||||
input.AddToClassList("input-field");
|
||||
input.style.marginBottom = 0; // Override default margin
|
||||
input.style.height = 30;
|
||||
input.style.fontSize = 14;
|
||||
|
||||
// Sync Slider -> Input
|
||||
slider.RegisterCallback<PointerEnterEvent>(evt => { _hoveredSlider = slider; _sliderMin = min; _sliderMax = max; });
|
||||
slider.RegisterCallback<PointerLeaveEvent>(evt => { if (_hoveredSlider == slider) _hoveredSlider = null; });
|
||||
|
||||
slider.RegisterValueChangedCallback(evt => {
|
||||
float val = Mathf.Round(evt.newValue * 10f) / 10f;
|
||||
// Kiểm tra xem input có đang được focus không để tránh ghi đè khi người dùng đang nhập
|
||||
bool isInputFocused = input.panel?.focusController?.focusedElement == input.ElementAt(0);
|
||||
if (!isInputFocused) input.value = val.ToString("F1");
|
||||
if (input.panel?.focusController?.focusedElement != input.ElementAt(0)) input.value = val.ToString("F1");
|
||||
OnValueChanged?.Invoke(val);
|
||||
});
|
||||
|
||||
// Sync Input -> Slider
|
||||
input.RegisterValueChangedCallback(evt => {
|
||||
if (float.TryParse(evt.newValue, out float val))
|
||||
{
|
||||
val = Mathf.Clamp(val, min, max);
|
||||
slider.value = val;
|
||||
OnValueChanged?.Invoke(val);
|
||||
}
|
||||
});
|
||||
|
||||
// Format on blur
|
||||
input.RegisterCallback<BlurEvent>(evt => {
|
||||
if (float.TryParse(input.value, out float val))
|
||||
{
|
||||
input.value = Mathf.Clamp(val, min, max).ToString("F1");
|
||||
if (float.TryParse(evt.newValue, out float val)) {
|
||||
slider.value = Mathf.Clamp(val, min, max);
|
||||
OnValueChanged?.Invoke(slider.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -212,111 +331,33 @@ namespace Hallucinate.UI
|
||||
return row;
|
||||
}
|
||||
|
||||
private void RenderControlSettings()
|
||||
private void OnKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
if (_inputActions == null)
|
||||
if (_hoveredSlider == null) return;
|
||||
float step = (_sliderMax - _sliderMin) / 100f;
|
||||
if (evt.keyCode == KeyCode.LeftArrow) _hoveredSlider.value -= step;
|
||||
if (evt.keyCode == KeyCode.RightArrow) _hoveredSlider.value += step;
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (_activeTab == "GENERAL" && _mouseMetricsLabel != null)
|
||||
{
|
||||
var errorLabel = new Label("Input Actions not found. Cannot rebind.");
|
||||
errorLabel.AddToClassList("text-body");
|
||||
_content.Add(errorLabel);
|
||||
return;
|
||||
var (polling, latency) = MouseMetricsHelper.GetMetrics();
|
||||
_mouseMetricsLabel.text = $"[(report: {polling}/sec latency: {latency:F0}ms)]";
|
||||
}
|
||||
|
||||
var playerMap = _inputActions.FindActionMap("Player");
|
||||
if (playerMap == null) return;
|
||||
|
||||
RenderSection("MOVEMENT", playerMap, new[] { "Move", "Jump", "Sprint", "Crouch" });
|
||||
RenderSection("COMBAT", playerMap, new[] { "Attack" });
|
||||
RenderSection("INTERACTION", playerMap, new[] { "Interact", "Next", "Previous" });
|
||||
RenderSection("VIEW", playerMap, new[] { "Look", "ToggleView", "Scroll" });
|
||||
}
|
||||
|
||||
private void RenderSection(string header, InputActionMap map, string[] actionNames)
|
||||
{
|
||||
_content.Add(CreateSection(header));
|
||||
foreach (var name in actionNames)
|
||||
{
|
||||
var action = map.FindAction(name);
|
||||
if (action != null) _content.Add(CreateRebindRow(action));
|
||||
}
|
||||
}
|
||||
|
||||
private VisualElement CreateSection(string title)
|
||||
{
|
||||
var header = new Label(title);
|
||||
header.AddToClassList("setting-section-header");
|
||||
return header;
|
||||
}
|
||||
|
||||
private VisualElement CreateRebindRow(InputAction action)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.AddToClassList("rebind-row");
|
||||
|
||||
var label = new Label(action.name.ToUpper());
|
||||
label.AddToClassList("rebind-label");
|
||||
|
||||
var rebindBtn = new Button();
|
||||
rebindBtn.AddToClassList("rebind-button");
|
||||
UpdateBindingText(action, rebindBtn);
|
||||
rebindBtn.clicked += () => StartRebind(action, rebindBtn);
|
||||
|
||||
row.Add(label);
|
||||
row.Add(rebindBtn);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void UpdateBindingText(InputAction action, Button btn)
|
||||
{
|
||||
int bindingIndex = action.GetBindingIndexForControl(action.controls[0]);
|
||||
btn.text = action.GetBindingDisplayString(bindingIndex);
|
||||
}
|
||||
|
||||
private void StartRebind(InputAction action, Button btn)
|
||||
{
|
||||
string oldText = btn.text;
|
||||
btn.text = "...";
|
||||
btn.style.color = Color.yellow;
|
||||
action.Disable();
|
||||
|
||||
var rebindOperation = action.PerformInteractiveRebinding()
|
||||
.WithControlsExcluding("<Mouse>/delta")
|
||||
.WithControlsExcluding("<Mouse>/scroll")
|
||||
.OnMatchWaitForAnother(0.1f)
|
||||
.OnComplete(operation => {
|
||||
btn.style.color = new Color(0f, 1f, 0.8f);
|
||||
UpdateBindingText(action, btn);
|
||||
action.Enable();
|
||||
operation.Dispose();
|
||||
})
|
||||
.OnCancel(operation => {
|
||||
btn.style.color = new Color(0f, 1f, 0.8f);
|
||||
btn.text = oldText;
|
||||
action.Enable();
|
||||
operation.Dispose();
|
||||
});
|
||||
|
||||
rebindOperation.Start();
|
||||
}
|
||||
|
||||
public override async Task PlayTransitionIn()
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
root.style.translate = new StyleTranslate(new Translate(0, 0));
|
||||
root.style.display = DisplayStyle.Flex;
|
||||
root.style.opacity = 1;
|
||||
}
|
||||
|
||||
root.style.display = DisplayStyle.Flex;
|
||||
_sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(-100), 0));
|
||||
await Tween.Custom(-100f, 0f, duration: 0.4f, ease: Ease.OutQuad,
|
||||
onValueChange: val => _sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(val), 0)));
|
||||
await Tween.Custom(-100f, 0f, duration: 0.4f, ease: Ease.OutQuad, val => _sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(val), 0)));
|
||||
}
|
||||
|
||||
public override async Task PlayTransitionOut()
|
||||
{
|
||||
await Tween.Custom(0f, -100f, duration: 0.4f, ease: Ease.InQuad,
|
||||
onValueChange: val => _sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(val), 0)));
|
||||
await Tween.Custom(0f, -100f, duration: 0.3f, ease: Ease.InQuad, val => _sidebar.style.translate = new StyleTranslate(new Translate(Length.Percent(val), 0)));
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user