first
This commit is contained in:
20
Assets/02_Scripts/Story/CharacterSlot.cs
Normal file
20
Assets/02_Scripts/Story/CharacterSlot.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 장소 프리팹 안의 캐릭터 자리 하나.
|
||||
// 이 캐릭터가 지금 이 장소에서 걸 수 있는 대화(StoryBeat)가 하나라도 있으면 캐릭터를 켜고, 없으면 끈다.
|
||||
// 갱신은 LocationManager가 장소 입장 시 / 스토리 상태 변화 시 Refresh를 호출해서 이뤄진다.
|
||||
//
|
||||
// 배치법: 이 컴포넌트는 항상 켜져 있는 빈 오브젝트에 붙이고,
|
||||
// 캐릭터 본체(CharacterVoiceObject + DialogPlayer)는 그 자식으로 두고 _character에 연결.
|
||||
// 참고: 조건 없는 잡담 비트를 하나 등록해 두면 그 캐릭터는 항상 등장한다.
|
||||
public class CharacterSlot : MonoBehaviour
|
||||
{
|
||||
[Tooltip("캐릭터 본체 (CharacterVoiceObject + DialogPlayer가 붙은 자식 오브젝트)")]
|
||||
[SerializeField] private CharacterVoiceObject _character;
|
||||
|
||||
public void Refresh(StoryDatabase database, LocationData location)
|
||||
{
|
||||
if (_character == null || _character.Character == null) return;
|
||||
_character.gameObject.SetActive(database.HasPlayableBeat(location, _character.Character));
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Story/CharacterSlot.cs.meta
Normal file
2
Assets/02_Scripts/Story/CharacterSlot.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8df8bed7639e304cbe98206c74e9f13
|
||||
8
Assets/02_Scripts/Story/Editor.meta
Normal file
8
Assets/02_Scripts/Story/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f8b437cb3f4c66489eed95f0c1158a7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
58
Assets/02_Scripts/Story/Editor/StoryBeatDrawer.cs
Normal file
58
Assets/02_Scripts/Story/Editor/StoryBeatDrawer.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
// StoryBeat를 리스트에서 접었을 때 "Element N" 대신 요약 제목을 보여준다.
|
||||
// Note가 있으면 Note 우선, 없으면 "[진행도] 장소 · 캐릭터 · 대화"를 자동 조합.
|
||||
[CustomPropertyDrawer(typeof(StoryBeat))]
|
||||
public class StoryBeatDrawer : PropertyDrawer
|
||||
{
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
=> EditorGUI.GetPropertyHeight(property, label, includeChildren: true);
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
string summary = BuildSummary(property);
|
||||
if (!string.IsNullOrEmpty(summary))
|
||||
label = new GUIContent(summary, label.tooltip);
|
||||
|
||||
EditorGUI.PropertyField(position, property, label, includeChildren: true);
|
||||
}
|
||||
|
||||
private static string BuildSummary(SerializedProperty property)
|
||||
{
|
||||
string note = property.FindPropertyRelative("Note").stringValue;
|
||||
if (!string.IsNullOrWhiteSpace(note))
|
||||
return note;
|
||||
|
||||
string location = NameOf(property.FindPropertyRelative("Location").objectReferenceValue);
|
||||
string character = NameOf(property.FindPropertyRelative("Character").objectReferenceValue);
|
||||
string group = NameOf(property.FindPropertyRelative("Group").objectReferenceValue);
|
||||
|
||||
// 아무것도 안 채워진 새 항목은 기본 라벨(Element N) 유지
|
||||
if (location == null && character == null && group == null)
|
||||
return null;
|
||||
|
||||
var progress = property.FindPropertyRelative("Condition").FindPropertyRelative("MainProgress");
|
||||
int min = progress.FindPropertyRelative("Min").intValue;
|
||||
int max = progress.FindPropertyRelative("Max").intValue;
|
||||
string range = max >= 9999 ? $"[{min}~]" : $"[{min}~{max}]";
|
||||
|
||||
return $"{range} {location ?? "?"} · {character ?? "?"} · {group ?? "?"}";
|
||||
}
|
||||
|
||||
// 에셋별 표시 이름 (지정 안 됐으면 에셋 이름, 그마저 없으면 null)
|
||||
private static string NameOf(Object obj)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case LocationData loc:
|
||||
return string.IsNullOrWhiteSpace(loc.DisplayName) ? loc.name : loc.DisplayName;
|
||||
case CharacterData ch:
|
||||
return string.IsNullOrWhiteSpace(ch.Name) ? ch.name : ch.Name;
|
||||
case DialogGroup grp:
|
||||
return string.IsNullOrWhiteSpace(grp.DialogGroupName) ? grp.name : grp.DialogGroupName;
|
||||
default:
|
||||
return obj != null ? obj.name : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Story/Editor/StoryBeatDrawer.cs.meta
Normal file
2
Assets/02_Scripts/Story/Editor/StoryBeatDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eade7ff33c9514b479d3f1e4ba2da912
|
||||
14
Assets/02_Scripts/Story/LocationData.cs
Normal file
14
Assets/02_Scripts/Story/LocationData.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 게임 속 장소 하나 (로비, 사무실, 법정 …).
|
||||
// StoryBeat가 "어느 장소의 대화인지"를 이 에셋으로 가리키고,
|
||||
// LocationManager가 이동 시 Prefab을 띄운다.
|
||||
[CreateAssetMenu(menuName = "Story/Location")]
|
||||
public class LocationData : ScriptableObject
|
||||
{
|
||||
[Tooltip("장소 표시 이름 (이동 메뉴 등 UI용)")]
|
||||
public string DisplayName;
|
||||
|
||||
[Tooltip("이 장소의 프리팹 — 배경 + 캐릭터 슬롯(CharacterSlot)들 + 이동 버튼 등")]
|
||||
public GameObject Prefab;
|
||||
}
|
||||
2
Assets/02_Scripts/Story/LocationData.cs.meta
Normal file
2
Assets/02_Scripts/Story/LocationData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e92ba75bdc5f28f4983e164d45c3b27d
|
||||
29
Assets/02_Scripts/Story/StoryBeat.cs
Normal file
29
Assets/02_Scripts/Story/StoryBeat.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
// 스토리 비트 하나 — "어느 장소에서, 어느 캐릭터가, 어떤 조건일 때, 무슨 대화를 하는가".
|
||||
// StoryDatabase의 목록이 게임 전체 스토리 흐름의 단일 원본이다.
|
||||
[Serializable]
|
||||
public class StoryBeat
|
||||
{
|
||||
[Tooltip("에디터 식별용 메모 (게임에는 안 나옴)")]
|
||||
public string Note;
|
||||
|
||||
[Tooltip("이 대화가 일어나는 장소")]
|
||||
public LocationData Location;
|
||||
|
||||
[Tooltip("대화를 거는 캐릭터")]
|
||||
public CharacterData Character;
|
||||
|
||||
[Tooltip("재생할 대화 그래프 (.dlg = DialogGroup)")]
|
||||
public DialogGroup Group;
|
||||
|
||||
[Tooltip("활성 조건 (진행도/호감도 범위 + 트리거)")]
|
||||
public DialogCondition Condition = new();
|
||||
|
||||
[Tooltip("이 대화를 처음 완료하면 메인 진행도 +N (필수 대화가 아니면 0)")]
|
||||
[Min(0)] public int ProgressOnComplete;
|
||||
|
||||
[Tooltip("대화 선택 메뉴에 표시할 이름 (비우면 그룹 이름 사용)")]
|
||||
public string MenuLabel;
|
||||
}
|
||||
2
Assets/02_Scripts/Story/StoryBeat.cs.meta
Normal file
2
Assets/02_Scripts/Story/StoryBeat.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b03cd6e1bb5fbb44b82670d620875c3
|
||||
51
Assets/02_Scripts/Story/StoryDatabase.cs
Normal file
51
Assets/02_Scripts/Story/StoryDatabase.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// 스토리 비트 전체 목록 — "누가 어디서 언제 무슨 대화를 하는가"의 단일 원본.
|
||||
// 캐릭터 슬롯 활성화(HasPlayableBeat)와 DialogPlayer의 대화 후보(GetPlayableBeats)가
|
||||
// 전부 이 에셋을 조회한다. 게임에 하나만 만들어 LocationManager에 꽂아 쓴다.
|
||||
[CreateAssetMenu(menuName = "Story/Story Database")]
|
||||
public class StoryDatabase : ScriptableObject
|
||||
{
|
||||
[Tooltip("스토리 비트 목록 — 위에 있을수록 대화 선택 메뉴에서 우선순위가 높다. " +
|
||||
"진행도 순으로 정렬해 두면 이 목록이 곧 시나리오 진행표가 된다")]
|
||||
[SerializeField] private List<StoryBeat> _beats = new();
|
||||
|
||||
public IReadOnlyList<StoryBeat> Beats => _beats;
|
||||
|
||||
// 이 장소·캐릭터의 비트 중 지금 조건을 만족하는 것들 (목록 순서 유지)
|
||||
public List<StoryBeat> GetPlayableBeats(LocationData location, CharacterData character)
|
||||
{
|
||||
var result = new List<StoryBeat>();
|
||||
foreach (var beat in _beats)
|
||||
if (IsPlayable(beat, location, character))
|
||||
result.Add(beat);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 이 장소에서 이 캐릭터가 걸 수 있는 대화가 하나라도 있는가 (캐릭터 슬롯 활성화 판정)
|
||||
public bool HasPlayableBeat(LocationData location, CharacterData character)
|
||||
{
|
||||
foreach (var beat in _beats)
|
||||
if (IsPlayable(beat, location, character))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 강제 재생(DialogPlayer.PlayGroup)용 — 이 그룹이 등록된 첫 비트 (없으면 null)
|
||||
public StoryBeat FindBeat(DialogGroup group)
|
||||
{
|
||||
if (group == null) return null;
|
||||
foreach (var beat in _beats)
|
||||
if (beat.Group == group)
|
||||
return beat;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsPlayable(StoryBeat beat, LocationData location, CharacterData character)
|
||||
{
|
||||
if (beat.Location != location || beat.Character != character || beat.Group == null)
|
||||
return false;
|
||||
return beat.Condition == null || beat.Condition.IsMet(character);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Story/StoryDatabase.cs.meta
Normal file
2
Assets/02_Scripts/Story/StoryDatabase.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c7c50927085b814097de120bc52f6c2
|
||||
65
Assets/02_Scripts/Story/StoryState.cs
Normal file
65
Assets/02_Scripts/Story/StoryState.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// 이야기 진행 상태 데이터 (메인 진행도 / 호감도 / 대화 이력 / 선택 이력).
|
||||
// 상태 보관과 JSON 변환만 담당한다 — 게임 로직에서는 StoryManager를 통해 접근할 것.
|
||||
public class StoryState
|
||||
{
|
||||
public int MainProgress;
|
||||
public readonly Dictionary<string, int> Affection = new(); // 캐릭터 Id → 호감도
|
||||
public readonly HashSet<string> CompletedDialogs = new(); // 완료한 DialogGroup 이름
|
||||
public readonly HashSet<string> ChosenCodes = new(); // 골랐던 선택지 Code
|
||||
public readonly HashSet<string> Triggers = new(); // 켜진 트리거 Id (대화 조건 검사용)
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
MainProgress = 0;
|
||||
Affection.Clear();
|
||||
CompletedDialogs.Clear();
|
||||
ChosenCodes.Clear();
|
||||
Triggers.Clear();
|
||||
}
|
||||
|
||||
// ── JSON 변환 ────────────────────────────────────────────────
|
||||
// JsonUtility가 Dictionary/HashSet을 직렬화하지 못해 리스트로 바꿔 저장한다.
|
||||
[Serializable]
|
||||
private class JsonData
|
||||
{
|
||||
public int MainProgress;
|
||||
public List<string> AffectionIds = new();
|
||||
public List<int> AffectionValues = new();
|
||||
public List<string> CompletedDialogs = new();
|
||||
public List<string> ChosenCodes = new();
|
||||
public List<string> Triggers = new();
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
var data = new JsonData { MainProgress = MainProgress };
|
||||
foreach (var kvp in Affection)
|
||||
{
|
||||
data.AffectionIds.Add(kvp.Key);
|
||||
data.AffectionValues.Add(kvp.Value);
|
||||
}
|
||||
data.CompletedDialogs.AddRange(CompletedDialogs);
|
||||
data.ChosenCodes.AddRange(ChosenCodes);
|
||||
data.Triggers.AddRange(Triggers);
|
||||
return JsonUtility.ToJson(data, prettyPrint: true);
|
||||
}
|
||||
|
||||
// json이 유효하지 않으면 null 반환
|
||||
public static StoryState FromJson(string json)
|
||||
{
|
||||
var data = JsonUtility.FromJson<JsonData>(json);
|
||||
if (data == null) return null;
|
||||
|
||||
var state = new StoryState { MainProgress = data.MainProgress };
|
||||
for (int i = 0; i < data.AffectionIds.Count && i < data.AffectionValues.Count; i++)
|
||||
state.Affection[data.AffectionIds[i]] = data.AffectionValues[i];
|
||||
state.CompletedDialogs.UnionWith(data.CompletedDialogs);
|
||||
state.ChosenCodes.UnionWith(data.ChosenCodes);
|
||||
state.Triggers.UnionWith(data.Triggers ?? new List<string>());
|
||||
return state;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Story/StoryState.cs.meta
Normal file
2
Assets/02_Scripts/Story/StoryState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d47259c7403ae7e42b37a411170bc21c
|
||||
Reference in New Issue
Block a user