Refactored MoveToInteractionTarget

This commit is contained in:
Jeonghyeon Ha 2025-08-21 19:13:39 +09:00
parent 4ef63ec9a9
commit 71fbd60719

View File

@ -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
{
/// <summary>
/// IAiMovement를 이용해 인터랙션 타겟(주로 RestaurantInteractionComponent가 붙은 오브젝트)으로 이동하는 액션.
/// - 타겟은 우선 블랙보드(IRestaurantCustomerBlackboard)에서 가져오고, 없으면 Interactor의 FocusedInteractable에서 가져옵니다.
/// - 타겟에 RestaurantInteractionComponent가 있다면 가장 가까운 InteractionPoint를 목적지로 사용합니다.
/// IAiMovement를 이용해 인터랙션 타겟으로 이동하는 액션
/// </summary>
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<IAiMovement>();
_repathTimer = 0f;
_started = false;
_resolvedTarget = ResolveTargetFromContext();
movement = gameObject.GetComponentInParent<IAiMovement>();
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<IAISharedBlackboard>()
?.GetCurrentInteractionTarget();
if (IsValidTarget(cachedTarget))
return cachedTarget;
// Interactor의 포커스된 타겟 검색
var interactor = gameObject.GetComponentInParent<IInteractor>();
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<RestaurantInteractionComponent>(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<IAISharedBlackboard>();
var bbTarget = sharedBlackboard?.GetCurrentInteractionTarget();
if (bbTarget != null) return bbTarget;
// 2) Interactor의 포커싱 대상에서 가져오기
var interactor = gameObject.GetComponentInParent<IInteractor>();
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<RestaurantInteractionComponent>(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;
}
}