Merge pull request #1 from DispenserBro/dev

dev
This commit is contained in:
Данил Ярош 2025-11-11 17:56:14 +03:00 committed by GitHub
commit 33b2ed843c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 52905 additions and 1424 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -266,18 +266,18 @@
"path": "<Gamepad>/dpad/up",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"groups": "",
"action": "Jump",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "c2638292-b3cd-4923-95b0-184c5c5fd5f3",
"path": "<XRController>/secondaryButton",
"id": "4b902c05-3ac4-47e2-b251-5772c1d59859",
"path": "<Gamepad>/rightShoulder",
"interactions": "",
"processors": "",
"groups": "XR",
"groups": "Gamepad",
"action": "Jump",
"isComposite": false,
"isPartOfComposite": false
@ -296,10 +296,10 @@
{
"name": "",
"id": "b68728dc-63a0-476c-96f9-9f2e24f2ca57",
"path": "<Gamepad>/buttonEast",
"path": "<Gamepad>/rightShoulder",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"groups": ";Gamepad",
"action": "Interact",
"isComposite": false,
"isPartOfComposite": false
@ -430,6 +430,15 @@
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "AddCredit",
"type": "Button",
"id": "dc985cb4-1124-4d19-b5da-8d3200cec195",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
@ -740,6 +749,50 @@
"action": "Secret",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "e9ac3494-4f47-41d5-8024-94294f144f2a",
"path": "<Keyboard>/f6",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "AddCredit",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "One Modifier",
"id": "8e7b642e-3796-45cc-8dc1-c5c8eb6649ba",
"path": "OneModifier",
"interactions": "",
"processors": "",
"groups": "",
"action": "AddCredit",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "modifier",
"id": "00cf67d9-ef62-4c49-a9ca-53afa7255cc0",
"path": "<Gamepad>/dpad/left",
"interactions": "",
"processors": "",
"groups": "",
"action": "AddCredit",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "binding",
"id": "43c6b038-fbb8-4585-853b-f43bc8cd1273",
"path": "<Gamepad>/rightShoulder",
"interactions": "",
"processors": "",
"groups": "",
"action": "AddCredit",
"isComposite": false,
"isPartOfComposite": true
}
]
}

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

