feat(ui): add UIPointer and ScrollSelectionFollower for improved UI navigation

This commit is contained in:
Данил Ярош 2025-11-10 17:04:55 +03:00
parent f84f868c82
commit 3ca9a4eb41
19 changed files with 6089 additions and 1290 deletions

View File

@ -85,8 +85,8 @@ Material:
- _Stencil: 0
- _StencilComp: 8
- _StencilOp: 0
- _StencilReadMask: 255
- _StencilWriteMask: 255
- _StencilReadMask: 1
- _StencilWriteMask: 0
- _TextureHeight: 1024
- _TextureWidth: 1024
- _UnderlayDilate: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 24de3c5a0031e584ab71384b5d77244e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!244 &-3902009871899815571
AudioMixerEffectController:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_EffectID: 889ce81b86755c040882cfc988492262
m_EffectName: Attenuation
m_MixLevel: 1db247ecc96499242b7ed19e5433c799
m_Parameters: []
m_SendTarget: {fileID: 0}
m_EnableWetMix: 0
m_Bypass: 0
--- !u!243 &-496499518004457780
AudioMixerGroupController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: SFX
m_AudioMixer: {fileID: 24100000}
m_GroupID: 3be00bcd5607507438c82ef262228106
m_Children: []
m_Volume: a3b50d40b0003814497bd8852988a7b2
m_Pitch: d452543f92d9d474aab2d808f3854055
m_Send: 00000000000000000000000000000000
m_Effects:
- {fileID: -3902009871899815571}
m_UserColorIndex: 4
m_Mute: 0
m_Solo: 0
m_BypassEffects: 0
--- !u!244 &-271018402943269990
AudioMixerEffectController:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_EffectID: 862e7507968483343ad2a663626ec77d
m_EffectName: Attenuation
m_MixLevel: 7920a99416b17464c85eded5bc8ca102
m_Parameters: []
m_SendTarget: {fileID: 0}
m_EnableWetMix: 0
m_Bypass: 0
--- !u!241 &24100000
AudioMixerController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Master
m_OutputGroup: {fileID: 0}
m_MasterGroup: {fileID: 24300002}
m_Snapshots:
- {fileID: 24500006}
m_StartSnapshot: {fileID: 24500006}
m_SuspendThreshold: -80
m_EnableSuspend: 1
m_UpdateMode: 0
m_ExposedParameters:
- guid: aeea8e463d035974b87c6895d9e3fdba
name: MusicVolume
- guid: a3b50d40b0003814497bd8852988a7b2
name: SFXVolume
m_AudioMixerGroupViews:
- guids:
- 2a3a16fafd41c1748838585b0e219678
- 237f6931e4e5ae94d9af212cfcc14c8d
- 3be00bcd5607507438c82ef262228106
name: View
m_CurrentViewIndex: 0
m_TargetSnapshot: {fileID: 24500006}
--- !u!243 &24300002
AudioMixerGroupController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Master
m_AudioMixer: {fileID: 24100000}
m_GroupID: 2a3a16fafd41c1748838585b0e219678
m_Children:
- {fileID: 3793028766014410084}
- {fileID: -496499518004457780}
m_Volume: cdcb200ffbb7c1f4ab3ff6316084de72
m_Pitch: 8672ed8ec37560243bb7aaa86c6081d6
m_Send: 00000000000000000000000000000000
m_Effects:
- {fileID: 24400004}
m_UserColorIndex: 0
m_Mute: 0
m_Solo: 0
m_BypassEffects: 0
--- !u!244 &24400004
AudioMixerEffectController:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_EffectID: 48a6e9f98b14f6644bd97ee178ee34dd
m_EffectName: Attenuation
m_MixLevel: f36758954a86b804cadaf2d8c7e1c3d8
m_Parameters: []
m_SendTarget: {fileID: 0}
m_EnableWetMix: 0
m_Bypass: 0
--- !u!245 &24500006
AudioMixerSnapshotController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Snapshot
m_AudioMixer: {fileID: 24100000}
m_SnapshotID: 5af2efb0fc4b2af4cb11efcd2f7e5b14
m_FloatValues:
cdcb200ffbb7c1f4ab3ff6316084de72: 0
m_TransitionOverrides: {}
--- !u!243 &3793028766014410084
AudioMixerGroupController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Music
m_AudioMixer: {fileID: 24100000}
m_GroupID: 237f6931e4e5ae94d9af212cfcc14c8d
m_Children: []
m_Volume: aeea8e463d035974b87c6895d9e3fdba
m_Pitch: 74392233bc98220459e7ad1995ce3d17
m_Send: 00000000000000000000000000000000
m_Effects:
- {fileID: -271018402943269990}
m_UserColorIndex: 1
m_Mute: 0
m_Solo: 0
m_BypassEffects: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4f44de6a220ee7d4b8179285074ead2d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 24100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -74,7 +74,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d793a5f3669a676418d43519feaf3a8d, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::ComWrapper
saveMoneyBetweenSessions: 0
saveMoneyBetweenSessions: 1
enablePrizeChecking: 0
--- !u!114 &-6245296037342915518
MonoBehaviour:
@ -141,4 +141,8 @@ MonoBehaviour:
target: 0
button: 10
dpadDirection: 1
- pinIndex: 12
target: 0
button: 4
dpadDirection: 1
logCreateDevice: 1

