Merge branch 'feature/customer_behavior' into develop

This commit is contained in:
Jeonghyeon Ha 2025-08-27 13:04:55 +09:00
commit b0528c5486
78 changed files with 2793 additions and 115 deletions

View File

@ -133,7 +133,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 201f9e6d7ca7404baa9945950292a392, type: 3}
m_Name:
m_EditorClassIdentifier:
_interactionType: 4
_interactionType: 2
_executionParameters:
_holdTime: 0
_displayParameters:
@ -141,6 +141,7 @@ MonoBehaviour:
_interactionAvailableFlows: 2
_aiInteractionPoints:
- {fileID: 1664322405549350652}
autoInitialize: 1
--- !u!114 &4456475204957017828
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@ -136,6 +136,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
_interactionType: 1
_executionParameters:
_holdTime: 0.1
_holdTime: 0.5
_displayParameters:
_messageKey: Test

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bab8aadc83d64f64b8280e6ebb957e65
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -234,10 +234,18 @@ MonoBehaviour:
m_Data:
m_TaskData: []
m_EventTaskData: []
m_SharedVariableData: []
m_SharedVariableData:
- m_ObjectType: 'Opsive.GraphDesigner.Runtime.Variables.SharedVariable`1[[UnityEngine.GameObject,
UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]'
m_ValueHashes:
m_LongValueHashes: 0d00eb254f8d1b29baa620a07799d549a996976a4a64278927dafeacd28e0b00
m_ValuePositions: 000000000e0000000e0000000f000000
m_Values: 53656c6647616d654f626a65637402ffffffff
m_UnityObjects: []
m_Version: 3.4
m_DisabledEventNodesData: []
m_DisabledLogicNodesData: []
m_UniqueID: -1885404201
m_UniqueID: 1495981264
m_LogicNodePropertiesData: []
m_EventNodePropertiesData: []
m_GroupPropertiesData: []

View File

@ -366,6 +366,9 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 5259510642736920361, guid: 3db3fc62639929c4ba6031ca4ae6600c, type: 3}
insertIndex: -1
addedObject: {fileID: 8993310060139522557}
- targetCorrespondingSourceObject: {fileID: 5259510642736920361, guid: 3db3fc62639929c4ba6031ca4ae6600c, type: 3}
insertIndex: -1
addedObject: {fileID: -6848683434426724985}
- targetCorrespondingSourceObject: {fileID: 6791841979869644848, guid: 3db3fc62639929c4ba6031ca4ae6600c, type: 3}
insertIndex: -1
addedObject: {fileID: 662634663174340165}
@ -423,7 +426,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 81e01dd8c1cc3404d805400eba1bb4ae, type: 3}
m_Name:
m_EditorClassIdentifier:
_availableInteractions: 1
_availableInteractions: 5
_nearColliders:
- {fileID: 0}
- {fileID: 0}
@ -461,6 +464,36 @@ MonoBehaviour:
m_EditorClassIdentifier:
blockOutlineAndGlow: 1
blockOverlay: 1
--- !u!114 &-6848683434426724985
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7316134055819320434}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cdaa3305fa954c45a80c9662aa6f425, type: 3}
m_Name:
m_EditorClassIdentifier:
m_GraphName: Behavior Tree
m_Index: 0
m_Data:
m_TaskData: []
m_EventTaskData: []
m_SharedVariableData: []
m_DisabledEventNodesData: []
m_DisabledLogicNodesData: []
m_UniqueID: 732308450
m_LogicNodePropertiesData: []
m_EventNodePropertiesData: []
m_GroupPropertiesData: []
m_StartWhenEnabled: 1
m_PauseWhenDisabled: 0
m_UpdateMode: 0
m_EvaluationType: 0
m_MaxEvaluationCount: 1
m_Subtree: {fileID: 0}
--- !u!4 &7511707580127947132 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 4993183601549197863, guid: 3db3fc62639929c4ba6031ca4ae6600c, type: 3}

View File

@ -365,7 +365,8 @@ PrefabInstance:
propertyPath: m_Name
value: Prop_CustomerTable_002
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 551358949302262764, guid: 0b1ba2f28535d5147bc0ddf354d0712f, type: 3}
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
@ -10722,6 +10723,92 @@ MeshFilter:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1771012164}
m_Mesh: {fileID: 0}
--- !u!1 &1775054119
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1775054121}
- component: {fileID: 1775054120}
m_Layer: 0
m_Name: AstarPath
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1775054120
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1775054119}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 78396926cbbfc4ac3b48fc5fc34a87d1, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 1073741824
data:
dataString: UEsDBBQAAAgIAABIIewaXtoYSgIAALMEAAALACQAZ3JhcGgxLmpzb24KACAAAAAAAAEAGAAAgD7V3rGdAQCAPtXesZ0BAIA+1d6xnQF1U8tu2zAQ/BVD56awZRuOe4ydpoc8CstAgxQ9rMm1RJgiXZKy4xj59+5SjyhNeyI0s7PP0TkRBTgQAd0KpKp88mUw/Dz9NEiENcFW7g6er52zjvCUUdQ6Uy8Yw1KOO4LewUbjN1R5EZqwFlxoVW66lCU8Z9ruWTwe1t/XMsdbNHkoWBlBZVaYK2uaMmPCUKpg3VppbMBReklwaIDHD8hTh1QeWcdzBVchIXt0t3BCd2el2ioBgWox/fMXkVKVaDwhxHKhZNkC42VC/AbELne2MnLt4IDOw0ZpFU4c+qMZmuMcajyACTcO9kVWuS0I7FLaextW+LtSLsYKqzUKbiPDEJTJuZtzD26FsW3Pkn1x8kr4TKBh5vz6F5YuW1THUcHvGBgkB9AVKy5GA2YD5A0Xp3fgyQi0vwXVVpKLdWvruDv0RdznFrR/x6zROVDmH5q1w/4JQh1YW6aE/dIejYdyr2n4r2TGaLdxsxruY9UmWmIApfm6xFqzqHfUtWQqrXkuZ0O8az30MzuQ0FPzvvAb599aJ1Be8T39Ag3V+L9iNPkgadzYCMatYtRK0rqKL+yRW3yoAk2Ib3tg4p6Ou7DG1Kfu77WVNfZ5kxkEhz5k9IjiwejT41NPhoZNeA+HkrSLKjrqTZtXSr63QjKepbNhms7mczEbXc7lBU7kdLZJp7M5yOlEDJM4hTIqKNDfKb8O7WLob+7d20AZM65Q0MEG0fxsV+ngeKNeStvzgDJbmwnyhXmoc3Rj06FB02bldfztez9Fcn5NXv8AUEsDBBQAAAgIAABIIexc6sO8cwAAAIUAAAAJACQAbWV0YS5qc29uCgAgAAAAAAABABgAAIA+1d6xnQEAgD7V3rGdAQCAPtXesZ0Bq1YqSy0qzszPU7JSMNUz1rPQUVBKL0osyCgGChiBOKWZKSB2dF5pTg6Qb2xuZG5gZGRuaZlsbmhhmaKbapJiap5kZGpumZhiapJsoBQLVFVSWZDql5ibiqwzILEkIy0zLyUzL10vKDU5sbjEHWSRUmwtAFBLAQItABQAAAgIAABIIewaXtoYSgIAALMEAAALACQAAAAAAAAAAAAAAAAAAABncmFwaDEuanNvbgoAIAAAAAAAAQAYAACAPtXesZ0BAIA+1d6xnQEAgD7V3rGdAVBLAQItABQAAAgIAABIIexc6sO8cwAAAIUAAAAJACQAAAAAAAAAAAAAAJcCAABtZXRhLmpzb24KACAAAAAAAAEAGAAAgD7V3rGdAQCAPtXesZ0BAIA+1d6xnQFQSwUGAAAAAAIAAgC4AAAAVQMAAAAA
file_cachedStartup: {fileID: 0}
cacheStartup: 0
showNavGraphs: 1
showUnwalkableNodes: 1
debugMode: 4
debugFloor: 0
debugRoof: 1
manualDebugFloorRoof: 0
showSearchTree: 0
unwalkableNodeDebugSize: 0.3
logPathResults: 0
maxNearestNodeDistance: 100
scanOnStartup: 1
fullGetNearestSearch: 0
prioritizeGraphs: 0
prioritizeGraphsLimit: 1
colorSettings:
_SolidColor: {r: 0.11764706, g: 0.4, b: 0.7882353, a: 0.9}
_UnwalkableNode: {r: 1, g: 0, b: 0, a: 0.5}
_BoundsHandles: {r: 0.29, g: 0.454, b: 0.741, a: 0.9}
_ConnectionLowLerp: {r: 0, g: 1, b: 0, a: 0.5}
_ConnectionHighLerp: {r: 1, g: 0, b: 0, a: 0.5}
_MeshEdgeColor: {r: 0, g: 0, b: 0, a: 0.5}
_AreaColors: []
tagNames: []
heuristic: 2
heuristicScale: 1
threadCount: -1
maxFrameTime: 10
batchGraphUpdates: 0
graphUpdateBatchingInterval: 0.2
navmeshUpdates:
updateInterval: 0
euclideanEmbedding:
mode: 0
seed: 0
pivotPointRoot: {fileID: 0}
spreadOutCount: 10
showGraphs: 1
--- !u!4 &1775054121
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1775054119}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!4 &1784230204 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 1061695247072719575, guid: 70f56d7d65d2e7842b5bd517ae7fe7fe, type: 3}
@ -15146,3 +15233,4 @@ SceneRoots:
- {fileID: 852575416}
- {fileID: 1311760301}
- {fileID: 504708576}
- {fileID: 1775054121}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c31792c3491f44878a9a5e8ee59504cf
timeCreated: 1755770768

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 49c654aa3aa94cb9928e2d161cad789a
timeCreated: 1755770768

