using System; using System.Collections; using System.Collections.Generic; using System.Linq; using BlueWater.Audios; using BlueWater.Items; using BlueWater.Players.Tycoons; using BlueWater.Tycoons; using BlueWater.Utility; using Sirenix.OdinInspector; using TMPro; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UI; namespace BlueWater { public class LiquidController : MonoBehaviour { #region Variables [Title("컴포넌트")] [SerializeField] private GameObject _liquidPanel; [SerializeField] private GameObject _shaker; [SerializeField] private Renderer _renderTexture; [SerializeField] private Renderer _liquidRenderer; [SerializeField] private Collider2D _reachedCollider; [SerializeField] private TMP_Text _amountText; [SerializeField] private Image _balloonImage; [SerializeField] private Image _completeCocktailImage; [SerializeField] private TMP_Text _completeText; [Title("스폰 데이터")] [SerializeField, Required] private Transform _spawnTransform; [SerializeField] private Transform _spawnLocation; [SerializeField] private Vector3 _pushDirection = new(-3f, -1f, 0f); [SerializeField] private float _pushPower = 50; [Title("사운드")] [SerializeField] private string _pouringSfxName = "Pouring"; [SerializeField] private string _succeedMakingCocktailSfxName = "SucceedMakingCocktail"; [SerializeField] private string _failedMakingCocktailSfxName = "FailedMakingCocktail"; [Title("Liquid / Garnish")] [SerializeField, Required, Tooltip("원액 프리팹")] private Liquid _liquidObject; [SerializeField, Required, Tooltip("가니쉬 프리팹")] private Garnish _garnishObject; [SerializeField, Tooltip("초당 생성되는 액체 수(ml)")] private int _liquidsPerSecond = 100; [SerializeField, Range(0f, 1f), Tooltip("목표 색상으로 변경되는데 걸리는 시간")] private float _colorLerpSpeed = 0.5f; [SerializeField, Range(1f, 5f), Tooltip("목표 색상 * 밝기")] private float _colorIntensity = 2f; [Title("도착 지점")] [SerializeField, Tooltip("도착 지점의 Lerp값")] private Vector2 _reachedLerpPosition = new(-10.5f, 4f); [Title("오브젝트 풀링")] [SerializeField, Tooltip("오브젝트 풀링 최대 개수")] private int _objectPoolCount = 1000; [Title("패널")] [SerializeField] private Sprite _centerBalloonImage; [SerializeField] private Sprite _playerBalloonImage; [SerializeField] private float _moveToPlayerDuration = 0.2f; [SerializeField] private float _moveToCenterDuration = 0.15f; [SerializeField] private Vector3 _centerPosition = new(-300f, 0f, 0f); [SerializeField] private Vector3 _endPositionOffset = new(0f, 20f, 0f); [SerializeField] private Vector3 _endScale = new(0.3f, 0.3f, 0.3f); [SerializeField] private Vector2 _cameraDistance = new(5f, 15f); [SerializeField] private Vector2 _offsetY = new(20f, 15f); private Barrel _currentBarrel; private IObjectPool _liquidObjectPool; private IObjectPool _garnishObjectPool; private List _activeLiquidDatas = new(); private List _activeGarnishDatas = new(); private Dictionary _liquidDataCounts = new(7); private Material _instanceMaterial; private bool _isShowingPanel; private bool _isPouring; private bool _isCompleted; private float _elapsedTime = float.PositiveInfinity; private int _maxLiquidCount; private int _instanceLiquidCount; private float _currentLiquidAmount; private float _liquidReachedTime; private float _timeInterval; private Color _currentMixedColor = Color.black; private Color _targetColor; private Camera _overlayCamera; private TycoonPlayer _tycoonPlayer; private Vector2 _originalReachedPosition; private Vector3 _lastPlayerPosition; private Vector3 _originalPanelScale; private Coroutine _movePanelToPlayerInstance; private Coroutine _movePanelToCenterInstance; // Hashes private static readonly int LiquidAmountHash = Shader.PropertyToID("_LiquidAmount"); private static readonly int LiquidColorHash = Shader.PropertyToID("_LiquidColor"); #endregion // Unity events #region Unity events private void Awake() { _liquidObjectPool = new ObjectPool(CreateLiquidObject, OnGetLiquidObject, OnReleaseLiquidObject, OnDestroyLiquidObject, maxSize: _objectPoolCount); _garnishObjectPool = new ObjectPool(CreateGarnishObject, OnGetGarnishObject, OnReleaseGarnishObject, OnDestroyGarnishObject, maxSize: _objectPoolCount); } private void Start() { EventManager.OnCocktailDiscarded += ReleaseAllObject; EventManager.OnPlaceOnServingTable += ReleaseAllObject; EventManager.OnCocktailServedToCustomer += ReleaseAllObject; EventManager.OnChangedRandomCocktail += ReleaseAllObject; LiquidIngredient.OnReachedTarget += OnTargetReached; Barrel.OnBarrelInteracted += HandleBarrelInteraction; Barrel.OnBarrelCancelInteracted += HandleBarrelCancelInteraction; _overlayCamera = TycoonCameraManager.Instance.LiquidOverlayCamera; _tycoonPlayer = GameManager.Instance.CurrentTycoonPlayer; _instanceMaterial = Instantiate(_liquidRenderer.material); _liquidRenderer.material = _instanceMaterial; _originalReachedPosition = _reachedCollider.transform.position; _originalPanelScale = _liquidPanel.transform.localScale; _instanceMaterial.SetFloat(LiquidAmountHash, 0f); _timeInterval = 1f / _liquidsPerSecond; _shaker.SetActive(true); _amountText.enabled = true; _completeCocktailImage.enabled = false; _completeText.enabled = false; _instanceLiquidCount = 0; _maxLiquidCount = ItemManager.Instance.CocktailDataSo.MaxLiquidCount; SetCurrentAmount(0f); } private void FixedUpdate() { if (_isPouring) { var currentBarrel = _currentBarrel; _elapsedTime += Time.deltaTime; while (_elapsedTime >= _timeInterval) { if (!currentBarrel.CanConsume(1)) { HandleBarrelCancelInteraction(); return; } switch (currentBarrel.GetLiquidData().Type) { case LiquidType.None: Debug.LogError("원액 종류 None 오류"); break; case LiquidType.Liquid: _liquidObjectPool.Get(); break; case LiquidType.Garnish: _garnishObjectPool.Get(); break; default: throw new ArgumentOutOfRangeException(); } if (!_liquidDataCounts.TryAdd(currentBarrel.GetLiquidData(), 1)) { _liquidDataCounts[currentBarrel.GetLiquidData()] += 1; } currentBarrel.Consume(1); _elapsedTime -= _timeInterval; // 술이 완성되었을 때 if (_instanceLiquidCount >= _maxLiquidCount) { StartCoroutine(nameof(CompleteCocktail)); return; } } } if (_liquidReachedTime + _colorLerpSpeed >= Time.time) { _currentMixedColor = Color.Lerp(_currentMixedColor, _targetColor, _colorLerpSpeed * Time.deltaTime); _instanceMaterial.SetColor(LiquidColorHash, _currentMixedColor * _colorIntensity); } } private void OnDestroy() { EventManager.OnCocktailDiscarded -= ReleaseAllObject; EventManager.OnPlaceOnServingTable -= ReleaseAllObject; EventManager.OnCocktailServedToCustomer -= ReleaseAllObject; EventManager.OnChangedRandomCocktail -= ReleaseAllObject; LiquidIngredient.OnReachedTarget -= OnTargetReached; Barrel.OnBarrelInteracted -= HandleBarrelInteraction; Barrel.OnBarrelCancelInteracted -= HandleBarrelCancelInteraction; } #endregion // Object pooling system #region Object pooling system // 원액 오브젝트 풀 private Liquid CreateLiquidObject() { var instance = Instantiate(_liquidObject, _spawnTransform.position, Quaternion.identity, _spawnLocation); instance.SetManagedPool(_liquidObjectPool); return instance; } private void OnGetLiquidObject(Liquid liquid) { _instanceLiquidCount++; liquid.Initialize(_spawnTransform.position, Quaternion.identity, _reachedCollider, _pushDirection.normalized * _pushPower, _currentBarrel.GetLiquidData().Color); _activeLiquidDatas.Add(liquid); } private void OnReleaseLiquidObject(Liquid liquid) { liquid.gameObject.SetActive(false); _activeLiquidDatas.Remove(liquid); } private void OnDestroyLiquidObject(Liquid liquid) { Destroy(liquid.gameObject); _activeLiquidDatas.Remove(liquid); } // 가니쉬 오브젝트 풀 private Garnish CreateGarnishObject() { var instance = Instantiate(_garnishObject, _spawnTransform.position, Quaternion.identity, _spawnLocation); instance.SetManagedPool(_garnishObjectPool); return instance; } private void OnGetGarnishObject(Garnish garnish) { _instanceLiquidCount++; garnish.Initialize(_spawnTransform.position, Quaternion.identity, _reachedCollider, _pushDirection.normalized * _pushPower, _currentBarrel.GetLiquidData().Sprite); _activeGarnishDatas.Add(garnish); } private void OnReleaseGarnishObject(Garnish garnish) { garnish.gameObject.SetActive(false); _activeGarnishDatas.Remove(garnish); } private void OnDestroyGarnishObject(Garnish garnish) { Destroy(garnish.gameObject); _activeGarnishDatas.Remove(garnish); } #endregion // Custom methods #region Custom methods /// /// 술 제조 과정 초기화 함수 /// public void ReleaseAllObject() { // 리스트 삭제는 뒤에서부터 해야 오류가 없음 for (var i = _activeLiquidDatas.Count - 1; i >= 0; i--) { _activeLiquidDatas[i].Destroy(); } _liquidDataCounts.Clear(); _isCompleted = false; _instanceLiquidCount = 0; _instanceMaterial.SetFloat(LiquidAmountHash, 0f); SetCurrentAmount(0f); HidePanel(); } public void ReleaseAllObject(CocktailData cocktailData) { ReleaseAllObject(); } public void ReleaseAllObject(CocktailData cocktailData, bool isServedPlayer) { if (!isServedPlayer) return; ReleaseAllObject(); } public void HandleBarrelInteraction(Barrel barrel) { _currentBarrel = barrel; if (_instanceLiquidCount == 0) { ShowPanelStarted(); _shaker.SetActive(true); _amountText.enabled = true; _completeCocktailImage.enabled = false; _completeText.enabled = false; _isCompleted = false; _currentMixedColor = barrel.GetLiquidData().Color; _instanceMaterial.SetColor(LiquidColorHash, _currentMixedColor * _colorIntensity); _reachedCollider.transform.position = _originalReachedPosition; EventManager.InvokeCocktailStarted(); } else if (_instanceLiquidCount >= _maxLiquidCount) { return; } _elapsedTime = 0f; _isPouring = true; AudioManager.Instance.PlaySfx(_pouringSfxName); // To Center 이동 코루틴이 활성화 중이지 않을 때 if (_movePanelToCenterInstance == null) { // To Player 이동 코루틴이 활성화 중이라면 멈추고 To Center 활성화 if (_movePanelToPlayerInstance != null) { StopCoroutine(_movePanelToPlayerInstance); _movePanelToPlayerInstance = null; } Utils.StartUniqueCoroutine(this, ref _movePanelToCenterInstance, MovePanelToCenter()); } } public void HandleBarrelCancelInteraction() { _isPouring = false; AudioManager.Instance.StopSfx(_pouringSfxName); Utils.StartUniqueCoroutine(this, ref _movePanelToPlayerInstance, MovePanelToPlayer()); } private void SetCurrentAmount(float value) { _currentLiquidAmount = value; if (_amountText) { var percent = (int)(_currentLiquidAmount / _maxLiquidCount * 100); _amountText.text = $"{percent}%"; } else { if (_amountText.enabled) { _amountText.enabled = false; } } } /// /// 술을 완성한 경우 /// private IEnumerator CompleteCocktail() { _isCompleted = true; HandleBarrelCancelInteraction(); yield return new WaitUntil(() => _currentLiquidAmount >= _maxLiquidCount); var currentCocktailIngredients = new List(7); foreach (var element in _liquidDataCounts) { var idx = element.Key.Idx; var amount = element.Value; currentCocktailIngredients.Add(new CocktailIngredient(idx, amount)); } // ItemManager를 통해 모든 CocktailData 가져오기 var cocktailDatas = ItemManager.Instance.CocktailDataSo.GetData(); CocktailData matchingCocktail = null; // 모든 칵테일 데이터를 순회하면서 조건에 맞는 칵테일 찾기 foreach (var cocktailData in cocktailDatas.Values) { var validIngredients = cocktailData.ValidIngredients; // 조건 1: 재료 개수 동일 체크 if (validIngredients.Count != currentCocktailIngredients.Count) continue; var allIngredientsMatch = true; // 현재 음료 재료를 하나씩 validIngredients와 비교 foreach (var currentIngredient in currentCocktailIngredients) { // 동일한 Idx를 가진 재료가 있는지 찾음 var matchingValidIngredient = validIngredients.FirstOrDefault(ingredient => ingredient.Idx == currentIngredient.Idx); // 만약 Idx가 일치하는 재료가 없으면 조건 불충족 if (matchingValidIngredient == null) { allIngredientsMatch = false; break; } // 조건 2: Amount 값이 RatioRange에 따른 오차 범위 내에 있는지 체크 var validAmount = matchingValidIngredient.Amount; //var maxLiquidCount = cocktailData.GetCocktailAmount(validIngredients); var range = _maxLiquidCount / 100 * cocktailData.RatioRange; var minAmount = validAmount - range; var maxAmount = validAmount + range; if (currentIngredient.Amount < minAmount || currentIngredient.Amount > maxAmount) { allIngredientsMatch = false; break; } } // 조건이 모두 만족하면 매칭되는 칵테일을 찾음 if (allIngredientsMatch) { matchingCocktail = cocktailData; break; } } // 조건에 만족하는 칵테일이 없음 if (matchingCocktail == null) { matchingCocktail = ItemManager.Instance.CocktailDataSo.GetDataByIdx("Cocktail000"); _completeText.text = Utils.GetLocalizedString("Failure"); AudioManager.Instance.PlaySfx(_failedMakingCocktailSfxName); } else { _completeText.text = $"{Utils.GetLocalizedString("Success")}!\n{Utils.GetLocalizedString(matchingCocktail.Idx)}"; AudioManager.Instance.PlaySfx(_succeedMakingCocktailSfxName); } _shaker.SetActive(false); _amountText.enabled = false; _completeCocktailImage.sprite = matchingCocktail.Sprite; _completeCocktailImage.enabled = true; _completeText.enabled = true; yield return new WaitForSeconds(1f); HidePanel(); EventManager.InvokeCocktailCompleted(matchingCocktail, true); } /// /// 사용된 색상의 비율에 맞게 색을 혼합시키는 함수 /// private Color MixColorsByTime() { var totalCounts = _liquidDataCounts.Values.Sum(); var mixedColor = Color.black; foreach (var element in _liquidDataCounts) { var color = element.Key.Color; var count = element.Value; var ratio = count / (float)totalCounts; mixedColor += color * ratio; } mixedColor.a = 1f; return mixedColor; } /// /// 액체가 특정 오브젝트에 충돌했을 때, 실행해야하는 과정 /// public void OnTargetReached() { _liquidReachedTime = Time.time; SetCurrentAmount(++_currentLiquidAmount); var liquidAmount = Mathf.Clamp(_currentLiquidAmount / _maxLiquidCount, 0f, 1f); var reachedColliderPositionY = Mathf.Lerp(_reachedLerpPosition.x, _reachedLerpPosition.y, liquidAmount); _reachedCollider.transform.position = new Vector3(_reachedCollider.transform.position.x, reachedColliderPositionY, _reachedCollider.transform.position.z); _instanceMaterial.SetFloat(LiquidAmountHash, liquidAmount); _targetColor = MixColorsByTime(); if (liquidAmount >= 1f) { HandleBarrelCancelInteraction(); } } public void ShowPanelStarted() { if (_isShowingPanel) return; _liquidPanel.transform.localScale = _originalPanelScale; _liquidPanel.transform.position = _centerPosition; _balloonImage.sprite = _centerBalloonImage; _liquidPanel.SetActive(true); _isShowingPanel = true; } private IEnumerator MovePanelToPlayer() { if (_isCompleted) { _movePanelToPlayerInstance = null; yield break; } yield return new WaitUntil(() => _activeLiquidDatas.Count == 0 && _activeGarnishDatas.Count == 0); var startScale = _liquidPanel.transform.localScale; var startPosition = _liquidPanel.transform.position; Camera mainCamera = TycoonCameraManager.Instance.MainCamera; var elapsedTime = 0f; while (elapsedTime <= _moveToPlayerDuration) { var playerPosition = _tycoonPlayer.transform.position; var distance = Vector3.Distance(playerPosition, mainCamera.transform.position); float t = Mathf.InverseLerp(_cameraDistance.x, _cameraDistance.y, distance); float offsetY = Mathf.Lerp(_offsetY.x, _offsetY.y, t); var playerViewportPoint = mainCamera.WorldToViewportPoint(playerPosition); var panelWorldPosition = _overlayCamera.ViewportToWorldPoint(new Vector3(playerViewportPoint.x, playerViewportPoint.y, playerViewportPoint.z)); panelWorldPosition.y += offsetY; var lerpTime = elapsedTime / _moveToPlayerDuration; _liquidPanel.transform.position = Vector3.Lerp(startPosition, panelWorldPosition, lerpTime); _liquidPanel.transform.localScale = Vector3.Lerp(startScale, _endScale, lerpTime); elapsedTime += Time.deltaTime; yield return null; } _balloonImage.sprite = _playerBalloonImage; _liquidPanel.transform.localScale = _endScale; // 완성되지 않았거나, 따르고 있지 않으면 플레이어를 추적함 var waitTime = new WaitForFixedUpdate(); while (!_isCompleted && !_isPouring) { var playerPosition = _tycoonPlayer.transform.position; var distance = Vector3.Distance(playerPosition, mainCamera.transform.position); float t = Mathf.InverseLerp(_cameraDistance.x, _cameraDistance.y, distance); float offsetY = Mathf.Lerp(_offsetY.x, _offsetY.y, t); var playerViewportPoint = mainCamera.WorldToViewportPoint(playerPosition); var panelWorldPosition = _overlayCamera.ViewportToWorldPoint(new Vector3(playerViewportPoint.x, playerViewportPoint.y, playerViewportPoint.z)); panelWorldPosition.y += offsetY; _liquidPanel.transform.position = panelWorldPosition; yield return waitTime; } _movePanelToPlayerInstance = null; } private IEnumerator MovePanelToCenter() { _balloonImage.sprite = _centerBalloonImage; var startScale = _liquidPanel.transform.localScale; var startPosition = _liquidPanel.transform.position; var elapsedTime = 0f; while (elapsedTime <= _moveToCenterDuration) { var lerpTime = elapsedTime / _moveToCenterDuration; _liquidPanel.transform.position = Vector3.Lerp(startPosition, _centerPosition, lerpTime); _liquidPanel.transform.localScale = Vector3.Lerp(startScale, _originalPanelScale, lerpTime); elapsedTime += Time.deltaTime; yield return null; } _liquidPanel.transform.position = _centerPosition; _liquidPanel.transform.localScale = _originalPanelScale; _movePanelToCenterInstance = null; } public void HidePanel() { if (!_isShowingPanel) return; _isShowingPanel = false; _liquidPanel.SetActive(false); } #endregion } }