File diff suppressed because it is too large Load Diff

View File

@ -270,7 +270,7 @@ public class ComWrapper : MonoBehaviour
private static void ClosePort()
{
GameManager.Logger.Log("ComWrapper","Закрываю все порты");
Debug.Log("Закрываю все порты");
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
@ -286,7 +286,7 @@ public class ComWrapper : MonoBehaviour
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
if (read > 0)
{
var s = Encoding.UTF8.GetString(buf, 0, read);
string s = Encoding.UTF8.GetString(buf, 0, read);
//GameManager.Logger.Log($"ReadLine len={read} data=[{s}]");
return s;
}

View File

@ -13,23 +13,25 @@ namespace RR.Common.Managers
// ==== Singleton ====
public static AudioManager Instance { get; private set; }
[Header("Music")] [SerializeField] private AudioSource musicSource; // child: MusicSource
[Header("Music")]
[SerializeField] private AudioSource musicSource; // child: MusicSource
[SerializeField] private AudioSource tempMusicSource; // child: TempMusicSource
[Tooltip("Группа микшера для музыки (необязательно)")] [SerializeField]
private AudioMixerGroup musicMixerGroup;
[Tooltip("Группа микшера для музыки (необязательно)")]
[SerializeField] private AudioMixerGroup musicMixerGroup;
[Tooltip("Целевая громкость музыки (после фейдов)")] [Range(0f, 1.5f)]
public float musicTargetVolume = 0.2f;
[Tooltip("Целевая громкость музыки (после фейдов)")]
[Range(0f, 1.5f)] public float musicTargetVolume = 1f;
[Tooltip("Длительность фейда по умолчанию (сек)")] [SerializeField]
private float defaultFadeDuration = 0.8f;
[Tooltip("Длительность фейда по умолчанию (сек)")]
[SerializeField] private float defaultFadeDuration = 0.8f;
[Header("SFX Pool")] [SerializeField] private int sfxPoolSize = 16;
[Header("SFX Pool")]
[SerializeField] private int sfxPoolSize = 16;
[SerializeField] private Transform sfxPoolParent; // child: SFX Pool
[Tooltip("Группа микшера для SFX (необязательно)")] [SerializeField]
private AudioMixerGroup sfxMixerGroup;
[Tooltip("Группа микшера для SFX (необязательно)")]
[SerializeField] private AudioMixerGroup sfxMixerGroup;
[Range(0f, 1.5f)] public float sfxDefaultVolume = 1f;
@ -44,7 +46,7 @@ namespace RR.Common.Managers
#region Unity Lifetime
public void Initialize()
public void Initialize(AudioMixer mixer = null)
{
// Singleton init
if (Instance != null && Instance != this)
@ -56,6 +58,14 @@ namespace RR.Common.Managers
Instance = this;
DontDestroyOnLoad(gameObject);
if (mixer != null)
{
if (musicMixerGroup == null)
musicMixerGroup = mixer.FindMatchingGroups("Music")[0];
if (sfxMixerGroup == null)
sfxMixerGroup = mixer.FindMatchingGroups("SFX")[0];
}
EnsureMusicSources();
EnsureSfxPool();
@ -69,13 +79,14 @@ namespace RR.Common.Managers
{
if (musicSource == null)
{
var t = transform.Find("MusicSource");
Transform t = transform.Find("MusicSource");
if (t != null) musicSource = t.GetComponent<AudioSource>();
}
if (tempMusicSource == null)
{
var t = transform.Find("TempMusicSource");
Transform t = transform.Find("TempMusicSource");
if (t != null) tempMusicSource = t.GetComponent<AudioSource>();
}
@ -167,12 +178,16 @@ namespace RR.Common.Managers
public void SetMusicVolume(float newVolume)
{
// TODO!!!!!
float vol = Mathf.Clamp01(newVolume);
vol = DbFrom01_Log(vol);
musicMixerGroup.audioMixer.SetFloat("MusicVolume", vol);
}
public void SetSfxVolume(float newVolume)
{
// TODO!!!!!
float vol = Mathf.Clamp01(newVolume);
vol = DbFrom01_Log(vol);
sfxMixerGroup.audioMixer.SetFloat("SFXVolume", vol);
}
/// <summary>
@ -392,5 +407,13 @@ namespace RR.Common.Managers
src.Play();
}
float DbFrom01_Log(float t, float minDb = -80f, float maxDb = 20f)
{
t = Mathf.Clamp01(t);
float minAmp = Mathf.Pow(10f, minDb / 20f); // 10^(-80/20) = 0.0001
float maxAmp = Mathf.Pow(10f, maxDb / 20f); // 10^(+20/20) = 10
float amp = Mathf.Lerp(minAmp, maxAmp, t); // линейная интерполяция по усилению
return 20f * Mathf.Log10(Mathf.Max(1e-6f, amp));
}
}
}

