mirror of
https://github.com/HoonTB/Project-AS.git
synced 2025-12-26 11:51:21 +09:00
feat: add ScriptManager for VN Gaming
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -19,7 +19,14 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
# Assets (Temp Setting)
|
# Assets (Temp Setting)
|
||||||
/[Aa]ssets/_MAIN
|
/[Aa]ssets/_MAIN/Audio
|
||||||
|
/[Aa]ssets/_MAIN/Audio.meta
|
||||||
|
/[Aa]ssets/_MAIN/Graphics
|
||||||
|
/[Aa]ssets/_MAIN/Graphics.meta
|
||||||
|
/[Aa]ssets/_MAIN/Resources
|
||||||
|
/[Aa]ssets/_MAIN/Resources.meta
|
||||||
|
/[Aa]ssets/_MAIN/Scenes
|
||||||
|
/[Aa]ssets/_MAIN/Scenes.meta
|
||||||
|
|
||||||
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
|
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
|
||||||
*.blend1
|
*.blend1
|
||||||
|
|||||||
8
Assets/_MAIN/Scripts.meta
Normal file
8
Assets/_MAIN/Scripts.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 92d6b7f47c91f3d46ae3984d674a216f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/_MAIN/Scripts/Core.meta
Normal file
8
Assets/_MAIN/Scripts/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3e4640df73c0c2348b4923873fe9ba55
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
45
Assets/_MAIN/Scripts/Core/Script.cs
Normal file
45
Assets/_MAIN/Scripts/Core/Script.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class Script
|
||||||
|
{
|
||||||
|
private List<ScriptAction> _actions;
|
||||||
|
private int _currentIndex = -1;
|
||||||
|
private Dictionary<string, int> _sceneMap = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
public Script(List<ScriptAction> actions, Dictionary<string, int> sceneMap)
|
||||||
|
{
|
||||||
|
_actions = actions;
|
||||||
|
_sceneMap = sceneMap;
|
||||||
|
_currentIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasNextAction()
|
||||||
|
{
|
||||||
|
return _currentIndex < _actions.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptAction Continue()
|
||||||
|
{
|
||||||
|
if (!HasNextAction())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
_currentIndex++;
|
||||||
|
ScriptAction currentAction = _actions[_currentIndex];
|
||||||
|
|
||||||
|
return currentAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptAction GetCurrent()
|
||||||
|
{
|
||||||
|
if (_currentIndex >= 0 && _currentIndex < _actions.Count)
|
||||||
|
return _actions[_currentIndex];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void JumpTo(string sceneName)
|
||||||
|
{
|
||||||
|
_currentIndex = _sceneMap[sceneName] - 1; // Continue() 호출 시 해당 인덱스가 되도록 -1
|
||||||
|
Debug.Log($"Script :: Jump to scene: {sceneName} (Index: {_currentIndex + 1})");
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_MAIN/Scripts/Core/Script.cs.meta
Normal file
2
Assets/_MAIN/Scripts/Core/Script.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3f1c6b3f22568a45932d3414362bf0b
|
||||||
13
Assets/_MAIN/Scripts/Core/ScriptAction.cs
Normal file
13
Assets/_MAIN/Scripts/Core/ScriptAction.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class ScriptAction
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public Dictionary<string, object> Params { get; set; } = new Dictionary<string, object>();
|
||||||
|
public List<Dictionary<string, string>> Choices { get; set; }
|
||||||
|
|
||||||
|
public string GetParam(string key, string defaultValue = "")
|
||||||
|
{
|
||||||
|
return Params.ContainsKey(key) ? Params[key].ToString() : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_MAIN/Scripts/Core/ScriptAction.cs.meta
Normal file
2
Assets/_MAIN/Scripts/Core/ScriptAction.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea31a181cac32d34c9511c7c988fea57
|
||||||
213
Assets/_MAIN/Scripts/Core/ScriptManager.cs
Normal file
213
Assets/_MAIN/Scripts/Core/ScriptManager.cs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using PrimeTween;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
public class ScriptManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField]
|
||||||
|
TextAsset scriptFile;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
TextMeshProUGUI speakerText;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
TextMeshProUGUI dialogueText;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
private GameObject choiceButtonPrefab;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
private Transform choiceButtonContainer;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
float charsPerSecond = 45f;
|
||||||
|
|
||||||
|
private readonly float shakeAmount = 1.1f;
|
||||||
|
private bool isChoiceAvailable = false;
|
||||||
|
private Tween dialogueTween;
|
||||||
|
private Script _currentScript;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
speakerText.SetText(" ");
|
||||||
|
speakerText.ForceMeshUpdate(true);
|
||||||
|
dialogueText.SetText(" ");
|
||||||
|
dialogueText.ForceMeshUpdate(true);
|
||||||
|
|
||||||
|
_currentScript = ScriptParser.Parse(scriptFile.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
DisplayEffects(dialogueText);
|
||||||
|
if (!isChoiceAvailable && !IsPointerOverInteractiveUI() && (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space)))
|
||||||
|
{
|
||||||
|
if (dialogueTween.isAlive)
|
||||||
|
dialogueTween.Complete();
|
||||||
|
else
|
||||||
|
NextStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DebugReload()
|
||||||
|
{
|
||||||
|
speakerText.SetText(" ");
|
||||||
|
speakerText.ForceMeshUpdate(true);
|
||||||
|
dialogueText.SetText(" ");
|
||||||
|
dialogueText.ForceMeshUpdate(true);
|
||||||
|
|
||||||
|
_currentScript = ScriptParser.Parse(scriptFile.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NextStep()
|
||||||
|
{
|
||||||
|
if (_currentScript.HasNextAction())
|
||||||
|
{
|
||||||
|
ScriptAction action = _currentScript.Continue();
|
||||||
|
ExecuteAction(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log("ScriptManager :: End of Script");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteAction(ScriptAction action)
|
||||||
|
{
|
||||||
|
if (action.Type == "scene")
|
||||||
|
{
|
||||||
|
string sceneName = action.GetParam("name");
|
||||||
|
Debug.Log($"ScriptManager :: Change Scene: {sceneName}");
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "bg")
|
||||||
|
{
|
||||||
|
string bgFile = action.GetParam("file");
|
||||||
|
Debug.Log($"ScriptManager :: Change Background: {bgFile}");
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "spk")
|
||||||
|
{
|
||||||
|
string speaker = action.GetParam("name");
|
||||||
|
speakerText.SetText(speaker);
|
||||||
|
speakerText.ForceMeshUpdate(true);
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "msg")
|
||||||
|
{
|
||||||
|
string dialogue = action.GetParam("content");
|
||||||
|
DisplayDialogue(dialogue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "goto")
|
||||||
|
{
|
||||||
|
string targetScene = action.GetParam("scene");
|
||||||
|
_currentScript.JumpTo(targetScene);
|
||||||
|
NextStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.Type == "choices")
|
||||||
|
{
|
||||||
|
Debug.Log("ScriptManager :: Show Choices");
|
||||||
|
isChoiceAvailable = true;
|
||||||
|
|
||||||
|
foreach (var choice in action.Choices)
|
||||||
|
{
|
||||||
|
string text = choice["content"];
|
||||||
|
string target = choice["goto"];
|
||||||
|
GameObject buttonObj = Instantiate(choiceButtonPrefab, choiceButtonContainer);
|
||||||
|
buttonObj.GetComponentInChildren<TextMeshProUGUI>().text = text;
|
||||||
|
buttonObj
|
||||||
|
.GetComponent<Button>()
|
||||||
|
.onClick.AddListener(() =>
|
||||||
|
{
|
||||||
|
foreach (Transform child in choiceButtonContainer)
|
||||||
|
Destroy(child.gameObject);
|
||||||
|
|
||||||
|
isChoiceAvailable = false;
|
||||||
|
_currentScript.JumpTo(target);
|
||||||
|
NextStep();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPointerOverInteractiveUI()
|
||||||
|
{
|
||||||
|
PointerEventData eventData = new PointerEventData(EventSystem.current);
|
||||||
|
eventData.position = Input.mousePosition;
|
||||||
|
List<RaycastResult> results = new List<RaycastResult>();
|
||||||
|
EventSystem.current.RaycastAll(eventData, results);
|
||||||
|
|
||||||
|
foreach (RaycastResult result in results)
|
||||||
|
if (result.gameObject.GetComponent<Selectable>() != null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisplayDialogue(string text)
|
||||||
|
{
|
||||||
|
// Unity 내부 최적화로 인해 줄이 바뀔 시 LinkInfo 배열이 초기화되지 않음.
|
||||||
|
// 따라서 수동으로 초기화를 수행.
|
||||||
|
dialogueText.textInfo.linkInfo = new TMP_LinkInfo[0];
|
||||||
|
dialogueText.SetText(text);
|
||||||
|
dialogueText.ForceMeshUpdate(true);
|
||||||
|
dialogueText.maxVisibleCharacters = 0;
|
||||||
|
|
||||||
|
dialogueTween = Tween.Custom(
|
||||||
|
startValue: 0f,
|
||||||
|
endValue: dialogueText.textInfo.characterCount,
|
||||||
|
duration: dialogueText.textInfo.characterCount / charsPerSecond,
|
||||||
|
onValueChange: x => dialogueText.maxVisibleCharacters = Mathf.RoundToInt(x),
|
||||||
|
ease: Ease.Linear
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisplayEffects(TextMeshProUGUI text)
|
||||||
|
{
|
||||||
|
text.ForceMeshUpdate(true);
|
||||||
|
|
||||||
|
TMP_TextInfo textInfo = text.textInfo;
|
||||||
|
TMP_LinkInfo[] linkInfo = textInfo.linkInfo;
|
||||||
|
|
||||||
|
Mesh mesh = text.mesh;
|
||||||
|
Vector3[] vertices = mesh.vertices;
|
||||||
|
|
||||||
|
foreach (var link in linkInfo)
|
||||||
|
{
|
||||||
|
string linkName = link.GetLinkID();
|
||||||
|
int start = link.linkTextfirstCharacterIndex;
|
||||||
|
int end = link.linkTextfirstCharacterIndex + link.linkTextLength;
|
||||||
|
|
||||||
|
for (var i = start; i < end; i++)
|
||||||
|
{
|
||||||
|
TMP_CharacterInfo c = textInfo.characterInfo[i];
|
||||||
|
int idx = c.vertexIndex;
|
||||||
|
|
||||||
|
if (!c.isVisible)
|
||||||
|
continue; // 공백은 VertexIndex 0 Return -> Visible이 안 되므로
|
||||||
|
|
||||||
|
if (linkName == "shake")
|
||||||
|
{
|
||||||
|
Vector3 offset = new Vector3(
|
||||||
|
Random.Range(-shakeAmount, shakeAmount),
|
||||||
|
Random.Range(-shakeAmount, shakeAmount)
|
||||||
|
);
|
||||||
|
for (byte j = 0; j < 4; j++)
|
||||||
|
vertices[idx + j] += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.vertices = vertices;
|
||||||
|
text.canvasRenderer.SetMesh(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_MAIN/Scripts/Core/ScriptManager.cs.meta
Normal file
2
Assets/_MAIN/Scripts/Core/ScriptManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bc6d6c3934879c149b42862a8626e5e9
|
||||||
93
Assets/_MAIN/Scripts/Core/ScriptParser.cs
Normal file
93
Assets/_MAIN/Scripts/Core/ScriptParser.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
public class ScriptParser
|
||||||
|
{
|
||||||
|
private static readonly Regex TagRegex = new Regex(@"^\[(\w+)(?:\s+(.*))?\]$");
|
||||||
|
private static readonly Regex AttrRegex = new Regex(@"(\w+)=(""[^""]*""|'[^']*'|[^ \t\]]+)");
|
||||||
|
private static readonly Regex ChoiceOptionRegex = new Regex(@"^\*\s*(.+?)\s*>\s*(.+)$");
|
||||||
|
|
||||||
|
public static Script Parse(string text)
|
||||||
|
{
|
||||||
|
List<ScriptAction> actions = new();
|
||||||
|
Dictionary<string, int> sceneMap = new();
|
||||||
|
|
||||||
|
ScriptAction lastChoice = null;
|
||||||
|
|
||||||
|
text = Regex.Replace(text, "<shake>", "<link=shake>");
|
||||||
|
text = Regex.Replace(text, "</shake>", "</link>");
|
||||||
|
text = Regex.Replace(text, "\r\n", "\n");
|
||||||
|
|
||||||
|
string[] lines = text.Split("\n");
|
||||||
|
|
||||||
|
foreach (var rawLine in lines)
|
||||||
|
{
|
||||||
|
string line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Match tagMatch = TagRegex.Match(line);
|
||||||
|
if (tagMatch.Success)
|
||||||
|
{
|
||||||
|
string tagName = tagMatch.Groups[1].Value;
|
||||||
|
string attrString = tagMatch.Groups[2].Value;
|
||||||
|
|
||||||
|
var scriptAction = new ScriptAction { Type = tagName };
|
||||||
|
ParseAttributes(attrString, scriptAction.Params);
|
||||||
|
|
||||||
|
if (tagName == "scene")
|
||||||
|
{
|
||||||
|
string sceneName = scriptAction.GetParam("name");
|
||||||
|
if (!string.IsNullOrEmpty(sceneName) && !sceneMap.ContainsKey(sceneName))
|
||||||
|
{
|
||||||
|
sceneMap[sceneName] = actions.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (tagName == "choices")
|
||||||
|
{
|
||||||
|
scriptAction.Choices = new List<Dictionary<string, string>>();
|
||||||
|
lastChoice = scriptAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.Add(scriptAction);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Match choiceMatch = ChoiceOptionRegex.Match(line);
|
||||||
|
if (choiceMatch.Success && lastChoice != null)
|
||||||
|
{
|
||||||
|
lastChoice.Choices.Add(
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "type", "msg" },
|
||||||
|
{ "content", choiceMatch.Groups[1].Value.Trim() },
|
||||||
|
{ "goto", choiceMatch.Groups[2].Value.Trim() },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.Add(new ScriptAction { Type = "msg", Params = { { "content", line } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Script(actions, sceneMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseAttributes(string attrString, Dictionary<string, object> paramDict)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(attrString))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (Match m in AttrRegex.Matches(attrString))
|
||||||
|
{
|
||||||
|
string key = m.Groups[1].Value;
|
||||||
|
string rawValue = m.Groups[2].Value;
|
||||||
|
|
||||||
|
if (rawValue.Length >= 2 && (rawValue.StartsWith("\"") || rawValue.StartsWith("'")))
|
||||||
|
rawValue = rawValue.Substring(1, rawValue.Length - 2);
|
||||||
|
|
||||||
|
paramDict[key] = rawValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_MAIN/Scripts/Core/ScriptParser.cs.meta
Normal file
2
Assets/_MAIN/Scripts/Core/ScriptParser.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5560b498ac9e7244e99ee64e197b7b84
|
||||||
Reference in New Issue
Block a user