diff --git a/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/LookAtInteractionTarget.cs b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/LookAtInteractionTarget.cs new file mode 100644 index 000000000..8abdaabe7 --- /dev/null +++ b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/LookAtInteractionTarget.cs @@ -0,0 +1,140 @@ +using Opsive.BehaviorDesigner.Runtime.Tasks; +using Opsive.BehaviorDesigner.Runtime.Tasks.Actions; +using UnityEngine; + +namespace DDD +{ + /// + /// 인터랙션 타겟을 바라보도록 시각(Spine/애니메이션) 컴포넌트에 위임하는 액션의 껍데기/골격. + /// 실제 회전/스파인 제어 로직은 별도의 Visual 컴포넌트(예: Spine/Animation Controller)에서 구현하십시오. + /// + public class LookAtInteractionTarget : Action + { + [Header("Target Settings")] + [Tooltip("InteractionPoints를 사용해 가장 적절한 지점을 바라봄")] + [SerializeField] private bool useInteractionPoints = true; + [Tooltip("타겟이 없을 때 즉시 실패할지 여부")] + [SerializeField] private bool failIfNoTarget = true; + + [Header("Update Settings")] + [Tooltip("프레임마다 갱신하여 지속적으로 바라볼지 (Running 반환) 여부. 비활성화 시 1회만 시도하고 성공 처리")] + [SerializeField] private bool continuousUpdate = true; + + // Visual 전용 컴포넌트(나중 구현)를 위한 최소 인터페이스 + // 실제 구현은 Spine/애니메이션 제어 컴포넌트에서 이 인터페이스를 구현하세요. + public interface ILookAtVisual + { + // 초기 시작 시도. 성공 여부를 반환할 수 있으나, 본 액션은 성공/실패에 민감하지 않습니다. + bool TryBeginLookAt(Vector3 worldPosition); + // 매 프레임 갱신 시 호출됩니다. + void UpdateLookAt(Vector3 worldPosition); + // 액션 종료 시 호출됩니다. + void EndLookAt(); + } + + private ILookAtVisual visual; + private GameObject cachedTarget; + private bool isLooking; + private Vector3 currentLookPosition; + + public override void OnStart() + { + visual = gameObject.GetComponentInParent(); + cachedTarget = null; + isLooking = false; + } + + public override TaskStatus OnUpdate() + { + var target = GetTarget(); + if (target == null) + { + if (isLooking) + { + // 타겟이 사라졌다면 정리 + visual?.EndLookAt(); + isLooking = false; + } + return failIfNoTarget ? TaskStatus.Failure : TaskStatus.Success; + } + + currentLookPosition = CalculateLookPosition(target); + + if (!isLooking) + { + visual?.TryBeginLookAt(currentLookPosition); + isLooking = true; + } + else + { + visual?.UpdateLookAt(currentLookPosition); + } + + // 연속 업데이트면 Running, 아니면 1회만 시도 후 Success 반환 + return continuousUpdate ? TaskStatus.Running : TaskStatus.Success; + } + + public override void OnEnd() + { + if (isLooking) + { + visual?.EndLookAt(); + isLooking = false; + } + 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 static bool IsValidTarget(GameObject target) => target != null && target; + + private Vector3 CalculateLookPosition(GameObject target) + { + if (!useInteractionPoints) + return target.transform.position; + + if (target.TryGetComponent(out var ric)) + { + var points = ric.GetInteractionPoints(); + if (points == null || points.Length == 0) + return target.transform.position; + + // 가장 가까운 상호작용 지점 선택 (MoveTo와 동일한 기준) + var agentPos = transform.position; + var nearest = target.transform.position; + var minSqr = float.MaxValue; + foreach (var p in points) + { + var d = (p - agentPos).sqrMagnitude; + if (d < minSqr) + { + minSqr = d; + nearest = p; + } + } + return nearest; + } + + return target.transform.position; + } + } +} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/LookAtInteractionTarget.cs.meta b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/LookAtInteractionTarget.cs.meta new file mode 100644 index 000000000..0f840f1cf --- /dev/null +++ b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/LookAtInteractionTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a973b52fbfe64f8981b2a1d33864d2eb +timeCreated: 1755771294 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs index b43e70e98..192081afc 100644 --- a/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs +++ b/Assets/_DDD/_Scripts/RestaurantCharacter/AI/Common/Actions/MoveToInteractionTarget.cs @@ -17,7 +17,7 @@ public class MoveToInteractionTarget : Action [Header("Movement Settings")] [Tooltip("목적지 도달 거리")] - [SerializeField] private float stoppingDistance = 0.5f; + [SerializeField] private float stoppingDistance = 0.01f; [Tooltip("목적지 재계산 주기(초), 0 이하면 비활성화")] [SerializeField] private float repathInterval = 0.5f;