This commit is contained in:
Lucastaa
2026-05-01 02:48:46 +07:00
15 changed files with 1018 additions and 187 deletions

View File

@@ -30,6 +30,11 @@ namespace Hallucinate.Audio
if (database != null) database.Initialize();
}
private void Start()
{
ApplyAllVolumes();
}
private void InitializePool()
{
_pool = new List<AudioSource>();
@@ -43,6 +48,27 @@ namespace Hallucinate.Audio
}
}
public void ApplyAllVolumes()
{
SetVolume("MasterVolume", PlayerPrefs.GetFloat("MasterVolume", 80f));
SetVolume("MusicVolume", PlayerPrefs.GetFloat("MusicVolume", 80f));
SetVolume("VFXVolume", PlayerPrefs.GetFloat("VFXVolume", 80f));
SetVolume("PlayerVolume", PlayerPrefs.GetFloat("PlayerVolume", 80f));
SetVolume("UIVolume", PlayerPrefs.GetFloat("UIVolume", 80f));
}
public void SetVolume(string key, float volume)
{
if (defaultGroup == null || defaultGroup.audioMixer == null) return;
// Chuyển đổi từ 0-100 sang dB (-80f đến 0f hoặc 20f tùy mixer)
// Công thức: dB = 20 * log10(volume / 100)
float db = volume <= 0.001f ? -80f : Mathf.Log10(volume / 100f) * 20f;
// Đảm bảo Parameter đã được EXPOSE trong AudioMixer với tên tương ứng (MasterVolume, MusicVolume, etc.)
defaultGroup.audioMixer.SetFloat(key, db);
}
public void Play(string sampleName, float volumeMult = 1f, float pitchMult = 1f, Vector3? position = null)
{
if (database == null) return;

View File

@@ -9,6 +9,51 @@ namespace OnlyScove.Scripts
[SerializeField] private InputActionAsset inputActions;
public InputActionAsset InputActions => inputActions;
private const string REBINDS_KEY = "InputRebinds";
private void OnEnable()
{
if (inputActions != null)
{
LoadBindings();
inputActions.Enable();
}
}
private void OnDisable()
{
if (inputActions != null)
{
inputActions.Disable();
}
}
public void SaveBindings()
{
if (inputActions == null) return;
string rebinds = inputActions.SaveBindingOverridesAsJson();
PlayerPrefs.SetString(REBINDS_KEY, rebinds);
PlayerPrefs.Save();
}
public void LoadBindings()
{
if (inputActions == null) return;
string rebinds = PlayerPrefs.GetString(REBINDS_KEY, string.Empty);
if (!string.IsNullOrEmpty(rebinds))
{
inputActions.LoadBindingOverridesFromJson(rebinds);
}
}
public void ResetBindings()
{
if (inputActions == null) return;
inputActions.RemoveAllBindingOverrides();
PlayerPrefs.DeleteKey(REBINDS_KEY);
PlayerPrefs.Save();
}
// Continuous Inputs
public virtual Vector2 MoveInput { get; protected set; }
public virtual Vector2 LookInput { get; protected set; }

View File

@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Audio;
using UnityEngine.InputSystem;
using System.Collections.Generic;
using System.Linq;
using System;
@@ -31,9 +32,13 @@ namespace Hallucinate.UI
private float _sliderMin, _sliderMax;
// Osu-style Volume Overlay
private VisualElement _volumeOverlay;
private VisualElement _volumeContainer;
private VisualElement _masterRing;
private Label _masterVolLabel;
private Dictionary<string, (VisualElement ring, Label label)> _subRings = new Dictionary<string, (VisualElement, Label)>();
private string _hoveredSubVolume = null;
private float _masterVol = 80f;
private int _overlayActiveCount = 0;
public override void Initialize(VisualElement uxmlRoot, UIManager manager)
{
@@ -43,9 +48,9 @@ namespace Hallucinate.UI
_tabTitle = root.Q<Label>("TabTitle");
_content = root.Q<ScrollView>("SettingsContent");
// Osu Volume Logic - Registering on Root for Global Wheel Catch
root.RegisterCallback<WheelEvent>(OnMouseWheel);
SetupVolumeOverlay();
// Global Volume Catch - Use TrickleDown to catch events before they are consumed by children (like ScrollViews)
uiManager.Root.RegisterCallback<WheelEvent>(OnMouseWheel, TrickleDown.TrickleDown);
SetupHierarchicalVolumeOverlay();
root.RegisterCallback<PointerDownEvent>(evt => {
if (evt.target == root) uiManager.ToggleSettings();
@@ -64,91 +69,251 @@ namespace Hallucinate.UI
_masterVol = PlayerPrefs.GetFloat("MasterVolume", 80f);
// Enforce Video Settings on start
ApplyVideoSettings();
SwitchTab("GENERAL");
}
private void SetupVolumeOverlay()
private void ApplyVideoSettings()
{
_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;
// Frame Limiter
int frameLimitIdx = PlayerPrefs.GetInt("FrameLimiter", 2);
ApplyFrameLimit(frameLimitIdx);
_masterVolLabel = new Label("80%");
_masterVolLabel.style.color = Color.white;
_masterVolLabel.style.fontSize = 24;
_masterVolLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_volumeOverlay.Add(_masterVolLabel);
// FPS Counter
_fpsVisible = PlayerPrefs.GetInt("ShowFPS", 0) == 1;
PerformanceOverlay.SetVisible(_fpsVisible);
root.Add(_volumeOverlay);
// Background Dim
float dim = PlayerPrefs.GetFloat("BackgroundDim", 50f);
ApplyBackgroundDim(dim);
// Fullscreen
bool isFull = PlayerPrefs.GetInt("Fullscreen", Screen.fullScreen ? 1 : 0) == 1;
Screen.fullScreen = isFull;
}
private void ApplyFrameLimit(int index)
{
switch (index)
{
case 0: QualitySettings.vSyncCount = 1; Application.targetFrameRate = -1; break;
case 1: QualitySettings.vSyncCount = 0; Application.targetFrameRate = 60; break;
case 2: QualitySettings.vSyncCount = 0; Application.targetFrameRate = 144; break;
case 3: QualitySettings.vSyncCount = 0; Application.targetFrameRate = 999; break;
}
PlayerPrefs.SetInt("FrameLimiter", index);
}
private void ApplyBackgroundDim(float value)
{
PlayerPrefs.SetFloat("BackgroundDim", value);
// Search for dim overlay in root
var dimOverlay = uiManager.Root.Q<VisualElement>("BackgroundDimOverlay");
if (dimOverlay != null)
{
dimOverlay.style.backgroundColor = new Color(0, 0, 0, value / 100f);
}
}
private void SetupHierarchicalVolumeOverlay()
{
_volumeContainer = new VisualElement();
_volumeContainer.name = "GlobalVolumeOverlay";
_volumeContainer.style.position = Position.Absolute;
_volumeContainer.style.right = 50;
_volumeContainer.style.bottom = 50;
_volumeContainer.style.width = 300;
_volumeContainer.style.height = 300;
_volumeContainer.style.display = DisplayStyle.None;
_volumeContainer.pickingMode = PickingMode.Ignore;
// Add to UIManager root so it stays even when Settings is hidden
uiManager.Root.Add(_volumeContainer);
// Master Ring (Bottom Right)
_masterRing = CreateRing("Master", 120, cyan: true);
_masterRing.style.right = 0;
_masterRing.style.bottom = 0;
_masterVolLabel = _masterRing.Q<Label>();
_volumeContainer.Add(_masterRing);
// Sub Rings (Music, VFX, Player, UI)
string[] subs = { "MusicVolume", "VFXVolume", "PlayerVolume", "UIVolume" };
string[] shortNames = { "MUS", "VFX", "PLY", "UI" };
// Layout sub-rings in an arc around Master
for (int i = 0; i < subs.Length; i++)
{
var ring = CreateRing(shortNames[i], 70, false);
// Angle 0 = Top, Angle 90 = Left
float angle = (i * 30f) * Mathf.Deg2Rad;
float radius = 140f;
// right moves element to the LEFT, bottom moves element UP
// Starting from top (right=25, bottom=140+25) to left (right=140+25, bottom=25)
ring.style.right = 25 + Mathf.Sin(angle) * radius;
ring.style.bottom = 25 + Mathf.Cos(angle) * radius;
string key = subs[i];
ring.RegisterCallback<PointerEnterEvent>(evt => _hoveredSubVolume = key);
ring.RegisterCallback<PointerLeaveEvent>(evt => { if (_hoveredSubVolume == key) _hoveredSubVolume = null; });
ring.pickingMode = PickingMode.Position; // Allow hover detection
_subRings[key] = (ring, ring.Q<Label>());
_volumeContainer.Add(ring);
}
}
private VisualElement CreateRing(string text, float size, bool cyan)
{
var ring = new VisualElement();
ring.style.width = size;
ring.style.height = size;
ring.style.backgroundColor = new Color(0, 0, 0, 0.85f);
ring.style.borderTopLeftRadius = size / 2;
ring.style.borderTopRightRadius = size / 2;
ring.style.borderBottomLeftRadius = size / 2;
ring.style.borderBottomRightRadius = size / 2;
ring.style.borderTopWidth = 3;
ring.style.borderBottomWidth = 3;
ring.style.borderLeftWidth = 3;
ring.style.borderRightWidth = 3;
ring.style.borderTopColor = ring.style.borderBottomColor = ring.style.borderLeftColor = ring.style.borderRightColor = cyan ? Color.cyan : new Color(0.7f, 0.7f, 0.7f);
ring.style.justifyContent = Justify.Center;
ring.style.alignItems = Align.Center;
ring.style.position = Position.Absolute;
var label = new Label("80%");
label.style.color = Color.white;
label.style.fontSize = size * 0.25f;
label.style.unityFontStyleAndWeight = FontStyle.Bold;
ring.Add(label);
var title = new Label(text);
title.style.color = Color.gray;
title.style.fontSize = size * 0.15f;
title.style.position = Position.Absolute;
title.style.bottom = size * 0.15f;
ring.Add(title);
return ring;
}
private void OnMouseWheel(WheelEvent evt)
{
// Osu style: Volume control with scroll wheel
// Only apply if in the SOUND tab
if (_activeTab != "SOUND") return;
// Debug Log to see if event is even reaching here
Debug.Log($"[SettingsController] Mouse Wheel Detected. Settings Open: {uiManager.IsSettingsOpen}");
if (_hoveredSlider != null)
// 1. Do not control volume if we are in MainMenu (unless Settings is explicitly open)
var mainMenuRoot = uiManager.Root.Q<VisualElement>("MainMenuRoot");
bool isMainMenuVisible = mainMenuRoot != null && mainMenuRoot.style.display == DisplayStyle.Flex;
if (!uiManager.IsSettingsOpen && isMainMenuVisible)
{
// Adjust the hovered slider's value
Debug.Log("[SettingsController] Volume control suppressed: Currently at Main Menu.");
return;
}
// 2. Detect target and context
VisualElement target = evt.target as VisualElement;
bool isDirectUIInteraction = _hoveredSubVolume != null || (_hoveredSlider != null && _activeTab == "SOUND");
// 3. If NOT direct interaction, check for ScrollView to prevent accidental volume changes while scrolling lists
if (!isDirectUIInteraction && target != null)
{
if (target is ScrollView || target.GetFirstAncestorOfType<ScrollView>() != null)
{
Debug.Log("[SettingsController] Ignoring volume scroll: Hovering a ScrollView.");
return;
}
}
// 4. Osu style volume control
_overlayActiveCount++;
ShowVolumeOverlay();
if (_hoveredSubVolume != null)
{
Debug.Log($"[SettingsController] Adjusting Sub Volume: {_hoveredSubVolume}");
UpdateSubVolume(_hoveredSubVolume, -evt.delta.y * 2f);
}
else if (_hoveredSlider != null && _activeTab == "SOUND")
{
// If hovering a slider in the Sound tab, adjust that
float currentVal = _hoveredSlider.value;
// Determine step size: default to 1% of range, adjusted for 0-100 range.
float step = (_sliderMax - _sliderMin) / 100f;
float newVal = Mathf.Clamp(currentVal - (evt.delta.y * step * 5f), _sliderMin, _sliderMax); // Multiply by a factor to make scroll smoother
float newVal = Mathf.Clamp(currentVal - (evt.delta.y * step * 5f), _sliderMin, _sliderMax);
_hoveredSlider.value = newVal;
// Trigger the associated OnValueChanged callback to save PlayerPrefs etc.
_hoveredOnChanged?.Invoke(newVal);
evt.StopPropagation(); // Consume the event so it doesn't affect other elements
}
else
{
// If not hovering a specific slider, control Master Volume
Debug.Log("[SettingsController] Adjusting Master Volume.");
UpdateMasterVolume(-evt.delta.y * 2f);
evt.StopPropagation(); // Consume the event
}
evt.StopPropagation();
}
private void UpdateMasterVolume(float delta)
{
_masterVol = Mathf.Clamp(_masterVol + delta, 0f, 100f);
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
AudioManager.Instance?.SetVolume("MasterVolume", _masterVol);
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
ShowVolumeOverlay();
// Refresh Sound Tab UI if visible
if (_activeTab == "SOUND") SwitchTab("SOUND");
}
private void UpdateSubVolume(string key, float delta)
{
float current = PlayerPrefs.GetFloat(key, 80f);
float newVal = Mathf.Clamp(current + delta, 0f, 100f);
PlayerPrefs.SetFloat(key, newVal);
AudioManager.Instance?.SetVolume(key, newVal);
if (_subRings.TryGetValue(key, out var data))
data.label.text = $"{Mathf.RoundToInt(newVal)}%";
if (_activeTab == "SOUND") SwitchTab("SOUND");
}
private async void ShowVolumeOverlay()
{
_volumeOverlay.style.display = DisplayStyle.Flex;
_volumeOverlay.style.opacity = 1f;
await Task.Delay(1500);
if (_volumeOverlay.style.opacity == 1f)
Debug.Log("[SettingsController] Showing Volume Overlay.");
// Ensure overlay is on top of other UI screens
_volumeContainer.BringToFront();
// CRITICAL: Ensure Virtual Cursor is ALWAYS on top of the volume rings
uiManager.Root.Q<VisualElement>("CursorLayer")?.BringToFront();
_volumeContainer.style.display = DisplayStyle.Flex;
_volumeContainer.style.opacity = 1f;
// Refresh all sub labels
foreach (var kvp in _subRings)
{
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _volumeOverlay.style.opacity = val)
.OnComplete(() => _volumeOverlay.style.display = DisplayStyle.None);
float val = PlayerPrefs.GetFloat(kvp.Key, 80f);
kvp.Value.label.text = $"{Mathf.RoundToInt(val)}%";
}
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
int currentId = _overlayActiveCount;
await Task.Delay(3000); // Wait 3s as requested
// Only fade out if no new scroll activity happened
if (currentId == _overlayActiveCount && _hoveredSubVolume == null)
{
Debug.Log("[SettingsController] Fading out Volume Overlay.");
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _volumeContainer.style.opacity = val)
.OnComplete(() => {
if (_volumeContainer.style.opacity == 0f)
_volumeContainer.style.display = DisplayStyle.None;
});
}
}
@@ -243,36 +408,50 @@ namespace Hallucinate.UI
private void RenderVideoTab()
{
_content.Add(CreateSection("RENDERER"));
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, 2);
int currentFrameLimit = PlayerPrefs.GetInt("FrameLimiter", 2);
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, currentFrameLimit);
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;
}
ApplyFrameLimit(frameLimit.index);
});
_content.Add(frameLimit);
var fpsToggle = new Toggle("Show FPS Counter") { value = _fpsVisible };
fpsToggle.RegisterValueChangedCallback(evt => { _fpsVisible = evt.newValue; PerformanceOverlay.SetVisible(_fpsVisible); });
fpsToggle.RegisterValueChangedCallback(evt => {
_fpsVisible = evt.newValue;
PlayerPrefs.SetInt("ShowFPS", _fpsVisible ? 1 : 0);
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")));
// Find current res in list
string currentResStr = $"{Screen.width}x{Screen.height}";
int currentResIdx = resList.FindIndex(s => s.StartsWith(currentResStr));
if (currentResIdx == -1) currentResIdx = resList.FindIndex(s => s.Contains("native"));
var resDropdown = new DropdownField("Resolution", resList, currentResIdx);
resDropdown.RegisterValueChangedCallback(evt => {
string[] parts = evt.newValue.Split(' ')[0].Split('x');
Screen.SetResolution(int.Parse(parts[0]), int.Parse(parts[1]), Screen.fullScreen);
int w = int.Parse(parts[0]);
int h = int.Parse(parts[1]);
Screen.SetResolution(w, h, Screen.fullScreen);
PlayerPrefs.SetInt("ScreenWidth", w);
PlayerPrefs.SetInt("ScreenHeight", h);
});
_content.Add(resDropdown);
var fullToggle = new Toggle("Fullscreen Mode") { value = Screen.fullScreen };
fullToggle.RegisterValueChangedCallback(evt => Screen.fullScreen = evt.newValue);
fullToggle.RegisterValueChangedCallback(evt => {
Screen.fullScreen = evt.newValue;
PlayerPrefs.SetInt("Fullscreen", evt.newValue ? 1 : 0);
});
_content.Add(fullToggle);
_content.Add(CreateSliderWithInput("Background Dim", 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => PlayerPrefs.SetFloat("BackgroundDim", val)));
_content.Add(CreateSliderWithInput("Background Dim", 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => ApplyBackgroundDim(val)));
_content.Add(CreateSliderWithInput("UI Scale", 0.5f, 2.0f, PlayerPrefs.GetFloat("UIScale", 1.0f), val => uiManager.SetUIScale(val)));
}
#endregion
@@ -292,13 +471,17 @@ namespace Hallucinate.UI
private VisualElement CreateAudioSlider(string label, string prefKey)
{
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => PlayerPrefs.SetFloat(prefKey, val));
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => {
PlayerPrefs.SetFloat(prefKey, val);
AudioManager.Instance?.SetVolume(prefKey, val);
});
// 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);
AudioManager.Instance?.SetVolume(prefKey, newVal);
// Visual update only (to avoid heavy re-render of whole list)
var slider = sliderRow.Q<Slider>();
@@ -313,9 +496,123 @@ namespace Hallucinate.UI
private void RenderControlTab()
{
_content.Add(CreateSection("KEY BINDINGS"));
var pendingLabel = new Label("Controls Implementation Pending context.");
pendingLabel.AddToClassList("text-body");
_content.Add(pendingLabel);
if (uiManager.InputReader == null || uiManager.InputReader.InputActions == null)
{
var errorLabel = new Label("Input Actions not found.");
errorLabel.AddToClassList("text-body");
_content.Add(errorLabel);
return;
}
foreach (var map in uiManager.InputReader.InputActions.actionMaps)
{
// Add a sub-header for each map
var mapHeader = new Label(map.name.ToUpper());
mapHeader.style.fontSize = 14;
mapHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
mapHeader.style.color = Color.cyan;
mapHeader.style.marginTop = 15;
mapHeader.style.marginBottom = 5;
_content.Add(mapHeader);
foreach (var action in map.actions)
{
// Skip internal/complex actions that shouldn't be rebound manually
if (action.name == "Look" || action.name == "Scroll" ||
action.name == "Navigate" || action.name == "Point" || action.name == "Click") continue;
if (action.bindings.Any(b => b.isComposite))
{
for (int i = 0; i < action.bindings.Count; i++)
{
if (action.bindings[i].isPartOfComposite)
{
if (action.bindings[i].groups.Contains("Keyboard&Mouse"))
{
string label = $"{action.name} {action.bindings[i].name}".ToUpper();
_content.Add(CreateRebindRow(action, i, label));
}
}
}
}
else
{
int bindingIndex = action.bindings.ToList().FindIndex(b => b.groups.Contains("Keyboard&Mouse"));
if (bindingIndex != -1)
{
_content.Add(CreateRebindRow(action, bindingIndex, action.name.ToUpper()));
}
}
}
}
var resetBtn = new Button { text = "RESET ALL TO DEFAULT" };
resetBtn.AddToClassList("button-spring");
resetBtn.style.marginTop = 30;
resetBtn.style.alignSelf = Align.Center;
resetBtn.clicked += () => {
uiManager.InputReader.ResetBindings();
SwitchTab("CONTROL");
};
_content.Add(resetBtn);
}
private VisualElement CreateRebindRow(UnityEngine.InputSystem.InputAction action, int bindingIndex, string labelText)
{
var row = new VisualElement();
row.AddToClassList("rebind-row");
row.style.flexDirection = FlexDirection.Row;
row.style.justifyContent = Justify.SpaceBetween;
row.style.alignItems = Align.Center;
row.style.paddingTop = 10;
row.style.paddingBottom = 10;
row.style.borderBottomWidth = 1;
row.style.borderBottomColor = new Color(1, 1, 1, 0.1f);
var label = new Label(labelText);
label.AddToClassList("rebind-label");
label.style.color = Color.white;
row.Add(label);
var bindingDisplay = action.GetBindingDisplayString(bindingIndex);
var btn = new Button { text = bindingDisplay.ToUpper() };
btn.AddToClassList("rebind-button");
btn.style.width = 150;
btn.clicked += () => StartRebinding(action, bindingIndex, btn);
row.Add(btn);
return row;
}
private void StartRebinding(UnityEngine.InputSystem.InputAction action, int bindingIndex, Button btn)
{
btn.text = "> <"; // Minecraft style "waiting"
btn.style.color = Color.yellow;
// Disable input while rebinding to avoid side effects
action.actionMap.Disable();
var rebindOperation = action.PerformInteractiveRebinding(bindingIndex)
.WithControlsExcluding("<Mouse>/position")
.WithControlsExcluding("<Mouse>/delta")
.WithControlsExcluding("<Keyboard>/escape") // Keep ESC for cancel if needed
.OnMatchWaitForAnother(0.1f)
.OnComplete(operation => {
btn.text = action.GetBindingDisplayString(bindingIndex).ToUpper();
btn.style.color = Color.white;
operation.Dispose();
action.actionMap.Enable();
uiManager.InputReader.SaveBindings();
})
.OnCancel(operation => {
btn.text = action.GetBindingDisplayString(bindingIndex).ToUpper();
btn.style.color = Color.white;
operation.Dispose();
action.actionMap.Enable();
});
rebindOperation.Start();
}
#endregion

View File

@@ -19,6 +19,7 @@ namespace Hallucinate.UI
private UIDocument _uiDocument;
private VisualElement _rootElement;
public VisualElement Root => _rootElement;
private VisualElement _cursorLayer;
private VisualElement _mainCursor;
@@ -60,6 +61,7 @@ namespace Hallucinate.UI
private Vector2 _lastMousePos;
private float _trailOpacity = 1f;
private bool _isSettingsOpen = false;
public bool IsSettingsOpen => _isSettingsOpen;
private const string UI_SCALE_KEY = "UIScale";