View File

@ -7,6 +7,7 @@ using RR.Common.Managers;
using RR.Common.Utility;
using RR.Game2D.Behaviour;
using RR.Game2D.Entities;
using UnityEngine.Audio;
namespace RR.Game2D.Core.Managers
{
@ -33,6 +34,7 @@ namespace RR.Game2D.Core.Managers
[Header("Globals")]
private GameObject globals;
[SerializeField] private string globalsPrefabResourcePath = "Prefabs/Global/Globals";
[SerializeField] private string AudioMixerResourcePath = "Audio/Master";
[Header("Пауза")]
public bool IsPaused { get; private set; }
@ -101,10 +103,11 @@ namespace RR.Game2D.Core.Managers
}
// Initialize Audio control
AudioMixer audioMixer = Resources.Load<AudioMixer>(AudioMixerResourcePath);
if (Audio == null)
{
Audio = gameObject.AddComponent<AudioManager>();
Audio.Initialize();
Audio.Initialize(audioMixer);
}
if (globals == null)
@ -130,7 +133,7 @@ namespace RR.Game2D.Core.Managers
}
gameStartTime = Time.unscaledTime;
Logger.Log(gameObject.name,$"Game start time = {gameStartTime}");
// Logger.Log(gameObject.name,$"Game start time = {gameStartTime}");
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
}
@ -186,6 +189,7 @@ namespace RR.Game2D.Core.Managers
Coins = 0;
CoinsChanged?.Invoke(Coins);
gameStartTime = Time.time;
Logger.Log(gameObject.name,$"Game start time = {gameStartTime}");
LoadLevel(1, 1);
}

View File

