feat: 스킬 레지스트리 도입으로 빌드 스킬 포함 보장 및 기본 장착 구조 개편
- SkillRegistry ScriptableObject 추가: _Skill_Player_ 에셋을 자동 수집하여 빌드 포함 보장 - PlayerSkillInput에 Registry 참조 추가 및 AutoRegisterPlayerSkills 개편 - 슬롯 0~5는 Inspector에서 디버그용 수동 설정 가능 (AutoRegister 불개입) - 슬롯 6(회피)에 구르기 스킬 자동 유지 - OnValidate 호출 순서 변경: SyncLegacySkillsToLoadoutEntries → AutoRegisterPlayerSkills - 플레이어 프리팹에 SkillRegistry 에셋 할당 (14개 플레이어 스킬 수집됨) - 에디터 전용 EditorAllPlayerSkills 정적 프로퍼티로 전체 스킬 목록 접근 가능
This commit is contained in:
@@ -62,8 +62,12 @@ namespace Colosseum.Player
|
||||
};
|
||||
#endif
|
||||
|
||||
[Header("Skill Registry")]
|
||||
[Tooltip("모든 플레이어 스킬을 관리하는 레지스트리. 참조하면 등록된 스킬이 빌드에 자동 포함됩니다.")]
|
||||
[SerializeField] private SkillRegistry skillRegistry;
|
||||
|
||||
[Header("Skill Slots")]
|
||||
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
|
||||
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 회피 슬롯). 슬롯 0~5는 디버그용으로 Inspector에서 수동 설정합니다.")]
|
||||
[SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount];
|
||||
[Tooltip("각 슬롯의 베이스 스킬 + 젬 조합")]
|
||||
[SerializeField] private SkillLoadoutEntry[] skillLoadoutEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
|
||||
@@ -155,6 +159,7 @@ namespace Colosseum.Player
|
||||
EnsureSkillSlotCapacity();
|
||||
EnsureSkillLoadoutCapacity();
|
||||
SyncLegacySkillsToLoadoutEntries();
|
||||
AutoRegisterPlayerSkills();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -235,6 +240,95 @@ namespace Colosseum.Player
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Registry에서 전체 플릴이어 스킬을 자동 수집하고,
|
||||
/// 회피 슬롯(index 6)에 구르기 스킬이 없으면 자동 할당합니다.
|
||||
/// 슬롯 0~5는 Inspector에서 수동 설정한 값을 유지합니다.
|
||||
/// </summary>
|
||||
private void AutoRegisterPlayerSkills()
|
||||
{
|
||||
// Registry가 할당되어 있으면 전체 스킬 자동 수집
|
||||
if (skillRegistry != null)
|
||||
skillRegistry.AutoCollectPlayerSkills();
|
||||
|
||||
// 회피 슬롯(마지막 슬롯)에 구르기 자동 할당
|
||||
int evadeSlotIndex = ExpectedSkillSlotCount - 1;
|
||||
SkillData evadeSkill = FindEvadeSkill();
|
||||
if (evadeSkill != null && skillSlots[evadeSlotIndex] != evadeSkill)
|
||||
{
|
||||
skillSlots[evadeSlotIndex] = evadeSkill;
|
||||
skillLoadoutEntries[evadeSlotIndex].SetBaseSkill(evadeSkill);
|
||||
Debug.Log($"[PlayerSkillInput] 회피 스킬 자동 장착: {evadeSkill.SkillName}", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registry 또는 에셋 폴더에서 구르기 스킬을 찾습니다.
|
||||
/// </summary>
|
||||
private SkillData FindEvadeSkill()
|
||||
{
|
||||
// 1. Registry에서 검색
|
||||
if (skillRegistry != null)
|
||||
{
|
||||
SkillData found = skillRegistry.FindPlayerSkillByNameContains("구르기");
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
// 2. 에셋 폴더에서 직접 검색 (Registry 미할당 시 폴백)
|
||||
string[] guids = AssetDatabase.FindAssets("t:SkillData", new[] { "Assets/_Game/Data/Skills" });
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string assetName = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (assetName.IndexOf("구르기", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
}
|
||||
|
||||
Debug.LogWarning("[PlayerSkillInput] 구르기 스킬을 찾을 수 없습니다.", this);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 에디터에서 접근 가능한 전체 플레이어 스킬 목록 (디버그/빌드 도구용).
|
||||
/// </summary>
|
||||
public static IReadOnlyList<SkillData> EditorAllPlayerSkills
|
||||
{
|
||||
get
|
||||
{
|
||||
// Registry에서 수집된 스킬이 있으면 사용
|
||||
string[] guids = AssetDatabase.FindAssets("t:SkillRegistry", null);
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
SkillRegistry registry = AssetDatabase.LoadAssetAtPath<SkillRegistry>(path);
|
||||
if (registry != null && registry.PlayerSkills.Count > 0)
|
||||
return registry.PlayerSkills;
|
||||
}
|
||||
|
||||
// Registry가 없으면 직접 수집
|
||||
var skills = new List<SkillData>();
|
||||
string[] skillGuids = AssetDatabase.FindAssets("t:SkillData", new[] { "Assets/_Game/Data/Skills" });
|
||||
foreach (string skillGuid in skillGuids)
|
||||
{
|
||||
string skillPath = AssetDatabase.GUIDToAssetPath(skillGuid);
|
||||
string assetName = Path.GetFileNameWithoutExtension(skillPath);
|
||||
|
||||
if (assetName.IndexOf("_Skill_Player_", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(skillPath);
|
||||
if (skill != null)
|
||||
skills.Add(skill);
|
||||
}
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 기존 SkillData 직렬화와 새 로드아웃 엔트리 구조를 동기화합니다.
|
||||
/// </summary>
|
||||
|
||||
146
Assets/_Game/Scripts/Skills/SkillRegistry.cs
Normal file
146
Assets/_Game/Scripts/Skills/SkillRegistry.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 프로젝트의 모든 스킬 데이터를 중앙에서 관리하는 ScriptableObject.
|
||||
/// 이 에셋을 프리팹이나 씬에서 참조하면, 등록된 모든 스킬이 빌드에 자동 포함됩니다.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "SkillRegistry", menuName = "Colosseum/SkillRegistry")]
|
||||
public class SkillRegistry : ScriptableObject
|
||||
{
|
||||
private const string PlayerSkillSearchFolder = "Assets/_Game/Data/Skills";
|
||||
private const string PlayerSkillNameFilter = "_Skill_Player_";
|
||||
|
||||
[Header("Player Skills")]
|
||||
[Tooltip("모든 플레이어 스킬 데이터 (에디터에서 자동 수집됨)")]
|
||||
[SerializeField] private List<SkillData> playerSkills = new();
|
||||
|
||||
/// <summary>
|
||||
/// 등록된 모든 플레이어 스킬 목록
|
||||
/// </summary>
|
||||
public IReadOnlyList<SkillData> PlayerSkills => playerSkills;
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 이름으로 플레이어 스킬을 조회합니다.
|
||||
/// </summary>
|
||||
public SkillData GetPlayerSkill(string skillName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skillName))
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < playerSkills.Count; i++)
|
||||
{
|
||||
SkillData skill = playerSkills[i];
|
||||
if (skill != null && string.Equals(skill.SkillName, skillName, StringComparison.Ordinal))
|
||||
return skill;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 에셋 이름으로 플레이어 스킬을 조회합니다.
|
||||
/// </summary>
|
||||
public SkillData GetPlayerSkillByAssetName(string assetName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetName))
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < playerSkills.Count; i++)
|
||||
{
|
||||
SkillData skill = playerSkills[i];
|
||||
if (skill != null && string.Equals(skill.name, assetName, StringComparison.Ordinal))
|
||||
return skill;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이름에 지정 문자열이 포함된 첫 번째 플레이어 스킬을 조회합니다.
|
||||
/// </summary>
|
||||
public SkillData FindPlayerSkillByNameContains(string fragment)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fragment))
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < playerSkills.Count; i++)
|
||||
{
|
||||
SkillData skill = playerSkills[i];
|
||||
if (skill != null && skill.name.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return skill;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// "_Skill_Player_" 이름이 포함된 모든 SkillData를 에디터에서 자동 수집합니다.
|
||||
/// OnValidate에서 호출되어 에셋 변경 시 자동 동기화됩니다.
|
||||
/// </summary>
|
||||
public void AutoCollectPlayerSkills()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(PlayerSkillSearchFolder))
|
||||
{
|
||||
Debug.LogWarning($"[SkillRegistry] 스킬 검색 폴더가 없습니다: {PlayerSkillSearchFolder}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets("t:SkillData", new[] { PlayerSkillSearchFolder });
|
||||
var skills = new List<SkillData>();
|
||||
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string assetName = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (assetName.IndexOf(PlayerSkillNameFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
if (skill != null)
|
||||
skills.Add(skill);
|
||||
}
|
||||
}
|
||||
|
||||
skills.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.Ordinal));
|
||||
|
||||
bool changed = playerSkills.Count != skills.Count;
|
||||
if (!changed)
|
||||
{
|
||||
for (int i = 0; i < skills.Count; i++)
|
||||
{
|
||||
if (playerSkills[i] != skills[i])
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
playerSkills.Clear();
|
||||
playerSkills.AddRange(skills);
|
||||
Debug.Log($"[SkillRegistry] 자동 수집: {skills.Count}개 Player 스킬", this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
AutoCollectPlayerSkills();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Skills/SkillRegistry.cs.meta
Normal file
2
Assets/_Game/Scripts/Skills/SkillRegistry.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 144e0ffda72c68941800f40c5755fee8
|
||||
Reference in New Issue
Block a user