AI 액션 정리: LookAtInteractionTarget 껍데기/골격 추가 및 MoveToInteractionTarget 경로 재계산/정지 처리 안정화

This commit is contained in:
Jeonghyeon Ha 2025-08-21 19:19:20 +09:00
parent 71fbd60719
commit f8122d8c70
3 changed files with 144 additions and 1 deletions

View File

@ -0,0 +1,140 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD
{
/// <summary>
/// 인터랙션 타겟을 바라보도록 시각(Spine/애니메이션) 컴포넌트에 위임하는 액션의 껍데기/골격.
/// 실제 회전/스파인 제어 로직은 별도의 Visual 컴포넌트(예: Spine/Animation Controller)에서 구현하십시오.
/// </summary>
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<ILookAtVisual>();
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<IAISharedBlackboard>()
?.GetCurrentInteractionTarget();
if (IsValidTarget(cachedTarget))
return cachedTarget;
// Interactor의 포커스된 타겟 검색
var interactor = gameObject.GetComponentInParent<IInteractor>();
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<RestaurantInteractionComponent>(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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a973b52fbfe64f8981b2a1d33864d2eb
timeCreated: 1755771294

View File

@ -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;