diff --git a/Assets/02_Scripts/Communication/Dialog/DialogNode.cs b/Assets/02_Scripts/Communication/Dialog/DialogNode.cs index 068f8e16..1b001213 100644 --- a/Assets/02_Scripts/Communication/Dialog/DialogNode.cs +++ b/Assets/02_Scripts/Communication/Dialog/DialogNode.cs @@ -33,6 +33,13 @@ public class DialogNode : ScriptableObject [Header("Behavior")] public bool LookAtPlayer; public bool ForcePlayerLook; // true면 이 대사 시작 시 플레이어(카메라)가 화자를 바라보도록 리그를 회전 + + [Tooltip("켜면 이 대사 시작 시 화자를 아래 Fixed Angle Y(월드 Y각)로 고정 회전한다. " + + "특정 각도에서만 자연스러운 제스처/애니메이션용. 대화 종료 시 원래 각도로 복원")] + public bool UseFixedAngle; + + [Tooltip("UseFixedAngle이 켜졌을 때 화자가 향할 월드 Y 회전(도). 0 = +Z(월드 forward), 90 = +X 방향")] + public float FixedAngleY; public bool WaitForInput; // true면 LineDuration 무시하고 B버튼(OnDialogNext) 입력까지 대기 public bool StagingOnly; // true면 대화창을 숨기고 연출(회전·제스처·VFX·이벤트·대기)만 수행 diff --git a/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs b/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs index 7c21aa18..6e66f6ce 100644 --- a/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs +++ b/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs @@ -393,6 +393,11 @@ private void RotateTowardPlayer(Transform target) private void RotateToRotation(Transform target, Quaternion rotation) => AddRotationJob(target, rotation, hold: false); + // 화자를 특정 월드 Y각(yaw)으로 고정 회전한다. hold=true라 대화 동안 Animator가 루트를 덮어써도 유지되고, + // 대화 종료 시 _originalRotations 복원으로 원래 각도로 돌아간다. (특정 각도 전용 애니메이션용) + private void RotateToFixedYaw(Transform target, float yawDegrees) + => AddRotationJob(target, Quaternion.Euler(0f, yawDegrees, 0f), hold: true); + // 플레이어가 지정 위치를 바라보도록 리그를 수평(yaw)으로만 돌린다. // VR에서는 HMD 카메라를 직접 못 돌리므로 "Player" 태그 루트(XR Origin)를 돌려서 // 카메라 정면이 목표를 향하게 한다. 대화가 끝나도 원상복구하지 않는다 (플레이어 시점이므로). @@ -548,6 +553,15 @@ private async Awaitable PlayNode(DialogNode node) RotatePlayerToward(voiceObj.transform.position); } + // 고정 각도 회전 — 특정 각도에서만 자연스러운 애니메이션/제스처용 (화자, 못 찾으면 대화 주인 NPC) + if (node.UseFixedAngle) + { + var speakerObj = node.Speaker != null ? CharacterVoiceObject.Find(node.Speaker) : null; + var fixedTarget = speakerObj != null ? speakerObj.transform : transform; + _originalRotations.TryAdd(fixedTarget, GetOriginalRotation(fixedTarget)); + RotateToFixedYaw(fixedTarget, node.FixedAngleY); + } + // 제스처/표정은 화자(Target)의 Animator에 적용 — 끼어든 NPC의 대사/연출 노드도 그 캐릭터가 움직인다. // 화자를 못 찾으면 대화 주인 NPC의 Animator로 폴백. if (node.Gesture != null || node.Expression != null) diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs index 760f083b..c9e4d829 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs @@ -10,7 +10,7 @@ namespace DinoLove.Dialog.GraphTool.Editor // .dlg 그래프 에셋을 기존 런타임 타입(DialogGroup / DialogNode / DialogChoice)으로 변환한다. // 생성된 DialogNode들은 서브에셋으로, DialogGroup이 메인 에셋으로 등록된다. // 따라서 DialogPlayer는 수정 없이 임포트된 .dlg 에셋(= DialogGroup)을 그대로 사용한다. - [ScriptedImporter(9, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨 + [ScriptedImporter(10, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨 internal class DialogGraphImporter : ScriptedImporter { public override void OnImportAsset(AssetImportContext ctx) @@ -96,6 +96,8 @@ public override void OnImportAsset(AssetImportContext ctx) dn.LineDuration = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_DURATION)); dn.LookAtPlayer = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_LOOKAT)); dn.ForcePlayerLook = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_FORCELOOK)); + dn.UseFixedAngle = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_USE_FIXED_ANGLE)); + dn.FixedAngleY = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_FIXED_ANGLE_Y)); dn.WaitForInput = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_WAITINPUT)); dn.Affection = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_AFFECTION)); dn.Progress = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.PORT_PROGRESS)); @@ -162,6 +164,8 @@ static void FillStagingNode(DialogStagingNode gn, DialogNode dn) dn.LineDuration = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_DURATION)); dn.LookAtPlayer = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_LOOKAT)); dn.ForcePlayerLook = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_FORCELOOK)); + dn.UseFixedAngle = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_USE_FIXED_ANGLE)); + dn.FixedAngleY = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_FIXED_ANGLE_Y)); dn.Affection = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_AFFECTION)); dn.Progress = GetInputPortValue(gn.GetInputPortByName(DialogStagingNode.PORT_PROGRESS)); diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs index 0273c3ab..804f5cb0 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs @@ -26,6 +26,8 @@ internal class DialogLineNode : DialogGraphNode public const string PORT_DURATION = "LineDuration"; public const string PORT_LOOKAT = "LookAtPlayer"; public const string PORT_FORCELOOK = "ForcePlayerLook"; + public const string PORT_USE_FIXED_ANGLE = "UseFixedAngle"; + public const string PORT_FIXED_ANGLE_Y = "FixedAngleY"; public const string PORT_WAITINPUT = "WaitForInput"; public const string PORT_AFFECTION = "Affection"; public const string PORT_PROGRESS = "Progress"; @@ -95,6 +97,10 @@ protected override void OnDefinePorts(IPortDefinitionContext context) context.AddInputPort(PORT_LOOKAT).WithDisplayName("Look At Player").Build(); context.AddInputPort(PORT_FORCELOOK).WithDisplayName("Force Player Look") .WithTooltip("이 대사 시작 시 플레이어(카메라)가 화자를 바라보도록 강제 회전").Build(); + context.AddInputPort(PORT_USE_FIXED_ANGLE).WithDisplayName("Use Fixed Angle") + .WithTooltip("켜면 화자를 Fixed Angle Y(월드 Y각)로 고정 회전. 특정 각도에서만 자연스러운 애니메이션용").Build(); + context.AddInputPort(PORT_FIXED_ANGLE_Y).WithDisplayName("Fixed Angle Y") + .WithTooltip("Use Fixed Angle이 켜졌을 때 화자가 향할 월드 Y 회전(도)").Build(); context.AddInputPort(PORT_WAITINPUT).WithDisplayName("Wait For Input").Build(); context.AddInputPort(PORT_AFFECTION).WithDisplayName("Affection ±") .WithTooltip("0이 아니면 이 대사 재생 시 화자(비우면 대화 주인 NPC)의 호감도를 이만큼 증감").Build(); diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogStagingNode.cs b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogStagingNode.cs index 93d962d3..21f12f17 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogStagingNode.cs +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogStagingNode.cs @@ -19,6 +19,8 @@ internal class DialogStagingNode : DialogGraphNode public const string PORT_DURATION = "Duration"; public const string PORT_LOOKAT = "LookAtPlayer"; public const string PORT_FORCELOOK = "ForcePlayerLook"; + public const string PORT_USE_FIXED_ANGLE = "UseFixedAngle"; + public const string PORT_FIXED_ANGLE_Y = "FixedAngleY"; public const string PORT_AFFECTION = "Affection"; public const string PORT_PROGRESS = "Progress"; @@ -50,6 +52,10 @@ protected override void OnDefinePorts(IPortDefinitionContext context) .WithTooltip("대상 캐릭터가 플레이어를 바라봄").Build(); context.AddInputPort(PORT_FORCELOOK).WithDisplayName("Force Player Look") .WithTooltip("플레이어가 대상 캐릭터를 바라봄").Build(); + context.AddInputPort(PORT_USE_FIXED_ANGLE).WithDisplayName("Use Fixed Angle") + .WithTooltip("켜면 대상을 Fixed Angle Y(월드 Y각)로 고정 회전. 특정 각도에서만 자연스러운 애니메이션용").Build(); + context.AddInputPort(PORT_FIXED_ANGLE_Y).WithDisplayName("Fixed Angle Y") + .WithTooltip("Use Fixed Angle이 켜졌을 때 대상이 향할 월드 Y 회전(도)").Build(); context.AddInputPort(PORT_AFFECTION).WithDisplayName("Affection ±") .WithTooltip("0이 아니면 이 연출 재생 시 대상(비우면 대화 주인 NPC)의 호감도를 이만큼 증감").Build(); context.AddInputPort(PORT_PROGRESS).WithDisplayName("Progress +")