데이터 파이프라인 개선 및 포탈 로직 생성

csv import 시 자동으로 완전한 프리팹이 생성될 수 있도록 함.
This commit is contained in:
2026-02-01 00:29:22 +09:00
parent b54e016283
commit 2593b6dd37
48 changed files with 11897 additions and 45 deletions

View File

@@ -1,3 +1,5 @@
using Northbound;
using Northbound.Data;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
@@ -6,7 +8,7 @@ using System.Linq;
[CustomEditor(typeof(EnemyPortal))]
public class EnemyPortalEditor : Editor
{
private const string MONSTER_DATA_FOLDER = "Assets/Data/ScriptableObjects/Monster";
private const string MONSTER_PREFAB_FOLDER = "Assets/Prefabs/Monster";
public override void OnInspectorGUI()
{
@@ -22,12 +24,12 @@ public class EnemyPortalEditor : Editor
LoadMonsterData(portal);
}
EditorGUILayout.HelpBox("Click 'Load Monster Data' to automatically load all monsters from " + MONSTER_DATA_FOLDER, MessageType.Info);
EditorGUILayout.HelpBox("Click 'Load Monster Data' to automatically load all monster prefabs from " + MONSTER_PREFAB_FOLDER, MessageType.Info);
}
private void LoadMonsterData(EnemyPortal portal)
{
string[] guids = AssetDatabase.FindAssets("t:MonsterData", new[] { MONSTER_DATA_FOLDER });
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { MONSTER_PREFAB_FOLDER });
List<EnemyPortal.MonsterEntry> entries = new List<EnemyPortal.MonsterEntry>();
int loadedCount = 0;
@@ -35,24 +37,23 @@ public class EnemyPortalEditor : Editor
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
Northbound.Data.MonsterData monsterData = AssetDatabase.LoadAssetAtPath<Northbound.Data.MonsterData>(assetPath);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (monsterData != null)
if (prefab != null)
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(monsterData.prefabPath);
MonsterDataComponent monsterDataComponent = prefab.GetComponent<MonsterDataComponent>();
if (prefab != null)
if (monsterDataComponent != null && monsterDataComponent.monsterData != null)
{
entries.Add(new EnemyPortal.MonsterEntry
{
monsterData = monsterData,
prefab = prefab
});
loadedCount++;
}
else
{
Debug.LogWarning($"[EnemyPortal] Could not load prefab at path: {monsterData.prefabPath} for monster {monsterData.id}");
Debug.LogWarning($"[EnemyPortal] Prefab {prefab.name} does not have MonsterDataComponent with valid monsterData reference");
}
}
}
@@ -65,13 +66,12 @@ public class EnemyPortalEditor : Editor
for (int i = 0; i < entries.Count; i++)
{
SerializedProperty element = entriesProperty.GetArrayElementAtIndex(i);
element.FindPropertyRelative("monsterData").objectReferenceValue = entries[i].monsterData;
element.FindPropertyRelative("prefab").objectReferenceValue = entries[i].prefab;
}
serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
Debug.Log($"[EnemyPortal] Loaded {loadedCount} monsters from {MONSTER_DATA_FOLDER}");
Debug.Log($"[EnemyPortal] Loaded {loadedCount} monsters from {MONSTER_PREFAB_FOLDER}");
}
}

View File

