mirror of
https://github.com/HoonTB/Project-AS.git
synced 2025-12-26 11:51:21 +09:00
feat: Implement core visual novel systems for variable management, character layout, and script handling
This commit is contained in:
@@ -37,6 +37,13 @@ public class Script
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ScriptAction PeekNext()
|
||||||
|
{
|
||||||
|
if (_currentIndex < _actions.Count - 1)
|
||||||
|
return _actions[_currentIndex + 1];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void JumpTo(string labelName)
|
public void JumpTo(string labelName)
|
||||||
{
|
{
|
||||||
_currentIndex = _labelMap[labelName] - 1; // Continue() 호출 시 해당 인덱스가 되도록 -1
|
_currentIndex = _labelMap[labelName] - 1; // Continue() 호출 시 해당 인덱스가 되도록 -1
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ public class ScriptManager : MonoBehaviour
|
|||||||
if (!isChoiceAvailable && !IsPointerOverInteractiveUI() && (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space)))
|
if (!isChoiceAvailable && !IsPointerOverInteractiveUI() && (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space)))
|
||||||
{
|
{
|
||||||
if (dialogueTween.isAlive)
|
if (dialogueTween.isAlive)
|
||||||
|
{
|
||||||
|
director.CompleteAllActions();
|
||||||
dialogueTween.Complete();
|
dialogueTween.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
NextStep();
|
NextStep();
|
||||||
}
|
}
|
||||||
@@ -107,13 +111,20 @@ public class ScriptManager : MonoBehaviour
|
|||||||
if (action.Type == "char")
|
if (action.Type == "char")
|
||||||
{
|
{
|
||||||
string charFile = action.GetParam("img");
|
string charFile = action.GetParam("img");
|
||||||
|
if (string.IsNullOrEmpty(charFile))
|
||||||
|
{
|
||||||
|
charFile = action.GetParam("target");
|
||||||
|
}
|
||||||
string charEntrance = action.GetParam("enter");
|
string charEntrance = action.GetParam("enter");
|
||||||
if (charEntrance == "") charEntrance = "center";
|
if (charEntrance == "") charEntrance = "center";
|
||||||
if (charEntrance.ToLower() == "center") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Center);
|
if (charEntrance.ToLower() == "center") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Center);
|
||||||
|
if (charEntrance.ToLower() == "top") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Top);
|
||||||
if (charEntrance.ToLower() == "left") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Left);
|
if (charEntrance.ToLower() == "left") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Left);
|
||||||
if (charEntrance.ToLower() == "right") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Right);
|
if (charEntrance.ToLower() == "right") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Right);
|
||||||
if (charEntrance.ToLower() == "bottomleft") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.BottomLeft);
|
if (charEntrance.ToLower() == "bottomleft") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.BottomLeft);
|
||||||
if (charEntrance.ToLower() == "bottomright") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.BottomRight);
|
if (charEntrance.ToLower() == "bottomright") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.BottomRight);
|
||||||
|
if (charEntrance.ToLower() == "leftrun") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.LeftRun);
|
||||||
|
if (charEntrance.ToLower() == "rightrun") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.RightRun);
|
||||||
Debug.Log($"ScriptManager :: Character: {charFile}");
|
Debug.Log($"ScriptManager :: Character: {charFile}");
|
||||||
NextStep();
|
NextStep();
|
||||||
return;
|
return;
|
||||||
@@ -124,12 +135,15 @@ public class ScriptManager : MonoBehaviour
|
|||||||
string exitType = action.GetParam("exit");
|
string exitType = action.GetParam("exit");
|
||||||
if (exitType == "") exitType = "center";
|
if (exitType == "") exitType = "center";
|
||||||
|
|
||||||
VisualNovelLayoutDirector.EntranceType type = VisualNovelLayoutDirector.EntranceType.Center;
|
VisualNovelLayoutDirector.EntranceType type = new();
|
||||||
|
if (exitType.ToLower() == "center") type = VisualNovelLayoutDirector.EntranceType.Center;
|
||||||
if (exitType.ToLower() == "left") type = VisualNovelLayoutDirector.EntranceType.Left;
|
if (exitType.ToLower() == "left") type = VisualNovelLayoutDirector.EntranceType.Left;
|
||||||
if (exitType.ToLower() == "right") type = VisualNovelLayoutDirector.EntranceType.Right;
|
if (exitType.ToLower() == "right") type = VisualNovelLayoutDirector.EntranceType.Right;
|
||||||
if (exitType.ToLower() == "bottomleft") type = VisualNovelLayoutDirector.EntranceType.BottomLeft;
|
if (exitType.ToLower() == "bottomleft") type = VisualNovelLayoutDirector.EntranceType.BottomLeft;
|
||||||
if (exitType.ToLower() == "bottomright") type = VisualNovelLayoutDirector.EntranceType.BottomRight;
|
if (exitType.ToLower() == "bottomright") type = VisualNovelLayoutDirector.EntranceType.BottomRight;
|
||||||
if (exitType.ToLower() == "top") type = VisualNovelLayoutDirector.EntranceType.Top;
|
if (exitType.ToLower() == "top") type = VisualNovelLayoutDirector.EntranceType.Top;
|
||||||
|
if (exitType.ToLower() == "leftrun") type = VisualNovelLayoutDirector.EntranceType.LeftRun;
|
||||||
|
if (exitType.ToLower() == "rightrun") type = VisualNovelLayoutDirector.EntranceType.RightRun;
|
||||||
|
|
||||||
director.RemoveCharacter(charName, type);
|
director.RemoveCharacter(charName, type);
|
||||||
Debug.Log($"ScriptManager :: Remove Character: {charName} to {exitType}");
|
Debug.Log($"ScriptManager :: Remove Character: {charName} to {exitType}");
|
||||||
@@ -143,7 +157,7 @@ public class ScriptManager : MonoBehaviour
|
|||||||
if (charAnim == "") charAnim = "center";
|
if (charAnim == "") charAnim = "center";
|
||||||
if (charAnim.ToLower() == "jump") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Jump);
|
if (charAnim.ToLower() == "jump") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Jump);
|
||||||
if (charAnim.ToLower() == "shake") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Shake);
|
if (charAnim.ToLower() == "shake") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Shake);
|
||||||
if (charAnim.ToLower() == "shakehorizontal") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.ShakeHorizontal);
|
if (charAnim.ToLower() == "run") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Run);
|
||||||
if (charAnim.ToLower() == "nod") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Nod);
|
if (charAnim.ToLower() == "nod") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Nod);
|
||||||
if (charAnim.ToLower() == "punch") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Punch);
|
if (charAnim.ToLower() == "punch") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Punch);
|
||||||
Debug.Log($"ScriptManager :: Action: {charName} {charAnim}");
|
Debug.Log($"ScriptManager :: Action: {charName} {charAnim}");
|
||||||
@@ -162,10 +176,13 @@ public class ScriptManager : MonoBehaviour
|
|||||||
if (action.Type == "spk")
|
if (action.Type == "spk")
|
||||||
{
|
{
|
||||||
string speaker = action.GetParam("name");
|
string speaker = action.GetParam("name");
|
||||||
|
if (speakerSprite.activeSelf == false)
|
||||||
|
speakerSprite.SetActive(true);
|
||||||
if (speaker == "")
|
if (speaker == "")
|
||||||
speakerSprite.SetActive(false);
|
speakerSprite.SetActive(false);
|
||||||
|
|
||||||
speaker = VariableManager.Instance.ReplaceVariables(speaker);
|
speaker = VariableManager.Instance.ReplaceVariables(speaker);
|
||||||
|
Debug.Log($"ScriptManager :: Speaker: {speaker}");
|
||||||
speakerText.SetText(speaker);
|
speakerText.SetText(speaker);
|
||||||
speakerText.ForceMeshUpdate(true);
|
speakerText.ForceMeshUpdate(true);
|
||||||
NextStep();
|
NextStep();
|
||||||
@@ -175,7 +192,14 @@ public class ScriptManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
string dialogue = action.GetParam("content");
|
string dialogue = action.GetParam("content");
|
||||||
dialogue = VariableManager.Instance.ReplaceVariables(dialogue);
|
dialogue = VariableManager.Instance.ReplaceVariables(dialogue);
|
||||||
|
|
||||||
|
|
||||||
DisplayDialogue(dialogue);
|
DisplayDialogue(dialogue);
|
||||||
|
|
||||||
|
if (_currentScript.PeekNext()?.Type == "choices")
|
||||||
|
{
|
||||||
|
NextStep();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action.Type == "goto")
|
if (action.Type == "goto")
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class VariableManager
|
|||||||
|
|
||||||
public string ReplaceVariables(string text)
|
public string ReplaceVariables(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text)) return text;
|
if (string.IsNullOrEmpty(text) || !text.Contains("{")) return text;
|
||||||
|
|
||||||
foreach (var kvp in _variables)
|
foreach (var kvp in _variables)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using PrimeTween;
|
using PrimeTween;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
@@ -6,8 +7,8 @@ using UnityEngine.UI;
|
|||||||
public class VisualNovelLayoutDirector : MonoBehaviour
|
public class VisualNovelLayoutDirector : MonoBehaviour
|
||||||
{
|
{
|
||||||
// ========================= [Enums] =========================
|
// ========================= [Enums] =========================
|
||||||
public enum EntranceType { Left, Right, BottomLeft, BottomRight, Center, Top }
|
public enum EntranceType { Left, Right, BottomLeft, BottomRight, Center, Top, LeftRun, RightRun }
|
||||||
public enum ActionType { Jump, Shake, Nod, Punch, ShakeHorizontal }
|
public enum ActionType { Jump, Shake, Nod, Punch, Run }
|
||||||
|
|
||||||
[Header("UI 연결")]
|
[Header("UI 연결")]
|
||||||
public Transform characterPanel;
|
public Transform characterPanel;
|
||||||
@@ -18,22 +19,81 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
public float defaultDuration = 0.5f;
|
public float defaultDuration = 0.5f;
|
||||||
public float moveDistance = 800f;
|
public float moveDistance = 800f;
|
||||||
|
|
||||||
|
// ========================= [Queue System] =========================
|
||||||
|
private Dictionary<string, Queue<IEnumerator>> actionQueues = new();
|
||||||
|
private Dictionary<string, Coroutine> activeCoroutines = new();
|
||||||
|
|
||||||
|
private void EnqueueAction(string charName, IEnumerator action)
|
||||||
|
{
|
||||||
|
if (!actionQueues.ContainsKey(charName))
|
||||||
|
{
|
||||||
|
actionQueues[charName] = new Queue<IEnumerator>();
|
||||||
|
}
|
||||||
|
actionQueues[charName].Enqueue(action);
|
||||||
|
|
||||||
|
if (!activeCoroutines.ContainsKey(charName) || activeCoroutines[charName] == null)
|
||||||
|
{
|
||||||
|
activeCoroutines[charName] = StartCoroutine(ProcessActionQueue(charName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteAllActions()
|
||||||
|
{
|
||||||
|
// 1. Stop all active processing coroutines
|
||||||
|
foreach (var kvp in activeCoroutines)
|
||||||
|
{
|
||||||
|
if (kvp.Value != null) StopCoroutine(kvp.Value);
|
||||||
|
}
|
||||||
|
activeCoroutines.Clear();
|
||||||
|
|
||||||
|
// 2. Process remaining items in queues immediately
|
||||||
|
foreach (var queue in actionQueues.Values)
|
||||||
|
{
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var action = queue.Dequeue();
|
||||||
|
RunImmediate(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Ensure all tweens are done (visuals snap to end)
|
||||||
|
Tween.CompleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunImmediate(IEnumerator enumerator)
|
||||||
|
{
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
var current = enumerator.Current;
|
||||||
|
if (current is IEnumerator nested)
|
||||||
|
{
|
||||||
|
RunImmediate(nested);
|
||||||
|
}
|
||||||
|
// Force complete any tweens that might have been started
|
||||||
|
Tween.CompleteAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ProcessActionQueue(string charName)
|
||||||
|
{
|
||||||
|
while (actionQueues.ContainsKey(charName) && actionQueues[charName].Count > 0)
|
||||||
|
{
|
||||||
|
IEnumerator action = actionQueues[charName].Dequeue();
|
||||||
|
yield return StartCoroutine(action);
|
||||||
|
}
|
||||||
|
activeCoroutines.Remove(charName);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================= [1. 등장 (Entry)] =========================
|
// ========================= [1. 등장 (Entry)] =========================
|
||||||
public void AddCharacter(string fileName, EntranceType type)
|
public void AddCharacter(string fileName, EntranceType type)
|
||||||
{
|
{
|
||||||
// 중복 방지
|
|
||||||
if (FindSlot(fileName) != null)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"이미 존재하는 캐릭터입니다: {fileName}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string path = "Images/Characters/" + fileName;
|
string path = "Images/Characters/" + fileName;
|
||||||
Sprite loadedSprite = Resources.Load<Sprite>(path);
|
Sprite loadedSprite = Resources.Load<Sprite>(path);
|
||||||
|
Debug.Log($"VisualNovelLayoutDirector :: AddCharacter: {fileName} ({path})");
|
||||||
|
|
||||||
if (loadedSprite != null)
|
if (loadedSprite != null)
|
||||||
{
|
{
|
||||||
StartCoroutine(SpawnRoutine(fileName, loadedSprite, type));
|
EnqueueAction(fileName, SpawnRoutine(fileName, loadedSprite, type));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -43,6 +103,11 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
|
|
||||||
private IEnumerator SpawnRoutine(string name, Sprite sprite, EntranceType type)
|
private IEnumerator SpawnRoutine(string name, Sprite sprite, EntranceType type)
|
||||||
{
|
{
|
||||||
|
if (FindSlot(name) != null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"이미 존재하는 캐릭터입니다: {name}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
// 1. 슬롯 생성
|
// 1. 슬롯 생성
|
||||||
GameObject newSlot = Instantiate(slotPrefab, characterPanel);
|
GameObject newSlot = Instantiate(slotPrefab, characterPanel);
|
||||||
|
|
||||||
@@ -51,9 +116,7 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
|
|
||||||
LayoutElement layoutElement = newSlot.GetComponent<LayoutElement>();
|
LayoutElement layoutElement = newSlot.GetComponent<LayoutElement>();
|
||||||
|
|
||||||
// [변경] MotionContainer 생성 및 계층 구조 변경
|
// Slot -> MotionContainer -> Image
|
||||||
// 기존: Slot -> Image
|
|
||||||
// 변경: Slot -> MotionContainer -> Image
|
|
||||||
GameObject motionContainer = new("MotionContainer");
|
GameObject motionContainer = new("MotionContainer");
|
||||||
RectTransform containerRect = motionContainer.AddComponent<RectTransform>();
|
RectTransform containerRect = motionContainer.AddComponent<RectTransform>();
|
||||||
motionContainer.transform.SetParent(newSlot.transform, false);
|
motionContainer.transform.SetParent(newSlot.transform, false);
|
||||||
@@ -71,82 +134,121 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
|
|
||||||
// 2. 초기화
|
// 2. 초기화
|
||||||
charImage.sprite = sprite;
|
charImage.sprite = sprite;
|
||||||
charImage.SetNativeSize();
|
FitImageToScreen(charImage);
|
||||||
layoutElement.preferredWidth = 0; layoutElement.minWidth = 0;
|
layoutElement.preferredWidth = 0; layoutElement.minWidth = 0;
|
||||||
|
|
||||||
// 3. 순서 재배치 (기존 로직 유지)
|
// 3. 순서 재배치
|
||||||
int totalCount = characterPanel.childCount;
|
int totalCount = characterPanel.childCount;
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case EntranceType.Left:
|
case EntranceType.Left:
|
||||||
|
case EntranceType.LeftRun:
|
||||||
case EntranceType.BottomLeft:
|
case EntranceType.BottomLeft:
|
||||||
newSlot.transform.SetSiblingIndex(0); break;
|
newSlot.transform.SetSiblingIndex(0); break;
|
||||||
case EntranceType.Right:
|
case EntranceType.Right:
|
||||||
|
case EntranceType.RightRun:
|
||||||
case EntranceType.BottomRight:
|
case EntranceType.BottomRight:
|
||||||
newSlot.transform.SetSiblingIndex(totalCount - 1); break;
|
newSlot.transform.SetSiblingIndex(totalCount - 1); break;
|
||||||
case EntranceType.Center:
|
case EntranceType.Center:
|
||||||
case EntranceType.Top:
|
case EntranceType.Top:
|
||||||
newSlot.transform.SetSiblingIndex((totalCount - 1) / 2); break;
|
// 삭제 중인 캐릭터를 제외하고 순서 계산
|
||||||
|
List<Transform> activeChildren = new();
|
||||||
|
for (int i = 0; i < totalCount; i++)
|
||||||
|
{
|
||||||
|
Transform child = characterPanel.GetChild(i);
|
||||||
|
if (child != newSlot.transform && !child.name.Contains("_Removing"))
|
||||||
|
{
|
||||||
|
activeChildren.Add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetIndex = activeChildren.Count / 2;
|
||||||
|
if (targetIndex < activeChildren.Count)
|
||||||
|
{
|
||||||
|
// activeChildren[targetIndex]의 현재 인덱스 앞에 배치
|
||||||
|
newSlot.transform.SetSiblingIndex(activeChildren[targetIndex].GetSiblingIndex());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 맨 뒤로
|
||||||
|
newSlot.transform.SetSiblingIndex(totalCount - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 위치 잡기 및 애니메이션
|
// 4. 위치 잡기 및 애니메이션
|
||||||
// [변경] 움직임은 MotionContainer가 담당
|
|
||||||
Vector2 startPos = GetDirectionVector(type);
|
Vector2 startPos = GetDirectionVector(type);
|
||||||
containerRect.anchoredPosition = startPos; // Image -> Container
|
containerRect.anchoredPosition = startPos;
|
||||||
charImage.color = new Color(1, 1, 1, 0);
|
charImage.color = new Color(1, 1, 1, 0);
|
||||||
|
|
||||||
yield return new WaitForEndOfFrame();
|
yield return new WaitForEndOfFrame();
|
||||||
|
|
||||||
Tween.Custom(layoutElement, 0f, charWidth, defaultDuration, (t, x) => t.preferredWidth = x, Ease.OutQuart);
|
// Tween 실행 및 대기
|
||||||
Tween.UIAnchoredPosition(containerRect, Vector2.zero, defaultDuration, Ease.OutQuart); // Image -> Container
|
switch (type)
|
||||||
Tween.Alpha(charImage, 1f, defaultDuration);
|
{
|
||||||
|
case EntranceType.LeftRun:
|
||||||
|
case EntranceType.RightRun:
|
||||||
|
StartCoroutine(PlayActionRoutine(newSlot.transform, ActionType.Run));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return Sequence.Create()
|
||||||
|
.Group(Tween.Custom(layoutElement, 0f, charWidth, defaultDuration, (t, x) => t.preferredWidth = x, Ease.OutQuart))
|
||||||
|
.Group(Tween.UIAnchoredPosition(containerRect, Vector2.zero, defaultDuration, Ease.OutQuart))
|
||||||
|
.Group(Tween.Alpha(charImage, 1f, defaultDuration))
|
||||||
|
.ToYieldInstruction();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================= [2. 퇴장 (Exit)] =========================
|
// ========================= [2. 퇴장 (Exit)] =========================
|
||||||
public void RemoveCharacter(string characterName, EntranceType exitTo)
|
public void RemoveCharacter(string characterName, EntranceType exitTo)
|
||||||
|
{
|
||||||
|
EnqueueAction(characterName, ExitRoutine(characterName, exitTo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ExitRoutine(string characterName, EntranceType exitTo)
|
||||||
{
|
{
|
||||||
Transform targetSlot = FindSlot(characterName);
|
Transform targetSlot = FindSlot(characterName);
|
||||||
|
|
||||||
if (targetSlot != null)
|
if (targetSlot == null)
|
||||||
{
|
|
||||||
StartCoroutine(ExitRoutine(targetSlot, exitTo));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"삭제 실패: '{characterName}' 캐릭터를 찾을 수 없습니다.");
|
Debug.LogWarning($"삭제 실패: '{characterName}' 캐릭터를 찾을 수 없습니다.");
|
||||||
}
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator ExitRoutine(Transform slotTransform, EntranceType exitTo)
|
// 중복 호출 방지를 위해 이름을 바꿔둠
|
||||||
{
|
targetSlot.name += "_Removing";
|
||||||
// 중복 호출 방지를 위해 이름을 바꿔둠 (빠르게 연타했을 때 에러 방지)
|
|
||||||
slotTransform.name += "_Removing";
|
|
||||||
|
|
||||||
LayoutElement layoutElement = slotTransform.GetComponent<LayoutElement>();
|
LayoutElement layoutElement = targetSlot.GetComponent<LayoutElement>();
|
||||||
|
|
||||||
// [변경] 계층 구조 반영
|
// [변경] 계층 구조 반영
|
||||||
Transform container = slotTransform.GetChild(0); // MotionContainer
|
Transform container = targetSlot.GetChild(0); // MotionContainer
|
||||||
RectTransform containerRect = container.GetComponent<RectTransform>();
|
RectTransform containerRect = container.GetComponent<RectTransform>();
|
||||||
Image charImage = container.GetChild(0).GetComponent<Image>(); // Image
|
Image charImage = container.GetChild(0).GetComponent<Image>(); // Image
|
||||||
|
|
||||||
Vector2 targetPos = GetDirectionVector(exitTo);
|
Vector2 targetPos = GetDirectionVector(exitTo);
|
||||||
|
|
||||||
// 이미지 날리기 & 투명화
|
switch (exitTo)
|
||||||
// [변경] 움직임은 Container, 투명도는 Image
|
{
|
||||||
Tween.UIAnchoredPosition(containerRect, targetPos, defaultDuration, Ease.OutQuart);
|
case EntranceType.LeftRun:
|
||||||
Tween.Alpha(charImage, 0f, defaultDuration * 0.8f);
|
case EntranceType.RightRun:
|
||||||
|
StartCoroutine(PlayActionRoutine(targetSlot, ActionType.Run));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 공간 닫기
|
// 이미지 날리기 & 투명화 & 공간 닫기 (동시 실행 및 대기)
|
||||||
yield return Tween.Custom(layoutElement, layoutElement.preferredWidth, 0f, defaultDuration,
|
yield return Sequence.Create()
|
||||||
(t, x) => t.preferredWidth = x, Ease.OutQuart).ToYieldInstruction();
|
.Group(Tween.UIAnchoredPosition(containerRect, targetPos, defaultDuration, Ease.OutQuart))
|
||||||
|
.Group(Tween.Alpha(charImage, 0f, defaultDuration * 0.8f))
|
||||||
|
.Group(Tween.Custom(layoutElement, layoutElement.preferredWidth, 0f, defaultDuration, (t, x) => t.preferredWidth = x, Ease.OutQuart))
|
||||||
|
.ToYieldInstruction();
|
||||||
|
|
||||||
Destroy(slotTransform.gameObject);
|
Destroy(targetSlot.gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================= [3. 액션 (Action)] =========================
|
// ========================= [3. 액션 (Action)] =========================
|
||||||
public void PlayAction(string characterName, ActionType action)
|
public void PlayAction(string characterName, ActionType action)
|
||||||
{
|
{
|
||||||
StartCoroutine(PlayActionRoutine(characterName, action));
|
EnqueueAction(characterName, PlayActionRoutine(characterName, action));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator PlayActionRoutine(string characterName, ActionType action)
|
private IEnumerator PlayActionRoutine(string characterName, ActionType action)
|
||||||
@@ -159,6 +261,11 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield return PlayActionRoutine(targetSlot, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator PlayActionRoutine(Transform targetSlot, ActionType action)
|
||||||
|
{
|
||||||
// [변경] 계층 구조 반영: Slot -> Container -> Image
|
// [변경] 계층 구조 반영: Slot -> Container -> Image
|
||||||
// 액션은 Image에만 적용 (Container는 이동 담당)
|
// 액션은 Image에만 적용 (Container는 이동 담당)
|
||||||
RectTransform targetImageRect = targetSlot.GetChild(0).GetChild(0).GetComponent<RectTransform>();
|
RectTransform targetImageRect = targetSlot.GetChild(0).GetChild(0).GetComponent<RectTransform>();
|
||||||
@@ -167,42 +274,56 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
Tween.StopAll(targetImageRect);
|
Tween.StopAll(targetImageRect);
|
||||||
targetImageRect.anchoredPosition = Vector2.zero;
|
targetImageRect.anchoredPosition = Vector2.zero;
|
||||||
|
|
||||||
|
Tween actionTween = default;
|
||||||
|
Sequence actionSequence = default;
|
||||||
|
bool isSequence = false;
|
||||||
|
|
||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case ActionType.Jump:
|
case ActionType.Jump:
|
||||||
// frequency: 2 (위로 갔다가 한두 번 띠용~ 하고 멈춤)
|
actionTween = Tween.PunchLocalPosition(targetImageRect, new Vector3(0, 100f, 0), 0.5f, frequency: 2);
|
||||||
Tween.PunchLocalPosition(targetImageRect, new Vector3(0, 100f, 0), 0.5f, frequency: 2);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ActionType.Shake:
|
case ActionType.Shake:
|
||||||
// 좌우 흔들기 (진동 횟수 10번)
|
actionTween = Tween.ShakeLocalPosition(targetImageRect, new Vector3(50f, 0, 0), 0.5f, frequency: 10);
|
||||||
Tween.ShakeLocalPosition(targetImageRect, new Vector3(50f, 0, 0), 0.5f, frequency: 10);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ActionType.ShakeHorizontal:
|
case ActionType.Run:
|
||||||
// 상하 흔들기 (진동 횟수 10번)
|
actionTween = Tween.PunchLocalPosition(targetImageRect, new Vector3(0, 50f, 0), 0.5f, frequency: 10);
|
||||||
Tween.PunchLocalPosition(targetImageRect, new Vector3(0, 50f, 0), 0.5f, frequency: 10);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ActionType.Nod:
|
case ActionType.Nod:
|
||||||
// (Sequence는 변경 없음)
|
isSequence = true;
|
||||||
Sequence.Create()
|
actionSequence = Sequence.Create()
|
||||||
.Chain(Tween.UIAnchoredPositionY(targetImageRect, -30f, 0.15f, Ease.OutQuad))
|
.Chain(Tween.UIAnchoredPositionY(targetImageRect, -30f, 0.15f, Ease.OutQuad))
|
||||||
.Chain(Tween.UIAnchoredPositionY(targetImageRect, 0f, 0.15f, Ease.InQuad));
|
.Chain(Tween.UIAnchoredPositionY(targetImageRect, 0f, 0.15f, Ease.InQuad));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ActionType.Punch:
|
case ActionType.Punch:
|
||||||
// frequency: 1 (커졌다가 딱 한 번 출렁이고 복구됨)
|
actionTween = Tween.PunchScale(targetImageRect, new Vector3(0.2f, 0.2f, 0), 0.4f, frequency: 1);
|
||||||
Tween.PunchScale(targetImageRect, new Vector3(0.2f, 0.2f, 0), 0.4f, frequency: 1);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSequence)
|
||||||
|
{
|
||||||
|
if (actionSequence.isAlive) yield return actionSequence.ToYieldInstruction();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (actionTween.isAlive) yield return actionTween.ToYieldInstruction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================= [4. 표정 변경 (Change Expression)] =========================
|
// ========================= [4. 표정 변경 (Change Expression)] =========================
|
||||||
public void ChangeExpression(string characterName, string spriteName)
|
public void ChangeExpression(string characterName, string spriteName)
|
||||||
|
{
|
||||||
|
EnqueueAction(characterName, ChangeExpressionRoutine(characterName, spriteName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ChangeExpressionRoutine(string characterName, string spriteName)
|
||||||
{
|
{
|
||||||
Transform targetSlot = FindSlot(characterName);
|
Transform targetSlot = FindSlot(characterName);
|
||||||
if (targetSlot == null) return;
|
if (targetSlot == null) yield break;
|
||||||
|
|
||||||
// [변경] 계층 구조 반영
|
// [변경] 계층 구조 반영
|
||||||
Image charImage = targetSlot.GetChild(0).GetChild(0).GetComponent<Image>();
|
Image charImage = targetSlot.GetChild(0).GetChild(0).GetComponent<Image>();
|
||||||
@@ -210,10 +331,7 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
|
|
||||||
if (newSprite != null)
|
if (newSprite != null)
|
||||||
{
|
{
|
||||||
// [수정] 기존 이미지를 복제하여 오버레이 생성
|
// 1. 마스크 컨테이너 생성
|
||||||
// Instantiate는 원본의 위치, 회전, 크기(Scale)를 그대로 복사하므로
|
|
||||||
// 별도로 위치나 스케일을 0/1로 초기화하면 안 됨 (좌우 반전된 캐릭터 등이 원상복구 되어버릴 수 있음)
|
|
||||||
// 1. 마스크 컨테이너 생성 (Softness 효과를 위해)
|
|
||||||
GameObject maskObj = new("MaskContainer");
|
GameObject maskObj = new("MaskContainer");
|
||||||
maskObj.transform.SetParent(charImage.transform, false); // [변경] 부모를 이미지로 설정하여 액션(Scale/Move) 동기화
|
maskObj.transform.SetParent(charImage.transform, false); // [변경] 부모를 이미지로 설정하여 액션(Scale/Move) 동기화
|
||||||
|
|
||||||
@@ -231,8 +349,7 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
RectMask2D rectMask = maskObj.AddComponent<RectMask2D>();
|
RectMask2D rectMask = maskObj.AddComponent<RectMask2D>();
|
||||||
rectMask.softness = new Vector2Int(0, (int)softnessOffset); // 세로 방향 Softness 설정
|
rectMask.softness = new Vector2Int(0, (int)softnessOffset); // 세로 방향 Softness 설정
|
||||||
|
|
||||||
// 2. 오버레이 이미지 생성 및 설정
|
// 2. 오버레이 이미지 생성
|
||||||
// [변경] Instantiate 대신 직접 생성 (이미지에 자식이 있을 경우 복제 방지)
|
|
||||||
GameObject overlayObj = new("ExpressionOverlay");
|
GameObject overlayObj = new("ExpressionOverlay");
|
||||||
overlayObj.transform.SetParent(maskObj.transform, false);
|
overlayObj.transform.SetParent(maskObj.transform, false);
|
||||||
|
|
||||||
@@ -251,7 +368,7 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
overlayRect.pivot = new Vector2(0.5f, 1f);
|
overlayRect.pivot = new Vector2(0.5f, 1f);
|
||||||
overlayRect.anchoredPosition = new Vector2(0, -softnessOffset); // 원위치 유지
|
overlayRect.anchoredPosition = new Vector2(0, -softnessOffset); // 원위치 유지
|
||||||
|
|
||||||
overlayImage.SetNativeSize();
|
FitImageToScreen(overlayImage);
|
||||||
|
|
||||||
// 렌더링 순서 보장 (마스크 컨테이너를 가장 앞으로)
|
// 렌더링 순서 보장 (마스크 컨테이너를 가장 앞으로)
|
||||||
maskObj.transform.SetAsLastSibling();
|
maskObj.transform.SetAsLastSibling();
|
||||||
@@ -260,15 +377,14 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
// 목표 높이: 캐릭터 이미지 높이 + 오프셋
|
// 목표 높이: 캐릭터 이미지 높이 + 오프셋
|
||||||
float targetHeight = overlayRect.sizeDelta.y + softnessOffset;
|
float targetHeight = overlayRect.sizeDelta.y + softnessOffset;
|
||||||
|
|
||||||
Tween.UISizeDelta(maskRect, new Vector2(currentWidth, targetHeight), 0.5f, Ease.OutQuart)
|
yield return Tween.UISizeDelta(maskRect, new Vector2(currentWidth, targetHeight), 0.5f, Ease.OutQuart)
|
||||||
.OnComplete(() =>
|
.ToYieldInstruction();
|
||||||
{
|
|
||||||
// 원본 교체 및 정리
|
// 원본 교체 및 정리
|
||||||
charImage.sprite = newSprite;
|
charImage.sprite = newSprite;
|
||||||
charImage.SetNativeSize();
|
FitImageToScreen(charImage);
|
||||||
|
|
||||||
Destroy(maskObj); // 마스크 컨테이너 삭제 (자식인 오버레이도 같이 삭제됨)
|
Destroy(maskObj);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -287,11 +403,32 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
{
|
{
|
||||||
EntranceType.Left => new Vector2(-moveDistance, 0),
|
EntranceType.Left or EntranceType.LeftRun => new Vector2(-moveDistance, 0),
|
||||||
EntranceType.Right => new Vector2(moveDistance, 0),
|
EntranceType.Right or EntranceType.RightRun => new Vector2(moveDistance, 0),
|
||||||
EntranceType.Center or EntranceType.BottomLeft or EntranceType.BottomRight => new Vector2(0, -moveDistance),
|
EntranceType.Center or EntranceType.BottomLeft or EntranceType.BottomRight => new Vector2(0, -moveDistance),
|
||||||
EntranceType.Top => new Vector2(0, moveDistance),
|
EntranceType.Top => new Vector2(0, moveDistance),
|
||||||
_ => Vector2.zero,
|
_ => Vector2.zero,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FitImageToScreen(Image image)
|
||||||
|
{
|
||||||
|
image.SetNativeSize();
|
||||||
|
|
||||||
|
Canvas rootCanvas = GetComponentInParent<Canvas>();
|
||||||
|
if (rootCanvas == null) return;
|
||||||
|
|
||||||
|
RectTransform canvasRect = rootCanvas.GetComponent<RectTransform>();
|
||||||
|
// 화면 높이의 95%를 넘지 않도록 설정
|
||||||
|
float maxHeight = canvasRect.rect.height * 0.95f;
|
||||||
|
|
||||||
|
if (image.rectTransform.rect.height > maxHeight)
|
||||||
|
{
|
||||||
|
float aspectRatio = image.rectTransform.rect.width / image.rectTransform.rect.height;
|
||||||
|
float newHeight = maxHeight;
|
||||||
|
float newWidth = newHeight * aspectRatio;
|
||||||
|
|
||||||
|
image.rectTransform.sizeDelta = new Vector2(newWidth, newHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user