@ -104,6 +104,7 @@ namespace RR.Common.Managers
return;
}
gameSettings.ValidateSettings();
try
{
string json = JsonUtility.ToJson(gameSettings, true);

View File

@ -75,7 +75,7 @@ namespace RR.Common.UI
GetComponent<Image>().color = new Color(0, 0,0, 100f/255f);
winText.text = $"Время игры: {GameEndUIController.FormatTime(gameTime)}";
winText.text = $"Время игры: {FormatTime(gameTime)}";
SetVisible(winPanel, true);
EventSystem.current.firstSelectedGameObject = mainMenuButtonWin.gameObject;
@ -136,7 +136,7 @@ namespace RR.Common.UI
{
TimeSpan interval = TimeSpan.FromSeconds(gameTime);
return interval.ToString(@"mm\:ss");
return interval.ToString(@"mm\:ss\.fff");
}
}
}

View File

@ -0,0 +1,143 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Collections;
public class ScrollSelectionFollower : MonoBehaviour
{
[SerializeField] private ScrollRect scrollRect;
[SerializeField] private float padding = 8f; // отступы сверху/снизу/слева/справа
[SerializeField] private float duration = 0.15f; // длительность анимации
[SerializeField] private bool onlyWhenNotVisible = true; // если false — центрируем всегда
private GameObject _lastSelected;
private Coroutine _anim;
private void Reset()
{
scrollRect = GetComponentInChildren<ScrollRect>();
}
private void LateUpdate()
{
GameObject sel = EventSystem.current ? EventSystem.current.currentSelectedGameObject : null;
if (!sel || sel == _lastSelected) return;
// Только если элемент внутри нашего Content
if (!scrollRect || sel.transform is not RectTransform tr || !tr.IsChildOf(scrollRect.content)) return;
ScrollTo(tr);
_lastSelected = sel;
}
public void ScrollTo(RectTransform target)
{
if (!scrollRect || !target) return;
// Сначала убедимся, что лейауты посчитались
Canvas.ForceUpdateCanvases();
RectTransform content = scrollRect.content;
RectTransform viewport = scrollRect.viewport != null ? scrollRect.viewport : (RectTransform)scrollRect.transform;
// Получаем мировые углы и преобразуем в локальное пространство viewport
Vector3[] itemCorners = new Vector3[4];
target.GetWorldCorners(itemCorners);
Vector3[] viewportCorners = new Vector3[4];
viewport.GetWorldCorners(viewportCorners);
// Преобразуем в локальное пространство content
for (int i = 0; i < 4; i++)
{
itemCorners[i] = content.InverseTransformPoint(itemCorners[i]);
viewportCorners[i] = content.InverseTransformPoint(viewportCorners[i]);
}
// Вычисляем границы
float itemTop = itemCorners[1].y;
float itemBottom = itemCorners[0].y;
float itemLeft = itemCorners[0].x;
float itemRight = itemCorners[2].x;
float viewTop = viewportCorners[1].y;
float viewBottom = viewportCorners[0].y;
float viewLeft = viewportCorners[0].x;
float viewRight = viewportCorners[2].x;
Vector2 offset = Vector2.zero;
// Вертикальная прокрутка
if (scrollRect.vertical)
{
float delta = 0f;
bool above = itemTop > viewTop - padding;
bool below = itemBottom < viewBottom + padding;
if (!onlyWhenNotVisible || above || below)
{
if (above && below)
{
// Элемент больше viewport - выравниваем по верхнему краю
delta = itemTop - (viewTop - padding);
}
else if (above)
{
delta = itemTop - (viewTop - padding);
}
else if (below)
{
delta = itemBottom - (viewBottom + padding);
}
offset.y = delta;
}
}
// Горизонтальная прокрутка
if (scrollRect.horizontal)
{
float delta = 0f;
bool left = itemLeft < viewLeft + padding;
bool right = itemRight > viewRight - padding;
if (!onlyWhenNotVisible || left || right)
{
if (left && right)
{
// Элемент больше viewport - выравниваем по левому краю
delta = itemLeft - (viewLeft + padding);
}
else if (left)
{
delta = itemLeft - (viewLeft + padding);
}
else if (right)
{
delta = itemRight - (viewRight - padding);
}
offset.x = delta;
}
}
if (offset == Vector2.zero) return;
Vector2 targetPos = content.anchoredPosition - offset;
if (_anim != null) StopCoroutine(_anim);
_anim = StartCoroutine(AnimateTo(content, targetPos, duration));
}
private static IEnumerator AnimateTo(RectTransform content, Vector2 target, float time)
{
Vector2 start = content.anchoredPosition;
float t = 0f;
while (t < 1f)
{
t += Time.unscaledDeltaTime / Mathf.Max(0.0001f, time);
// лёгкая ease-in-out
float s = t * t * (3f - 2f * t);
content.anchoredPosition = Vector2.LerpUnclamped(start, target, s);
yield return null;
}
content.anchoredPosition = target;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 738ffd3a084a4114b918b316ca68c168

View File

@ -1,14 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.EventSystems;
using RR.Common.Managers;
using RR.Game2D.Core.Managers;
using RR.Common.Behaviour.ScriptableObjects;
namespace RR.Common.UI
{
@ -23,16 +18,17 @@ namespace RR.Common.UI
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Transform content;
[SerializeField] private float fadeDuration = 0.15f;
SettingsUIList settingsUIList;
private SettingsUIList settingsUIList;
private SettingsManager settingsManager;
private GameSettings currentSettings;
private void Awake()
{
settingsManager = SettingsManager.Instance;
if (triggerKey != null) triggerKey.action.started += Toggle;
if (settingsManager == null)
{
GameManager.Logger.LogError(name, "SettingsManager is null!");
@ -42,14 +38,45 @@ namespace RR.Common.UI
settingsManager.OnSettingsLoaded += UpdateSettings;
settingsManager.OnSettingsSaved += SaveSettingsHandler;
currentSettings = settingsManager.GetCurrentGameSettings();
if (saveButton) saveButton.onClick.AddListener(SaveAndClose);
gameObject.SetActive(false);
}
public void Open()
private void OnDestroy()
{
if (settingsManager != null)
{
settingsManager.OnSettingsLoaded -= UpdateSettings;
settingsManager.OnSettingsSaved -= SaveSettingsHandler;
}
if (triggerKey != null) triggerKey.action.started -= Toggle;
if (saveButton) saveButton.onClick.RemoveListener(SaveAndClose);
}
// private void Update()
// {
// if (!gameObject.activeSelf && triggerKey != null && triggerKey.action.WasPerformedThisFrame())
// {
// Open();
// }
// }
private void Toggle(InputAction.CallbackContext ctx)
{
if (gameObject.activeSelf)
{
SaveAndClose();
}
else
{
Open();
}
}
private void Open()
{
gameObject.SetActive(true);
StopAllCoroutines();
@ -58,15 +85,16 @@ namespace RR.Common.UI
private void Close()
{
StartCoroutine(Fade(1f, 0f, fadeDuration, () => { gameObject.SetActive(false); }));
}
private void Save()
{
UpdateSettings();
settingsUIList.SaveSettings();
}
public void SaveAndClose()
private void SaveAndClose()
{
Save();
Close();
@ -74,11 +102,8 @@ namespace RR.Common.UI
private void UpdateSettings()
{
settingsUIList = content.GetComponent<SettingsUIList>();
if (settingsUIList != null)
{
settingsUIList.UpdateSettings(settingsManager.GetCurrentGameSettings());
}
if (settingsUIList == null) settingsUIList = content.GetComponent<SettingsUIList>();
if (settingsUIList != null) settingsUIList.UpdateSettings(settingsManager.GetCurrentGameSettings());
}
private void SaveSettingsHandler()

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using RR.Common.Behaviour.ScriptableObjects;
using RR.Common.Managers;
using RR.Game2D.Core.Managers;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
@ -13,8 +14,6 @@ namespace RR.Common.UI
public List<Selectable> settingsList = new();
[Header("Настройки")]
// Fullscreen
[SerializeField] private Toggle fullscreenToggle;
// Volume
[SerializeField] private Slider musicVolumeSlider;
@ -27,7 +26,13 @@ namespace RR.Common.UI
// Player
[SerializeField] private Slider startLivesSlider;
[SerializeField] private Slider increasedLivesSlider;
[SerializeField] private Toggle resetLivesToggle;
[Header("Текст в UI")]
// Text
[SerializeField] private TextMeshProUGUI volumePercentageText;
[SerializeField] private TextMeshProUGUI sfxVolumePercentageText;
[SerializeField] private TextMeshProUGUI startLivesCountText;
[SerializeField] private TextMeshProUGUI increasedLivesCountText;
private SettingsManager settingsManager;
private GameSettings currentSettings;
@ -42,61 +47,161 @@ namespace RR.Common.UI
return;
}
settingsList.Add(fullscreenToggle);
settingsList.Add(musicVolumeSlider);
settingsList.Add(sfxVolumeSlider);
settingsList.Add(freeGameToggle);
settingsList.Add(givingPrizesToggle);
settingsList.Add(startLivesSlider);
settingsList.Add(increasedLivesSlider);
settingsList.Add(resetLivesToggle);
currentSettings = settingsManager.GetCurrentGameSettings();
AddListeners();
UpdateSettings(currentSettings);
}
public void UpdateSettings(GameSettings newSettings)
private void OnDestroy()
{
RemoveListeners();
}
public void UpdateSettings(GameSettings newSettings = null)
{
if (newSettings != null) currentSettings = newSettings;
RemoveListeners();
fullscreenToggle.isOn = currentSettings.isFullscreen;
musicVolumeSlider.value = currentSettings.musicVolume;
sfxVolumeSlider.value = currentSettings.sfxVolume;
freeGameToggle.isOn = currentSettings.isFreeGame;
givingPrizesToggle.isOn = currentSettings.isGivingPrizes;
startLivesSlider.value = currentSettings.startLivesCount;
increasedLivesSlider.value = currentSettings.livesIncreasedByCredits;
resetLivesToggle.isOn = currentSettings.setLivesToDefaultAfterFirstLose;
AddListeners();
}
public void SaveSettings()
{
settingsManager.SaveSettings();
}
private void AddListeners()
{
fullscreenToggle.onValueChanged.AddListener(settingsManager.SetFullscreen);
musicVolumeSlider.onValueChanged.AddListener(settingsManager.SetMusicVolume);
sfxVolumeSlider.onValueChanged.AddListener(settingsManager.SetSfxVolume);
freeGameToggle.onValueChanged.AddListener(settingsManager.SetIsFreeGame);
givingPrizesToggle.onValueChanged.AddListener(settingsManager.SetIsGivingPrizes);
startLivesSlider.onValueChanged.AddListener(value => settingsManager.SetStartLivesCount((int)(value / 10)));
increasedLivesSlider.onValueChanged.AddListener(value => settingsManager.SetLivesIncreasedByCredits((int)(value / 10)));
resetLivesToggle.onValueChanged.AddListener(settingsManager.SetSetLivesToDefaultAfterFirstLose);
// Music Volume
if (musicVolumeSlider != null)
{
musicVolumeSlider.onValueChanged.AddListener(settingsManager.SetMusicVolume);
musicVolumeSlider.onValueChanged.AddListener(SetMusicVolumePercentage);
musicVolumeSlider.onValueChanged.AddListener(GameManager.Instance.Audio.SetMusicVolume);
}
// SFX Volume
if (sfxVolumeSlider != null)
{
sfxVolumeSlider.onValueChanged.AddListener(settingsManager.SetSfxVolume);
sfxVolumeSlider.onValueChanged.AddListener(SetSfxVolumePercentage);
sfxVolumeSlider.onValueChanged.AddListener(GameManager.Instance.Audio.SetSfxVolume);
}
// Free game
if (freeGameToggle != null)
{
freeGameToggle.onValueChanged.AddListener(settingsManager.SetIsFreeGame);
}
// Giving prizes
if (givingPrizesToggle != null)
{
givingPrizesToggle.onValueChanged.AddListener(settingsManager.SetIsGivingPrizes);
}
// Start lives
if (startLivesSlider != null)
{
startLivesSlider.onValueChanged.AddListener(value =>
settingsManager.SetStartLivesCount((int)value));
startLivesSlider.onValueChanged.AddListener(SetStartLivesCountText);
}
// Increased lives
if (increasedLivesSlider != null)
{
increasedLivesSlider.onValueChanged.AddListener(value =>
settingsManager.SetLivesIncreasedByCredits((int)value));
increasedLivesSlider.onValueChanged.AddListener(SetIncreasedLivesCountText);
}
}
private void RemoveListeners()
{
fullscreenToggle.onValueChanged.RemoveListener(settingsManager.SetFullscreen);
musicVolumeSlider.onValueChanged.RemoveListener(settingsManager.SetMusicVolume);
sfxVolumeSlider.onValueChanged.RemoveListener(settingsManager.SetSfxVolume);
freeGameToggle.onValueChanged.RemoveListener(settingsManager.SetIsFreeGame);
givingPrizesToggle.onValueChanged.RemoveListener(settingsManager.SetIsGivingPrizes);
startLivesSlider.onValueChanged.RemoveListener(value => settingsManager.SetStartLivesCount((int)(value / 10)));
increasedLivesSlider.onValueChanged.RemoveListener(value => settingsManager.SetLivesIncreasedByCredits((int)(value / 10)));
resetLivesToggle.onValueChanged.RemoveListener(settingsManager.SetSetLivesToDefaultAfterFirstLose);
// Music Volume
if (musicVolumeSlider != null)
{
musicVolumeSlider.onValueChanged.RemoveListener(settingsManager.SetMusicVolume);
musicVolumeSlider.onValueChanged.AddListener(SetMusicVolumePercentage);
}
// SFX Volume
if (sfxVolumeSlider != null)
{
sfxVolumeSlider.onValueChanged.RemoveListener(settingsManager.SetSfxVolume);
sfxVolumeSlider.onValueChanged.AddListener(SetSfxVolumePercentage);
}
// Free game
if (freeGameToggle != null)
{
freeGameToggle.onValueChanged.RemoveListener(settingsManager.SetIsFreeGame);
}
// Giving prizes
if (givingPrizesToggle != null)
{
givingPrizesToggle.onValueChanged.RemoveListener(settingsManager.SetIsGivingPrizes);
}
// Start lives
if (startLivesSlider != null)
{
startLivesSlider.onValueChanged.RemoveListener(value =>
settingsManager.SetStartLivesCount((int)value));
startLivesSlider.onValueChanged.AddListener(SetStartLivesCountText);
}
// Increased lives
if (increasedLivesSlider != null)
{
increasedLivesSlider.onValueChanged.RemoveListener(value =>
settingsManager.SetLivesIncreasedByCredits((int)value));
increasedLivesSlider.onValueChanged.AddListener(SetIncreasedLivesCountText);
}
}
private void SetMusicVolumePercentage(float value)
{
if (volumePercentageText == null) return;
int volumePercent = Mathf.RoundToInt(value * 100);
volumePercentageText.text = $"{volumePercent}%";
}
private void SetSfxVolumePercentage(float value)
{
if (sfxVolumePercentageText == null) return;
int sfxVolumePercent = Mathf.RoundToInt(value * 100);
sfxVolumePercentageText.text = $"{sfxVolumePercent}%";
}
private void SetStartLivesCountText(float value)
{
if (startLivesCountText == null) return;
startLivesCountText.text = $"{(int)value}";
}
private void SetIncreasedLivesCountText(float value)
{
if (increasedLivesCountText == null) return;
increasedLivesCountText.text = $"{(int)value}";
}
}
}

View File

@ -0,0 +1,126 @@
using UnityEngine;
using UnityEngine.EventSystems;
public class UIPointer : MonoBehaviour
{
[Header("Настройки")]
[SerializeField] private GameObject pointer;
[SerializeField] private RectTransform content; // Content ScrollView, где находятся элементы
[SerializeField] private float smoothSpeed = 10f; // Скорость перемещения указателя
[SerializeField] private Vector2 offset = Vector2.zero; // Смещение указателя относительно элемента
private RectTransform pointerRect;
private void Start()
{
if (pointer != null)
{
pointerRect = pointer.GetComponent<RectTransform>();
if (pointerRect == null)
{
Debug.LogError("UIPointer: pointer не имеет RectTransform!");
}
}
else
{
Debug.LogError("UIPointer: pointer не назначен!");
}
// Если content не задан, пытаемся найти его автоматически
if (content == null)
{
content = GetComponent<RectTransform>();
}
}
private void LateUpdate()
{
if (!pointerRect || !content) return;
// Получаем текущий выбранный элемент из EventSystem
GameObject selected = EventSystem.current?.currentSelectedGameObject;
// Если ничего не выбрано, скрываем указатель
if (selected == null)
{
if (pointer.activeSelf)
pointer.SetActive(false);
return;
}
// Проверяем, что выбранный элемент находится внутри нашего content
RectTransform selectedRect = selected.GetComponent<RectTransform>();
if (!selectedRect || !selectedRect.IsChildOf(content))
{
if (pointer.activeSelf)
pointer.SetActive(false);
return;
}
// Показываем указатель, если он был скрыт
if (!pointer.activeSelf)
pointer.SetActive(true);
// Вычисляем целевую позицию
Vector2 targetPos = GetTargetPosition(selectedRect);
// Плавно перемещаем указатель
if (smoothSpeed > 0)
{
pointerRect.anchoredPosition = Vector2.Lerp(
pointerRect.anchoredPosition,
targetPos,
Time.unscaledDeltaTime * smoothSpeed
);
}
else
{
pointerRect.anchoredPosition = targetPos;
}
}
private Vector2 GetTargetPosition(RectTransform target)
{
// Получаем позицию целевого элемента относительно content
Vector2 targetAnchoredPos = target.anchoredPosition;
// Если элементы не прямые дети content, нужно пересчитать позицию
if (target.parent == content)
return new Vector2(
pointerRect.anchoredPosition.x + offset.x, // X оставляем как есть (или применяем offset)
targetAnchoredPos.y + offset.y // Y берем от целевого элемента
);
// Конвертируем мировую позицию в локальную позицию content
Vector3[] corners = new Vector3[4];
target.GetWorldCorners(corners);
Vector3 center = (corners[0] + corners[2]) * 0.5f;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
content,
RectTransformUtility.WorldToScreenPoint(null, center),
null,
out Vector2 localPos
);
targetAnchoredPos = localPos;
// Применяем смещение
return new Vector2(
pointerRect.anchoredPosition.x + offset.x, // X оставляем как есть (или применяем offset)
targetAnchoredPos.y + offset.y // Y берем от целевого элемента
);
}
// Метод для ручного обновления позиции указателя (если нужно)
public void UpdatePointerPosition()
{
if (pointerRect == null || content == null) return;
GameObject selected = EventSystem.current?.currentSelectedGameObject;
if (selected == null) return;
RectTransform selectedRect = selected.GetComponent<RectTransform>();
if (selectedRect == null || !selectedRect.IsChildOf(content)) return;
pointerRect.anchoredPosition = GetTargetPosition(selectedRect);
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d4367909e6d008a41b067a1974733d6f

View File

@ -12,7 +12,7 @@ PlayerSettings:
targetDevice: 2
useOnDemandResources: 0
accelerometerFrequency: 60
companyName: RR
companyName: Robotic Retailers
productName: Danro
defaultCursor: {fileID: 0}
cursorHotspot: {x: 0, y: 0}
@ -144,7 +144,7 @@ PlayerSettings:
loadStoreDebugModeEnabled: 0
visionOSBundleVersion: 1.0
tvOSBundleVersion: 1.0
bundleVersion: 0.0.6a
bundleVersion: 0.2.2b
preloadedAssets:
- {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
- {fileID: 11400000, guid: bf6b6fd5471a43d40b76a4625c2f3b62, type: 2}