diff --git a/Assets/_MAIN/Scripts/Core/ScriptManager.cs b/Assets/_MAIN/Scripts/Core/ScriptManager.cs index 09d720c..b1bc74f 100644 --- a/Assets/_MAIN/Scripts/Core/ScriptManager.cs +++ b/Assets/_MAIN/Scripts/Core/ScriptManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections; using PrimeTween; using TMPro; using UnityEngine; @@ -37,23 +38,35 @@ public class ScriptManager : MonoBehaviour dialogueText.SetText(" "); dialogueText.ForceMeshUpdate(true); - director.AddCharacter("chino01"); - Invoke("test1", 2f); - Invoke("test2", 4f); - + StartCoroutine(TestAnim()); _currentScript = ScriptParser.Parse(scriptFile.text); NextStep(); } - void test1() + IEnumerator TestAnim() { - director.AddCharacter("chino01"); - } - - void test2() - { - director.AddCharacter("chino01"); + director.AddCharacter("chino01", VisualNovelLayoutDirector.EntranceType.Center); + yield return new WaitForSeconds(1f); + director.AddCharacter("chino02", VisualNovelLayoutDirector.EntranceType.Left); + yield return new WaitForSeconds(1f); + director.AddCharacter("chino03", VisualNovelLayoutDirector.EntranceType.Right); + yield return new WaitForSeconds(1f); + director.RemoveCharacter("chino02", VisualNovelLayoutDirector.EntranceType.Left); + yield return new WaitForSeconds(1f); + director.RemoveCharacter("chino03", VisualNovelLayoutDirector.EntranceType.Right); + yield return new WaitForSeconds(1f); + director.PlayAction("chino01", VisualNovelLayoutDirector.ActionType.Jump); + yield return new WaitForSeconds(1f); + director.PlayAction("chino01", VisualNovelLayoutDirector.ActionType.Shake); + yield return new WaitForSeconds(1f); + director.PlayAction("chino01", VisualNovelLayoutDirector.ActionType.Nod); + yield return new WaitForSeconds(1f); + director.PlayAction("chino01", VisualNovelLayoutDirector.ActionType.Punch); + yield return new WaitForSeconds(1f); + director.AddCharacter("chino02", VisualNovelLayoutDirector.EntranceType.Left); + yield return new WaitForSeconds(1f); + director.AddCharacter("chino03", VisualNovelLayoutDirector.EntranceType.Center); } void Update() diff --git a/Assets/_MAIN/Scripts/Core/VisualNovelLayoutDirector.cs b/Assets/_MAIN/Scripts/Core/VisualNovelLayoutDirector.cs index 8a59af1..34d263f 100644 --- a/Assets/_MAIN/Scripts/Core/VisualNovelLayoutDirector.cs +++ b/Assets/_MAIN/Scripts/Core/VisualNovelLayoutDirector.cs @@ -5,86 +5,186 @@ using System.Collections; public class VisualNovelLayoutDirector : MonoBehaviour { + // ========================= [Enums] ========================= + public enum EntranceType { Left, Right, BottomLeft, BottomRight, Center, Top } + public enum ActionType { Jump, Shake, Nod, Punch } + [Header("UI 연결")] - public Transform characterPanel; // Horizontal Layout Group이 달린 부모 - public GameObject slotPrefab; // 슬롯 프리팹 (빈 부모 + 이미지 자식) + public Transform characterPanel; + public GameObject slotPrefab; - [Header("연출 설정")] - public float charWidth = 350f; // 캐릭터 하나가 차지할 최종 너비 - public float enterDuration = 0.6f; // 등장(위로 올라옴 + 투명도) 시간 - public float slideDuration = 0.5f; // 옆으로 밀려나는 시간 - public float startOffsetY = -100f; // 시작 Y 위치 (화면 아래쪽 오프셋) + [Header("설정")] + public float charWidth = 350f; + public float defaultDuration = 0.5f; + public float moveDistance = 800f; - // 파일명으로 캐릭터 등장시키기 - public void AddCharacter(string fileName) + // ========================= [1. 등장 (Entry)] ========================= + public void AddCharacter(string fileName, EntranceType type) { - // 1. 리소스 로드 - Sprite loadedSprite = Resources.Load("Images/Characters/" + fileName); + // 중복 방지 + if (FindSlot(fileName) != null) + { + Debug.LogWarning($"이미 존재하는 캐릭터입니다: {fileName}"); + return; + } + + string path = "Images/Characters/" + fileName; + Sprite loadedSprite = Resources.Load(path); if (loadedSprite != null) { - StartCoroutine(SpawnRoutine(loadedSprite)); + StartCoroutine(SpawnRoutine(fileName, loadedSprite, type)); } else { - Debug.LogError($"이미지 로드 실패: Resources/Images/Characters/{fileName} 파일을 확인하세요."); + Debug.LogError($"이미지 로드 실패: {path}"); } } - private IEnumerator SpawnRoutine(Sprite sprite) + private IEnumerator SpawnRoutine(string name, Sprite sprite, EntranceType type) { - // 2. 슬롯 생성 (Panel의 자식으로) + // 1. 슬롯 생성 GameObject newSlot = Instantiate(slotPrefab, characterPanel); - // 컴포넌트 찾아오기 + // 오브젝트 이름을 파일명(ID)으로 설정 + newSlot.name = name; + LayoutElement layoutElement = newSlot.GetComponent(); - Image charImage = newSlot.transform.GetChild(0).GetComponent(); // 자식에 있는 이미지 + Image charImage = newSlot.transform.GetChild(0).GetComponent(); - // 3. 초기 세팅 + // 2. 초기화 charImage.sprite = sprite; - charImage.SetNativeSize(); // 이미지 원본 비율 맞춤 + charImage.SetNativeSize(); + layoutElement.preferredWidth = 0; layoutElement.minWidth = 0; - // [중요] 공간을 0으로 만들어둠 -> 기존 캐릭터들이 아직 움직이지 않음 - layoutElement.preferredWidth = 0; - layoutElement.minWidth = 0; + // 3. 순서 재배치 (기존 로직 유지) + int totalCount = characterPanel.childCount; + switch (type) + { + case EntranceType.Left: + case EntranceType.BottomLeft: + newSlot.transform.SetSiblingIndex(0); break; + case EntranceType.Right: + case EntranceType.BottomRight: + newSlot.transform.SetSiblingIndex(totalCount - 1); break; + case EntranceType.Center: + case EntranceType.Top: + newSlot.transform.SetSiblingIndex((totalCount - 1) / 2); break; + } - // [중요] 이미지는 화면 아래(startOffsetY)에 배치하고 투명하게 설정 - // 슬롯(부모)은 Layout에 묶여도, 이미지(자식)는 자유롭게 움직일 수 있음 - charImage.rectTransform.anchoredPosition = new Vector2(0, startOffsetY); - charImage.color = new Color(1, 1, 1, 0); // Alpha 0 (투명) + // 4. 위치 잡기 및 애니메이션 + Vector2 startPos = GetDirectionVector(type); + charImage.rectTransform.anchoredPosition = startPos; + charImage.color = new Color(1, 1, 1, 0); - // UI 갱신 대기 (필수) yield return new WaitForEndOfFrame(); - // 4. 애니메이션 실행 - - // A. 공간 벌리기 (기존 캐릭터들이 스르륵 밀려남) - Tween.Custom(layoutElement, layoutElement.preferredWidth, charWidth, slideDuration, - (target, x) => target.preferredWidth = x, - ease: Ease.OutQuart); - - // B. 이미지 등장 (아래에서 위로 올라오며 선명해짐) - Tween.UIAnchoredPositionY(charImage.rectTransform, 0, enterDuration, ease: Ease.OutBack); - Tween.Alpha(charImage, 1, enterDuration); + Tween.Custom(layoutElement, 0f, charWidth, defaultDuration, (t, x) => t.preferredWidth = x, Ease.OutQuart); + Tween.UIAnchoredPosition(charImage.rectTransform, Vector2.zero, defaultDuration, Ease.OutQuart); + Tween.Alpha(charImage, 1f, defaultDuration); } - // (참고) 캐릭터 퇴장 기능 - public void RemoveCharacter(int index) + // ========================= [2. 퇴장 (Exit)] ========================= + public void RemoveCharacter(string characterName, EntranceType exitTo) { - if (index < characterPanel.childCount) + Transform targetSlot = FindSlot(characterName); + + if (targetSlot != null) { - Transform targetSlot = characterPanel.GetChild(index); - LayoutElement le = targetSlot.GetComponent(); - Image img = targetSlot.GetChild(0).GetComponent(); + StartCoroutine(ExitRoutine(targetSlot, exitTo)); + } + else + { + Debug.LogWarning($"삭제 실패: '{characterName}' 캐릭터를 찾을 수 없습니다."); + } + } - // 1. 이미지 사라지기 (Fade Out) - Tween.Alpha(img, 0, slideDuration); + private IEnumerator ExitRoutine(Transform slotTransform, EntranceType exitTo) + { + // 중복 호출 방지를 위해 이름을 바꿔둠 (빠르게 연타했을 때 에러 방지) + slotTransform.name = slotTransform.name + "_Removing"; - // 2. 공간 닫기 & 종료 후 삭제 - Tween.Custom(le, le.preferredWidth, 0, slideDuration, - (target, x) => target.preferredWidth = x, - ease: Ease.OutQuart) - .OnComplete(targetSlot.gameObject, go => Object.Destroy(go)); + LayoutElement layoutElement = slotTransform.GetComponent(); + Image charImage = slotTransform.GetChild(0).GetComponent(); + Vector2 targetPos = GetDirectionVector(exitTo); + + // 이미지 날리기 & 투명화 + Tween.UIAnchoredPosition(charImage.rectTransform, targetPos, defaultDuration, Ease.OutQuart); + Tween.Alpha(charImage, 0f, defaultDuration * 0.8f); + + // 공간 닫기 + yield return Tween.Custom(layoutElement, layoutElement.preferredWidth, 0f, defaultDuration, + (t, x) => t.preferredWidth = x, Ease.OutQuart).ToYieldInstruction(); + + Destroy(slotTransform.gameObject); + } + + // ========================= [3. 액션 (Action)] ========================= + public void PlayAction(string characterName, ActionType action) + { + Transform targetSlot = FindSlot(characterName); + + if (targetSlot == null) + { + Debug.LogWarning($"액션 실패: '{characterName}' 캐릭터를 찾을 수 없습니다."); + return; + } + + RectTransform targetImageRect = targetSlot.GetChild(0).GetComponent(); + + // 기존 애니메이션 정지 및 초기화 + Tween.StopAll(targetImageRect); + targetImageRect.anchoredPosition = Vector2.zero; + + switch (action) + { + case ActionType.Jump: + // frequency: 2 (위로 갔다가 한두 번 띠용~ 하고 멈춤) + Tween.PunchLocalPosition(targetImageRect, new Vector3(0, 100f, 0), 0.5f, frequency: 2); + break; + + case ActionType.Shake: + // 좌우 흔들기 (진동 횟수 10번) + Tween.ShakeLocalPosition(targetImageRect, new Vector3(50f, 0, 0), 0.5f, frequency: 10); + break; + + case ActionType.Nod: + // (Sequence는 변경 없음) + Sequence.Create() + .Chain(Tween.UIAnchoredPositionY(targetImageRect, -30f, 0.15f, Ease.OutQuad)) + .Chain(Tween.UIAnchoredPositionY(targetImageRect, 0f, 0.15f, Ease.InQuad)); + break; + + case ActionType.Punch: + // frequency: 1 (커졌다가 딱 한 번 출렁이고 복구됨) + Tween.PunchScale(targetImageRect, new Vector3(0.2f, 0.2f, 0), 0.4f, frequency: 1); + break; + } + } + + // [Helper] 이름으로 슬롯 찾기 + private Transform FindSlot(string name) + { + // CharacterPanel 바로 아래 자식들 중에서 이름을 검색 + return characterPanel.Find(name); + } + + private Vector2 GetDirectionVector(EntranceType type) + { + switch (type) + { + case EntranceType.Left: + return new Vector2(-moveDistance, 0); + case EntranceType.Right: + return new Vector2(moveDistance, 0); + case EntranceType.Center: + case EntranceType.BottomLeft: + case EntranceType.BottomRight: + return new Vector2(0, -moveDistance); + case EntranceType.Top: + return new Vector2(0, moveDistance); + default: + return Vector2.zero; } } } \ No newline at end of file