WaitingQueue 관련 AI 및 환경 로직 추가: 행동 트리 업데이트, 행동 및 데코레이터 스크립트 추가, RestaurantWaitingQueue 구현 및 Prefab 생성.

This commit is contained in:
김산 2025-09-02 18:38:06 +09:00
parent 78556c08ca
commit e9e4e1895f
26 changed files with 534 additions and 15 deletions

View File

@ -0,0 +1,93 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &2029713930320939465
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3697702677815423220, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3761059052922690693, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_Sprite
value:
objectReference: {fileID: 21300000, guid: 42a128f365d0c4c52b151466427d1cc9, type: 3}
- target: {fileID: 4103096974375017811, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: m_Name
value: PointMarker Variant
objectReference: {fileID: 0}
- target: {fileID: 7433508832753786351, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
propertyPath: _pointType
value: 2
objectReference: {fileID: 0}
m_RemovedComponents:
- {fileID: 7433508832753786351, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 4103096974375017811, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
insertIndex: -1
addedObject: {fileID: 157773281879251590}
m_SourcePrefab: {fileID: 100100000, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
--- !u!1 &2655961481751516314 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 4103096974375017811, guid: 186d28777ccbc484780568f74c110ff7, type: 3}
m_PrefabInstance: {fileID: 2029713930320939465}
m_PrefabAsset: {fileID: 0}
--- !u!114 &157773281879251590
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2655961481751516314}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8dc8393b9e144fd1a225f29babfb089d, type: 3}
m_Name:
m_EditorClassIdentifier:
_pointType: 2
_maxWaitingCount: 10
_currentWaitingCount: 0
_waitingRangeRadius: 20
_targetGizmoColor: {r: 1, g: 0.92156863, b: 0.015686275, a: 1}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ba6905dd36b009a4aa3b6ad80520ee3c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

View File

@ -119,6 +119,19 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []

View File

@ -119,6 +119,19 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []

View File

@ -119,6 +119,19 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []

View File

@ -119,6 +119,19 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []

View File

@ -119,6 +119,19 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []

View File

@ -0,0 +1,145 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
using UnityEngine.AI;
namespace DDD.Restaurant
{
public class MoveToPoint : Action
{
private IAiMovement _movement;
private Vector3 _destination;
private bool _isInitialized;
private bool _isMoving;
[SerializeField] private float _stoppingDistance = 0.01f;
[Tooltip("디버그 선 색상")]
[SerializeField] private Color _debugLineColor = Color.red;
[Tooltip("타겟 위치 기즈모 색상")]
[SerializeField] private Color _targetGizmoColor = Color.yellow;
public override void OnStart()
{
if (!gameObject.TryGetComponent(out _movement))
{
Debug.LogError($"[{GetType().Name}] NavMeshAgent를 찾을 수 없습니다: {gameObject.name}");
return;
}
var blackboard = gameObject.GetComponent<IAISharedBlackboard<RestaurantCustomerBlackboardKey>>();
if (blackboard == null)
{
Debug.LogError($"[{GetType().Name}] Blackboard를 찾을 수 없습니다: {gameObject.name}");
return;
}
var waitingQueue = blackboard.GetBlackboardValue<IWaitingQueue>(RestaurantCustomerBlackboardKey.WaitingQueue);
if (waitingQueue == null)
{
Debug.LogError($"[{GetType().Name}] WaitingQueue를 찾을 수 없습니다: {gameObject.name}");
return;
}
_destination = waitingQueue.GetRandomPointPosition();
_isInitialized = true;
}
public override TaskStatus OnUpdate()
{
if (!_isInitialized)
return TaskStatus.Failure;
StartOrUpdateMovement();
return CheckMovementCompletion();
}
private void StartOrUpdateMovement()
{
if (!_isMoving)
{
if (_movement.TryMoveToPosition(_destination))
{
_movement.EnableMove();
_movement.PlayMove();
_isMoving = true;
}
}
else
{
_movement.TryMoveToPosition(_destination);
}
}
private TaskStatus CheckMovementCompletion()
{
Vector3 distance2D = _destination - GetAgentPosition();
distance2D.y = 0f;
var distanceSqr = (distance2D).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;
protected override void OnDrawGizmos()
{
if (!_isInitialized) return;
// 타겟 위치에 기즈모 그리기
Gizmos.color = _targetGizmoColor;
if (_isMoving && _destination != Vector3.zero)
{
Gizmos.DrawWireSphere(_destination, 0.5f);
}
Gizmos.color = _debugLineColor;
Vector3 targetPos = _isMoving && _destination != Vector3.zero
? _destination
: Vector3.zero;
if (targetPos != Vector3.zero)
{
Gizmos.DrawLine(transform.position, targetPos);
}
// 현재 오브젝트 위치에 작은 기즈모 그리기
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(transform.position, Vector3.one * 0.3f);
}
protected override void OnDrawGizmosSelected()
{
if (!_isInitialized) return;
// 선택되었을 때 추가 정보 표시
#if UNITY_EDITOR
Vector3 targetPos = _isMoving && _destination != Vector3.zero
? _destination
: Vector3.zero;
float distance = Vector3.Distance(transform.position, targetPos);
UnityEditor.Handles.Label(targetPos + Vector3.up * 1f,
$"Distance: {distance:F2}m\nStopping: {_stoppingDistance:F2}m");
#endif
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17040c9d459f4976b8745dd341809e16
timeCreated: 1756801920

View File

@ -0,0 +1,36 @@
using System;
using Opsive.BehaviorDesigner.Runtime.Components;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Decorators;
using UnityEngine;
namespace DDD
{
public abstract class BlackboardValueExist<T, V> : DecoratorNode where T : Enum where V : class
{
[SerializeField] private T _blackboardKey;
private bool _isBlackboardValueExists;
public sealed override void OnStart()
{
var blackboard = gameObject.GetComponent<IAISharedBlackboard<T>>();
if (blackboard == null) return;
var value = blackboard.GetBlackboardValue<V>(_blackboardKey);
if (value == null) return;
_isBlackboardValueExists = true;
}
public sealed override TaskStatus OnUpdate()
{
if (!_isBlackboardValueExists) return TaskStatus.Failure;
// 자식(TaskComponent)은 Decorator 바로 다음 인덱스로 가정
var taskBuffer = m_BehaviorTree.World.EntityManager.GetBuffer<TaskComponent>(m_BehaviorTree.Entity);
var childStatus = taskBuffer[Index + 1].Status;
if (childStatus == TaskStatus.Success) return TaskStatus.Success;
return TaskStatus.Running;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5477779bedee4697adc2461e2b37d26b
timeCreated: 1756802812

View File

@ -0,0 +1,33 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD.Restaurant
{
public class WaitingQueueRegisterer : Action
{
private bool _isRegistered;
public override void OnStart()
{
var blackboard = gameObject.GetComponent<IAISharedBlackboard<RestaurantCustomerBlackboardKey>>();
var environmentState = RestaurantState.Instance?.EnvironmentState;
if (environmentState == null) return;
var waitingAreas = environmentState.GetPointProviderByType(PointType.WaitingArea);
foreach (var waitingArea in waitingAreas)
{
if (waitingArea is not IWaitingQueue waitingQueue) continue;
if (!waitingQueue.RegisterWaitingQueue(gameObject)) continue;
blackboard.SetBlackboardValue(RestaurantCustomerBlackboardKey.WaitingQueue, waitingQueue);
_isRegistered = true;
break;
}
}
public override TaskStatus OnUpdate()
{
return _isRegistered ? TaskStatus.Success : TaskStatus.Failure;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aa4a39f9e9c8427db6abbc79077d2c3c
timeCreated: 1756800604

View File

@ -0,0 +1,21 @@
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD.Restaurant
{
public class WaitingQueueUnRegisterer : Action
{
public override void OnStart()
{
var blackboard = gameObject.GetComponent<IAISharedBlackboard<RestaurantCustomerBlackboardKey>>();
if (blackboard == null)
{
Debug.LogWarning($"[{GetType().Name}] 블랙보드가 없습니다. 오브젝트 해시코드 : {gameObject.GetHashCode()}");
return;
}
var waitingQueue = blackboard.GetBlackboardValue<IWaitingQueue>(RestaurantCustomerBlackboardKey.CumulativeOrderCount);
if (waitingQueue == null) return;
waitingQueue.UnregisterWaitingQueue(gameObject);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7a6e15a2b3f5436db9b80cb8daaee571
timeCreated: 1756801165

View File

@ -1,5 +1,6 @@
using System;
using Sirenix.OdinInspector;
using UnityEngine;
namespace DDD.Restaurant
@ -7,13 +8,14 @@ namespace DDD.Restaurant
[Serializable]
public class CustomerBlackboardSo : BlackboardSo<RestaurantCustomerBlackboardKey>
{
public GameObject SelfGameObject;
public string CustomerDataId;
public GameObject CurrentTargetGameObject;
public EmotionType SatisfactionLevel;
public int CumulativeOrderCount;
public float MaxPatienceTime;
public float RemainingPatienceTime;
[SerializeField, ReadOnly]public GameObject SelfGameObject;
[SerializeField, ReadOnly]public string CustomerDataId;
[SerializeField, ReadOnly]public GameObject CurrentTargetGameObject;
[SerializeField, ReadOnly]public EmotionType SatisfactionLevel;
[SerializeField, ReadOnly]public int CumulativeOrderCount;
[SerializeField, ReadOnly]public float MaxPatienceTime;
[SerializeField, ReadOnly]public float RemainingPatienceTime;
[SerializeField, ReadOnly]public RestaurantWaitingQueue WaitingQueue;
public override void SetVariable<T1>(RestaurantCustomerBlackboardKey key, T1 value)
{
@ -41,6 +43,9 @@ public override void SetVariable<T1>(RestaurantCustomerBlackboardKey key, T1 val
case RestaurantCustomerBlackboardKey.RemainingPatienceTime:
RemainingPatienceTime = (float)(object)value;
break;
case RestaurantCustomerBlackboardKey.WaitingQueue:
WaitingQueue = value as RestaurantWaitingQueue;
break;
default:
throw new ArgumentOutOfRangeException(nameof(key), key, null);
}

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace DDD.Restaurant
{
public class CustomerTargetObjectExist : BlackboardValueExist<RestaurantCustomerBlackboardKey, GameObject>
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ce800e5a35634feea9723ef410098569
timeCreated: 1756802975

View File

@ -17,7 +17,8 @@ public enum RestaurantCustomerBlackboardKey
CumulativeOrderCount,
MaxPatienceTime,
RemainingPatienceTime,
Reward
Reward,
WaitingQueue
}
public interface ICustomerBlackboard

View File

@ -20,7 +20,7 @@ public void Initialize()
foreach (var pointProvider in pointProviders)
{
if (!pointProvider.CanProvidePoint(PointType.Entry)) continue;
_spawnPoint = pointProvider.GetPosition();
_spawnPoint = pointProvider.GetPointPosition();
break;
}
}

View File

@ -14,8 +14,15 @@ public enum PointType
public interface IEnvironmentPointProvider
{
bool CanProvidePoint(PointType pointType);
Vector3 GetPosition();
Vector3 GetPointPosition();
GameObject GetGameObject();
}
public interface IWaitingQueue : IEnvironmentPointProvider
{
Vector3 GetRandomPointPosition();
bool RegisterWaitingQueue(GameObject waitingCustomer);
void UnregisterWaitingQueue(GameObject waitingCustomer);
}
}

View File

@ -23,7 +23,7 @@ public bool CanProvidePoint(PointType pointType)
return _pointType == pointType;
}
public Vector3 GetPosition()
public Vector3 GetPointPosition()
{
return transform.position;
}

View File

@ -0,0 +1,79 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
namespace DDD.Restaurant
{
public class RestaurantWaitingQueue : MonoBehaviour, IWaitingQueue
{
[SerializeField]
private PointType _pointType;
[SerializeField]
private int _maxWaitingCount;
[SerializeField, ReadOnly]
private int _currentWaitingCount;
[SerializeField]
private float _waitingRangeRadius = 1f;
[SerializeField] private Color _targetGizmoColor = Color.yellow;
private HashSet<GameObject> _waitingSet = new();
private void Start()
{
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.RegisterPointProvider(this);
}
private void OnDisable()
{
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.UnRegisterPointProvider(this);
}
public bool CanProvidePoint(PointType pointType)
{
return _pointType == pointType && _currentWaitingCount < _maxWaitingCount;
}
public Vector3 GetPointPosition()
{
return transform.position;
}
public GameObject GetGameObject()
{
return gameObject;
}
public Vector3 GetRandomPointPosition()
{
Vector2 randomOffset = Random.insideUnitCircle * _waitingRangeRadius;
return transform.position + new Vector3(randomOffset.x, 0f, randomOffset.y);
}
public bool RegisterWaitingQueue(GameObject waitingCustomer)
{
if (_currentWaitingCount > _maxWaitingCount) return false;
_waitingSet.Add(waitingCustomer);
_currentWaitingCount = _waitingSet.Count;
return true;
}
public void UnregisterWaitingQueue(GameObject waitingCustomer)
{
_waitingSet.Remove(waitingCustomer);
_currentWaitingCount = _waitingSet.Count;
}
private void OnDrawGizmosSelected()
{
// 타겟 위치에 기즈모 그리기
Gizmos.color = _targetGizmoColor;
Gizmos.DrawWireSphere(transform.position, _waitingRangeRadius);
UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
$"Waiting Count: {_currentWaitingCount}");
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8dc8393b9e144fd1a225f29babfb089d
timeCreated: 1756799072