@@ -0,0 +1,10 @@
using UnityEngine;
namespace Northbound.Editor
{
public interface IPrefabSetup
{
string GetTemplateName();
void SetupPrefab(GameObject prefab, ScriptableObject data);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ddd133259cf685c4da37370144003c52

View File

@@ -0,0 +1,99 @@
using Northbound.Data;
using UnityEditor;
using UnityEngine;
namespace Northbound.Editor
{
public class MonsterPrefabSetup : IPrefabSetup
{
public string GetTemplateName()
{
return "MonsterTemplate";
}
public void SetupPrefab(GameObject prefab, ScriptableObject data)
{
if (!(data is MonsterData monsterData))
{
Debug.LogWarning($"[MonsterPrefabSetup] Expected MonsterData, got {data.GetType().Name}");
return;
}
var monsterDataComponent = prefab.GetComponent<MonsterDataComponent>();
if (monsterDataComponent != null)
{
monsterDataComponent.monsterData = monsterData;
monsterDataComponent.ApplyMonsterData();
}
if (!string.IsNullOrEmpty(monsterData.meshPath))
{
RemoveOldModel(prefab);
if (monsterData.meshPath.ToLower().EndsWith(".fbx"))
{
GameObject fbxModel = AssetDatabase.LoadAssetAtPath<GameObject>(monsterData.meshPath);
if (fbxModel != null)
{
GameObject fbxInstance = GameObject.Instantiate(fbxModel);
fbxInstance.name = "Model";
fbxInstance.transform.SetParent(prefab.transform, false);
fbxInstance.transform.localPosition = Vector3.zero;
fbxInstance.transform.localRotation = Quaternion.identity;
fbxInstance.transform.localScale = Vector3.one;
Debug.Log($"[MonsterPrefabSetup] Applied FBX model: {monsterData.meshPath}");
}
else
{
Debug.LogWarning($"[MonsterPrefabSetup] Could not load FBX model: {monsterData.meshPath}");
}
}
else
{
var meshFilter = prefab.GetComponent<MeshFilter>();
if (meshFilter != null)
{
Mesh mesh = AssetDatabase.LoadAssetAtPath<Mesh>(monsterData.meshPath);
if (mesh != null)
{
meshFilter.sharedMesh = mesh;
Debug.Log($"[MonsterPrefabSetup] Applied mesh: {monsterData.meshPath}");
}
else
{
Debug.LogWarning($"[MonsterPrefabSetup] Could not load mesh: {monsterData.meshPath}");
}
}
}
}
if (!string.IsNullOrEmpty(monsterData.animatorControllerPath))
{
Animator animator = prefab.GetComponent<Animator>();
if (animator != null)
{
RuntimeAnimatorController controller = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(monsterData.animatorControllerPath);
if (controller != null)
{
animator.runtimeAnimatorController = controller;
Debug.Log($"[MonsterPrefabSetup] Applied Animator Controller: {monsterData.animatorControllerPath}");
}
else
{
Debug.LogWarning($"[MonsterPrefabSetup] Could not load Animator Controller: {monsterData.animatorControllerPath}");
}
}
}
}
private void RemoveOldModel(GameObject prefab)
{
Transform oldModel = prefab.transform.Find("Model");
if (oldModel != null)
{
GameObject.DestroyImmediate(oldModel.gameObject);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9d78ef5bde300404a89bb3af71df1610

View File

@@ -0,0 +1,180 @@
using Unity.Netcode;
using UnityEditor;
using UnityEngine;
using UnityEngine.AI;
namespace Northbound.Editor
{
public class TemplateCreator
{
private const string TEMPLATE_BASE_PATH = "Assets/Data/Templates";
[MenuItem("Tools/Data/Create Monster Template")]
public static void CreateMonsterTemplate()
{
CreateTemplate("Monster", SetupMonsterComponents);
}
[MenuItem("Tools/Data/Create Tower Template")]
public static void CreateTowerTemplate()
{
CreateTemplate("Tower", SetupTowerComponents);
}
[MenuItem("Tools/Data/Create Player Template")]
public static void CreatePlayerTemplate()
{
CreateTemplate("Player", SetupPlayerComponents);
}
private static void CreateTemplate(string typeName, System.Action<GameObject> setupComponents)
{
if (!AssetDatabase.IsValidFolder(TEMPLATE_BASE_PATH))
{
System.IO.Directory.CreateDirectory(Application.dataPath + "/Data/Templates");
AssetDatabase.Refresh();
}
string templateName = $"{typeName}Template";
string templatePath = $"{TEMPLATE_BASE_PATH}/{templateName}.prefab";
GameObject templateGO = new GameObject(templateName);
setupComponents(templateGO);
PrefabUtility.SaveAsPrefabAsset(templateGO, templatePath);
GameObject.DestroyImmediate(templateGO);
Debug.Log($"[TemplateCreator] Created template: {templatePath}");
AssetDatabase.Refresh();
Selection.activeObject = AssetDatabase.LoadAssetAtPath<GameObject>(templatePath);
}
private static void SetupMonsterComponents(GameObject go)
{
Transform t = go.transform;
t.localPosition = Vector3.zero;
t.localRotation = Quaternion.identity;
t.localScale = Vector3.one;
if (go.GetComponent<CapsuleCollider>() == null)
{
CapsuleCollider collider = go.AddComponent<CapsuleCollider>();
collider.isTrigger = false;
collider.radius = 0.5f;
collider.height = 2f;
collider.direction = 1;
collider.center = Vector3.zero;
}
if (go.GetComponent<Animator>() == null)
{
Animator animator = go.AddComponent<Animator>();
animator.runtimeAnimatorController = null;
animator.avatar = null;
animator.applyRootMotion = false;
animator.updateMode = AnimatorUpdateMode.Normal;
animator.cullingMode = AnimatorCullingMode.CullUpdateTransforms;
}
if (go.GetComponent<NetworkObject>() == null)
go.AddComponent<NetworkObject>();
if (go.GetComponent<EnemyUnit>() == null)
go.AddComponent<EnemyUnit>();
if (go.GetComponent<MonsterDataComponent>() == null)
go.AddComponent<MonsterDataComponent>();
if (go.GetComponent<NavMeshAgent>() == null)
{
NavMeshAgent agent = go.AddComponent<NavMeshAgent>();
agent.speed = 2.6f;
agent.angularSpeed = 120f;
agent.acceleration = 8f;
agent.stoppingDistance = 0f;
agent.autoBraking = true;
agent.radius = 0.5f;
agent.height = 2f;
}
if (go.GetComponent<EnemyAIController>() == null)
{
EnemyAIController ai = go.AddComponent<EnemyAIController>();
ai.aiType = TeamType.Monster;
ai.detectionRange = 6f;
ai.detectionAngle = 360f;
ai.maxChaseDistance = 30f;
ai.attackRange = 1f;
ai.attackInterval = 1.2f;
ai.attackDamage = 3;
ai.moveSpeed = 2.6f;
ai.chaseSpeedMultiplier = 1.2f;
}
MeshFilter meshFilter = go.GetComponent<MeshFilter>();
if (meshFilter == null)
meshFilter = go.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = go.GetComponent<MeshRenderer>();
if (meshRenderer == null)
meshRenderer = go.AddComponent<MeshRenderer>();
int monsterLayer = LayerMask.NameToLayer("Monster");
if (monsterLayer >= 0)
{
go.layer = monsterLayer;
}
else
{
go.layer = 0;
}
NetworkObject netObj = go.GetComponent<NetworkObject>();
if (netObj != null)
{
netObj.SynchronizeTransform = true;
netObj.SpawnWithObservers = true;
netObj.AlwaysReplicateAsRoot = false;
}
EnemyUnit enemyUnit = go.GetComponent<EnemyUnit>();
if (enemyUnit != null)
{
enemyUnit.enemyTeam = TeamType.Monster;
}
}
private static void SetupTowerComponents(GameObject go)
{
if (go.GetComponent<NetworkObject>() == null)
go.AddComponent<NetworkObject>();
int defaultLayer = LayerMask.NameToLayer("Default");
if (defaultLayer >= 0)
{
go.layer = defaultLayer;
}
else
{
go.layer = 0;
}
}
private static void SetupPlayerComponents(GameObject go)
{
if (go.GetComponent<NetworkObject>() == null)
go.AddComponent<NetworkObject>();
int playerLayer = LayerMask.NameToLayer("Player");
if (playerLayer >= 0)
{
go.layer = playerLayer;
}
else
{
go.layer = 0;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9b0786fac9db245429b33a2597a2d7b4