mirror of
https://github.com/HoonTB/Project-AS.git
synced 2025-12-26 11:51:21 +09:00
feat: refactor character animation system with motion container
- Separate character hierarchy into Slot → MotionContainer → Image - Improve expression change with mask-based smooth transition - Replace scene-based navigation with label-based jump system - Add Top direction support and refactor direction vector logic - Minor code improvements (C# index operator, cleaner initialization)
This commit is contained in:
@@ -5,12 +5,12 @@ public class Script
|
|||||||
{
|
{
|
||||||
private List<ScriptAction> _actions;
|
private List<ScriptAction> _actions;
|
||||||
private int _currentIndex = -1;
|
private int _currentIndex = -1;
|
||||||
private Dictionary<string, int> _sceneMap = new Dictionary<string, int>();
|
private Dictionary<string, int> _labelMap = new();
|
||||||
|
|
||||||
public Script(List<ScriptAction> actions, Dictionary<string, int> sceneMap)
|
public Script(List<ScriptAction> actions, Dictionary<string, int> labelMap)
|
||||||
{
|
{
|
||||||
_actions = actions;
|
_actions = actions;
|
||||||
_sceneMap = sceneMap;
|
_labelMap = labelMap;
|
||||||
_currentIndex = -1;
|
_currentIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@ public class Script
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void JumpTo(string sceneName)
|
public void JumpTo(string labelName)
|
||||||
{
|
{
|
||||||
_currentIndex = _sceneMap[sceneName] - 1; // Continue() 호출 시 해당 인덱스가 되도록 -1
|
_currentIndex = _labelMap[labelName] - 1; // Continue() 호출 시 해당 인덱스가 되도록 -1
|
||||||
Debug.Log($"Script :: Jump to scene: {sceneName} (Index: {_currentIndex + 1})");
|
Debug.Log($"Script :: Jump to label: {labelName} (Index: {_currentIndex + 1})");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections;
|
|
||||||
using PrimeTween;
|
using PrimeTween;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -38,36 +37,10 @@ public class ScriptManager : MonoBehaviour
|
|||||||
dialogueText.SetText(" ");
|
dialogueText.SetText(" ");
|
||||||
dialogueText.ForceMeshUpdate(true);
|
dialogueText.ForceMeshUpdate(true);
|
||||||
|
|
||||||
StartCoroutine(TestAnim());
|
|
||||||
|
|
||||||
_currentScript = ScriptParser.Parse(scriptFile.text);
|
_currentScript = ScriptParser.Parse(scriptFile.text);
|
||||||
NextStep();
|
NextStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator TestAnim()
|
|
||||||
{
|
|
||||||
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()
|
void Update()
|
||||||
{
|
{
|
||||||
@@ -96,10 +69,10 @@ public class ScriptManager : MonoBehaviour
|
|||||||
|
|
||||||
private void ExecuteAction(ScriptAction action)
|
private void ExecuteAction(ScriptAction action)
|
||||||
{
|
{
|
||||||
if (action.Type == "scene")
|
if (action.Type == "label")
|
||||||
{
|
{
|
||||||
string sceneName = action.GetParam("name");
|
string labelName = action.GetParam("content");
|
||||||
Debug.Log($"ScriptManager :: Change Scene: {sceneName}");
|
Debug.Log($"ScriptManager :: Change Label: {labelName}");
|
||||||
NextStep();
|
NextStep();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,6 +83,61 @@ public class ScriptManager : MonoBehaviour
|
|||||||
NextStep();
|
NextStep();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (action.Type == "char")
|
||||||
|
{
|
||||||
|
string charFile = action.GetParam("img");
|
||||||
|
string charEntrance = action.GetParam("enter");
|
||||||
|
if (charEntrance == "") charEntrance = "center";
|
||||||
|
if (charEntrance.ToLower() == "center") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Center);
|
||||||
|
if (charEntrance.ToLower() == "left") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Left);
|
||||||
|
if (charEntrance.ToLower() == "right") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.Right);
|
||||||
|
if (charEntrance.ToLower() == "bottomleft") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.BottomLeft);
|
||||||
|
if (charEntrance.ToLower() == "bottomright") director.AddCharacter(charFile, VisualNovelLayoutDirector.EntranceType.BottomRight);
|
||||||
|
Debug.Log($"ScriptManager :: Character: {charFile}");
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "remove")
|
||||||
|
{
|
||||||
|
string charName = action.GetParam("target");
|
||||||
|
string exitType = action.GetParam("exit");
|
||||||
|
if (exitType == "") exitType = "center";
|
||||||
|
|
||||||
|
VisualNovelLayoutDirector.EntranceType type = VisualNovelLayoutDirector.EntranceType.Center;
|
||||||
|
if (exitType.ToLower() == "left") type = VisualNovelLayoutDirector.EntranceType.Left;
|
||||||
|
if (exitType.ToLower() == "right") type = VisualNovelLayoutDirector.EntranceType.Right;
|
||||||
|
if (exitType.ToLower() == "bottomleft") type = VisualNovelLayoutDirector.EntranceType.BottomLeft;
|
||||||
|
if (exitType.ToLower() == "bottomright") type = VisualNovelLayoutDirector.EntranceType.BottomRight;
|
||||||
|
if (exitType.ToLower() == "top") type = VisualNovelLayoutDirector.EntranceType.Top;
|
||||||
|
|
||||||
|
director.RemoveCharacter(charName, type);
|
||||||
|
Debug.Log($"ScriptManager :: Remove Character: {charName} to {exitType}");
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "action")
|
||||||
|
{
|
||||||
|
string charName = action.GetParam("target");
|
||||||
|
string charAnim = action.GetParam("anim");
|
||||||
|
if (charAnim == "") charAnim = "center";
|
||||||
|
if (charAnim.ToLower() == "jump") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Jump);
|
||||||
|
if (charAnim.ToLower() == "shake") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Shake);
|
||||||
|
if (charAnim.ToLower() == "shakehorizontal") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.ShakeHorizontal);
|
||||||
|
if (charAnim.ToLower() == "nod") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Nod);
|
||||||
|
if (charAnim.ToLower() == "punch") director.PlayAction(charName, VisualNovelLayoutDirector.ActionType.Punch);
|
||||||
|
Debug.Log($"ScriptManager :: Action: {charName} {charAnim}");
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "expr")
|
||||||
|
{
|
||||||
|
string charName = action.GetParam("target");
|
||||||
|
string charExpr = action.GetParam("expr");
|
||||||
|
director.ChangeExpression(charName, charExpr);
|
||||||
|
Debug.Log($"ScriptManager :: Expression: {charName} {charExpr}");
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (action.Type == "spk")
|
if (action.Type == "spk")
|
||||||
{
|
{
|
||||||
string speaker = action.GetParam("name");
|
string speaker = action.GetParam("name");
|
||||||
@@ -129,8 +157,8 @@ public class ScriptManager : MonoBehaviour
|
|||||||
}
|
}
|
||||||
if (action.Type == "goto")
|
if (action.Type == "goto")
|
||||||
{
|
{
|
||||||
string targetScene = action.GetParam("scene");
|
string targetLabel = action.GetParam("content");
|
||||||
_currentScript.JumpTo(targetScene);
|
_currentScript.JumpTo(targetLabel);
|
||||||
NextStep();
|
NextStep();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -182,9 +210,11 @@ public class ScriptManager : MonoBehaviour
|
|||||||
|
|
||||||
private bool IsPointerOverInteractiveUI()
|
private bool IsPointerOverInteractiveUI()
|
||||||
{
|
{
|
||||||
PointerEventData eventData = new PointerEventData(EventSystem.current);
|
PointerEventData eventData = new(EventSystem.current)
|
||||||
eventData.position = Input.mousePosition;
|
{
|
||||||
List<RaycastResult> results = new List<RaycastResult>();
|
position = Input.mousePosition
|
||||||
|
};
|
||||||
|
List<RaycastResult> results = new();
|
||||||
EventSystem.current.RaycastAll(eventData, results);
|
EventSystem.current.RaycastAll(eventData, results);
|
||||||
|
|
||||||
foreach (RaycastResult result in results)
|
foreach (RaycastResult result in results)
|
||||||
@@ -238,7 +268,7 @@ public class ScriptManager : MonoBehaviour
|
|||||||
|
|
||||||
if (linkName == "shake")
|
if (linkName == "shake")
|
||||||
{
|
{
|
||||||
Vector3 offset = new Vector3(
|
Vector3 offset = new(
|
||||||
Random.Range(-shakeAmount, shakeAmount),
|
Random.Range(-shakeAmount, shakeAmount),
|
||||||
Random.Range(-shakeAmount, shakeAmount)
|
Random.Range(-shakeAmount, shakeAmount)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
public class ScriptParser
|
public class ScriptParser
|
||||||
{
|
{
|
||||||
private static readonly Regex TagRegex = new Regex(@"^\[(\w+)(?:\s+(.*))?\]$");
|
private static readonly Regex TagRegex = new(@"^\[(\w+)(?:\s+(.*))?\]$");
|
||||||
private static readonly Regex AttrRegex = new Regex(@"(\w+)=(""[^""]*""|'[^']*'|[^ \t\]]+)");
|
private static readonly Regex AttrRegex = new(@"(\w+)=(""[^""]*""|'[^']*'|[^ \t\]]+)");
|
||||||
private static readonly Regex ChoiceOptionRegex = new Regex(@"^\*\s*(.+?)\s*>\s*(.+)$");
|
private static readonly Regex ChoiceOptionRegex = new(@"^\*\s*(.+?)\s*>\s*(.+)$");
|
||||||
|
|
||||||
public static Script Parse(string text)
|
public static Script Parse(string text)
|
||||||
{
|
{
|
||||||
List<ScriptAction> actions = new();
|
List<ScriptAction> actions = new();
|
||||||
Dictionary<string, int> sceneMap = new();
|
Dictionary<string, int> labelMap = new();
|
||||||
|
|
||||||
ScriptAction lastChoice = null;
|
ScriptAction lastChoice = null;
|
||||||
|
|
||||||
@@ -31,16 +32,19 @@ public class ScriptParser
|
|||||||
{
|
{
|
||||||
string tagName = tagMatch.Groups[1].Value;
|
string tagName = tagMatch.Groups[1].Value;
|
||||||
string attrString = tagMatch.Groups[2].Value;
|
string attrString = tagMatch.Groups[2].Value;
|
||||||
|
Debug.Log($"ScriptParser :: Tag: {tagName} {attrString}");
|
||||||
|
|
||||||
var scriptAction = new ScriptAction { Type = tagName };
|
var scriptAction = new ScriptAction { Type = tagName };
|
||||||
ParseAttributes(attrString, scriptAction.Params);
|
|
||||||
|
|
||||||
if (tagName == "scene")
|
if (!attrString.Contains("=")) scriptAction.Params["content"] = attrString;
|
||||||
|
else ParseAttributes(attrString, scriptAction.Params);
|
||||||
|
|
||||||
|
if (tagName == "label")
|
||||||
{
|
{
|
||||||
string sceneName = scriptAction.GetParam("name");
|
string label = scriptAction.GetParam("content");
|
||||||
if (!string.IsNullOrEmpty(sceneName) && !sceneMap.ContainsKey(sceneName))
|
if (!string.IsNullOrEmpty(label) && !labelMap.ContainsKey(label))
|
||||||
{
|
{
|
||||||
sceneMap[sceneName] = actions.Count;
|
labelMap[label] = actions.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +75,7 @@ public class ScriptParser
|
|||||||
actions.Add(new ScriptAction { Type = "msg", Params = { { "content", line } } });
|
actions.Add(new ScriptAction { Type = "msg", Params = { { "content", line } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Script(actions, sceneMap);
|
return new Script(actions, labelMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ParseAttributes(string attrString, Dictionary<string, object> paramDict)
|
private static void ParseAttributes(string attrString, Dictionary<string, object> paramDict)
|
||||||
@@ -85,7 +89,7 @@ public class ScriptParser
|
|||||||
string rawValue = m.Groups[2].Value;
|
string rawValue = m.Groups[2].Value;
|
||||||
|
|
||||||
if (rawValue.Length >= 2 && (rawValue.StartsWith("\"") || rawValue.StartsWith("'")))
|
if (rawValue.Length >= 2 && (rawValue.StartsWith("\"") || rawValue.StartsWith("'")))
|
||||||
rawValue = rawValue.Substring(1, rawValue.Length - 2);
|
rawValue = rawValue[1..^1];
|
||||||
|
|
||||||
paramDict[key] = rawValue;
|
paramDict[key] = rawValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using PrimeTween;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
using PrimeTween;
|
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
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 }
|
||||||
public enum ActionType { Jump, Shake, Nod, Punch }
|
public enum ActionType { Jump, Shake, Nod, Punch, ShakeHorizontal }
|
||||||
|
|
||||||
[Header("UI 연결")]
|
[Header("UI 연결")]
|
||||||
public Transform characterPanel;
|
public Transform characterPanel;
|
||||||
@@ -18,6 +19,8 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
public float defaultDuration = 0.5f;
|
public float defaultDuration = 0.5f;
|
||||||
public float moveDistance = 800f;
|
public float moveDistance = 800f;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ========================= [1. 등장 (Entry)] =========================
|
// ========================= [1. 등장 (Entry)] =========================
|
||||||
public void AddCharacter(string fileName, EntranceType type)
|
public void AddCharacter(string fileName, EntranceType type)
|
||||||
{
|
{
|
||||||
@@ -50,7 +53,24 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
newSlot.name = name;
|
newSlot.name = name;
|
||||||
|
|
||||||
LayoutElement layoutElement = newSlot.GetComponent<LayoutElement>();
|
LayoutElement layoutElement = newSlot.GetComponent<LayoutElement>();
|
||||||
Image charImage = newSlot.transform.GetChild(0).GetComponent<Image>();
|
|
||||||
|
// [변경] MotionContainer 생성 및 계층 구조 변경
|
||||||
|
// 기존: Slot -> Image
|
||||||
|
// 변경: Slot -> MotionContainer -> Image
|
||||||
|
GameObject motionContainer = new("MotionContainer");
|
||||||
|
RectTransform containerRect = motionContainer.AddComponent<RectTransform>();
|
||||||
|
motionContainer.transform.SetParent(newSlot.transform, false);
|
||||||
|
|
||||||
|
// Container 설정 (부모 꽉 채우기)
|
||||||
|
containerRect.anchorMin = Vector2.zero;
|
||||||
|
containerRect.anchorMax = Vector2.one;
|
||||||
|
containerRect.sizeDelta = Vector2.zero;
|
||||||
|
|
||||||
|
// 기존 Image를 Container 자식으로 이동
|
||||||
|
Transform imageTransform = newSlot.transform.GetChild(0); // Prefab의 첫 번째 자식이 Image라고 가정
|
||||||
|
imageTransform.SetParent(motionContainer.transform, false);
|
||||||
|
|
||||||
|
Image charImage = imageTransform.GetComponent<Image>();
|
||||||
|
|
||||||
// 2. 초기화
|
// 2. 초기화
|
||||||
charImage.sprite = sprite;
|
charImage.sprite = sprite;
|
||||||
@@ -73,14 +93,15 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 위치 잡기 및 애니메이션
|
// 4. 위치 잡기 및 애니메이션
|
||||||
|
// [변경] 움직임은 MotionContainer가 담당
|
||||||
Vector2 startPos = GetDirectionVector(type);
|
Vector2 startPos = GetDirectionVector(type);
|
||||||
charImage.rectTransform.anchoredPosition = startPos;
|
containerRect.anchoredPosition = startPos; // Image -> Container
|
||||||
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.Custom(layoutElement, 0f, charWidth, defaultDuration, (t, x) => t.preferredWidth = x, Ease.OutQuart);
|
||||||
Tween.UIAnchoredPosition(charImage.rectTransform, Vector2.zero, defaultDuration, Ease.OutQuart);
|
Tween.UIAnchoredPosition(containerRect, Vector2.zero, defaultDuration, Ease.OutQuart); // Image -> Container
|
||||||
Tween.Alpha(charImage, 1f, defaultDuration);
|
Tween.Alpha(charImage, 1f, defaultDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +123,20 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
private IEnumerator ExitRoutine(Transform slotTransform, EntranceType exitTo)
|
private IEnumerator ExitRoutine(Transform slotTransform, EntranceType exitTo)
|
||||||
{
|
{
|
||||||
// 중복 호출 방지를 위해 이름을 바꿔둠 (빠르게 연타했을 때 에러 방지)
|
// 중복 호출 방지를 위해 이름을 바꿔둠 (빠르게 연타했을 때 에러 방지)
|
||||||
slotTransform.name = slotTransform.name + "_Removing";
|
slotTransform.name += "_Removing";
|
||||||
|
|
||||||
LayoutElement layoutElement = slotTransform.GetComponent<LayoutElement>();
|
LayoutElement layoutElement = slotTransform.GetComponent<LayoutElement>();
|
||||||
Image charImage = slotTransform.GetChild(0).GetComponent<Image>();
|
|
||||||
|
// [변경] 계층 구조 반영
|
||||||
|
Transform container = slotTransform.GetChild(0); // MotionContainer
|
||||||
|
RectTransform containerRect = container.GetComponent<RectTransform>();
|
||||||
|
Image charImage = container.GetChild(0).GetComponent<Image>(); // Image
|
||||||
|
|
||||||
Vector2 targetPos = GetDirectionVector(exitTo);
|
Vector2 targetPos = GetDirectionVector(exitTo);
|
||||||
|
|
||||||
// 이미지 날리기 & 투명화
|
// 이미지 날리기 & 투명화
|
||||||
Tween.UIAnchoredPosition(charImage.rectTransform, targetPos, defaultDuration, Ease.OutQuart);
|
// [변경] 움직임은 Container, 투명도는 Image
|
||||||
|
Tween.UIAnchoredPosition(containerRect, targetPos, defaultDuration, Ease.OutQuart);
|
||||||
Tween.Alpha(charImage, 0f, defaultDuration * 0.8f);
|
Tween.Alpha(charImage, 0f, defaultDuration * 0.8f);
|
||||||
|
|
||||||
// 공간 닫기
|
// 공간 닫기
|
||||||
@@ -121,16 +148,23 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
|
|
||||||
// ========================= [3. 액션 (Action)] =========================
|
// ========================= [3. 액션 (Action)] =========================
|
||||||
public void PlayAction(string characterName, ActionType action)
|
public void PlayAction(string characterName, ActionType action)
|
||||||
|
{
|
||||||
|
StartCoroutine(PlayActionRoutine(characterName, action));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator PlayActionRoutine(string characterName, ActionType action)
|
||||||
{
|
{
|
||||||
Transform targetSlot = FindSlot(characterName);
|
Transform targetSlot = FindSlot(characterName);
|
||||||
|
|
||||||
if (targetSlot == null)
|
if (targetSlot == null)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"액션 실패: '{characterName}' 캐릭터를 찾을 수 없습니다.");
|
Debug.LogWarning($"액션 실패: '{characterName}' 캐릭터를 찾을 수 없습니다.");
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
RectTransform targetImageRect = targetSlot.GetChild(0).GetComponent<RectTransform>();
|
// [변경] 계층 구조 반영: Slot -> Container -> Image
|
||||||
|
// 액션은 Image에만 적용 (Container는 이동 담당)
|
||||||
|
RectTransform targetImageRect = targetSlot.GetChild(0).GetChild(0).GetComponent<RectTransform>();
|
||||||
|
|
||||||
// 기존 애니메이션 정지 및 초기화
|
// 기존 애니메이션 정지 및 초기화
|
||||||
Tween.StopAll(targetImageRect);
|
Tween.StopAll(targetImageRect);
|
||||||
@@ -148,6 +182,11 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
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:
|
||||||
|
// 상하 흔들기 (진동 횟수 10번)
|
||||||
|
Tween.PunchLocalPosition(targetImageRect, new Vector3(0, 50f, 0), 0.5f, frequency: 10);
|
||||||
|
break;
|
||||||
|
|
||||||
case ActionType.Nod:
|
case ActionType.Nod:
|
||||||
// (Sequence는 변경 없음)
|
// (Sequence는 변경 없음)
|
||||||
Sequence.Create()
|
Sequence.Create()
|
||||||
@@ -162,6 +201,84 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================= [4. 표정 변경 (Change Expression)] =========================
|
||||||
|
public void ChangeExpression(string characterName, string spriteName)
|
||||||
|
{
|
||||||
|
Transform targetSlot = FindSlot(characterName);
|
||||||
|
if (targetSlot == null) return;
|
||||||
|
|
||||||
|
// [변경] 계층 구조 반영
|
||||||
|
Image charImage = targetSlot.GetChild(0).GetChild(0).GetComponent<Image>();
|
||||||
|
Sprite newSprite = Resources.Load<Sprite>("Images/Characters/" + spriteName);
|
||||||
|
|
||||||
|
if (newSprite != null)
|
||||||
|
{
|
||||||
|
// [수정] 기존 이미지를 복제하여 오버레이 생성
|
||||||
|
// Instantiate는 원본의 위치, 회전, 크기(Scale)를 그대로 복사하므로
|
||||||
|
// 별도로 위치나 스케일을 0/1로 초기화하면 안 됨 (좌우 반전된 캐릭터 등이 원상복구 되어버릴 수 있음)
|
||||||
|
// 1. 마스크 컨테이너 생성 (Softness 효과를 위해)
|
||||||
|
GameObject maskObj = new("MaskContainer");
|
||||||
|
maskObj.transform.SetParent(charImage.transform, false); // [변경] 부모를 이미지로 설정하여 액션(Scale/Move) 동기화
|
||||||
|
|
||||||
|
RectTransform maskRect = maskObj.AddComponent<RectTransform>();
|
||||||
|
maskRect.anchorMin = new Vector2(0.5f, 1f); // Top Center
|
||||||
|
maskRect.anchorMax = new Vector2(0.5f, 1f);
|
||||||
|
maskRect.pivot = new Vector2(0.5f, 1f);
|
||||||
|
|
||||||
|
// 마스크 영역을 위로 올려서 상단 Softness가 이미지에 영향을 주지 않도록 함
|
||||||
|
float softnessOffset = 100f;
|
||||||
|
float currentWidth = charImage.rectTransform.rect.width; // [변경] 실제 이미지 너비 사용
|
||||||
|
maskRect.anchoredPosition = new Vector2(0, softnessOffset);
|
||||||
|
maskRect.sizeDelta = new Vector2(currentWidth, 0); // 너비는 캐릭터 폭, 높이는 0부터 시작
|
||||||
|
|
||||||
|
RectMask2D rectMask = maskObj.AddComponent<RectMask2D>();
|
||||||
|
rectMask.softness = new Vector2Int(0, (int)softnessOffset); // 세로 방향 Softness 설정
|
||||||
|
|
||||||
|
// 2. 오버레이 이미지 생성 및 설정
|
||||||
|
// [변경] Instantiate 대신 직접 생성 (이미지에 자식이 있을 경우 복제 방지)
|
||||||
|
GameObject overlayObj = new("ExpressionOverlay");
|
||||||
|
overlayObj.transform.SetParent(maskObj.transform, false);
|
||||||
|
|
||||||
|
Image overlayImage = overlayObj.AddComponent<Image>();
|
||||||
|
overlayImage.sprite = newSprite;
|
||||||
|
overlayImage.color = charImage.color;
|
||||||
|
overlayImage.material = charImage.material;
|
||||||
|
overlayImage.raycastTarget = charImage.raycastTarget;
|
||||||
|
overlayImage.type = Image.Type.Simple;
|
||||||
|
overlayImage.preserveAspect = true;
|
||||||
|
|
||||||
|
// 오버레이 위치 보정 (마스크가 올라간 만큼 내려줌)
|
||||||
|
RectTransform overlayRect = overlayImage.rectTransform;
|
||||||
|
overlayRect.anchorMin = new Vector2(0.5f, 1f);
|
||||||
|
overlayRect.anchorMax = new Vector2(0.5f, 1f);
|
||||||
|
overlayRect.pivot = new Vector2(0.5f, 1f);
|
||||||
|
overlayRect.anchoredPosition = new Vector2(0, -softnessOffset); // 원위치 유지
|
||||||
|
|
||||||
|
overlayImage.SetNativeSize();
|
||||||
|
|
||||||
|
// 렌더링 순서 보장 (마스크 컨테이너를 가장 앞으로)
|
||||||
|
maskObj.transform.SetAsLastSibling();
|
||||||
|
|
||||||
|
// 3. 애니메이션 실행 (마스크 높이를 키워서 이미지를 드러냄)
|
||||||
|
// 목표 높이: 캐릭터 이미지 높이 + 오프셋
|
||||||
|
float targetHeight = overlayRect.sizeDelta.y + softnessOffset;
|
||||||
|
|
||||||
|
Tween.UISizeDelta(maskRect, new Vector2(currentWidth, targetHeight), 0.5f, Ease.OutQuart)
|
||||||
|
.OnComplete(() =>
|
||||||
|
{
|
||||||
|
// 원본 교체 및 정리
|
||||||
|
charImage.sprite = newSprite;
|
||||||
|
charImage.SetNativeSize();
|
||||||
|
|
||||||
|
Destroy(maskObj); // 마스크 컨테이너 삭제 (자식인 오버레이도 같이 삭제됨)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError($"표정 스프라이트를 찾을 수 없습니다: {spriteName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// [Helper] 이름으로 슬롯 찾기
|
// [Helper] 이름으로 슬롯 찾기
|
||||||
private Transform FindSlot(string name)
|
private Transform FindSlot(string name)
|
||||||
{
|
{
|
||||||
@@ -171,20 +288,13 @@ public class VisualNovelLayoutDirector : MonoBehaviour
|
|||||||
|
|
||||||
private Vector2 GetDirectionVector(EntranceType type)
|
private Vector2 GetDirectionVector(EntranceType type)
|
||||||
{
|
{
|
||||||
switch (type)
|
return type switch
|
||||||
{
|
{
|
||||||
case EntranceType.Left:
|
EntranceType.Left => new Vector2(-moveDistance, 0),
|
||||||
return new Vector2(-moveDistance, 0);
|
EntranceType.Right => new Vector2(moveDistance, 0),
|
||||||
case EntranceType.Right:
|
EntranceType.Center or EntranceType.BottomLeft or EntranceType.BottomRight => new Vector2(0, -moveDistance),
|
||||||
return new Vector2(moveDistance, 0);
|
EntranceType.Top => new Vector2(0, moveDistance),
|
||||||
case EntranceType.Center:
|
_ => Vector2.zero,
|
||||||
case EntranceType.BottomLeft:
|
};
|
||||||
case EntranceType.BottomRight:
|
|
||||||
return new Vector2(0, -moveDistance);
|
|
||||||
case EntranceType.Top:
|
|
||||||
return new Vector2(0, moveDistance);
|
|
||||||
default:
|
|
||||||
return Vector2.zero;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user