@ -14,6 +14,7 @@ GameObject:
- component: {fileID: 4577859410310799261}
- component: {fileID: -6245296037342915518}
- component: {fileID: 6566017136146807207}
- component: {fileID: 6824661392768214298}
- component: {fileID: -3912971501865491093}
m_Layer: 0
m_Name: Globals
@ -102,6 +103,30 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PrizeSystem
prizeErrorSound: {fileID: 0}
--- !u!114 &6824661392768214298
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4763496076470024150}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 60e317a2a046c5644ae8e9281284c244, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Managers.PaymentManager
saveMoneyBetweenSessions: 0
enableFreePlay: 0
addCreditAction: {fileID: -2889265436170138120, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
OnCreditsChanged:
m_PersistentCalls:
m_Calls: []
OnCreditAdded:
m_PersistentCalls:
m_Calls: []
OnCreditRemoved:
m_PersistentCalls:
m_Calls: []
--- !u!114 &-3912971501865491093
MonoBehaviour:
m_ObjectHideFlags: 0
@ -135,10 +160,14 @@ MonoBehaviour:
dpadDirection: 1
- pinIndex: 4
target: 0
button: 5
button: 11
dpadDirection: 0
- pinIndex: 10
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

@ -13,8 +13,8 @@ using UnityEngine.UI;
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool saveMoneyBetweenSessions;
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
[SerializeField] private bool saveMoneyBetweenSessions;
[SerializeField] private bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
public static UnityEvent<int> OnCreditAdded;
@ -165,7 +165,7 @@ public class ComWrapper : MonoBehaviour
// if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл")
// {
// //Счетчики пробега
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
// PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
// PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
//
// return true;
@ -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

@ -1,5 +1,6 @@
using System;
using System.IO;
using RR.Common.Behaviour.ScriptableObjects;
using UnityEngine;
using UnityEngine.SceneManagement;
@ -7,6 +8,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
{
@ -26,29 +28,30 @@ namespace RR.Game2D.Core.Managers
public AudioManager Audio {get; private set;}
// Payment control for game
public PaymentManager Payment {get; private set;}
// Globals
[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; }
[Header("Базовые настройки игры")]
[SerializeField] private int maxLives = 3;
public int StartingLives { get; private set; } = 3;
public int MaxLives { get; private set; } = 3;
public int LivesIncrement { get; private set; } = 0;
[Header("Информация для игры")]
public int World { get; private set; } = 1;
public int Stage { get; private set; } = 1;
public int StagesByWorld { get; private set; } = 3;
private int StagesByWorld { get; set; } = 3;
public int Lives { get; set; } = 3;
public int Coins { get; private set; } = 0;
[HideInInspector] public bool ResetLives { get; set; } = true;
private float gameStartTime;
private float _gameStartTime;
[Header("Игрок")]
[SerializeField] private GameObject playerPrefab;
@ -67,16 +70,16 @@ namespace RR.Game2D.Core.Managers
public event Action<float> GameOverEvent; // Когда игрок проигрывает
public event Action<float> GameWonEvent; // Когда игрок выигрывает
#region Unity Lifetime
// Singleton
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
if (Instance == null)
{
GameObject go = new GameObject(nameof(GameManager));
Instance = go.AddComponent<GameManager>();
DontDestroyOnLoad(go);
}
if (Instance != null) return;
GameObject go = new GameObject(nameof(GameManager));
Instance = go.AddComponent<GameManager>();
DontDestroyOnLoad(go);
}
private void Awake()
@ -101,12 +104,14 @@ 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);
}
// Initialize Global Scripts from prefab
if (globals == null)
{
GameObject prefab = Resources.Load<GameObject>(globalsPrefabResourcePath);
@ -117,21 +122,27 @@ namespace RR.Game2D.Core.Managers
}
}
// Initialize Settings Manager from Globals
if (Settings == null)
{
Settings = globals.GetComponent<SettingsManager>();
}
Settings.OnSettingsLoaded += OnSettingsChanged;
Settings.OnSettingsSaved += OnSettingsChanged;
// Initialize Payment control
if (Payment == null)
{
Payment = gameObject.AddComponent<PaymentManager>();
Payment.Initialize();
Payment = globals.GetComponent<PaymentManager>();
}
gameStartTime = Time.unscaledTime;
Logger.Log(gameObject.name,$"Game start time = {gameStartTime}");
Payment.OnCreditAdded.AddListener(OnCreditAdded);
_gameStartTime = Time.unscaledTime;
MaxLives = StartingLives;
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
}
@ -141,17 +152,6 @@ namespace RR.Game2D.Core.Managers
if (Instance == this) Instance = null;
}
//private void Start()
//{
// //Application.targetFrameRate = 60;
// //LoadScene("MainMenu", false);
// //NewGame();
// if (SceneManager.GetActiveScene().name != "MainMenu")
// {
// TrySpawnPlayerForActiveScene();
// }
//}
/// <summary>
/// Поиск префаба игрока.
/// Если не найдет префаб из инспектора, ищет в "Resources"
@ -173,6 +173,71 @@ namespace RR.Game2D.Core.Managers
}
return loaded;
}
#endregion
#region Payment Event Handlers
private void OnCreditAdded(int newCount)
{
if (SceneManager.GetActiveScene().name == "MainMenu")
{
if (newCount > 1)
{
MaxLives += LivesIncrement;
}
}
else
{
Lives += LivesIncrement;
LivesChanged?.Invoke(Lives);
}
Debug.Log($"Max Lives: {MaxLives}");
}
#endregion
#region Settings Event Handlers
private void OnSettingsChanged()
{
GameSettings newSettings = Settings.GetCurrentGameSettings();
int livesDiff = MaxLives - StartingLives;
StartingLives = newSettings.startLivesCount;
MaxLives = StartingLives + livesDiff;
LivesIncrement = newSettings.livesIncreasedByCredits;
}
#endregion
#region Pause
/// <summary>
/// Метод паузы игры.
/// Ставит на паузу все SFX и саму игру
/// </summary>
/// <param name="pause">Ставить ли на паузу</param>
public void SetPause(bool pause)
{
if (IsPaused == pause) return;
IsPaused = pause;
Time.timeScale = pause ? 0f : 1f;
Audio.SmoothPauseMusic(pause);
PauseStateChanged?.Invoke(IsPaused);
}
// Можно заменить на другое поведение
/// <summary>
/// Метод переключателя паузы.
/// </summary>
public void TogglePause() => SetPause(!IsPaused);
// --
#endregion
#region Levels and Scene management
/// <summary>
/// Начало новой игры.
@ -181,23 +246,25 @@ namespace RR.Game2D.Core.Managers
/// </summary>
public void NewGame()
{
Lives = maxLives;
Lives = MaxLives;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
gameStartTime = Time.time;
_gameStartTime = Time.time;
Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}");
if (!Payment.IsFreePlayEnabled()) Payment.ProcessPayment();
LoadLevel(1, 1);
}
public void ContinueGame()
{
Lives = maxLives + 1;
Lives = MaxLives;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
RespawnPlayer();
// gameStartTime = Time.time;
// LoadLevel(World, Stage);
}
// Можно заменить на другое поведение
@ -207,23 +274,50 @@ namespace RR.Game2D.Core.Managers
/// </summary>
private void GameOver() // => LoadScene("MainMenu", false);
{
// LoadScene("MainMenu", false);
// Lives = maxLives;
// LivesChanged?.Invoke(Lives);
float totalTime = Time.unscaledTime - gameStartTime;
float totalTime = Time.unscaledTime - _gameStartTime;
GameOverEvent?.Invoke(totalTime);
}
// --
private void GameWin() // => LoadScene("MainMenu", false);
{
// LoadScene("MainMenu", false);
// Lives = maxLives;
// LivesChanged?.Invoke(Lives);
float totalTime = Time.unscaledTime - gameStartTime;
float totalTime = Time.unscaledTime - _gameStartTime;
GameWonEvent?.Invoke(totalTime);
}
/// <summary>
/// Метод переход на следующий уровень.
/// Может загружать уровни и миры в прямой зависимости от
/// Количества уровней на мир(StagesByWorld)
/// </summary>
public void NextLevel()
{
if (Stage >= StagesByWorld)
{
World++;
Stage = 1;
}
else Stage++;
LoadLevel(World, Stage);
}
/// <summary>
/// Метод сброса уровня.
/// При сбросе также сбрасывает монеты и отнимает жизни
/// </summary>
[Obsolete("Используйте RespawnPlayer() для респавна игрока без сброса уровня и жизней.")]
public void ResetLevel()
{
Lives--;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
//print($"Lives: {lives}");
if (Lives > 0) LoadLevel(World, Stage);
else GameOver();
}
/// <summary>
/// Загрузка уровня по данным.
/// Загружает уровень по номерам мира и уровня
@ -242,10 +336,6 @@ namespace RR.Game2D.Core.Managers
}
else
{
//NewGame();
// Logger.Log("WIN");
// LoadScene("MainMenu", false);
// Можно заменить на другое поведение
GameWin();
@ -275,7 +365,32 @@ namespace RR.Game2D.Core.Managers
else _shouldSpawnPlayer = false;
}
/// <summary>
/// Метод проверки сцены на существование в настройках билда
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <returns></returns>
private static bool SceneInBuildSettingsExists(string sceneName)
{
var count = SceneManager.sceneCountInBuildSettings;
for (var i = 0; i < count; i++)
{
var path = SceneUtility.GetScenePathByBuildIndex(i);
if (Path.GetFileNameWithoutExtension(path) == sceneName)
return true;
}
return false;
}
/// <summary>
/// Метод обработки события загрузки сцены.
/// При загрузке сцены проверяет её название на наличие "-" (уровень формата "1-1")
/// Если находит, спавнит игрока
/// Также обновляет номера мира и уровня
/// </summary>
/// <param name="scene">Сцена</param>
/// <param name="mode">Режим загрузки сцены</param>
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
string currentSceneName = SceneManager.GetActiveScene().name;
@ -289,6 +404,10 @@ namespace RR.Game2D.Core.Managers
Stage = Convert.ToInt32(levelInfo[1]);
}
#endregion
#region Player management
/// <summary>
/// Метод для спавна игрока.
/// Ищет первый спавнпоинт, спавнит игрока на его позиции.
@ -313,7 +432,11 @@ namespace RR.Game2D.Core.Managers
PlayerSpawned?.Invoke(PlayerObj);
}
// Новый метод: респавн игрока без перезагрузки сцены
/// <summary>
/// Метод респавна игрока.
/// Отнимает жизнь, если жизней не осталось — вызывает GameOver()
/// Спавнит игрока на активном чекпоинте или на дефолтной позиции
/// </summary>
public void RespawnPlayer()
{
// отнимем жизнь (оставляем текущее поведение); монеты не сбрасываем
@ -353,7 +476,7 @@ namespace RR.Game2D.Core.Managers
if (PlayerObj == null)
{
// Создадим нового игрока, если объект был уничтожен
var prefab = ResolvePlayerPrefab();
GameObject prefab = ResolvePlayerPrefab();
if (prefab == null) return;
PlayerObj = Instantiate(prefab, respawnPos, Quaternion.identity);
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
@ -362,7 +485,7 @@ namespace RR.Game2D.Core.Managers
else
{
// Остановим движение и телепортируем
var rb = PlayerObj.GetComponent<Rigidbody2D>();
Rigidbody2D rb = PlayerObj.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.linearVelocity = Vector2.zero;
@ -373,56 +496,26 @@ namespace RR.Game2D.Core.Managers
}
// Повернём в нужную сторону (по X)
if (PlayerObj != null)
if (PlayerObj == null) return;
if (respawnForward.x != 0)
{
if (respawnForward.x != 0)
{
PlayerObj.transform.localEulerAngles = respawnForward.x >= 0
? Vector3.zero
: new Vector3(0, 180f, 0);
}
// Восстановим здоровье
var hp = PlayerObj.GetComponent<Health>();
if (hp != null)
hp.ResetHealth();
PlayerObj.transform.localEulerAngles = respawnForward.x >= 0
? Vector3.zero
: new Vector3(0, 180f, 0);
}
// Восстановим здоровье
Health playerHealth = PlayerObj.GetComponent<Health>();
if (playerHealth != null)
playerHealth.ResetHealth();
}
/// <summary>
/// Метод переход на следующий уровень.
/// Может загружать уровни и миры в прямой зависимости от
/// Количества уровней на мир(StagesByWorld)
/// </summary>
public void NextLevel()
{
if (Stage >= StagesByWorld)
{
World++;
Stage = 1;
}
else Stage++;
LoadLevel(World, Stage);
}
#endregion
#region Coins and Lives management
/// <summary>
/// Метод сброса уровня.
/// При сбросе также сбрасывает монеты и отнимает жизни
/// </summary>
public void ResetLevel()
{
Lives--;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
//print($"Lives: {lives}");
if (Lives > 0) LoadLevel(World, Stage);
else GameOver();
}
/// <summary>
/// Метод добавления монеты
/// Метод добавления внутриигровой монеты
/// </summary>
public void AddCoin(int count = 1)
{
@ -437,29 +530,6 @@ namespace RR.Game2D.Core.Managers
CoinsChanged?.Invoke(Coins);
}
/// <summary>
/// Метод паузы игры.
/// Ставит на паузу все SFX и саму игру
/// </summary>
/// <param name="pause">Ставить ли на паузу</param>
public void SetPause(bool pause)
{
if (IsPaused == pause) return;
IsPaused = pause;
Time.timeScale = pause ? 0f : 1f;
Audio.SmoothPauseMusic(pause);
PauseStateChanged?.Invoke(IsPaused);
}
// Можно заменить на другое поведение
/// <summary>
/// Метод переключателя паузы.
/// </summary>
public void TogglePause() => SetPause(!IsPaused);
// --
// Можно заменить на другое поведение
/// <summary>
/// Метод для добавления жизни.
@ -471,21 +541,6 @@ namespace RR.Game2D.Core.Managers
}
// --
/// <summary>
/// Метод проверки сцены на существование в настройках билда
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <returns></returns>
private static bool SceneInBuildSettingsExists(string sceneName)
{
var count = SceneManager.sceneCountInBuildSettings;
for (var i = 0; i < count; i++)
{
var path = SceneUtility.GetScenePathByBuildIndex(i);
if (Path.GetFileNameWithoutExtension(path) == sceneName)
return true;
}
return false;
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5cbfc8b1557f401886b9e62505e9e6a6
timeCreated: 1762846161

View File

@ -11,6 +11,7 @@ namespace RR.Common.Managers
private ComWrapper _comWrapper;
private In _in;
private PrizeSystem _prizeSystem;
private PaymentManager _paymentManager;
private ComInputToInputSystemAdapter _adapter;
private void Awake()
@ -20,12 +21,14 @@ namespace RR.Common.Managers
_comWrapper = GetComponent<ComWrapper>();
_in = GetComponent<In>();
_prizeSystem = GetComponent<PrizeSystem>();
_paymentManager = GetComponent<PaymentManager>();
_adapter = GetComponent<ComInputToInputSystemAdapter>();
if (_comWrapper == null) gameObject.AddComponent<ComWrapper>();
if (_in == null) gameObject.AddComponent<In>();
if (_prizeSystem == null) gameObject.AddComponent<PrizeSystem>();
if (_settingsManager == null) gameObject.AddComponent<SettingsManager>();
if (_paymentManager == null) gameObject.AddComponent<PaymentManager>();
if (_adapter == null) gameObject.AddComponent<ComInputToInputSystemAdapter>();
_settingsManager.Initialize();
@ -40,6 +43,9 @@ namespace RR.Common.Managers
_prizeSystem.Initialize();
GameManager.Logger.Log(name, "PrizeSystem successfully initialized.");
_paymentManager.Initialize();
GameManager.Logger.Log(name, "PaymentManager successfully initialized.");
_adapter.Initialize();
GameManager.Logger.Log(name, "InputAdapter successfully initialized.");
}