View File

@ -0,0 +1,15 @@
using UnityEngine;
namespace DDD
{
/// <summary>
/// 공용 AI 블랙보드 인터페이스.
/// - 다양한 캐릭터 AI에서 공통으로 참조하는 현재 인터랙션 타겟만 정의합니다.
/// - 필요 시 키-값 확장을 고려하되, 현재는 최소 요구만 충족합니다.
/// </summary>
public interface IAISharedBlackboard
{
void SetCurrentInteractionTarget(GameObject targetGameObject);
GameObject GetCurrentInteractionTarget();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 62510fba6cb047419ca463dc523ae536
timeCreated: 1755770768

View File

@ -13,6 +13,7 @@ public interface IInteractionSubsystemObject
public interface IInteractionSubsystemObject<T> : IInteractionSubsystemObject where T : Enum
{
T GetInteractionSubsystemType();
void SetInteractionSubsystemType(T inValue);
}
public interface IInteractionSubsystemSolver

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1fed3b9fae5245cdbf255f627a82a1e6
timeCreated: 1755747969

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfc8e456b2134c4a87b9fcd0d385cf1d
timeCreated: 1755767029

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ab824a41c52d4cca8cafb1fc96d5d8e7
timeCreated: 1755769401

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

@ -0,0 +1,173 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD
{
/// <summary>
/// IAiMovement를 이용해 인터랙션 타겟으로 이동하는 액션
/// </summary>
public class MoveToInteractionTarget : Action
{
[Header("Target Settings")]
[Tooltip("InteractionPoints를 사용해 가장 가까운 지점으로 이동")]
[SerializeField] private bool useInteractionPoints = true;
[Tooltip("타겟이 없을 때 즉시 실패할지 여부")]
[SerializeField] private bool failIfNoTarget = true;
[Header("Movement Settings")]
[Tooltip("목적지 도달 거리")]
[SerializeField] private float stoppingDistance = 0.01f;
[Tooltip("목적지 재계산 주기(초), 0 이하면 비활성화")]
[SerializeField] private float repathInterval = 0.5f;
private IAiMovement movement;
private float repathTimer;
private Vector3 currentDestination;
private bool isMoving;
private GameObject cachedTarget;
public override void OnStart()
{
if (cachedTarget != null) return;
movement = gameObject.GetComponentInParent<IAiMovement>();
repathTimer = 0f;
isMoving = false;
cachedTarget = null;
}
public override TaskStatus OnUpdate()
{
if (movement == null)
return TaskStatus.Failure;
var target = GetTarget();
if (target == null)
return failIfNoTarget ? TaskStatus.Failure : TaskStatus.Success;
Debug.Log(target.name);
if (ShouldUpdateDestination())
{
currentDestination = CalculateDestination(target);
StartOrUpdateMovement();
}
return CheckMovementCompletion();
}
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)
{
var distanceSqr = (point - agentPosition).sqrMagnitude;
if (distanceSqr < minDistanceSqr)
{
minDistanceSqr = distanceSqr;
nearestPoint = point;
}
}
return nearestPoint;
}
private void StartOrUpdateMovement()
{
if (!isMoving)
{
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;
}
private void StopMovement()
{
if (movement != null && isMoving)
{
movement.StopMove();
movement.DisableMove();
isMoving = false;
}
}
private Vector3 GetAgentPosition() =>
movement?.CurrentPosition ?? transform.position;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d97cd08353334cb698807d4b526d01b6
timeCreated: 1755769413

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f96045235fcc43c880f2e0ee857b6f2e
timeCreated: 1756111444

View File

@ -0,0 +1,231 @@
using Opsive.BehaviorDesigner.Runtime;
using Opsive.BehaviorDesigner.Runtime.Components;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Decorators;
using Opsive.GraphDesigner.Runtime;
using Opsive.Shared.Utility;
using Unity.Burst;
using Unity.Entities;
using UnityEngine;
namespace DDD
{
[NodeDescription("자식 태스크의 실행 시간을 제한합니다")]
public class TimeLimiter : ILogicNode, IParentNode, ITaskComponentData, IDecorator, ISavableTask
{
[Tooltip("The index of the node.")]
[SerializeField, HideInInspector] ushort _Index;
[Tooltip("The parent index of the node. ushort.MaxValue indicates no parent.")]
[SerializeField, HideInInspector] ushort _ParentIndex;
[Tooltip("The sibling index of the node. ushort.MaxValue indicates no sibling.")]
[SerializeField, HideInInspector] ushort _SiblingIndex;
[Tooltip("최대 실행 시간(초)")]
[SerializeField] float _timeLimit = 30.0f;
[Tooltip("시간 초과 시 반환할 상태")]
[SerializeField] private TaskStatus _timeoutStatus = TaskStatus.Failure;
private ushort _ComponentIndex;
public ushort Index
{
get => _Index;
set => _Index = value;
}
public ushort ParentIndex
{
get => _ParentIndex;
set => _ParentIndex = value;
}
public ushort SiblingIndex
{
get => _SiblingIndex;
set => _SiblingIndex = value;
}
public ushort RuntimeIndex { get; set; }
public float TimeLimit
{
get => _timeLimit;
set => _timeLimit = value;
}
public int MaxChildCount
{
get { return 1; }
}
public ComponentType Tag
{
get => typeof(TimeLimiterTag);
}
public System.Type SystemType
{
get => typeof(TimeLimiterTaskSystem);
}
public void AddBufferElement(World world, Entity entity)
{
DynamicBuffer<TimeLimiterComponent> buffer;
if (world.EntityManager.HasBuffer<TimeLimiterComponent>(entity))
{
buffer = world.EntityManager.GetBuffer<TimeLimiterComponent>(entity);
}
else
{
buffer = world.EntityManager.AddBuffer<TimeLimiterComponent>(entity);
}
buffer.Add(new TimeLimiterComponent()
{
Index = RuntimeIndex,
TimeLimit = _timeLimit,
TimeoutStatus = _timeoutStatus,
});
_ComponentIndex = (ushort)(buffer.Length - 1);
}
public void ClearBufferElement(World world, Entity entity)
{
if (world.EntityManager.HasBuffer<TimeLimiterComponent>(entity))
{
var buffer = world.EntityManager.GetBuffer<TimeLimiterComponent>(entity);
buffer.Clear();
}
}
public MemberVisibility GetSaveReflectionType(int index)
{
return MemberVisibility.None;
}
public object Save(World world, Entity entity)
{
var timeLimiterComponents = world.EntityManager.GetBuffer<TimeLimiterComponent>(entity);
var timeLimiterComponent = timeLimiterComponents[_ComponentIndex];
return timeLimiterComponent.StartTime;
}
public void Load(object saveData, World world, Entity entity)
{
var timeLimiterComponents = world.EntityManager.GetBuffer<TimeLimiterComponent>(entity);
var timeLimiterComponent = timeLimiterComponents[_ComponentIndex];
timeLimiterComponent.StartTime = (float)saveData;
timeLimiterComponents[_ComponentIndex] = timeLimiterComponent;
}
}
public struct TimeLimiterComponent : IBufferElementData
{
[Tooltip("The index of the node.")]
public ushort Index;
[Tooltip("최대 실행 시간(초)")]
public float TimeLimit;
[Tooltip("실행 시작 시간(초)")]
public float StartTime;
[Tooltip("Should the task end when the child returns failure?")]
public TaskStatus TimeoutStatus;
}
public struct TimeLimiterTag : IComponentData, IEnableableComponent { }
[DisableAutoCreation]
public partial struct TimeLimiterTaskSystem : ISystem
{
[BurstCompile]
private void OnUpdate(ref SystemState state)
{
var query = SystemAPI.QueryBuilder().WithAllRW<BranchComponent>().WithAllRW<TaskComponent>().WithAllRW<TimeLimiterComponent>().WithAll<TimeLimiterTag, EvaluationComponent>().Build();
state.Dependency = new TimeLimiterJob()
{
CurrentTime = (float)SystemAPI.Time.ElapsedTime
}.ScheduleParallel(query, state.Dependency);
}
[BurstCompile]
private partial struct TimeLimiterJob : IJobEntity
{
public float CurrentTime;
[BurstCompile]
public void Execute(ref DynamicBuffer<BranchComponent> branchComponents,
ref DynamicBuffer<TaskComponent> taskComponents,
ref DynamicBuffer<TimeLimiterComponent> timeLimiterComponents)
{
for (int i = 0; i < timeLimiterComponents.Length; ++i)
{
var timeLimiterComponent = timeLimiterComponents[i];
var taskComponent = taskComponents[timeLimiterComponent.Index];
var branchComponent = branchComponents[taskComponent.BranchIndex];
TaskComponent childTaskComponent;
if (taskComponent.Status == TaskStatus.Queued)
{
taskComponent.Status = TaskStatus.Running;
taskComponents[taskComponent.Index] = taskComponent;
timeLimiterComponent.StartTime = CurrentTime;
timeLimiterComponents[i] = timeLimiterComponent;
childTaskComponent = taskComponents[taskComponent.Index + 1];
childTaskComponent.Status = TaskStatus.Queued;
taskComponents[taskComponent.Index + 1] = childTaskComponent;
branchComponent.NextIndex = taskComponent.Index + 1;
branchComponents[taskComponent.BranchIndex] = branchComponent;
continue;
}
else if (taskComponent.Status != TaskStatus.Running)
{
continue;
}
if (timeLimiterComponent.StartTime >= 0f &&
CurrentTime - timeLimiterComponent.StartTime >= timeLimiterComponent.TimeLimit) {
// 시간 초과
taskComponent.Status = timeLimiterComponent.TimeoutStatus;
taskComponents[taskComponent.Index] = taskComponent;
// 자식 태스크가 실행 중이면 중단
childTaskComponent = taskComponents[taskComponent.Index + 1];
if (childTaskComponent.Status == TaskStatus.Running ||
childTaskComponent.Status == TaskStatus.Queued) {
childTaskComponent.Status = timeLimiterComponent.TimeoutStatus;
taskComponents[taskComponent.Index + 1] = childTaskComponent;
}
branchComponent.NextIndex = taskComponent.ParentIndex;
branchComponents[taskComponent.BranchIndex] = branchComponent;
continue;
}
childTaskComponent = taskComponents[taskComponent.Index + 1];
if (childTaskComponent.Status == TaskStatus.Queued ||
childTaskComponent.Status == TaskStatus.Running) {
// The child should keep running.
continue;
}
taskComponent.Status = childTaskComponent.Status;
taskComponents[taskComponent.Index] = taskComponent;
branchComponent.NextIndex = taskComponent.ParentIndex;
branchComponents[taskComponent.BranchIndex] = branchComponent;
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5da50af45c8438eb9677fd18378f9b4
timeCreated: 1756111556

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6f1cc55df1da4604a2a7f445527710de
timeCreated: 1755765247

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6777cf4d7cf4408fafe4bc9097c32b01
timeCreated: 1755767888

View File

@ -0,0 +1,7 @@
namespace DDD
{
public class ContinueRestaurantOrder
{
// 이미 있는 인터랙션 타겟을 대상으로 진행함
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d0b954edf40f4172964dbd6e4fe22b1a
timeCreated: 1755772289

View File

@ -0,0 +1,70 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD
{
public class StartRestaurantOrder : Action
{
[Tooltip("상호작용할 RestaurantOrderType")]
[SerializeField] private RestaurantOrderType _targetOrderType = RestaurantOrderType.Wait;
[Tooltip("실제 상호작용 가능 여부를 확인하고 수행합니다")]
[SerializeField] private bool _requireCanInteract = true;
[Tooltip("성공 시 블랙보드에 현재 인터랙션 대상을 등록합니다")]
[SerializeField] private bool _registerOnBlackboard = true;
private IInteractor _interactor;
private bool _isGetInteractor;
public override void OnStart()
{
_isGetInteractor = gameObject.TryGetComponent(out _interactor);
if (!_isGetInteractor)
Debug.LogError($"[{GetType().Name}] IInteractor를 찾을 수 없습니다: {gameObject.name}");
}
public override TaskStatus OnUpdate()
{
// TODO : 아래 타겟 찾기가 RestaurantOrderAvailable과 동일해야 함, 동일하면 중복될 필요 없으니 스태틱 유틸 함수정도로 만들어서 공유하기.
// 레스토랑 주문 인터랙션 후보를 가져옴
TaskStatus targetSearchSuccess = RestaurantOrderAvailable.FindAvailableOrderInteractable(_requireCanInteract, _targetOrderType, out var
outInteractable);
if (targetSearchSuccess == TaskStatus.Failure)
{
return TaskStatus.Failure;
}
// TODO : 아래 상호작용 수행 로직이 우리 프로젝트의 권장하는 방식이 아님. 플레이어가 오브젝트에 인터랙션하는 것과 비슷한 흐름으로 NPC가 오브젝트에 인터랙션하게 만들 것.
// 상호작용 수행: 액션이 붙은 에이전트를 Interactor로 사용
if (!_isGetInteractor || !_interactor.CanInteractTo(outInteractable))
{
return TaskStatus.Failure;
}
// TODO : 이벤트 통해서 인터랙션. 직접 호출하지 말 것!
var interacted = outInteractable.OnInteracted(_interactor);
if (!interacted)
{
return TaskStatus.Failure;
}
if (_registerOnBlackboard)
{
// 공용 블랙보드 우선
var shared = gameObject.GetComponentInChildren<IAISharedBlackboard>();
if (shared != null)
{
shared.SetCurrentInteractionTarget(outInteractable.gameObject);
}
else
{
// 하위 호환: 고객 전용 블랙보드 지원
var customerBb = gameObject.GetComponentInParent<IRestaurantCustomerBlackboard>();
customerBb?.SetCurrentInteractionTarget(outInteractable.gameObject);
}
}
return TaskStatus.Success;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84b6c26acd1e41afa6b07ed4c6caf860
timeCreated: 1755767930

View File

@ -0,0 +1,102 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD
{
//범용적으로 사용할 수 있을 것 같음
//차후 제네릭으로 변경 가능성 있음
public class WaitForPlayerInteraction : Action
{
[Tooltip("기다릴 상호작용 타입")]
[SerializeField] private RestaurantMealType _targetOrderType = RestaurantMealType.WaitForOrder;
private IInteractionSubsystemObject<RestaurantMealType> _interactionSubsystem;
private bool _isGetInteractionSubsystem;
public override void OnStart()
{
// 의자가 복합 상태를 가지게 될 경우
// GameObject interactionTarget = null;
// var shared = gameObject.GetComponentInChildren<IAISharedBlackboard>();
// if (shared != null)
// {
// interactionTarget = shared.GetCurrentInteractionTarget();
// }
// else
// {
// // 하위 호환: 고객 전용 블랙보드 지원
// var customerBb = gameObject.GetComponentInParent<IRestaurantCustomerBlackboard>();
// interactionTarget = customerBb?.GetCurrentInteractionTarget();
// }
//
// if (interactionTarget == null)
// {
// Debug.LogError($"[{GetType().Name}] interactionTarget을 찾을 수 없습니다: {gameObject.name}");
// return;
// }
//
// if (!interactionTarget.TryGetComponent<RestaurantInteractionComponent>(out var interactionComponent))
// Debug.LogError($"[{interactionTarget.name}] {nameof(interactionComponent)}를 찾을 수 없습니다: {gameObject.name}");
// if (interactionComponent is IInteractionSubsystemOwner subsystemOwner)
// {
// if (!subsystemOwner.TryGetSubsystemObject<RestaurantMealType>(out var subsystem))
// {
// Debug.LogError($"[{GetType().Name}] {nameof(_targetOrderType)}의 Subsystem을 찾을 수 없습니다: {gameObject.name}");
// _isGetInteractionSubsystem = false;
// return;
// }
//
// _isGetInteractionSubsystem = true;
// subsystem.SetInteractionSubsystemType(_targetOrderType);
//
// if (!gameObject.TryGetComponent<IInteractor>(out var interactor))
// {
// Debug.LogError($"[{GetType().Name}] IInteractor를 찾을 수 없습니다: {gameObject.name}");
// return;
// }
//
// interactor.CanInteractTo(interactionComponent);
//
// _interactionSubsystem = subsystem;
// }
if (!gameObject.TryGetComponent<RestaurantInteractionComponent>(out var interactionComponent))
{
Debug.LogError($"[{GetType().Name}]에서 interactionComponent를 찾을 수 없습니다: {gameObject.name}");
return;
}
if (interactionComponent is not IInteractionSubsystemOwner subsystemOwner)
{
Debug.LogError($"[{GetType().Name}]에서 {nameof(IInteractionSubsystemOwner)}를 찾을 수 없습니다: {gameObject.name}");
return;
}
if (!subsystemOwner.TryGetSubsystemObject(out _interactionSubsystem))
{
Debug.LogError($"[{GetType().Name}]에서 {nameof(IInteractionSubsystemObject)}를 찾을 수 없습니다: {gameObject.name}");
return;
}
_interactionSubsystem.SetInteractionSubsystemType(_targetOrderType);
_isGetInteractionSubsystem = true;
}
public override TaskStatus OnUpdate()
{
if (!_isGetInteractionSubsystem) return TaskStatus.Failure;
TaskStatus result = CheckToSubsystemStatus();
if (result == TaskStatus.Success) Debug.Log($"[{GetType().Name}] Success");
return result;
}
private TaskStatus CheckToSubsystemStatus()
{
return _interactionSubsystem.GetInteractionSubsystemType() == _targetOrderType
? TaskStatus.Running
: TaskStatus.Success;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4bb97122045148169906d2f7b04a712e
timeCreated: 1756171540

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c4f20081a3974c08b60b9efdbe1568a7
timeCreated: 1755765256

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using UnityEngine;
using DDD;
namespace DDD
{
public class RestaurantOrderAvailable : Conditional
{
[Tooltip("검사할 RestaurantOrderType")]
[SerializeField] private RestaurantOrderType _targetOrderType = RestaurantOrderType.Wait;
[Tooltip("인터랙션 가능 여부까지 확인할지 선택")]
[SerializeField] private bool _checkCanInteract = true;
public RestaurantOrderType TargetOrderType
{
get => _targetOrderType;
set => _targetOrderType = value;
}
public bool CheckCanInteract
{
get => _checkCanInteract;
set => _checkCanInteract = value;
}
public override TaskStatus OnUpdate()
{
TaskStatus targetSearchSuccess = FindAvailableOrderInteractable(_checkCanInteract, _targetOrderType, out var
outInteractable);
return targetSearchSuccess;
}
public static TaskStatus FindAvailableOrderInteractable<T>(bool checkCanInteract, T targetOrderType, out RestaurantInteractionComponent outInteractable) where T : Enum
{
outInteractable = null;
var environmentState = RestaurantState.Instance?.EnvironmentState;
if (environmentState == null)
{
return TaskStatus.Failure;
}
var interactables = environmentState.GetInteractablesByType(InteractionType.RestaurantOrder);
foreach (var interactable in interactables)
{
// 서브시스템에서 RestaurantOrderType을 가져와 비교
outInteractable = interactable as RestaurantInteractionComponent;
if (outInteractable == null) continue;
if (!outInteractable.TryGetSubsystemObject<T>(out var subsystem)) continue;
if (EqualityComparer<T>.Default.Equals(subsystem.GetInteractionSubsystemType(), targetOrderType)
)
{
// CheckCanInteract이 false면 타입만 맞으면 성공
if (!checkCanInteract)
{
return TaskStatus.Success;
}
// CheckCanInteract이 true면 실제 인터랙션 가능 여부까지 확인
if (interactable.CanInteract())
{
return TaskStatus.Success;
}
}
}
return TaskStatus.Failure;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bef354fd7cae4273b4cb4e7ff9e270f5
timeCreated: 1755765519

View File

@ -0,0 +1,57 @@
using System;
using System.Threading.Tasks;
using Opsive.BehaviorDesigner.Runtime;
using Unity.Entities;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace DDD
{
[RequireComponent(typeof(BehaviorTree))]
[RequireComponent(typeof(RestaurantCustomerBlackboardComponent))]
public class RestaurantCustomerAiComponent : MonoBehaviour, IRestaurantCustomerAi
{
protected BehaviorTree _behaviorTree;
protected RestaurantCustomerBlackboardComponent _blackboardComponent;
private void Awake()
{
_behaviorTree = GetComponent<BehaviorTree>();
_blackboardComponent = GetComponent<RestaurantCustomerBlackboardComponent>();
}
public void InitializeAi(CustomerData inCustomerData)
{
try
{
InitializeAiInternal(inCustomerData);
}
catch (Exception e)
{
// Log
Debug.LogError(e);
throw; // TODO 예외 처리
}
}
private async Task InitializeAiInternal(CustomerData inCustomerData)
{
var customerState = RestaurantState.Instance.CustomerState;
var subtree = customerState.GetLoadedSubtree(inCustomerData.CustomerType);
if (subtree == null)
{
Debug.LogError(
$"[CustomerCharacter] No preloaded subtree found for CustomerType: {inCustomerData.CustomerType}. Make sure CustomerBehaviorData is loaded.");
subtree = await customerState.GetOrLoadSubtree(inCustomerData.CustomerType);
}
_behaviorTree.Subgraph = subtree;
_blackboardComponent.InitializeWithBehaviorTree(subtree);
_blackboardComponent.SetCustomerData(inCustomerData);
// TODO : 1. Subtree - Action, Condition
// TODO : 2. Blackboard
_behaviorTree.StartBehavior();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: af69e82818254bfa9cabb2dbf9430850
timeCreated: 1755748000

View File

@ -0,0 +1,39 @@
using Opsive.BehaviorDesigner.Runtime;
using UnityEngine;
namespace DDD
{
public class RestaurantCustomerBlackboardComponent : MonoBehaviour, IRestaurantCustomerBlackboard, IAISharedBlackboard
{
private Subtree _subtree;
private GameObject _currentInteractionTarget;
public void InitializeWithBehaviorTree(Subtree subtree)
{
_subtree = subtree;
if (_subtree != null)
{
_subtree.SetVariableValue(nameof(RestaurantCustomerBlackboardKey.SelfGameObject), gameObject);
}
}
public void SetCustomerData(CustomerData inCustomerData)
{
if (_subtree == null) return;
_subtree.SetVariableValue(nameof(RestaurantCustomerBlackboardKey.CustomerData), inCustomerData);
}
public void SetCurrentInteractionTarget(GameObject targetGameObject)
{
_currentInteractionTarget = targetGameObject;
if (_subtree == null) return;
_subtree.SetVariableValue(nameof(RestaurantCustomerBlackboardKey.CurrentInteractionTarget), targetGameObject);
}
public GameObject GetCurrentInteractionTarget()
{
// 캐시 우선 반환. 필요 시 Subtree에서 직접 조회하도록 확장 가능.
return _currentInteractionTarget;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 784c770c13244dc0a0804056065eaf92
timeCreated: 1755748886

View File

@ -14,10 +14,6 @@ protected virtual void Awake()
{
_interactionComponent = GetComponent<RestaurantCharacterInteraction>();
_spineController = GetComponent<SpineController>();
}
protected virtual void Start()
{
foreach (var typeToSolver in RestaurantInteractionEventSolvers.TypeToSolver)
{
var flag = typeToSolver.Key;
@ -32,6 +28,11 @@ protected virtual void Start()
}
}
protected virtual void Start()
{
}
public GameObject GetInteractorGameObject()
{
return _interactionComponent.GetInteractorGameObject();

View File

@ -0,0 +1,7 @@
namespace DDD
{
public interface IRestaurantCustomerAi
{
void InitializeAi(CustomerData inCustomerData);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 308488f2a02448d3853514eff04711fa
timeCreated: 1755748296

View File

@ -0,0 +1,18 @@
using UnityEngine;
namespace DDD
{
public enum RestaurantCustomerBlackboardKey
{
SelfGameObject,
CustomerData,
CurrentInteractionTarget,
}
public interface IRestaurantCustomerBlackboard
{
void SetCustomerData(CustomerData inCustomerData);
void SetCurrentInteractionTarget(GameObject targetGameObject);
GameObject GetCurrentInteractionTarget();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a4f20d91da7045e4bc226be60254ef2b
timeCreated: 1755748894

View File

@ -2,54 +2,28 @@
using Opsive.BehaviorDesigner.Runtime;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace DDD
{
[RequireComponent(typeof(RestaurantCustomerAiComponent))]
public class CustomerCharacter : RestaurantNpcCharacter, ICustomerInitializer
{
private CustomerData _customerData;
private AsyncOperationHandle<Subtree> _subtreeHandle;
public async void Initialize(CustomerData customerData)
protected IRestaurantCustomerAi restaurantCustomerAi;
protected override void Awake()
{
base.Awake();
restaurantCustomerAi = GetComponent<IRestaurantCustomerAi>();
}
public void Initialize(CustomerData customerData)
{
_customerData = customerData;
restaurantCustomerAi.InitializeAi(_customerData);
// 스킨 설정
_spineController.SetSkin(_customerData.SpineSkinKey);
// CustomerType에 따른 behavior tree subtree 할당
await InitializeBehaviorTree();
}
private async Task InitializeBehaviorTree()
{
var customerData = RestaurantData.Instance.CustomerData;
if (customerData?.CustomerBehaviorData?.TryGetValue(_customerData.CustomerType, out var subtreeReference) != true)
{
Debug.LogError($"[CustomerCharacter] No behavior data found for CustomerType: {_customerData.CustomerType}");
return;
}
try
{
var subtree = await AssetManager.LoadAsset<Subtree>(subtreeReference.AssetGUID);
if (subtree != null)
{
_behaviorTree.Subgraph = subtree;
_behaviorTree.StartBehavior();
}
else
{
Debug.LogError($"[CustomerCharacter] Failed to load subtree for CustomerType: {_customerData.CustomerType}");
}
}
catch (System.Exception e)
{
Debug.LogError($"[CustomerCharacter] Error loading subtree for CustomerType {_customerData.CustomerType}: {e.Message}");
}
}
}
}

View File

@ -3,16 +3,12 @@
namespace DDD
{
[RequireComponent(typeof(BehaviorTree))]
[RequireComponent(typeof(RestaurantNpcMovement))]
public class RestaurantNpcCharacter : RestaurantCharacter
{
protected BehaviorTree _behaviorTree;
protected override void Awake()
{
base.Awake();
_behaviorTree = GetComponent<BehaviorTree>();
}
}
}

View File

@ -101,6 +101,7 @@ public bool HasReachedDestination()
public bool IsPositionMovable(Vector3 endPosition)
{
var nearestNode = AstarPath.active.GetNearest(endPosition).node;
return nearestNode != null && nearestNode.Walkable;
}

View File

@ -1,5 +1,8 @@
using UnityEngine;
namespace DDD
{
[RequireComponent(typeof(RestaurantPlayerMovement))]
public class RestaurantPlayerCharacter : RestaurantCharacter
{
protected override async void Awake()

View File

@ -27,6 +27,9 @@ public override Task InitializeController()
{
_restaurantCustomerStateSo = RestaurantState.Instance.CustomerState;
_restaurantRunStateSo = RestaurantState.Instance.RunState;
_iCustomerFactory ??= new CustomerFactory();
return Task.CompletedTask;
}
@ -41,6 +44,9 @@ public override async Task OnReadyNewFlow(GameFlowState newFlowState)
{
if (newFlowState == GameFlowState.RunRestaurant)
{
_iCustomerFactory.LoadAssets();
await _restaurantCustomerStateSo.LoadCustomerBehaviorData();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
@ -52,6 +58,9 @@ public override Task OnExitCurrentFlow(GameFlowState exitingFlowState)
{
if (exitingFlowState == GameFlowState.RunRestaurant)
{
_iCustomerFactory.UnloadAssets();
_restaurantCustomerStateSo.UnloadCustomerBehaviorData();
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
@ -61,8 +70,6 @@ public override Task OnExitCurrentFlow(GameFlowState exitingFlowState)
private async Task StartSpawnLoopAsync(CancellationToken token)
{
_iCustomerFactory ??= new CustomerFactory();
var currentGameLevel = GameState.Instance.LevelState.Level;
_levelDataSo ??= DataManager.Instance.GetDataSo<LevelDataSo>();
_customerDataSo ??= DataManager.Instance.GetDataSo<CustomerDataSo>();
@ -115,7 +122,7 @@ SpawnSchedule MakeSchedule() => scheduleBuilder.Build(new SpawnScheduleBuildArgs
{
var rotation = Quaternion.identity;
_ = _iCustomerFactory.CreateAsync(new CustomerSpawnArgs
await _iCustomerFactory.CreateAsync(new CustomerSpawnArgs
{
CustomerData = customerData,
Position = _restaurantRunStateSo.SpawnPoint,

View File

@ -1,11 +1,15 @@
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace DDD
{
public interface ICustomerFactory
{
Task<GameObject> CreateAsync(CustomerSpawnArgs args);
void LoadAssets();
void UnloadAssets();
}
public interface ICustomerInitializer
@ -24,26 +28,14 @@ public struct CustomerSpawnArgs
public class CustomerFactory : ICustomerFactory
{
private GameObject _customerPrefab;
private AsyncOperationHandle<GameObject> _customerPrefabHandle;
public async Task<GameObject> CreateAsync(CustomerSpawnArgs args)
{
if (!_customerPrefab)
{
var customerDataAsset = RestaurantData.Instance ? RestaurantData.Instance.CustomerData : null;
if (customerDataAsset == null || customerDataAsset.CustomerPrefab == null)
{
Debug.LogError("[CustomerFactory] RestaurantCustomerData or its CustomerPrefab reference is not set or not loaded.");
return null;
}
var handle = customerDataAsset.CustomerPrefab.LoadAssetAsync<GameObject>();
await handle.Task;
if (handle.Result == null)
{
Debug.LogError("[CustomerFactory] Failed to load customer prefab from AssetReference.");
return null;
}
_customerPrefab = handle.Result;
Debug.LogError("[CustomerFactory] Customer prefab is not loaded. Call LoadAssets() first.");
await LoadCustomerAsset();
}
var newCustomer = Object.Instantiate(_customerPrefab, args.Position, args.Rotation, args.Parent);
@ -54,5 +46,47 @@ public async Task<GameObject> CreateAsync(CustomerSpawnArgs args)
}
return newCustomer;
}
public async void LoadAssets()
{
await LoadCustomerAsset();
}
private async Task LoadCustomerAsset()
{
if (_customerPrefab != null)
{
return;
}
var customerDataAsset = RestaurantData.Instance ? RestaurantData.Instance.CustomerData : null;
if (customerDataAsset == null || customerDataAsset.CustomerPrefab == null)
{
Debug.LogError("[CustomerFactory] RestaurantCustomerData or its CustomerPrefab reference is not set.");
return;
}
_customerPrefabHandle = customerDataAsset.CustomerPrefab.LoadAssetAsync<GameObject>();
await _customerPrefabHandle.Task;
if (_customerPrefabHandle.Result == null)
{
Debug.LogError("[CustomerFactory] Failed to load customer prefab from AssetReference.");
return;
}
_customerPrefab = _customerPrefabHandle.Result;
Debug.Log("[CustomerFactory] Customer prefab loaded successfully.");
}
public void UnloadAssets()
{
if (_customerPrefabHandle.IsValid())
{
Addressables.Release(_customerPrefabHandle);
}
_customerPrefab = null;
Debug.Log("[CustomerFactory] Customer prefab unloaded.");
}
}
}

View File

@ -37,17 +37,22 @@ public RestaurantManagementType GetInteractionSubsystemType()
return _managementType;
}
public virtual void InitializeSubsystem()
public void SetInteractionSubsystemType(RestaurantManagementType inValue)
{
_managementType = inValue;
}
public void InitializeSubsystem()
{
}
public virtual bool CanInteract()
public bool CanInteract()
{
return true;
}
public virtual bool OnInteracted(IInteractor interactor, ScriptableObject payloadSo = null)
public bool OnInteracted(IInteractor interactor, ScriptableObject payloadSo = null)
{
return true;
}

View File

@ -0,0 +1,62 @@
using System;
using UnityEngine;
namespace DDD
{
public enum RestaurantMealType : uint
{
None = 0u,
WaitForOrder = 1u,
WaitForServe = 1u << 1
}
public class RestaurantMealInteractionSubsystem : MonoBehaviour, IInteractionSubsystemObject<RestaurantMealType>
{
private RestaurantMealType _currentRestaurantMealType;
private void Awake()
{
_currentRestaurantMealType = RestaurantMealType.None;
}
public RestaurantMealType GetInteractionSubsystemType()
{
return _currentRestaurantMealType;
}
public void SetInteractionSubsystemType(RestaurantMealType inValue)
{
_currentRestaurantMealType = inValue;
}
public void InitializeSubsystem()
{
_currentRestaurantMealType = RestaurantMealType.None;
}
public bool CanInteract()
{
return _currentRestaurantMealType != RestaurantMealType.None;
}
public bool OnInteracted(IInteractor interactor, ScriptableObject payloadSo = null)
{
var prev = _currentRestaurantMealType;
_currentRestaurantMealType = GetNextState(prev);
return true;
}
public ScriptableObject GetPayload()
{
return null;
}
private RestaurantMealType GetNextState(RestaurantMealType state)
{
switch (state)
{
case RestaurantMealType.None : return RestaurantMealType.WaitForOrder;
case RestaurantMealType.WaitForOrder : return RestaurantMealType.WaitForServe;
case RestaurantMealType.WaitForServe : return RestaurantMealType.None;
default: return RestaurantMealType.None;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 31d5c600061a4f05b19824e068e0c2af
timeCreated: 1756176676

View File

@ -3,13 +3,14 @@
namespace DDD
{
[Flags]
public enum RestaurantOrderType : uint
{
Wait = 0u,
Reserved = 1u,
Order = 1u << 1,
Serve = 1u << 2,
Busy = 1u << 3,
Dirty = 1u << 4,
}
public class RestaurantOrderInteractionSubsystem : MonoBehaviour, IInteractionSubsystemObject<RestaurantOrderType>
@ -24,20 +25,18 @@ private void Start()
public bool CanInteract()
{
if (GetInteractionSubsystemType() == RestaurantOrderType.Wait)
{
return true;
}
return false;
//if (GetInteractionSubsystemType() == RestaurantOrderType.Wait)
//{
// return true;
//}
return true;
}
public bool OnInteracted(IInteractor interactor, ScriptableObject payloadSo = null)
{
if (GetInteractionSubsystemType() == RestaurantOrderType.Wait)
{
// DO WAIT CUSTOMER
}
// 간단한 상태 전이: 현재 상태에서 다음 상태로 이동
var prev = currentRestaurantOrderType;
currentRestaurantOrderType = GetNextState(prev);
return true;
}
@ -48,12 +47,31 @@ public ScriptableObject GetPayload()
public void InitializeSubsystem()
{
currentRestaurantOrderType = orderType;
}
public RestaurantOrderType GetInteractionSubsystemType()
{
return currentRestaurantOrderType;
}
public void SetInteractionSubsystemType(RestaurantOrderType inValue)
{
currentRestaurantOrderType = inValue;
}
private RestaurantOrderType GetNextState(RestaurantOrderType state)
{
switch (state)
{
case RestaurantOrderType.Wait: return RestaurantOrderType.Reserved;
case RestaurantOrderType.Reserved: return RestaurantOrderType.Order;
case RestaurantOrderType.Order: return RestaurantOrderType.Serve;
case RestaurantOrderType.Serve: return RestaurantOrderType.Busy;
case RestaurantOrderType.Busy: return RestaurantOrderType.Dirty;
case RestaurantOrderType.Dirty: return RestaurantOrderType.Wait;
default: return RestaurantOrderType.Wait;
}
}
}
}

View File

@ -12,7 +12,8 @@ public static class RestaurantInteractionSubsystems
public static Dictionary<InteractionType, Type> TypeToSubsystem = new()
{
{InteractionType.RestaurantOrder, typeof(RestaurantOrderInteractionSubsystem)},
{InteractionType.RestaurantManagement, typeof(RestaurantManagementInteractionSubsystem)}
{InteractionType.RestaurantManagement, typeof(RestaurantManagementInteractionSubsystem)},
{InteractionType.RestaurantMeal, typeof(RestaurantMealInteractionSubsystem)}
};
}
@ -27,12 +28,35 @@ public class RestaurantInteractionComponent : MonoBehaviour, IInteractable, IInt
[SerializeField] protected GameFlowState _interactionAvailableFlows;
[SerializeField] private Transform[] _aiInteractionPoints;
[SerializeField] private bool autoInitialize = true;
private bool _isInitialized = false;
private Dictionary<InteractionType, IInteractionSubsystemObject> _subsystems = new();
private void OnEnable()
{
// Register this interactable to environment state
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.RegisterInteractable(this);
if (autoInitialize && !_isInitialized)
{
InitializeInteraction(_interactionType);
}
}
private void OnDisable()
{
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.UnregisterInteractable(this);
}
private void Start()
{
if (autoInitialize)
// 보수적으로 Start에서도 등록 시도 (OnEnable 시점에 EnvironmentState가 없었을 경우 대비)
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.RegisterInteractable(this);
if (autoInitialize && !_isInitialized)
{
InitializeInteraction(_interactionType);
}
@ -92,6 +116,7 @@ public GameObject GetInteractableGameObject()
public virtual void InitializeInteraction(InteractionType interactionType)
{
_interactionType = interactionType;
_isInitialized = true;
InitializeSubsystems();
}

View File

@ -14,7 +14,8 @@ public static class RestaurantInteractionEventSolvers
public static Dictionary<InteractionType, Type> TypeToSolver = new()
{
{InteractionType.RestaurantManagement, typeof(RestaurantManagementSolver)},
{InteractionType.RestaurantOrder, typeof(RestaurantOrderSolver)}
{InteractionType.RestaurantOrder, typeof(RestaurantOrderSolver)},
{InteractionType.RestaurantMeal, typeof(RestaurantMealSolver)}
};
}

View File

@ -3,20 +3,18 @@
namespace DDD
{
public static class RestaurantManagementSolvers
public class RestaurantManagementSolver : RestaurantSubsystemSolver<RestaurantManagementType>
{
public static Dictionary<RestaurantManagementType, Type> TypeToManagementSolver = new()
private Dictionary<RestaurantManagementType, Type> _typeToManagementSolver = new()
{
{ RestaurantManagementType.OpenRestaurantMenu, typeof(RestaurantManagementSolver_Menu) },
{ RestaurantManagementType.StartRestaurant, typeof(RestaurantManagementSolver_Start) },
{ RestaurantManagementType.OpenCookUi, typeof(RestaurantManagementSolver_Cook) },
};
}
public class RestaurantManagementSolver : RestaurantSubsystemSolver<RestaurantManagementType>
{
protected override Dictionary<RestaurantManagementType, Type> GetSubsystemSolverTypeMappings()
{
return RestaurantManagementSolvers.TypeToManagementSolver;
return _typeToManagementSolver;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 601164c0231c43fca9349170e1e0ccec
timeCreated: 1756176395

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace DDD
{
public class RestaurantMealSolver : RestaurantSubsystemSolver<RestaurantMealType>
{
private Dictionary<RestaurantMealType, Type> _typeToMealSolver = new()
{
{ RestaurantMealType.WaitForOrder, typeof(RestaurantMealSolver_WaitForOrder) },
{ RestaurantMealType.WaitForServe, typeof(RestaurantMealSolver_WaitForServe) }
};
protected override Dictionary<RestaurantMealType, Type> GetSubsystemSolverTypeMappings()
{
return _typeToMealSolver;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 391c551614be4f21a2e700f44569e92a
timeCreated: 1756176491

View File

@ -0,0 +1,19 @@
using UnityEngine;
namespace DDD
{
public class RestaurantMealSolver_WaitForOrder : MonoBehaviour, IInteractionSubsystemSolver<RestaurantMealType>
{
public bool ExecuteInteractionSubsystem(IInteractor interactor, IInteractable interactable, ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
return true;
}
public bool CanExecuteInteractionSubsystem(IInteractor interactor = null, IInteractable interactable = null,
ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cff2611181194e4a92576bdbcead4fad
timeCreated: 1756181225

View File

@ -0,0 +1,18 @@
using UnityEngine;
namespace DDD
{
public class RestaurantMealSolver_WaitForServe : MonoBehaviour, IInteractionSubsystemSolver<RestaurantMealType>
{
public bool ExecuteInteractionSubsystem(IInteractor interactor, IInteractable interactable, ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
return true;
}
public bool CanExecuteInteractionSubsystem(IInteractor interactor = null, IInteractable interactable = null,
ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e9292616267b4299a3d2e0d29c84f69b
timeCreated: 1756181667

View File

@ -5,22 +5,20 @@
namespace DDD
{
public static class RestaurantOrderSolvers
public class RestaurantOrderSolver : RestaurantSubsystemSolver<RestaurantOrderType>
{
public static Dictionary<RestaurantOrderType, Type> TypeToOrderSolver = new()
private Dictionary<RestaurantOrderType, Type> _typeToOrderSolver = new()
{
{ RestaurantOrderType.Wait, typeof(RestaurantOrderSolver_Wait) },
{ RestaurantOrderType.Reserved, typeof(RestaurantOrderSolver_Reserved) },
{ RestaurantOrderType.Order, typeof(RestaurantOrderSolver_Order) },
{ RestaurantOrderType.Serve, typeof(RestaurantOrderSolver_Serve) }
{ RestaurantOrderType.Serve, typeof(RestaurantOrderSolver_Serve) },
{ RestaurantOrderType.Busy, typeof(RestaurantOrderSolver_Busy) },
{ RestaurantOrderType.Dirty, typeof(RestaurantOrderSolver_Dirty) }
};
}
public class RestaurantOrderSolver : RestaurantSubsystemSolver<RestaurantOrderType>
{
protected override Dictionary<RestaurantOrderType, Type> GetSubsystemSolverTypeMappings()
{
return RestaurantOrderSolvers.TypeToOrderSolver;
return _typeToOrderSolver;
}
}
}

View File

@ -0,0 +1,21 @@
using UnityEngine;
namespace DDD.RestaurantOrders
{
public class RestaurantOrderSolver_Busy : MonoBehaviour, IInteractionSubsystemSolver<RestaurantOrderType>
{
public bool ExecuteInteractionSubsystem(IInteractor interactor, IInteractable interactable,
ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
// TODO : DO SOMETHING!!!
return true;
}
public bool CanExecuteInteractionSubsystem(IInteractor interactor = null, IInteractable interactable = null,
ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
// TODO : DO SOMETHING!!!
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c185b3957ffe47088703be10e709ff66
timeCreated: 1755761370

View File

@ -0,0 +1,19 @@
using UnityEngine;
namespace DDD.RestaurantOrders
{
public class RestaurantOrderSolver_Dirty : MonoBehaviour, IInteractionSubsystemSolver<RestaurantOrderType>
{
public bool ExecuteInteractionSubsystem(IInteractor interactor, IInteractable interactable, ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
// TODO : DO SOMETHING!!!
return true;
}
public bool CanExecuteInteractionSubsystem(IInteractor interactor = null, IInteractable interactable = null,
ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b97bc6ce36df4e05a4d329f11daef43f
timeCreated: 1755761294

View File

@ -12,7 +12,8 @@ public bool ExecuteInteractionSubsystem(IInteractor interactor, IInteractable in
public bool CanExecuteInteractionSubsystem(IInteractor interactor = null, IInteractable interactable = null, ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
return true;
// Interactable's CurrentInteractor is me? => Can execute
return false;
}
}
}

View File

@ -1,3 +1,4 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using UnityEngine;
namespace DDD.RestaurantOrders
@ -6,7 +7,12 @@ public class RestaurantOrderSolver_Wait : MonoBehaviour, IInteractionSubsystemSo
{
public bool ExecuteInteractionSubsystem(IInteractor interactor, IInteractable interactable, ScriptableObject causerPayload = null, ScriptableObject targetPayloadSo = null)
{
if (CanExecuteInteractionSubsystem(interactor, interactable, causerPayload, targetPayloadSo) == false) return false;
// TODO : DO SOMETHING!!!
/* TODO
* OnInteracted에서 , , ? CanInteractTo에서 ?
* IInteractable CurrentInteractor를 ?
*/
return true;
}

View File

@ -10,7 +10,7 @@ public abstract class RestaurantSubsystemSolver<T> : MonoBehaviour, IInteraction
protected abstract Dictionary<T, Type> GetSubsystemSolverTypeMappings();
private void Start()
private void Awake()
{
foreach (var subsystemSolverType in GetSubsystemSolverTypeMappings())
{

View File

@ -1,9 +1,106 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Opsive.BehaviorDesigner.Runtime;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace DDD
{
public class RestaurantCustomerState : ScriptableObject
{
private Dictionary<CustomerType, Subtree> _loadedSubtrees = new Dictionary<CustomerType, Subtree>();
private Dictionary<CustomerType, AsyncOperationHandle<Subtree>> _subtreeHandles = new Dictionary<CustomerType, AsyncOperationHandle<Subtree>>();
public async Task LoadCustomerBehaviorData()
{
var customerData = RestaurantData.Instance?.CustomerData;
if (customerData?.CustomerBehaviorData == null)
{
Debug.LogError("[RestaurantCustomerState] RestaurantCustomerData or CustomerBehaviorData is null");
return;
}
var loadTasks = new List<Task>();
foreach (var behaviorPair in customerData.CustomerBehaviorData)
{
var customerType = behaviorPair.Key;
var subtreeReference = behaviorPair.Value;
if (_loadedSubtrees.ContainsKey(customerType))
continue; // Already loaded
loadTasks.Add(LoadSubtreeAsync(customerType, subtreeReference));
}
await Task.WhenAll(loadTasks);
Debug.Log($"[RestaurantCustomerState] Loaded {_loadedSubtrees.Count} customer behavior subtrees");
}
private async Task LoadSubtreeAsync(CustomerType customerType, AssetReference subtreeReference)
{
var handle = Addressables.LoadAssetAsync<Subtree>(subtreeReference);
_subtreeHandles[customerType] = handle;
await handle.Task;
if (handle.Result != null)
{
_loadedSubtrees[customerType] = handle.Result;
Debug.Log($"[RestaurantCustomerState] Loaded subtree for {customerType}");
}
else
{
Debug.LogError($"[RestaurantCustomerState] Failed to load subtree for {customerType}");
}
}
public void UnloadCustomerBehaviorData()
{
foreach (var handle in _subtreeHandles.Values)
{
if (handle.IsValid())
{
Addressables.Release(handle);
}
}
_loadedSubtrees.Clear();
_subtreeHandles.Clear();
Debug.Log("[RestaurantCustomerState] Unloaded all customer behavior subtrees");
}
public Subtree GetLoadedSubtree(CustomerType customerType)
{
_loadedSubtrees.TryGetValue(customerType, out var subtree);
return subtree;
}
public async Task<Subtree> GetOrLoadSubtree(CustomerType customerType)
{
if (IsSubtreeLoaded(customerType))
{
return GetLoadedSubtree(customerType);
}
else
{
var customerData = RestaurantData.Instance?.CustomerData;
if (customerData?.CustomerBehaviorData == null ||
!customerData.CustomerBehaviorData.TryGetValue(customerType, out var subtreeReference))
{
Debug.LogError($"[RestaurantCustomerState] No behavior data found for {customerType}");
return null;
}
await LoadSubtreeAsync(customerType, subtreeReference);
return GetLoadedSubtree(customerType);
}
}
public bool IsSubtreeLoaded(CustomerType customerType)
{
return _loadedSubtrees.ContainsKey(customerType);
}
}
}

View File

@ -21,5 +21,54 @@ public class RestaurantEnvironmentState : ScriptableObject
{
public List<RestaurantPropLocation> Props = new List<RestaurantPropLocation>();
public List<RestaurantPropLocation> Objects = new List<RestaurantPropLocation>();
// 인터랙션 가능한 객체(IInteractable)를 관리하기 위한 리스트 (런타임 전용)
private readonly List<IInteractable> _registeredInteractables = new List<IInteractable>();
/// <summary>
/// 인터랙션 가능한 객체를 등록합니다
/// </summary>
public void RegisterInteractable(IInteractable interactable)
{
if (interactable == null) return;
if (_registeredInteractables.Contains(interactable)) return;
_registeredInteractables.Add(interactable);
}
/// <summary>
/// 인터랙션 가능한 객체를 해제합니다
/// </summary>
public void UnregisterInteractable(IInteractable interactable)
{
if (interactable == null) return;
_registeredInteractables.Remove(interactable);
}
/// <summary>
/// 특정 InteractionType에 해당하는 인터랙션 객체들을 반환합니다
/// </summary>
public List<IInteractable> GetInteractablesByType(InteractionType interactionType)
{
var result = new List<IInteractable>();
// null 또는 Destroyed 오브젝트 정리
_registeredInteractables.RemoveAll(item => item == null || (item as UnityEngine.Object) == null);
foreach (var interactable in _registeredInteractables)
{
if (interactable.GetInteractionType() == interactionType)
{
result.Add(interactable);
}
}
return result;
}
/// <summary>
/// 모든 등록된 인터랙션 객체들을 반환합니다
/// </summary>
public List<IInteractable> GetAllInteractables()
{
_registeredInteractables.RemoveAll(item => item == null || (item as UnityEngine.Object) == null);
return new List<IInteractable>(_registeredInteractables);
}
}
}

BIN
ProjectSettings/EditorBuildSettings.asset (Stored with Git LFS)

Binary file not shown.

BIN
ProjectSettings/TagManager.asset (Stored with Git LFS)

Binary file not shown.