feat: Implement additional animations

- Also performed minor structural refactoring for better readability.
This commit is contained in:
2025-11-28 05:56:41 +09:00
parent d14817c4f7
commit 4230966305
2 changed files with 174 additions and 61 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections;
using PrimeTween; using PrimeTween;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
@@ -37,23 +38,35 @@ public class ScriptManager : MonoBehaviour
dialogueText.SetText(" "); dialogueText.SetText(" ");
dialogueText.ForceMeshUpdate(true); dialogueText.ForceMeshUpdate(true);
director.AddCharacter("chino01"); StartCoroutine(TestAnim());
Invoke("test1", 2f);
Invoke("test2", 4f);
_currentScript = ScriptParser.Parse(scriptFile.text); _currentScript = ScriptParser.Parse(scriptFile.text);
NextStep(); NextStep();
} }
void test1() IEnumerator TestAnim()
{ {
director.AddCharacter("chino01"); director.AddCharacter("chino01", VisualNovelLayoutDirector.EntranceType.Center);
} yield return new WaitForSeconds(1f);
director.AddCharacter("chino02", VisualNovelLayoutDirector.EntranceType.Left);
void test2() yield return new WaitForSeconds(1f);
{ director.AddCharacter("chino03", VisualNovelLayoutDirector.EntranceType.Right);
director.AddCharacter("chino01"); 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() void Update()

View File

@@ -5,86 +5,186 @@ using System.Collections;
public class VisualNovelLayoutDirector : MonoBehaviour public class VisualNovelLayoutDirector : MonoBehaviour
{ {
// ========================= [Enums] =========================
public enum EntranceType { Left, Right, BottomLeft, BottomRight, Center, Top }
public enum ActionType { Jump, Shake, Nod, Punch }
[Header("UI 연결")] [Header("UI 연결")]
public Transform characterPanel; // Horizontal Layout Group이 달린 부모 public Transform characterPanel;
public GameObject slotPrefab; // 슬롯 프리팹 (빈 부모 + 이미지 자식) public GameObject slotPrefab;
[Header("연출 설정")] [Header("설정")]
public float charWidth = 350f; // 캐릭터 하나가 차지할 최종 너비 public float charWidth = 350f;
public float enterDuration = 0.6f; // 등장(위로 올라옴 + 투명도) 시간 public float defaultDuration = 0.5f;
public float slideDuration = 0.5f; // 옆으로 밀려나는 시간 public float moveDistance = 800f;
public float startOffsetY = -100f; // 시작 Y 위치 (화면 아래쪽 오프셋)
// 파일명으로 캐릭터 등장시키기 // ========================= [1. 등장 (Entry)] =========================
public void AddCharacter(string fileName) public void AddCharacter(string fileName, EntranceType type)
{ {
// 1. 리소스 로드 // 중복 방지
Sprite loadedSprite = Resources.Load<Sprite>("Images/Characters/" + fileName); if (FindSlot(fileName) != null)
{
Debug.LogWarning($"이미 존재하는 캐릭터입니다: {fileName}");
return;
}
string path = "Images/Characters/" + fileName;
Sprite loadedSprite = Resources.Load<Sprite>(path);
if (loadedSprite != null) if (loadedSprite != null)
{ {
StartCoroutine(SpawnRoutine(loadedSprite)); StartCoroutine(SpawnRoutine(fileName, loadedSprite, type));
} }
else 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); GameObject newSlot = Instantiate(slotPrefab, characterPanel);
// 컴포넌트 찾아오기 // 오브젝트 이름을 파일명(ID)으로 설정
newSlot.name = name;
LayoutElement layoutElement = newSlot.GetComponent<LayoutElement>(); LayoutElement layoutElement = newSlot.GetComponent<LayoutElement>();
Image charImage = newSlot.transform.GetChild(0).GetComponent<Image>(); // 자식에 있는 이미지 Image charImage = newSlot.transform.GetChild(0).GetComponent<Image>();
// 3. 초기 세팅 // 2. 초기
charImage.sprite = sprite; charImage.sprite = sprite;
charImage.SetNativeSize(); // 이미지 원본 비율 맞춤 charImage.SetNativeSize();
layoutElement.preferredWidth = 0; layoutElement.minWidth = 0;
// [중요] 공간을 0으로 만들어둠 -> 기존 캐릭터들이 아직 움직이지 않음 // 3. 순서 재배치 (기존 로직 유지)
layoutElement.preferredWidth = 0; int totalCount = characterPanel.childCount;
layoutElement.minWidth = 0; 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)에 배치하고 투명하게 설정 // 4. 위치 잡기 및 애니메이션
// 슬롯(부모)은 Layout에 묶여도, 이미지(자식)는 자유롭게 움직일 수 있음 Vector2 startPos = GetDirectionVector(type);
charImage.rectTransform.anchoredPosition = new Vector2(0, startOffsetY); charImage.rectTransform.anchoredPosition = startPos;
charImage.color = new Color(1, 1, 1, 0); // Alpha 0 (투명) charImage.color = new Color(1, 1, 1, 0);
// UI 갱신 대기 (필수)
yield return new WaitForEndOfFrame(); yield return new WaitForEndOfFrame();
// 4. 애니메이션 실행 Tween.Custom(layoutElement, 0f, charWidth, defaultDuration, (t, x) => t.preferredWidth = x, Ease.OutQuart);
Tween.UIAnchoredPosition(charImage.rectTransform, Vector2.zero, defaultDuration, Ease.OutQuart);
// A. 공간 벌리기 (기존 캐릭터들이 스르륵 밀려남) Tween.Alpha(charImage, 1f, defaultDuration);
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);
} }
// (참고) 캐릭터 퇴장 기능 // ========================= [2. 퇴장 (Exit)] =========================
public void RemoveCharacter(int index) public void RemoveCharacter(string characterName, EntranceType exitTo)
{ {
if (index < characterPanel.childCount) Transform targetSlot = FindSlot(characterName);
if (targetSlot != null)
{ {
Transform targetSlot = characterPanel.GetChild(index); StartCoroutine(ExitRoutine(targetSlot, exitTo));
LayoutElement le = targetSlot.GetComponent<LayoutElement>(); }
Image img = targetSlot.GetChild(0).GetComponent<Image>(); else
{
Debug.LogWarning($"삭제 실패: '{characterName}' 캐릭터를 찾을 수 없습니다.");
}
}
// 1. 이미지 사라지기 (Fade Out) private IEnumerator ExitRoutine(Transform slotTransform, EntranceType exitTo)
Tween.Alpha(img, 0, slideDuration); {
// 중복 호출 방지를 위해 이름을 바꿔둠 (빠르게 연타했을 때 에러 방지)
slotTransform.name = slotTransform.name + "_Removing";
// 2. 공간 닫기 & 종료 후 삭제 LayoutElement layoutElement = slotTransform.GetComponent<LayoutElement>();
Tween.Custom(le, le.preferredWidth, 0, slideDuration, Image charImage = slotTransform.GetChild(0).GetComponent<Image>();
(target, x) => target.preferredWidth = x, Vector2 targetPos = GetDirectionVector(exitTo);
ease: Ease.OutQuart)
.OnComplete(targetSlot.gameObject, go => Object.Destroy(go)); // 이미지 날리기 & 투명화
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<RectTransform>();
// 기존 애니메이션 정지 및 초기화
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;
} }
} }
} }