View File

@ -1,12 +1,34 @@
using RR.Common.Behaviour.ScriptableObjects;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
namespace RR.Common.Managers
{
/// <summary>
/// Менеджер системы оплаты. Управляет кредитами и интегрируется с ComWrapper.
/// </summary>
public class PaymentManager : MonoBehaviour
{
public static PaymentManager Instance { get; private set; }
public int CreditsCount;
[Header("Настройки")]
[SerializeField] private bool saveMoneyBetweenSessions = false;
[SerializeField] private bool enableFreePlay = false;
[Header("Управление")]
[SerializeField] private InputActionReference addCreditAction;
[Header("Состояние")]
public int CreditsCount { get; private set; }
// События для уведомления других систем
public UnityEvent<int> OnCreditsChanged;
public UnityEvent<int> OnCreditAdded;
public UnityEvent<int> OnCreditRemoved;
#region Unity Lifetime
public void Initialize()
{
@ -18,21 +40,177 @@ namespace RR.Common.Managers
Instance = this;
DontDestroyOnLoad(gameObject);
CreditsCount = 0;
ComWrapper.OnCreditAdded.AddListener(UpdateCredits);
}
private void UpdateCredits(int newCreditsCount)
{
CreditsCount = newCreditsCount;
}
// Инициализация событий
OnCreditsChanged ??= new UnityEvent<int>();
OnCreditAdded ??= new UnityEvent<int>();
OnCreditRemoved ??= new UnityEvent<int>();
// Загрузка кредитов из сохранений
if (!saveMoneyBetweenSessions)
{
PlayerPrefs.SetInt("CoinsCount", 0);
}
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
// Подписка на события ComWrapper
ComWrapper.OnCreditAdded?.AddListener(HandleCreditAdded);
ComWrapper.OnCreditRemoved?.AddListener(HandleCreditRemoved);
// Привязка действия добавления кредита (для тестирования)
if (addCreditAction != null)
{
addCreditAction.action.performed += AddCreditsManually;
addCreditAction.action.Enable();
}
SettingsManager manager = SettingsManager.Instance;
if (manager != null)
{
manager.OnSettingsLoaded += SetFreePlay;
manager.OnSettingsSaved += SetFreePlay;
}
Debug.Log($"Инициализирован. Кредиты: {CreditsCount}, Бесплатная игра: {enableFreePlay}", this);
}
// Если не инициализируется в GameManager, инициализировать тут
// private void Awake()
// {
// Initialize();
// }
private void OnDestroy()
{
// Отписка от событий ComWrapper
ComWrapper.OnCreditAdded?.RemoveListener(HandleCreditAdded);
ComWrapper.OnCreditRemoved?.RemoveListener(HandleCreditRemoved);
// Отключение действия добавления кредита
if (addCreditAction == null) return;
addCreditAction.action.performed -= AddCreditsManually;
addCreditAction.action.Disable();
SettingsManager manager = SettingsManager.Instance;
if (manager == null) return;
manager.OnSettingsLoaded -= SetFreePlay;
manager.OnSettingsSaved -= SetFreePlay;
}
#endregion
#region Private Methods
/// <summary>
/// Обработчик добавления кредита из ComWrapper
/// </summary>
/// <param name="newCreditsCount">Новое количество кредитов</param>
private void HandleCreditAdded(int newCreditsCount)
{
CreditsCount = newCreditsCount;
OnCreditsChanged?.Invoke(CreditsCount);
OnCreditAdded?.Invoke(CreditsCount);
GameManager.Logger?.Log("PaymentManager", $"Кредит добавлен. Всего: {CreditsCount}");
}
/// <summary>
/// Обработчик удаления кредита из ComWrapper
/// </summary>
/// <param name="newCreditsCount">Новое количество кредитов</param>
private void HandleCreditRemoved(int newCreditsCount)
{
CreditsCount = newCreditsCount;
OnCreditsChanged?.Invoke(CreditsCount);
OnCreditRemoved?.Invoke(CreditsCount);
GameManager.Logger?.Log("PaymentManager", $"Кредит удален. Осталось: {CreditsCount}");
}
private void SetFreePlay()
{
if (SettingsManager.Instance == null) return;
GameSettings currentSettings = SettingsManager.Instance.GetCurrentGameSettings();
enableFreePlay = currentSettings.isFreeGame;
Debug.Log($"Бесплатная игра установлена из настроек: {enableFreePlay}", this);
}
#endregion
#region Public API
/// <summary>
/// Проверяет, может ли игрок начать игру (есть ли кредиты или включена бесплатная игра)
/// </summary>
/// <returns>true если игрок может начать игру</returns>
public bool CanStartGame()
{
if (enableFreePlay)
{
GameManager.Logger?.Log("PaymentManager", "Бесплатная игра включена");
return true;
}
bool canStart = CreditsCount > 0;
GameManager.Logger?.Log("PaymentManager", $"Проверка возможности старта: {canStart} (кредиты: {CreditsCount})");
return canStart;
}
/// <summary>
/// Выполняет списание платы за запуск игры через ComWrapper.Pay()
/// </summary>
/// <returns>true если оплата прошла успешно</returns>
public bool ProcessPayment()
{
if (enableFreePlay)
{
GameManager.Logger?.Log("PaymentManager", "Оплата пропущена - бесплатная игра");
return true;
}
bool paymentSuccess = ComWrapper.Pay();
Debug.Log(paymentSuccess ? "Оплата успешна" : "Оплата не прошла - недостаточно кредитов", this);
return paymentSuccess;
}
/// <summary>
/// Добавляет кредиты вручную (для тестирования)
/// </summary>
/// <param name="amount">Количество кредитов</param>
public static void AddCreditsManually(int amount = 1)
{
for (int i = 0; i < amount; i++)
{
ComWrapper.AddCredit();
}
}
private static void AddCreditsManually(InputAction.CallbackContext context)
{
AddCreditsManually();
// OnCreditAdded?.Invoke(CreditsCount);
// OnCreditsChanged?.Invoke(CreditsCount);
}
/// <summary>
/// Включает/выключает режим бесплатной игры
/// </summary>
/// <param name="enable">Включить/выключить</param>
public void SetFreePlayMode(bool enable)
{
enableFreePlay = enable;
GameManager.Logger?.Log("PaymentManager", $"Бесплатная игра: {enableFreePlay}");
}
/// <summary>
/// Получить текущее состояние режима бесплатной игры
/// </summary>
public bool IsFreePlayEnabled() => enableFreePlay;
public void SetFreePlay(bool freePlayEnabled)
{
enableFreePlay = freePlayEnabled;
}
#endregion
}
}

