chip-gameV3/Assets/Scripts/Characters/CharactersController.cs

534 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using DG.Tweening;
using PrizeSystem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.Serialization;
using UnityEngine.Video;
namespace CharacterSystem
{
public class CharactersController : MonoBehaviour
{
public static CharactersController I;
[SerializeField, FormerlySerializedAs("characterPrefabs")]
private List<CharacterDefinition> _characterDefinitions;
private readonly Dictionary<int, LoadedCharacter> _loadedCharacters = new();
private readonly Dictionary<int, Task<Character>> _loadingCharacters = new();
private readonly Dictionary<int, AsyncOperationHandle<VideoClip>> _loadedVideos = new();
private readonly Dictionary<int, Task<VideoClip>> _loadingVideos = new();
private readonly HashSet<int> _desiredIndices = new();
private bool[] _prizeAvailable;
private int _loadGeneration;
public static int CharacterIndex { get; private set; } = -1;
public static int CharactersAmount => I?._characterDefinitions?.Count ?? 0;
public static Character CurrentCharacter => I.GetLoadedCharacter(CharacterIndex);
public static bool HasActiveCharacter
=> I != null && CharacterIndex >= 0 && I._loadedCharacters.ContainsKey(CharacterIndex);
public IReadOnlyCollection<int> LoadedCharacterIndices => _loadedCharacters.Keys;
public IReadOnlyCollection<int> LoadedVideoIndices => _loadedVideos.Keys;
private void OnDestroy()
{
SettingsManager.OnSettingsChanged -= PullDifficultySettings;
_loadGeneration++;
ReleaseAllCharacters();
ReleaseAllVideos(true);
if (I == this)
I = null;
}
public async Task InitializeAsync()
{
I = this;
if (_characterDefinitions == null || _characterDefinitions.Count == 0)
throw new InvalidOperationException("Не заданы определения ботов в контроллере.");
ValidateDefinitions();
_prizeAvailable = new bool[_characterDefinitions.Count];
SetAllCharactersPrizeAvailable();
SettingsManager.OnSettingsChanged -= PullDifficultySettings;
SettingsManager.OnSettingsChanged += PullDifficultySettings;
PullDifficultySettings();
await PrepareWindowAsync(-1);
}
public void SetAllCharactersPrizeAvailable()
{
if (_prizeAvailable == null)
return;
for (int index = 0; index < _prizeAvailable.Length; index++)
_prizeAvailable[index] = true;
foreach (LoadedCharacter loadedCharacter in _loadedCharacters.Values)
loadedCharacter.Character.prizeModule.ResetModule();
}
public static void ResetAllCharactersDifficulty()
{
if (I == null)
return;
foreach (CharacterDefinition definition in I._characterDefinitions)
definition.DifficultyModule?.ResetPresetIndex();
}
public static void ResetSystem()
{
if (I == null)
return;
ResetAllCharactersDifficulty();
CharacterIndex = -1;
}
public void HideCharacter()
{
if (!HasActiveCharacter)
return;
CurrentCharacter.visualModule.GetHideSequence().PlayForward();
}
public void ReturnToMenu()
{
int oldIndex = CharacterIndex;
CharacterIndex = -1;
ResetAllCharactersDifficulty();
if (!_loadedCharacters.TryGetValue(oldIndex, out LoadedCharacter loadedCharacter))
{
PreloadMenuCharacter();
return;
}
Sequence sequence = loadedCharacter.Character.visualModule.GetHideSequence();
sequence.AppendCallback(PreloadMenuCharacter);
sequence.PlayForward();
}
public void StartSession(Action onComplete = null)
{
ResetSystem();
SetAllCharactersPrizeAvailable();
ShowCharacter(0, onComplete);
}
public void ApplyFirstCharacterColorsToBoard()
{
if (!_loadedCharacters.TryGetValue(0, out LoadedCharacter loadedCharacter))
{
Debug.LogError("Кьюб не подготовлен к запуску PvE.", this);
return;
}
VisualModule visualModule = loadedCharacter.Character.visualModule;
Board.Instance.SetColorsImmediately(
visualModule.InnerColor,
visualModule.OutlineColor,
visualModule.CellsColor);
}
public CharacterDefinition[] GetDefinitionsArray()
=> _characterDefinitions.ToArray();
public CharacterDefinition GetDefinition(int characterIndex)
{
if (characterIndex < 0 || characterIndex >= _characterDefinitions.Count)
throw new ArgumentOutOfRangeException(nameof(characterIndex));
return _characterDefinitions[characterIndex];
}
public IReadOnlyList<Prize> GetPrizes(int characterIndex)
{
List<PrizeSet> sets = PrizeSystemManager.PrizeContainer?.Sets;
if (sets == null || characterIndex < 0 || characterIndex >= sets.Count)
return Array.Empty<Prize>();
return sets[characterIndex]?.Prizes ?? (IReadOnlyList<Prize>)Array.Empty<Prize>();
}
public bool HasConfiguredPrize(int characterIndex)
=> GetPrizes(characterIndex).Any(prize => prize is not EmptyPrize);
public bool TryFetchCurrentPrizes(out List<Prize> prizes)
{
prizes = null;
if (CharacterIndex < 0 || CharacterIndex >= _prizeAvailable.Length || !_prizeAvailable[CharacterIndex])
return false;
_prizeAvailable[CharacterIndex] = false;
prizes = GetPrizes(CharacterIndex).ToList();
if (_loadedCharacters.TryGetValue(CharacterIndex, out LoadedCharacter loadedCharacter))
loadedCharacter.Character.prizeModule.TryFetchPrize(out _);
return true;
}
public bool TryGetCurrentPrize<T>(out T prize) where T : Prize
{
prize = null;
if (CharacterIndex < 0 || CharacterIndex >= _prizeAvailable.Length || !_prizeAvailable[CharacterIndex])
return false;
prize = GetPrizes(CharacterIndex).OfType<T>().FirstOrDefault();
return prize != null;
}
public void RefreshPrizes()
{
foreach (KeyValuePair<int, LoadedCharacter> entry in _loadedCharacters)
SetCharacterPrizes(entry.Key, entry.Value.Character);
}
public async void ShowCharacter(int newIndex, Action onComplete = null)
{
if (newIndex < 0 || newIndex >= _characterDefinitions.Count)
throw new ArgumentOutOfRangeException(nameof(newIndex));
int oldIndex = CharacterIndex;
try
{
await PrepareWindowAsync(newIndex, oldIndex);
}
catch (Exception exception)
{
Debug.LogException(exception, this);
return;
}
if (!_desiredIndices.Contains(newIndex) || !_loadedCharacters.ContainsKey(newIndex))
return;
CharacterIndex = newIndex;
if (newIndex == oldIndex)
{
onComplete?.Invoke();
return;
}
VideoUtility.CrossFadeToCharacter(newIndex, GetLoadedVideo(newIndex));
Character newCharacter = GetLoadedCharacter(newIndex);
Sequence sequence = DOTween.Sequence();
if (_loadedCharacters.TryGetValue(oldIndex, out LoadedCharacter oldCharacter))
{
sequence.Append(oldCharacter.Character.visualModule.GetHideSequence());
sequence.AppendInterval(0.1f);
}
sequence.Append(newCharacter.visualModule.GetShowSequence());
sequence.AppendCallback(() =>
{
SetDesiredWindow(newIndex);
ReleaseUndesiredCharacters();
ReleaseUndesiredVideos();
if (oldIndex >= 0 && newIndex < oldIndex)
EventBus.TriggerEvent(GameEvent.Return);
else
EventBus.TriggerEvent(GameEvent.Appear);
onComplete?.Invoke();
});
sequence.PlayForward();
}
public void SuspendForPvp()
{
CharacterIndex = -1;
_loadGeneration++;
_desiredIndices.Clear();
ReleaseUndesiredCharacters();
ReleaseUndesiredVideos();
}
public async void PreloadMenuCharacter()
{
try
{
await PrepareWindowAsync(-1);
}
catch (Exception exception)
{
Debug.LogException(exception, this);
}
}
private async Task PrepareWindowAsync(int centerIndex, int additionalIndex = -1)
{
int generation = ++_loadGeneration;
SetDesiredWindow(centerIndex, additionalIndex);
int[] indicesToLoad = _desiredIndices.ToArray();
List<Task> loadingTasks = new();
foreach (int characterIndex in indicesToLoad)
{
loadingTasks.Add(EnsureCharacterLoadedAsync(characterIndex));
loadingTasks.Add(EnsureVideoLoadedAsync(characterIndex));
}
await Task.WhenAll(loadingTasks);
if (generation == _loadGeneration)
{
ReleaseUndesiredCharacters();
ReleaseUndesiredVideos();
}
}
private async Task<Character> EnsureCharacterLoadedAsync(int characterIndex)
{
if (_loadedCharacters.TryGetValue(characterIndex, out LoadedCharacter loadedCharacter))
return loadedCharacter.Character;
if (_loadingCharacters.TryGetValue(characterIndex, out Task<Character> loadingTask))
return await loadingTask;
Task<Character> newLoadingTask = LoadCharacterAsync(characterIndex);
_loadingCharacters[characterIndex] = newLoadingTask;
try
{
return await newLoadingTask;
}
finally
{
_loadingCharacters.Remove(characterIndex);
}
}
private async Task<Character> LoadCharacterAsync(int characterIndex)
{
CharacterDefinition definition = _characterDefinitions[characterIndex];
AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(definition.Prefab.RuntimeKey);
await handle.Task;
if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null)
{
if (handle.IsValid())
Addressables.Release(handle);
throw new InvalidOperationException($"Не удалось загрузить персонажа {definition.CharacterName}.");
}
Character character = handle.Result.GetComponent<Character>();
if (character == null)
{
Addressables.ReleaseInstance(handle);
throw new InvalidOperationException($"У prefab персонажа {definition.CharacterName} отсутствует Character.");
}
character.Initialize(characterIndex);
SetCharacterPrizes(characterIndex, character);
_loadedCharacters[characterIndex] = new LoadedCharacter(character, handle);
if (!_desiredIndices.Contains(characterIndex))
ReleaseCharacter(characterIndex);
return character;
}
private async Task<VideoClip> EnsureVideoLoadedAsync(int characterIndex)
{
if (_loadedVideos.TryGetValue(characterIndex, out AsyncOperationHandle<VideoClip> loadedHandle))
return loadedHandle.Result;
if (_loadingVideos.TryGetValue(characterIndex, out Task<VideoClip> loadingTask))
return await loadingTask;
Task<VideoClip> newLoadingTask = LoadVideoAsync(characterIndex);
_loadingVideos[characterIndex] = newLoadingTask;
try
{
return await newLoadingTask;
}
finally
{
_loadingVideos.Remove(characterIndex);
}
}
private async Task<VideoClip> LoadVideoAsync(int characterIndex)
{
CharacterDefinition definition = _characterDefinitions[characterIndex];
AsyncOperationHandle<VideoClip> handle = Addressables.LoadAssetAsync<VideoClip>(
definition.BackgroundVideo.RuntimeKey);
await handle.Task;
if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null)
{
if (handle.IsValid())
Addressables.Release(handle);
throw new InvalidOperationException($"Не удалось загрузить видеофон {definition.CharacterName}.");
}
_loadedVideos[characterIndex] = handle;
if (!_desiredIndices.Contains(characterIndex))
ReleaseVideo(characterIndex);
return handle.Result;
}
private void SetDesiredWindow(int centerIndex, int additionalIndex = -1)
{
_desiredIndices.Clear();
if (centerIndex < 0)
{
_desiredIndices.Add(0);
}
else
{
int firstIndex = Mathf.Max(0, centerIndex - 1);
int lastIndex = Mathf.Min(_characterDefinitions.Count - 1, centerIndex + 1);
for (int index = firstIndex; index <= lastIndex; index++)
_desiredIndices.Add(index);
}
if (additionalIndex >= 0 && additionalIndex < _characterDefinitions.Count)
_desiredIndices.Add(additionalIndex);
}
private void ReleaseUndesiredCharacters()
{
int[] indicesToRelease = _loadedCharacters.Keys
.Where(index => !_desiredIndices.Contains(index))
.ToArray();
foreach (int characterIndex in indicesToRelease)
ReleaseCharacter(characterIndex);
}
private void ReleaseUndesiredVideos()
{
int[] indicesToRelease = _loadedVideos.Keys
.Where(index => !_desiredIndices.Contains(index))
.ToArray();
foreach (int characterIndex in indicesToRelease)
ReleaseVideo(characterIndex);
}
private void ReleaseAllCharacters()
{
foreach (int characterIndex in _loadedCharacters.Keys.ToArray())
ReleaseCharacter(characterIndex);
}
private void ReleaseAllVideos(bool immediately)
{
foreach (int characterIndex in _loadedVideos.Keys.ToArray())
ReleaseVideo(characterIndex, immediately);
}
private void ReleaseCharacter(int characterIndex)
{
if (!_loadedCharacters.Remove(characterIndex, out LoadedCharacter loadedCharacter))
return;
if (loadedCharacter.Handle.IsValid())
Addressables.ReleaseInstance(loadedCharacter.Handle);
}
private void ReleaseVideo(int characterIndex, bool immediately = false)
{
if (!_loadedVideos.Remove(characterIndex, out AsyncOperationHandle<VideoClip> handle) ||
!handle.IsValid())
return;
Action release = () =>
{
if (handle.IsValid())
Addressables.Release(handle);
};
if (immediately)
release();
else
VideoUtility.ReleaseBackgroundWhenUnused(handle.Result, release);
}
private Character GetLoadedCharacter(int characterIndex)
{
if (!_loadedCharacters.TryGetValue(characterIndex, out LoadedCharacter loadedCharacter))
throw new InvalidOperationException($"Персонаж с индексом {characterIndex} не загружен.");
return loadedCharacter.Character;
}
private VideoClip GetLoadedVideo(int characterIndex)
{
if (!_loadedVideos.TryGetValue(characterIndex, out AsyncOperationHandle<VideoClip> handle))
throw new InvalidOperationException($"Видеофон с индексом {characterIndex} не загружен.");
return handle.Result;
}
private void SetCharacterPrizes(int characterIndex, Character character)
{
List<PrizeSet> sets = PrizeSystemManager.PrizeContainer?.Sets;
if (sets == null || characterIndex < 0 || characterIndex >= sets.Count || sets[characterIndex] == null)
return;
character.prizeModule.SetPrizes(sets[characterIndex]);
if (_prizeAvailable != null && _prizeAvailable[characterIndex])
character.prizeModule.ResetModule();
}
private void PullDifficultySettings()
{
foreach (CharacterDefinition definition in _characterDefinitions)
definition.DifficultyModule?.PullSettings();
}
private void ValidateDefinitions()
{
for (int index = 0; index < _characterDefinitions.Count; index++)
{
CharacterDefinition definition = _characterDefinitions[index];
if (definition == null || definition.CharacterIndex != index ||
!definition.Prefab.RuntimeKeyIsValid() || !definition.BackgroundVideo.RuntimeKeyIsValid())
throw new InvalidOperationException($"Некорректное определение персонажа с индексом {index}.");
}
}
private sealed class LoadedCharacter
{
public LoadedCharacter(Character character, AsyncOperationHandle<GameObject> handle)
{
Character = character;
Handle = handle;
}
public Character Character { get; }
public AsyncOperationHandle<GameObject> Handle { get; }
}
}
}