Exit, Entry 이벤트 추가, emotion 표현 액션, 만족도 평가 액션, 마커 탐색-등록 액션 추가, 마커 컴포넌트 추가

This commit is contained in:
김산 2025-08-29 17:16:48 +09:00
parent f9bb1fb262
commit 36cfe1d923
24 changed files with 434 additions and 57 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1f7569e20bdc77e429d3e140c38b7185
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,103 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &4103096974375017811
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3697702677815423220}
- component: {fileID: 3761059052922690693}
- component: {fileID: 7433508832753786351}
m_Layer: 7
m_Name: PointMarker
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3697702677815423220
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4103096974375017811}
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!212 &3761059052922690693
SpriteRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4103096974375017811}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_Sprite: {fileID: 21300000, guid: 22cbc31c0f91548f096d10d462447973, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 0.41, y: 3.41}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!114 &7433508832753786351
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4103096974375017811}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01f8d593287c4672a63146e6b4905db5, type: 3}
m_Name:
m_EditorClassIdentifier:
_pointType: 0

View File

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

View File

@ -0,0 +1,53 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using Unity.VisualScripting;
using UnityEngine;
namespace DDD.Restaurant
{
public enum EmotionType
{
Angry,
Satisfied,
}
public class ExpressEmotion : Action
{
public interface IEmotionVisual
{
bool HasEmotionAvailable(EmotionType emotionType);
void ShowEmotion(EmotionType emotionType);
void EndEmotion();
}
//이를 파생해서 기본값을 주거나, 바로 사용하면 될 듯
[SerializeField] protected string _emotionBlackboardKey;
private IEmotionVisual _emotionVisual;
public override void OnStart()
{
var currentEmotion = (EmotionType)m_BehaviorTree.GetVariable<int>(_emotionBlackboardKey).Value;
_emotionVisual = gameObject.GetComponentInChildren<IEmotionVisual>();
if (_emotionVisual == null)
{
Debug.LogWarning($"[{GetType().Name}] Emotion Interface가 없습니다. 게임오브젝트 해시코드: {gameObject.GetHashCode()}");
return;
}
if (!_emotionVisual.HasEmotionAvailable(currentEmotion))
{
Debug.LogWarning($"[{GetType().Name}] {nameof(currentEmotion)}이 없습니다. 게임오브젝트 해시코드: {gameObject.GetHashCode()}");
return;
}
_emotionVisual.ShowEmotion(currentEmotion);
}
public override void OnEnd()
{
_emotionVisual?.EndEmotion();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c30f2fbe7c3d4b05bf0cd92810314db5
timeCreated: 1756447956

View File

@ -37,7 +37,7 @@ public override void OnStart()
_isLooking = false;
var blackboard = gameObject.GetComponent<IAISharedBlackboard>();
_cachedTarget = blackboard.GetBlackboardValue<GameObject>(nameof(RestaurantCustomerBlackboardKey.CurrentInteractionTarget));
_cachedTarget = m_BehaviorTree.GetVariable<GameObject>(nameof(RestaurantCustomerBlackboardKey.CurrentInteractionTarget)).Value;
}
public override TaskStatus OnUpdate()

View File

@ -0,0 +1,33 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace DDD.Restaurant
{
public class SearchAndRegisterMarker : Action
{
[SerializeField] private PointType _pointType;
private string _markerBlackboardKey;
private bool _isRegistered;
public override void OnStart()
{
var environmentState = RestaurantState.Instance?.EnvironmentState;
if (environmentState == null) return;
var pointProviders = environmentState.GetPointProviderByType(_pointType);
foreach (var pointProvider in pointProviders)
{
if (!pointProvider.IsSupportsType(_pointType)) continue;
_isRegistered = RestaurantEvents.EnvironmentPointQueryEvent.RequestEvent(gameObject, pointProvider.GetGameObject(), _pointType);
// 사실 이런 일은 없어야 함
if (!_isRegistered) continue;
m_BehaviorTree.SetVariableValue(_markerBlackboardKey, pointProvider.GetGameObject());
}
}
public override TaskStatus OnUpdate()
{
return _isRegistered ? TaskStatus.Success : TaskStatus.Failure;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cdd22f2712964a81b4a55f95c605ae13
timeCreated: 1756454016

View File

@ -0,0 +1,31 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using Opsive.GraphDesigner.Runtime;
using UnityEngine;
namespace DDD.Restaurant
{
public enum EvaluationStep
{
FoodSatisfactionCheck,
FavoriteTasteCheck,
}
[NodeDescription("만족도 평가 테스크")]
public class SatisfactionEvaluator : Action
{
[SerializeField] private string _satisfactionBlackboardKey;
[SerializeField] private EvaluationStep _evaluationStep;
public override void OnStart()
{
var currentSatisfaction = CalculateSatisfaction();
m_BehaviorTree.SetVariableValue(_satisfactionBlackboardKey, (int)currentSatisfaction);
}
//TODO 만족도 계산?
private EmotionType CalculateSatisfaction()
{
return EmotionType.Satisfied;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9ada80a75a61473394d6ab301a4daeaf
timeCreated: 1756449593

View File

@ -66,7 +66,7 @@ public override TaskStatus OnUpdate()
return TaskStatus.Failure;
}
RestaurantEvents.InteractionEvent.RequestInteraction(_interactor.GetInteractorGameObject(), currentInteractable.GetInteractableGameObject(), currentInteractable.GetInteractionType());
RestaurantEvents.InteractionEvent.RequestEvent(_interactor.GetInteractorGameObject(), currentInteractable.GetInteractableGameObject(), currentInteractable.GetInteractionType());
if (_targetOrderType == RestaurantOrderType.Busy)
{

View File

@ -42,7 +42,7 @@ protected void TryInteraction(IInteractable interactable, ScriptableObject paylo
var causer = gameObject;
var target = interactable.GetInteractableGameObject();
var interactionType = _nearestInteractable.GetInteractionType();
RestaurantEvents.InteractionEvent.RequestInteraction(causer, target, interactionType, payload);
RestaurantEvents.InteractionEvent.RequestEvent(causer, target, interactionType, payload);
}
public IInteractionSolver GetInteractionSolver(InteractionType interactionType)

View File

@ -0,0 +1,20 @@
using System;
using UnityEngine;
using UnityEngine.Splines;
namespace DDD.Restaurant
{
public enum PointType
{
Entry,
Exit,
}
public interface IEnvironmentPointProvider
{
bool IsSupportsType(PointType pointType);
Vector3 GetPosition();
GameObject GetGameObject();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f01a9fb897547f4bd10c39acade7489
timeCreated: 1756445406

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 82eedd995f09472a9e3c6b93eeecf79f
timeCreated: 1756445877

View File

@ -0,0 +1,36 @@
using UnityEngine;
namespace DDD.Restaurant
{
public class EnvironmentPointMarker : MonoBehaviour, IEnvironmentPointProvider
{
[SerializeField] PointType _pointType;
private void Start()
{
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.RegisterPointProvider(this);
}
private void OnDisable()
{
var environmentState = RestaurantState.Instance?.EnvironmentState;
environmentState?.UnRegisterPointProvider(this);
}
public bool IsSupportsType(PointType pointType)
{
return _pointType == pointType;
}
public Vector3 GetPosition()
{
return transform.position;
}
public GameObject GetGameObject()
{
return gameObject;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01f8d593287c4672a63146e6b4905db5
timeCreated: 1756446387

View File

@ -0,0 +1,15 @@
using UnityEngine;
namespace DDD.Restaurant
{
public class RestaurantEnvironmentPointQueryEvent : RestaurantEventBase<PointType>
{
protected override bool EventSolve(GameObject causer, GameObject target, PointType eventType, ScriptableObject payload)
{
if (!target.TryGetComponent(out IEnvironmentPointProvider provider)) return false;
if (!provider.IsSupportsType(eventType)) return false;
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17b28cfc7a5a4d43a050d247e22004dc
timeCreated: 1756453073

View File

@ -0,0 +1,39 @@
using System;
using UnityEngine;
namespace DDD.Restaurant
{
public abstract class RestaurantEventBase<T> : IEvent where T : Enum
{
protected GameObject Causer;
protected GameObject Target;
protected T EventType;
protected ScriptableObject Payload;
protected bool EventResult = false;
protected RestaurantEventBase<T> MakeInteractionEvent(GameObject causer, GameObject target,
T eventType,
ScriptableObject payload = null)
{
Causer = causer;
Target = target;
EventType = eventType;
Payload = payload;
return this;
}
public bool RequestEvent(GameObject causer, GameObject target, T eventType,
ScriptableObject payload = null, bool shouldBroadcastAfterSolve = true)
{
var evt = MakeInteractionEvent(causer, target, eventType, payload);
// Solve event directly. 이벤트 처리는 여기서 하고, 이벤트 호출로는 이런 이벤트가 호출되었고 결과가 어떻다는 거 전파하는 식으로.
evt.EventResult = EventSolve(causer, target, eventType, payload);
EventBus.Broadcast(evt); // 이벤트 결과를 이거 받아서 처리하면 될듯.
return evt.EventResult;
}
protected abstract bool EventSolve(GameObject causer, GameObject target, T eventType,
ScriptableObject payload);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa6f80fbb0ca4c2ba33616a9f070bced
timeCreated: 1756452619

View File

@ -9,6 +9,7 @@ public static class RestaurantEvents
public static TodayMenuRemovedEvent TodayMenuRemovedEvent = new();
public static RestaurantInteractionEvent InteractionEvent = new();
public static RestaurantEnvironmentPointQueryEvent EnvironmentPointQueryEvent = new();
}
#region RestaurantInteractionEvents

View File

@ -8,74 +8,47 @@ public static class RestaurantInteractionEventSolvers
{
public static Dictionary<InteractionType, Type> TypeToSolver = new()
{
{InteractionType.RestaurantManagement, typeof(RestaurantManagementSolver)},
{InteractionType.RestaurantOrder, typeof(RestaurantOrderSolver)},
{InteractionType.RestaurantCook, typeof(RestaurantCookSolver)}
{ InteractionType.RestaurantManagement, typeof(RestaurantManagementSolver) },
{ InteractionType.RestaurantOrder, typeof(RestaurantOrderSolver) },
{ InteractionType.RestaurantCook, typeof(RestaurantCookSolver) }
};
public static Dictionary<InteractionType, Type> TypeToPlayerSolver = new()
{
{InteractionType.RestaurantOrder, typeof(RestaurantOrderPlayerSolver)},
{ InteractionType.RestaurantOrder, typeof(RestaurantOrderPlayerSolver) },
};
}
public class RestaurantInteractionEvent : IEvent
public class RestaurantInteractionEvent : RestaurantEventBase<InteractionType>
{
public GameObject Causer;
public GameObject Target;
public InteractionType InteractionType;
public ScriptableObject Payload;
public bool EventResult = false;
public RestaurantInteractionEvent MakeInteractionEvent(GameObject causer, GameObject target, InteractionType interactionType,
ScriptableObject payload = null)
protected override bool EventSolve(GameObject causer, GameObject target, InteractionType interactionType,
ScriptableObject payload)
{
Causer = causer;
Target = target;
InteractionType = interactionType;
Payload = payload;
return this;
}
public bool RequestInteraction(GameObject causer, GameObject target, InteractionType interactionType,
ScriptableObject payload = null, bool shouldBroadcastAfterSolve = true)
{
if (interactionType == InteractionType.None)
if (!RestaurantInteractionEventSolvers.TypeToSolver.TryGetValue(interactionType, out var solverType))
{
return false;
}
var evt = MakeInteractionEvent(causer, target, interactionType, payload);
evt.EventResult = false;
// Solve event directly. 이벤트 처리는 여기서 하고, 이벤트 호출로는 이런 이벤트가 호출되었고 결과가 어떻다는 거 전파하는 식으로.
if (RestaurantInteractionEventSolvers.TypeToSolver.TryGetValue(interactionType, out var solverType))
Component solverComponent = causer.GetComponent(solverType);
IInteractionSolver solver = solverComponent as IInteractionSolver;
IInteractor interactor = causer.GetComponent<IInteractor>();
IInteractable interactable = target.GetComponent<IInteractable>();
// Cast solverComponent to IInteractable
if (solver is not null && interactor is not null)
{
Component solverComponent = causer.GetComponent(solverType);
IInteractionSolver solver = solverComponent as IInteractionSolver;
IInteractor interactor = causer.GetComponent<IInteractor>();
IInteractable interactable = target.GetComponent<IInteractable>();
bool canExecute = solver.CanExecuteInteraction(interactor, interactable, payload);
if (canExecute)
{
return solver.ExecuteInteraction(interactor, interactable, payload);
}
// Cast solverComponent to IInteractable
if (solver is not null && interactor is not null)
{
bool canExecute = solver.CanExecuteInteraction(interactor, interactable, payload);
if (canExecute)
{
evt.EventResult = solver.ExecuteInteraction(interactor, interactable, payload);
}
else
{
evt.EventResult = false;
}
}
else
{
// Should not reach here!
Debug.Assert(false, "Solver Component or Interactor is null");
}
return false;
}
EventBus.Broadcast(evt);// 이벤트 결과를 이거 받아서 처리하면 될듯.
return evt.EventResult;
// Should not reach here!
Debug.Assert(false, "Solver Component or Interactor is null");
return false;
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using DDD.Restaurant;
using UnityEngine;
namespace DDD
@ -24,6 +25,7 @@ public class RestaurantEnvironmentState : ScriptableObject
// 인터랙션 가능한 객체(IInteractable)를 관리하기 위한 리스트 (런타임 전용)
private readonly List<IInteractable> _registeredInteractables = new List<IInteractable>();
private readonly List<IEnvironmentPointProvider> _registeredPointProviders = new List<IEnvironmentPointProvider>();
/// <summary>
/// 인터랙션 가능한 객체를 등록합니다
@ -35,6 +37,13 @@ public void RegisterInteractable(IInteractable interactable)
_registeredInteractables.Add(interactable);
}
public void RegisterPointProvider(IEnvironmentPointProvider provider)
{
if (provider == null) return;
if (_registeredPointProviders.Contains(provider)) return;
_registeredPointProviders.Add(provider);
}
/// <summary>
/// 인터랙션 가능한 객체를 해제합니다
/// </summary>
@ -44,6 +53,12 @@ public void UnregisterInteractable(IInteractable interactable)
_registeredInteractables.Remove(interactable);
}
public void UnRegisterPointProvider(IEnvironmentPointProvider provider)
{
if (provider == null) return;
_registeredPointProviders.Remove(provider);
}
/// <summary>
/// 특정 InteractionType에 해당하는 인터랙션 객체들을 반환합니다
/// </summary>
@ -61,6 +76,19 @@ public List<IInteractable> GetInteractablesByType(InteractionType interactionTyp
}
return result;
}
public List<IEnvironmentPointProvider> GetPointProviderByType(PointType pointType)
{
var result = new List<IEnvironmentPointProvider>();
// null 또는 Destroyed 오브젝트 정리
_registeredPointProviders.RemoveAll(item => item == null || (item as UnityEngine.Object) == null);
foreach (var provider in _registeredPointProviders)
{
if (!provider.IsSupportsType(pointType)) continue;
result.Add(provider);
}
return result;
}
/// <summary>
/// 모든 등록된 인터랙션 객체들을 반환합니다
@ -70,5 +98,11 @@ public List<IInteractable> GetAllInteractables()
_registeredInteractables.RemoveAll(item => item == null || (item as UnityEngine.Object) == null);
return new List<IInteractable>(_registeredInteractables);
}
public List<IEnvironmentPointProvider> GetAllPointProviders()
{
_registeredPointProviders.RemoveAll(item => item == null || (item as UnityEngine.Object) == null);
return new List<IEnvironmentPointProvider>(_registeredPointProviders);
}
}
}