View File

@ -5,6 +5,7 @@ using UnityEngine;
using RR.Game2D.Core.Managers;
using RR.Common.Behaviour.ScriptableObjects;
using UnityEngine.Events;
namespace RR.Common.Managers
{
@ -17,8 +18,8 @@ namespace RR.Common.Managers
private const string SETTINGS_FILE_NAME = "settings.json";
private string SettingsFilePath => Path.Combine(Application.persistentDataPath, SETTINGS_FILE_NAME);
public Action OnSettingsLoaded;
public Action OnSettingsSaved;
public event Action OnSettingsLoaded;
public event Action OnSettingsSaved;
public void Initialize()
{
@ -103,6 +104,7 @@ namespace RR.Common.Managers
return;
}
gameSettings.ValidateSettings();
try
{
string json = JsonUtility.ToJson(gameSettings, true);
@ -159,12 +161,6 @@ namespace RR.Common.Managers
Debug.Log($"{name}: Установлены жизни при пополнении баланса: {newLivesIncreasedByCredits}.");
}
public void SetSetLivesToDefaultAfterFirstLose(bool newSetLivesToDefaultAfterFirstLose)
{
gameSettings.setLivesToDefaultAfterFirstLose = newSetLivesToDefaultAfterFirstLose;
Debug.Log($"{name}: Установлен флаг 'set lives to default after first lose': {newSetLivesToDefaultAfterFirstLose}.");
}
public GameSettings GetCurrentGameSettings() => gameSettings;
}
}

