diff --git a/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs index 953f6a5d4..b43e70e98 100644 --- a/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs +++ b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs @@ -1,4 +1,3 @@ -using System; using Opsive.BehaviorDesigner.Runtime.Tasks; using Opsive.BehaviorDesigner.Runtime.Tasks.Actions; using UnityEngine; @@ -6,160 +5,167 @@ namespace DDD { /// - /// IAiMovement를 이용해 인터랙션 타겟(주로 RestaurantInteractionComponent가 붙은 오브젝트)으로 이동하는 액션. - /// - 타겟은 우선 블랙보드(IRestaurantCustomerBlackboard)에서 가져오고, 없으면 Interactor의 FocusedInteractable에서 가져옵니다. - /// - 타겟에 RestaurantInteractionComponent가 있다면 가장 가까운 InteractionPoint를 목적지로 사용합니다. + /// IAiMovement를 이용해 인터랙션 타겟으로 이동하는 액션 /// - public class MoveToInteractionTarget : Opsive.BehaviorDesigner.Runtime.Tasks.Actions.Action + public class MoveToInteractionTarget : Action { - [Header("Target")] - [Tooltip("타겟에 RestaurantInteractionComponent가 있을 때 InteractionPoints를 사용해 가장 가까운 지점으로 이동합니다.")] - [SerializeField] private bool _useInteractionPoints = true; + [Header("Target Settings")] + [Tooltip("InteractionPoints를 사용해 가장 가까운 지점으로 이동")] + [SerializeField] private bool useInteractionPoints = true; [Tooltip("타겟이 없을 때 즉시 실패할지 여부")] - [SerializeField] private bool _failIfNoTarget = true; + [SerializeField] private bool failIfNoTarget = true; - [Header("Movement")] - [Tooltip("목적지에 도달했다고 간주할 거리")] - [SerializeField] private float _stoppingDistance = 0.5f; - [Tooltip("이동 중 목적지를 재계산하는 주기(초). 0 이하면 재계산하지 않음")] - [SerializeField] private float _repathInterval = 0.5f; + [Header("Movement Settings")] + [Tooltip("목적지 도달 거리")] + [SerializeField] private float stoppingDistance = 0.5f; + [Tooltip("목적지 재계산 주기(초), 0 이하면 비활성화")] + [SerializeField] private float repathInterval = 0.5f; - private IAiMovement _movement; - private float _repathTimer; - private Vector3 _currentDestination; - private bool _started; - private GameObject _resolvedTarget; + private IAiMovement movement; + private float repathTimer; + private Vector3 currentDestination; + private bool isMoving; + private GameObject cachedTarget; public override void OnStart() { - _movement = gameObject.GetComponentInParent(); - _repathTimer = 0f; - _started = false; - _resolvedTarget = ResolveTargetFromContext(); + movement = gameObject.GetComponentInParent(); + repathTimer = 0f; + isMoving = false; + cachedTarget = null; } public override TaskStatus OnUpdate() { - if (_movement == null) - { + if (movement == null) return TaskStatus.Failure; + + var target = GetTarget(); + if (target == null) + return failIfNoTarget ? TaskStatus.Failure : TaskStatus.Success; + + if (ShouldUpdateDestination()) + { + currentDestination = CalculateDestination(target); + StartOrUpdateMovement(); } - // 매 프레임 타겟이 갱신될 수 있으므로 재해결 (필요 시 비용 줄일 수 있음) - _resolvedTarget = _resolvedTarget ?? ResolveTargetFromContext(); + return CheckMovementCompletion(); + } - if (_resolvedTarget == null) + public override void OnEnd() + { + StopMovement(); + cachedTarget = null; + } + + private GameObject GetTarget() + { + // 캐시된 타겟이 유효하면 재사용 + if (IsValidTarget(cachedTarget)) + return cachedTarget; + + // 블랙보드에서 타겟 검색 + cachedTarget = gameObject.GetComponentInParent() + ?.GetCurrentInteractionTarget(); + + if (IsValidTarget(cachedTarget)) + return cachedTarget; + + // Interactor의 포커스된 타겟 검색 + var interactor = gameObject.GetComponentInParent(); + var focusedInteractable = interactor?.GetFocusedInteractable(); + cachedTarget = focusedInteractable?.GetInteractableGameObject(); + + return cachedTarget; + } + + private bool IsValidTarget(GameObject target) => + target != null && target; + + private bool ShouldUpdateDestination() + { + repathTimer -= Time.deltaTime; + return !isMoving || (repathInterval > 0f && repathTimer <= 0f); + } + + private Vector3 CalculateDestination(GameObject target) + { + repathTimer = repathInterval; + + if (!useInteractionPoints) + return target.transform.position; + + return target.TryGetComponent(out var ric) + ? GetNearestInteractionPoint(ric) + : target.transform.position; + } + + private Vector3 GetNearestInteractionPoint(RestaurantInteractionComponent ric) + { + var points = ric.GetInteractionPoints(); + if (points == null || points.Length == 0) + return ric.transform.position; + + var agentPosition = GetAgentPosition(); + var nearestPoint = ric.transform.position; + var minDistanceSqr = float.MaxValue; + + foreach (var point in points) { - if (_failIfNoTarget) return TaskStatus.Failure; - return TaskStatus.Success; - } - - // 타겟이 파괴되었는지 확인 - if (_resolvedTarget == null || (_resolvedTarget as UnityEngine.Object) == null) - { - return TaskStatus.Failure; - } - - // 목적지 계산 및 이동 시작/유지 - _repathTimer -= Time.deltaTime; - if (!_started || _repathInterval > 0f && _repathTimer <= 0f) - { - _currentDestination = GetBestDestination(_resolvedTarget); - _repathTimer = _repathInterval; - - if (!_started) + var distanceSqr = (point - agentPosition).sqrMagnitude; + if (distanceSqr < minDistanceSqr) { - if (!_movement.TryMoveToPosition(_currentDestination)) - { - return TaskStatus.Failure; - } - _movement.EnableMove(); - _movement.PlayMove(); - _started = true; - } - else - { - // 이동 중 목적지 갱신 시도 - _movement.TryMoveToPosition(_currentDestination); + minDistanceSqr = distanceSqr; + nearestPoint = point; } } - // 도달 판정 - var sqrDist = (GetAgentPosition() - _currentDestination).sqrMagnitude; - if (sqrDist <= _stoppingDistance * _stoppingDistance || _movement.HasReachedDestination()) + return nearestPoint; + } + + private void StartOrUpdateMovement() + { + if (!isMoving) { - _movement.StopMove(); - _movement.DisableMove(); + if (movement.TryMoveToPosition(currentDestination)) + { + movement.EnableMove(); + movement.PlayMove(); + isMoving = true; + } + } + else + { + movement.TryMoveToPosition(currentDestination); + } + } + + private TaskStatus CheckMovementCompletion() + { + var distanceSqr = (GetAgentPosition() - currentDestination).sqrMagnitude; + var stoppingDistanceSqr = stoppingDistance * stoppingDistance; + + if (distanceSqr <= stoppingDistanceSqr || movement.HasReachedDestination()) + { + StopMovement(); return TaskStatus.Success; } return TaskStatus.Running; } - public override void OnEnd() + private void StopMovement() { - // 액션 종료 시 이동 중지(안전장치) - if (_movement != null) + if (movement != null && isMoving) { - _movement.StopMove(); - _movement.DisableMove(); + movement.StopMove(); + movement.DisableMove(); + isMoving = false; } - _resolvedTarget = null; } - private GameObject ResolveTargetFromContext() - { - // 1) 공용 블랙보드에서 가져오기 - var sharedBlackboard = gameObject.GetComponentInParent(); - var bbTarget = sharedBlackboard?.GetCurrentInteractionTarget(); - if (bbTarget != null) return bbTarget; - - // 2) Interactor의 포커싱 대상에서 가져오기 - var interactor = gameObject.GetComponentInParent(); - var focused = interactor?.GetFocusedInteractable(); - if (focused != null) - { - return focused.GetInteractableGameObject(); - } - - return null; - } - - private Vector3 GetAgentPosition() - { - return _movement != null ? _movement.CurrentPosition : transform.position; - } - - private Vector3 GetBestDestination(GameObject target) - { - if (!_useInteractionPoints) - { - return target.transform.position; - } - - if (target.TryGetComponent(out var ric)) - { - var points = ric.GetInteractionPoints(); - if (points != null && points.Length > 0) - { - var from = GetAgentPosition(); - float bestSqr = float.MaxValue; - Vector3 best = target.transform.position; - for (int i = 0; i < points.Length; i++) - { - var p = points[i]; - var sqr = (p - from).sqrMagnitude; - if (sqr < bestSqr) - { - bestSqr = sqr; - best = p; - } - } - return best; - } - } - - return target.transform.position; - } + private Vector3 GetAgentPosition() => + movement?.CurrentPosition ?? transform.position; } } \ No newline at end of file