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