View File

@ -20,7 +20,6 @@ namespace RR.Common.Behaviour.ScriptableObjects
// Player settings
public int startLivesCount;
public int livesIncreasedByCredits;
public bool setLivesToDefaultAfterFirstLose;
public void SetDefaultValues()
{
@ -31,17 +30,16 @@ namespace RR.Common.Behaviour.ScriptableObjects
isFreeGame = false;
isGivingPrizes = true;
setLivesToDefaultAfterFirstLose = true;
startLivesCount = 3;
livesIncreasedByCredits = 1;
}
public void ValidateSettings()
{
if (musicVolume < 0f || musicVolume > 1f)
if (musicVolume is < 0f or > 1f)
musicVolume = Mathf.Clamp01(musicVolume);
if (sfxVolume < 0f || sfxVolume > 1f)
if (sfxVolume is < 0f or > 1f)
sfxVolume = Mathf.Clamp01(sfxVolume);
if (startLivesCount < 1)
@ -58,9 +56,6 @@ namespace RR.Common.Behaviour.ScriptableObjects
if (GameManager.Instance != null)
{
if (!setLivesToDefaultAfterFirstLose)
GameManager.Instance.ResetLives = true;
GameManager.Instance.Audio.SetMusicVolume(musicVolume);
GameManager.Instance.Audio.SetSfxVolume(sfxVolume);
}

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

@ -76,13 +76,13 @@ namespace RR.Common.UI
}
}
struct LevelEntry { public string name; public int world; public int stage; }
private struct LevelEntry { public string name; public int world; public int stage; }
/// <summary>
/// Метод для перебора уровней в игре
/// </summary>
/// <returns>LevelEntry - текущий мир при переборе</returns>
IEnumerable<LevelEntry> EnumerateLevelScenesInBuild()
private static IEnumerable<LevelEntry> EnumerateLevelScenesInBuild()
{
int n = SceneManager.sceneCountInBuildSettings;
for (int i = 0; i < n; i++)
@ -111,7 +111,7 @@ namespace RR.Common.UI
/// <param name="to">Конечная прозрачнеость</param>
/// <param name="time">Время перехода</param>
/// <param name="onDone">Действие при выполнении перехода</param>
IEnumerator Fade(float from, float to, float time, System.Action onDone = null)
private IEnumerator Fade(float from, float to, float time, System.Action onDone = null)
{
if (!canvasGroup) { onDone?.Invoke(); yield break; }
float t = 0f;
@ -128,7 +128,7 @@ namespace RR.Common.UI
onDone?.Invoke();
}
void Update()
private void Update()
{
// ESC закрыть
if (gameObject.activeSelf && close.action.WasPressedThisFrame())

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,18 +1,139 @@
using System;
using System.Collections.Generic;
using System.Collections;
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
{
[RequireComponent(typeof(CanvasGroup), typeof(SettingsUIList))]
public class SettingsUI : MonoBehaviour
{
[Header("Управление")]
[SerializeField] private InputActionReference triggerKey;
[SerializeField] private Button saveButton;
[Header("UI")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Transform content;
[SerializeField] private float fadeDuration = 0.15f;
private SettingsUIList settingsUIList;
private SettingsManager settingsManager;
private void Awake()
{
settingsManager = SettingsManager.Instance;
if (triggerKey != null) triggerKey.action.started += Toggle;
if (settingsManager == null)
{
GameManager.Logger.LogError(name, "SettingsManager is null!");
return;
}
settingsManager.OnSettingsLoaded += UpdateSettings;
settingsManager.OnSettingsSaved += SaveSettingsHandler;
if (saveButton) saveButton.onClick.AddListener(SaveAndClose);
gameObject.SetActive(false);
}
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();
StartCoroutine(Fade(0f, 1f, fadeDuration));
}
private void Close()
{
StartCoroutine(Fade(1f, 0f, fadeDuration, () => { gameObject.SetActive(false); }));
}
private void Save()
{
UpdateSettings();
settingsUIList.SaveSettings();
}
private void SaveAndClose()
{
Save();
Close();
}
private void UpdateSettings()
{
if (settingsUIList == null) settingsUIList = content.GetComponent<SettingsUIList>();
if (settingsUIList != null) settingsUIList.UpdateSettings(settingsManager.GetCurrentGameSettings());
}
private void SaveSettingsHandler()
{
GameManager.Logger.Log(name, "Saved settings");
}
/// <summary>
/// Метод для плавного перехода (затухания)
/// </summary>
/// <param name="from">Начальная прозрачность</param>
/// <param name="to">Конечная прозрачнеость</param>
/// <param name="time">Время перехода</param>
/// <param name="onDone">Действие при выполнении перехода</param>
private IEnumerator Fade(float from, float to, float time, System.Action onDone = null)
{
if (!canvasGroup) { onDone?.Invoke(); yield break; }
float t = 0f;
canvasGroup.alpha = from;
canvasGroup.blocksRaycasts = true;
while (t < time)
{
t += Time.unscaledDeltaTime;
canvasGroup.alpha = Mathf.Lerp(from, to, t / time);
yield return null;
}
canvasGroup.alpha = to;
canvasGroup.blocksRaycasts = to > 0.99f;
onDone?.Invoke();
}
}
}

View File

@ -0,0 +1,207 @@
using System;
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;
namespace RR.Common.UI
{
public class SettingsUIList : MonoBehaviour
{
public List<Selectable> settingsList = new();
[Header("Настройки")]
// Volume
[SerializeField] private Slider musicVolumeSlider;
[SerializeField] private Slider sfxVolumeSlider;
// Payment
[SerializeField] private Toggle freeGameToggle;
[SerializeField] private Toggle givingPrizesToggle;
// Player
[SerializeField] private Slider startLivesSlider;
[SerializeField] private Slider increasedLivesSlider;
[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;
private void Awake()
{
settingsManager = SettingsManager.Instance;
if (settingsManager == null)
{
GameManager.Logger.LogError(name, "SettingsManager is null!");
return;
}
settingsList.Add(musicVolumeSlider);
settingsList.Add(sfxVolumeSlider);
settingsList.Add(freeGameToggle);
settingsList.Add(givingPrizesToggle);
settingsList.Add(startLivesSlider);
settingsList.Add(increasedLivesSlider);
currentSettings = settingsManager.GetCurrentGameSettings();
AddListeners();
UpdateSettings(currentSettings);
}
private void OnDestroy()
{
RemoveListeners();
}
public void UpdateSettings(GameSettings newSettings = null)
{
if (newSettings != null) currentSettings = newSettings;
RemoveListeners();
musicVolumeSlider.value = currentSettings.musicVolume;
sfxVolumeSlider.value = currentSettings.sfxVolume;
freeGameToggle.isOn = currentSettings.isFreeGame;
givingPrizesToggle.isOn = currentSettings.isGivingPrizes;
startLivesSlider.value = currentSettings.startLivesCount;
increasedLivesSlider.value = currentSettings.livesIncreasedByCredits;
AddListeners();
}
public void SaveSettings()
{
settingsManager.SaveSettings();
}
private void AddListeners()
{
// 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()
{
// 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,3 @@
fileFormatVersion: 2
guid: 98f414c07d2f47e794d25468e8519ba7
timeCreated: 1762519929

View File

@ -0,0 +1,39 @@
using System;
using RR.Common.Managers;
using RR.Game2D.Core.Managers;
using TMPro;
using UnityEngine;
public class UICreditsTextController : MonoBehaviour
{
// UI elements
[SerializeField] private TextMeshProUGUI creditsText;
[SerializeField] private TextMeshProUGUI livesText;
// Strings to display
[SerializeField] private string creditsString = "У вас {0} кредитов";
[SerializeField] private string livesString = "У вас будет {0}";
private void Start()
{
UpdateCreditsText(0);
UpdateLivesText(0);
// Subscribe to events
PaymentManager.Instance?.OnCreditsChanged.AddListener(UpdateCreditsText);
PaymentManager.Instance?.OnCreditsChanged.AddListener(UpdateLivesText);
}
private void UpdateCreditsText(int creditsCount)
{
creditsText.text = string.Format(creditsString, creditsCount);
}
private void UpdateLivesText(int creditsCount)
{
int totalLives = creditsCount > 1 ?
GameManager.Instance.StartingLives + GameManager.Instance.LivesIncrement * (creditsCount - 1) :
GameManager.Instance.StartingLives;
livesText.text = string.Format(livesString, totalLives);
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 693c49fe51646a7419844c26fc01f369

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

36437
Assets/_Recovery/0 (5).unity Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fa0cc7aabc3ce124e9fefec4607c9d6b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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}