Unity 6.5 업그레이드와 신규 프로토타입 기반 구축
This commit is contained in:
@@ -1,410 +0,0 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using Northbound.Data;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public class CSVToSOImporter : EditorWindow
|
||||
{
|
||||
private static Dictionary<string, IPrefabSetup> prefabSetups;
|
||||
|
||||
[MenuItem("Tools/Data/Import All CSV")]
|
||||
public static void ImportAllCSV()
|
||||
{
|
||||
InitializePrefabSetups();
|
||||
string dataPath = Path.Combine(Application.dataPath, "../GameData");
|
||||
|
||||
if (!Directory.Exists(dataPath))
|
||||
{
|
||||
Debug.LogError("[CSVToSOImporter] GameData folder not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] csvFiles = Directory.GetFiles(dataPath, "*.csv");
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
bool towerImported = false;
|
||||
|
||||
foreach (string csvFile in csvFiles)
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(csvFile);
|
||||
if (ImportCSV(fileName))
|
||||
{
|
||||
successCount++;
|
||||
if (fileName == "Tower")
|
||||
towerImported = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"<color=green>[CSVToSOImporter] Import complete: {successCount} succeeded, {failCount} failed</color>");
|
||||
|
||||
if (towerImported)
|
||||
{
|
||||
AutoConfigureBuildingManager();
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
private static void InitializePrefabSetups()
|
||||
{
|
||||
prefabSetups = new Dictionary<string, IPrefabSetup>
|
||||
{
|
||||
{ "Monster", new MonsterPrefabSetup() },
|
||||
{ "Creep", new CreepPrefabSetup() },
|
||||
{ "Tower", new TowerPrefabSetup() },
|
||||
{ "Player", new PlayerPrefabSetup() },
|
||||
{ "Upgrade", new UpgradePrefabSetup() }
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ImportCSV(string typeName)
|
||||
{
|
||||
string csvPath = Path.Combine(Application.dataPath, "../GameData", $"{typeName}.csv");
|
||||
|
||||
if (!File.Exists(csvPath))
|
||||
{
|
||||
Debug.LogWarning($"[CSVToSOImporter] CSV file not found: {csvPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!prefabSetups.ContainsKey(typeName))
|
||||
{
|
||||
Debug.LogWarning($"[CSVToSOImporter] No prefab setup found for type: {typeName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
IPrefabSetup prefabSetup = prefabSetups[typeName];
|
||||
|
||||
// Upgrade는 템플릿이 필요 없음
|
||||
GameObject template = null;
|
||||
if (typeName != "Upgrade")
|
||||
{
|
||||
string templateName = prefabSetup.GetTemplateName();
|
||||
string templatePath = $"Assets/Data/Templates/{templateName}.prefab";
|
||||
template = AssetDatabase.LoadAssetAtPath<GameObject>(templatePath);
|
||||
if (template == null)
|
||||
{
|
||||
Debug.LogError($"[CSVToSOImporter] Template not found: {templatePath}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string[] csvLines = File.ReadAllLines(csvPath);
|
||||
if (csvLines.Length < 2)
|
||||
{
|
||||
Debug.LogWarning($"[CSVToSOImporter] CSV file is empty or has no data: {csvPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] headers = ParseCSVLine(csvLines[0]);
|
||||
int successCount = 0;
|
||||
|
||||
for (int i = 1; i < csvLines.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csvLines[i]))
|
||||
continue;
|
||||
|
||||
string[] values = ParseCSVLine(csvLines[i]);
|
||||
if (CreatePrefabFromRow(typeName, headers, values, template, prefabSetup))
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeName == "Player")
|
||||
{
|
||||
Debug.Log($"[CSVToSOImporter] {typeName}: Player prefab updated successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[CSVToSOImporter] {typeName}: {successCount} prefabs created/updated");
|
||||
}
|
||||
|
||||
if (typeName == "Tower")
|
||||
{
|
||||
Debug.Log($"<color=cyan>[CSVToSOImporter] Tower import complete!</color>");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CreatePrefabFromRow(string typeName, string[] headers, string[] values, GameObject template, IPrefabSetup prefabSetup)
|
||||
{
|
||||
string soPath;
|
||||
if (typeName == "Upgrade")
|
||||
{
|
||||
// Upgrade는 Resources 폴더에 저장 (런타임 자동 로드용)
|
||||
soPath = "Assets/Resources/Data/ScriptableObjects/Upgrade";
|
||||
Directory.CreateDirectory(Path.Combine(Application.dataPath, "Resources/Data/ScriptableObjects/Upgrade"));
|
||||
}
|
||||
else
|
||||
{
|
||||
soPath = $"Assets/Data/ScriptableObjects/{typeName}";
|
||||
Directory.CreateDirectory(Path.Combine(Application.dataPath, $"ScriptableObjects/{typeName}"));
|
||||
}
|
||||
|
||||
|
||||
int id = 0;
|
||||
for (int i = 0; i < headers.Length && i < values.Length; i++)
|
||||
{
|
||||
if (headers[i].ToLower() == "id")
|
||||
{
|
||||
int.TryParse(values[i], out id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
Debug.LogWarning($"[CSVToSOImporter] No valid ID found in row");
|
||||
return false;
|
||||
}
|
||||
|
||||
ScriptableObject data;
|
||||
string soAssetPath = $"{soPath}/{typeName}{id}.asset";
|
||||
data = AssetDatabase.LoadAssetAtPath<ScriptableObject>(soAssetPath);
|
||||
|
||||
bool isNew = false;
|
||||
if (data == null)
|
||||
{
|
||||
data = CreateInstance(typeName + "Data");
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < headers.Length && i < values.Length; i++)
|
||||
{
|
||||
string fieldName = CSVToCamelCase(headers[i]);
|
||||
var field = data.GetType().GetField(fieldName);
|
||||
if (field != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
object value = ParseValue(values[i], field.FieldType);
|
||||
field.SetValue(data, value);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[CSVToSOImporter] Failed to set {fieldName}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GameObject prefabObj;
|
||||
if (typeName == "Player")
|
||||
{
|
||||
PlayerPrefabSetup.UpdatePlayerPrefab((PlayerData)data);
|
||||
prefabObj = null;
|
||||
}
|
||||
else if (typeName == "Upgrade")
|
||||
{
|
||||
// Upgrade는 프리팹이 필요 없음
|
||||
prefabObj = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
string prefabPath = $"Assets/Prefabs/{typeName}/{typeName}{id}.prefab";
|
||||
Directory.CreateDirectory(Path.Combine(Application.dataPath, $"Prefabs/{typeName}"));
|
||||
|
||||
GameObject prefabInstance = GameObject.Instantiate(template);
|
||||
prefabInstance.name = $"{typeName}{id}";
|
||||
|
||||
prefabSetup.SetupPrefab(prefabInstance, data);
|
||||
|
||||
prefabObj = PrefabUtility.SaveAsPrefabAsset(prefabInstance, prefabPath);
|
||||
GameObject.DestroyImmediate(prefabInstance);
|
||||
}
|
||||
|
||||
// Now set the prefab reference on data
|
||||
if (data is TowerData towerData)
|
||||
{
|
||||
towerData.prefab = prefabObj;
|
||||
Debug.Log($"[CSVToSOImporter] Set prefab reference: {towerData.name} -> {prefabObj.name}");
|
||||
}
|
||||
|
||||
// Save data asset
|
||||
if (isNew)
|
||||
{
|
||||
AssetDatabase.CreateAsset(data, soAssetPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.SetDirty(data);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void AutoConfigureBuildingManager()
|
||||
{
|
||||
BuildingManager buildingManager = GameObject.FindFirstObjectByType<BuildingManager>();
|
||||
|
||||
if (buildingManager == null)
|
||||
{
|
||||
Debug.LogError("<color=red>[CSVToSOImporter] BuildingManager not found in scene! Please add a BuildingManager component to a GameObject in your scene.</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] towerDataGuids = AssetDatabase.FindAssets("t:TowerData", new[] { "Assets/Data/ScriptableObjects" });
|
||||
List<TowerData> allTowers = new List<TowerData>();
|
||||
|
||||
Debug.Log($"<color=cyan>[CSVToSOImporter] Found {towerDataGuids.Length} TowerData assets</color>");
|
||||
|
||||
foreach (string guid in towerDataGuids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
TowerData towerData = AssetDatabase.LoadAssetAtPath<TowerData>(assetPath);
|
||||
|
||||
if (towerData == null)
|
||||
{
|
||||
Debug.LogWarning($"[CSVToSOImporter] Failed to load TowerData: {assetPath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (towerData.prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"<color=yellow>[CSVToSOImporter] TowerData {towerData.name} has no prefab reference - skipping</color>");
|
||||
continue;
|
||||
}
|
||||
|
||||
allTowers.Add(towerData);
|
||||
Debug.Log($"<color=green>[CSVToSOImporter] Added tower: {towerData.buildingName}</color>");
|
||||
}
|
||||
|
||||
if (allTowers.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("<color=yellow>[CSVToSOImporter] No TowerData with valid prefabs found!</color>");
|
||||
Debug.LogWarning("<color=yellow>Run 'Northbound > Diagnose Tower System' to see what's wrong</color>");
|
||||
return;
|
||||
}
|
||||
|
||||
allTowers.Sort((a, b) => string.Compare(a.buildingName, b.buildingName));
|
||||
|
||||
buildingManager.availableBuildings.Clear();
|
||||
foreach (var towerData in allTowers)
|
||||
{
|
||||
buildingManager.availableBuildings.Add(towerData);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(buildingManager);
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
Debug.Log($"<color=cyan>🏗️ TOWER IMPORT COMPLETE!</color>");
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
Debug.Log($"<color=green>✓ BuildingManager automatically configured!</color>");
|
||||
Debug.Log($"<color=green>✓ Added {allTowers.Count} TowerData to availableBuildings list</color>");
|
||||
Debug.Log($"<color=green>✓ Ready to play - all towers will appear in quickslot!</color>");
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
foreach (var towerData in allTowers)
|
||||
{
|
||||
Debug.Log($"<color=green> - {towerData.buildingName}</color>");
|
||||
}
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
}
|
||||
|
||||
private static object ParseValue(string value, System.Type type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return type.IsValueType ? System.Activator.CreateInstance(type) : null;
|
||||
}
|
||||
|
||||
// List<T> 타입 처리 (세미콜론 구분)
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
System.Type elementType = type.GetGenericArguments()[0];
|
||||
var elements = value.Split(';').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||
|
||||
IList list = (IList)System.Activator.CreateInstance(type);
|
||||
|
||||
foreach (string element in elements)
|
||||
{
|
||||
object elementValue = ParseValue(element, elementType);
|
||||
if (elementValue != null)
|
||||
{
|
||||
list.Add(elementValue);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
int result;
|
||||
if (int.TryParse(value, out result)) return result;
|
||||
}
|
||||
else if (type == typeof(float))
|
||||
{
|
||||
float result;
|
||||
if (float.TryParse(value, out result)) return result;
|
||||
}
|
||||
else if (type == typeof(bool))
|
||||
{
|
||||
bool result;
|
||||
if (bool.TryParse(value, out result)) return result;
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return type.IsValueType ? System.Activator.CreateInstance(type) : null;
|
||||
}
|
||||
|
||||
private static string CSVToCamelCase(string csvName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(csvName))
|
||||
return csvName;
|
||||
|
||||
string[] parts = csvName.Split('_');
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parts[i]))
|
||||
{
|
||||
parts[i] = char.ToUpper(parts[i][0]) + parts[i].Substring(1);
|
||||
}
|
||||
}
|
||||
return string.Join("", parts);
|
||||
}
|
||||
|
||||
private static string[] ParseCSVLine(string line)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
bool inQuotes = false;
|
||||
string current = "";
|
||||
|
||||
foreach (char c in line)
|
||||
{
|
||||
if (c == '"')
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
else if (c == ',' && !inQuotes)
|
||||
{
|
||||
result.Add(current);
|
||||
current = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
current += c;
|
||||
}
|
||||
}
|
||||
result.Add(current);
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c00f900c9a822184dbb4dc85440d40dd
|
||||
@@ -1,118 +0,0 @@
|
||||
using Northbound.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public class CreepPrefabSetup : IPrefabSetup
|
||||
{
|
||||
public string GetTemplateName()
|
||||
{
|
||||
return "CreepTemplate";
|
||||
}
|
||||
|
||||
public void SetupPrefab(GameObject prefab, ScriptableObject data)
|
||||
{
|
||||
if (!(data is CreepData creepData))
|
||||
{
|
||||
Debug.LogWarning($"[CreepPrefabSetup] Expected CreepData, got {data.GetType().Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
var creepDataComponent = prefab.GetComponent<CreepDataComponent>();
|
||||
if (creepDataComponent != null)
|
||||
{
|
||||
creepDataComponent.creepData = creepData;
|
||||
creepDataComponent.ApplyCreepData();
|
||||
}
|
||||
|
||||
var animationController = prefab.GetComponent<MonsterAnimationController>();
|
||||
if (animationController == null)
|
||||
{
|
||||
animationController = prefab.AddComponent<MonsterAnimationController>();
|
||||
Debug.Log($"[CreepPrefabSetup] Added MonsterAnimationController component");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(creepData.modelPath))
|
||||
{
|
||||
RemoveOldModel(prefab);
|
||||
|
||||
if (creepData.modelPath.ToLower().EndsWith(".fbx"))
|
||||
{
|
||||
GameObject fbxModel = AssetDatabase.LoadAssetAtPath<GameObject>(creepData.modelPath);
|
||||
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($"[CreepPrefabSetup] Applied FBX model: {creepData.modelPath}");
|
||||
|
||||
Avatar modelAvatar = fbxModel.GetComponent<Animator>()?.avatar;
|
||||
|
||||
if (modelAvatar != null)
|
||||
{
|
||||
Animator prefabAnimator = prefab.GetComponent<Animator>();
|
||||
if (prefabAnimator != null)
|
||||
{
|
||||
prefabAnimator.avatar = modelAvatar;
|
||||
Debug.Log($"[CreepPrefabSetup] Applied Avatar: {modelAvatar.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[CreepPrefabSetup] Could not load FBX model: {creepData.modelPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var meshFilter = prefab.GetComponent<MeshFilter>();
|
||||
if (meshFilter != null)
|
||||
{
|
||||
Mesh mesh = AssetDatabase.LoadAssetAtPath<Mesh>(creepData.modelPath);
|
||||
if (mesh != null)
|
||||
{
|
||||
meshFilter.sharedMesh = mesh;
|
||||
Debug.Log($"[CreepPrefabSetup] Applied mesh: {creepData.modelPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[CreepPrefabSetup] Could not load mesh: {creepData.modelPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(creepData.animationControllerPath))
|
||||
{
|
||||
Animator animator = prefab.GetComponent<Animator>();
|
||||
if (animator != null)
|
||||
{
|
||||
RuntimeAnimatorController controller = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(creepData.animationControllerPath);
|
||||
if (controller != null)
|
||||
{
|
||||
animator.runtimeAnimatorController = controller;
|
||||
Debug.Log($"[CreepPrefabSetup] Applied Animator Controller: {creepData.animationControllerPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[CreepPrefabSetup] Could not load Animator Controller: {creepData.animationControllerPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOldModel(GameObject prefab)
|
||||
{
|
||||
Transform oldModel = prefab.transform.Find("Model");
|
||||
if (oldModel != null)
|
||||
{
|
||||
GameObject.DestroyImmediate(oldModel.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74efb75ea8ccbe54a9c86d0fd503f85f
|
||||
@@ -1,10 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public interface IPrefabSetup
|
||||
{
|
||||
string GetTemplateName();
|
||||
void SetupPrefab(GameObject prefab, ScriptableObject data);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddd133259cf685c4da37370144003c52
|
||||
@@ -1,118 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
var animationController = prefab.GetComponent<MonsterAnimationController>();
|
||||
if (animationController == null)
|
||||
{
|
||||
animationController = prefab.AddComponent<MonsterAnimationController>();
|
||||
Debug.Log($"[MonsterPrefabSetup] Added MonsterAnimationController component");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(monsterData.modelPath))
|
||||
{
|
||||
RemoveOldModel(prefab);
|
||||
|
||||
if (monsterData.modelPath.ToLower().EndsWith(".fbx"))
|
||||
{
|
||||
GameObject fbxModel = AssetDatabase.LoadAssetAtPath<GameObject>(monsterData.modelPath);
|
||||
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.modelPath}");
|
||||
|
||||
Avatar modelAvatar = fbxModel.GetComponent<Animator>()?.avatar;
|
||||
|
||||
if (modelAvatar != null)
|
||||
{
|
||||
Animator prefabAnimator = prefab.GetComponent<Animator>();
|
||||
if (prefabAnimator != null)
|
||||
{
|
||||
prefabAnimator.avatar = modelAvatar;
|
||||
Debug.Log($"[MonsterPrefabSetup] Applied Avatar: {modelAvatar.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[MonsterPrefabSetup] Could not load FBX model: {monsterData.modelPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var meshFilter = prefab.GetComponent<MeshFilter>();
|
||||
if (meshFilter != null)
|
||||
{
|
||||
Mesh mesh = AssetDatabase.LoadAssetAtPath<Mesh>(monsterData.modelPath);
|
||||
if (mesh != null)
|
||||
{
|
||||
meshFilter.sharedMesh = mesh;
|
||||
Debug.Log($"[MonsterPrefabSetup] Applied mesh: {monsterData.modelPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[MonsterPrefabSetup] Could not load mesh: {monsterData.modelPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(monsterData.animationControllerPath))
|
||||
{
|
||||
Animator animator = prefab.GetComponent<Animator>();
|
||||
if (animator != null)
|
||||
{
|
||||
RuntimeAnimatorController controller = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(monsterData.animationControllerPath);
|
||||
if (controller != null)
|
||||
{
|
||||
animator.runtimeAnimatorController = controller;
|
||||
Debug.Log($"[MonsterPrefabSetup] Applied Animator Controller: {monsterData.animationControllerPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[MonsterPrefabSetup] Could not load Animator Controller: {monsterData.animationControllerPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOldModel(GameObject prefab)
|
||||
{
|
||||
Transform oldModel = prefab.transform.Find("Model");
|
||||
if (oldModel != null)
|
||||
{
|
||||
GameObject.DestroyImmediate(oldModel.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d78ef5bde300404a89bb3af71df1610
|
||||
@@ -1,87 +0,0 @@
|
||||
using Northbound.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public class PlayerPrefabSetup : IPrefabSetup
|
||||
{
|
||||
public string GetTemplateName()
|
||||
{
|
||||
return "Player";
|
||||
}
|
||||
|
||||
public static void UpdatePlayerPrefab(PlayerData playerData)
|
||||
{
|
||||
string prefabPath = playerData.prefabPath;
|
||||
if (!prefabPath.EndsWith(".prefab"))
|
||||
{
|
||||
prefabPath += ".prefab";
|
||||
}
|
||||
|
||||
GameObject prefabObj = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefabObj == null)
|
||||
{
|
||||
Debug.LogError($"[PlayerPrefabSetup] Player prefab not found at: {prefabPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetupPrefabComponents(prefabObj, playerData);
|
||||
EditorUtility.SetDirty(prefabObj);
|
||||
PrefabUtility.SavePrefabAsset(prefabObj);
|
||||
Debug.Log($"[PlayerPrefabSetup] Player prefab updated from: {prefabPath}");
|
||||
}
|
||||
|
||||
public void SetupPrefab(GameObject prefab, ScriptableObject data)
|
||||
{
|
||||
if (!(data is PlayerData playerData))
|
||||
{
|
||||
Debug.LogWarning($"[PlayerPrefabSetup] Expected PlayerData, got {data.GetType().Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetupPrefabComponents(prefab, playerData);
|
||||
}
|
||||
|
||||
private static void SetupPrefabComponents(GameObject prefab, PlayerData playerData)
|
||||
{
|
||||
// PlayerStats에 모든 스탯 설정
|
||||
var playerStats = prefab.GetComponent<PlayerStats>();
|
||||
if (playerStats != null)
|
||||
{
|
||||
SerializedObject so = new SerializedObject(playerStats);
|
||||
so.FindProperty("baseMaxHp").intValue = playerData.maxHp;
|
||||
so.FindProperty("baseDamage").intValue = playerData.atkDamage;
|
||||
so.FindProperty("baseCapacity").intValue = playerData.capacity;
|
||||
so.FindProperty("baseManpower").floatValue = playerData.manpower;
|
||||
so.FindProperty("baseMoveSpeed").floatValue = playerData.moveSpeed;
|
||||
so.FindProperty("baseSight").floatValue = playerData.sight;
|
||||
so.FindProperty("baseAttackRange").floatValue = playerData.atkRange;
|
||||
so.ApplyModifiedProperties();
|
||||
Debug.Log($"[PlayerPrefabSetup] Updated PlayerStats: " +
|
||||
$"maxHp={playerData.maxHp}, damage={playerData.atkDamage}, " +
|
||||
$"capacity={playerData.capacity}, manpower={playerData.manpower}, " +
|
||||
$"moveSpeed={playerData.moveSpeed}, sight={playerData.sight}, " +
|
||||
$"attackRange={playerData.atkRange}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[PlayerPrefabSetup] PlayerStats component not found on prefab!");
|
||||
}
|
||||
|
||||
// AttackAction의 attackCooldown은 별도 설정 (스탯이 아님)
|
||||
var attackAction = prefab.GetComponent<AttackAction>();
|
||||
if (attackAction != null)
|
||||
{
|
||||
SerializedObject so = new SerializedObject(attackAction);
|
||||
so.FindProperty("attackCooldown").floatValue = playerData.atkIntervalSec;
|
||||
so.ApplyModifiedProperties();
|
||||
Debug.Log($"[PlayerPrefabSetup] Updated AttackAction: attackCooldown={playerData.atkIntervalSec}");
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(prefab);
|
||||
PrefabUtility.SavePrefabAsset(prefab);
|
||||
Debug.Log($"[PlayerPrefabSetup] Player prefab updated successfully from {playerData.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5d9fa32f3480914abc8b2320d315554
|
||||
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Northbound.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// ScriptableObject를 단일 원본으로 사용하는 게임 데이터의 참조와 값 범위를 검사합니다.
|
||||
/// </summary>
|
||||
public static class ScriptableObjectDataValidator
|
||||
{
|
||||
private const string DataRoot = "Assets/Data/ScriptableObjects";
|
||||
|
||||
[MenuItem("Tools/Northbound/Data/Validate All ScriptableObjects")]
|
||||
public static void ValidateAll()
|
||||
{
|
||||
int errors = 0;
|
||||
int warnings = 0;
|
||||
|
||||
List<TowerData> towers = LoadAll<TowerData>();
|
||||
List<MonsterData> monsters = LoadAll<MonsterData>();
|
||||
List<CreepData> creeps = LoadAll<CreepData>();
|
||||
List<PlayerData> players = LoadAll<PlayerData>();
|
||||
List<UpgradeData> upgrades = LoadAll<UpgradeData>();
|
||||
List<DefaultSettingsData> settings = LoadAll<DefaultSettingsData>();
|
||||
|
||||
ValidateUniqueIds(towers, data => data.id, "Tower", ref errors);
|
||||
ValidateUniqueIds(monsters, data => data.id, "Monster", ref errors);
|
||||
ValidateUniqueIds(creeps, data => data.id, "Creep", ref errors);
|
||||
ValidateUniqueIds(players, data => data.id, "Player", ref errors);
|
||||
ValidateUniqueIds(upgrades, data => data.id, "Upgrade", ref errors);
|
||||
ValidateUniqueIds(settings, data => data.id, "DefaultSettings", ref errors);
|
||||
|
||||
ValidateTowers(towers, ref errors, ref warnings);
|
||||
ValidateUnits(monsters, data => data.id, data => data.maxHp, data => data.moveSpeed,
|
||||
data => data.cost, data => data.weight, "Monster", ref errors);
|
||||
ValidateUnits(creeps, data => data.id, data => data.maxHp, data => data.moveSpeed,
|
||||
data => data.cost, data => data.weight, "Creep", ref errors);
|
||||
ValidatePlayers(players, ref errors);
|
||||
ValidateUpgrades(upgrades, ref errors, ref warnings);
|
||||
ValidateSettings(settings, ref errors, ref warnings);
|
||||
|
||||
string summary = $"[SO Data Validation] {towers.Count} towers, {monsters.Count} monsters, " +
|
||||
$"{creeps.Count} creeps, {players.Count} players, {upgrades.Count} upgrades, " +
|
||||
$"{settings.Count} settings — {errors} errors, {warnings} warnings";
|
||||
|
||||
if (errors > 0)
|
||||
Debug.LogError(summary);
|
||||
else if (warnings > 0)
|
||||
Debug.LogWarning(summary);
|
||||
else
|
||||
Debug.Log(summary);
|
||||
}
|
||||
|
||||
private static List<T> LoadAll<T>() where T : ScriptableObject
|
||||
{
|
||||
return AssetDatabase.FindAssets($"t:{typeof(T).Name}", new[] { DataRoot })
|
||||
.Select(AssetDatabase.GUIDToAssetPath)
|
||||
.Select(AssetDatabase.LoadAssetAtPath<T>)
|
||||
.Where(asset => asset != null)
|
||||
.OrderBy(asset => asset.name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static void ValidateUniqueIds<T>(IEnumerable<T> assets, Func<T, int> getId,
|
||||
string dataType, ref int errors) where T : UnityEngine.Object
|
||||
{
|
||||
var ids = new Dictionary<int, T>();
|
||||
foreach (T asset in assets)
|
||||
{
|
||||
int id = getId(asset);
|
||||
if (id <= 0)
|
||||
{
|
||||
Error($"{dataType} ID는 1 이상이어야 합니다: {asset.name}", asset, ref errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ids.TryGetValue(id, out T existing))
|
||||
Error($"{dataType} ID {id}가 중복됩니다: {existing.name}, {asset.name}", asset, ref errors);
|
||||
else
|
||||
ids.Add(id, asset);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTowers(List<TowerData> towers, ref int errors, ref int warnings)
|
||||
{
|
||||
var towerIds = towers.Select(data => data.id).ToHashSet();
|
||||
foreach (TowerData data in towers)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data.buildingName))
|
||||
Error($"Tower {data.id}의 표시 이름이 비어 있습니다.", data, ref errors);
|
||||
if (data.prefab == null)
|
||||
Error($"Tower {data.id}의 프리팹이 없습니다.", data, ref errors);
|
||||
if (data.mana < 0 || data.manpower < 0)
|
||||
Error($"Tower {data.id}의 비용은 음수일 수 없습니다.", data, ref errors);
|
||||
if (data.maxHp <= 0)
|
||||
Error($"Tower {data.id}의 체력은 1 이상이어야 합니다.", data, ref errors);
|
||||
if (data.sizeX <= 0 || data.sizeY <= 0 || data.sizeZ <= 0)
|
||||
Error($"Tower {data.id}의 배치 크기는 모두 1 이상이어야 합니다.", data, ref errors);
|
||||
if (data.upgradeTo > 0 && !towerIds.Contains(data.upgradeTo))
|
||||
Error($"Tower {data.id}의 업그레이드 대상 {data.upgradeTo}가 없습니다.", data, ref errors);
|
||||
if (data.atkDamage > 0 && data.atkRange <= 0)
|
||||
Warn($"Tower {data.id}는 공격력이 있지만 사거리가 없습니다.", data, ref warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateUnits<T>(IEnumerable<T> units, Func<T, int> getId,
|
||||
Func<T, int> getHealth, Func<T, float> getSpeed, Func<T, int> getCost,
|
||||
Func<T, float> getWeight, string dataType, ref int errors) where T : UnityEngine.Object
|
||||
{
|
||||
foreach (T data in units)
|
||||
{
|
||||
int id = getId(data);
|
||||
if (getHealth(data) <= 0)
|
||||
Error($"{dataType} {id}의 체력은 1 이상이어야 합니다.", data, ref errors);
|
||||
if (getSpeed(data) < 0)
|
||||
Error($"{dataType} {id}의 이동 속도는 음수일 수 없습니다.", data, ref errors);
|
||||
if (getCost(data) <= 0 || getWeight(data) <= 0)
|
||||
Error($"{dataType} {id}의 생성 비용과 가중치는 0보다 커야 합니다.", data, ref errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePlayers(IEnumerable<PlayerData> players, ref int errors)
|
||||
{
|
||||
foreach (PlayerData data in players)
|
||||
{
|
||||
if (data.maxHp <= 0 || data.moveSpeed <= 0)
|
||||
Error($"Player {data.id}의 체력과 이동 속도는 0보다 커야 합니다.", data, ref errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateUpgrades(List<UpgradeData> upgrades, ref int errors, ref int warnings)
|
||||
{
|
||||
var ids = upgrades.Select(data => data.id).ToHashSet();
|
||||
foreach (UpgradeData data in upgrades)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data.memo))
|
||||
Warn($"Upgrade {data.id}의 표시용 메모가 비어 있습니다.", data, ref warnings);
|
||||
if (data.mana < 0)
|
||||
Error($"Upgrade {data.id}의 비용은 음수일 수 없습니다.", data, ref errors);
|
||||
if (data.effectStatList.Count != data.effectOpList.Count ||
|
||||
data.effectStatList.Count != data.effectValueList.Count)
|
||||
Error($"Upgrade {data.id}의 효과 목록 길이가 서로 다릅니다.", data, ref errors);
|
||||
|
||||
foreach (int prerequisiteId in data.requireUpgradeId)
|
||||
{
|
||||
if (prerequisiteId == data.id)
|
||||
Error($"Upgrade {data.id}가 자기 자신을 선행 조건으로 참조합니다.", data, ref errors);
|
||||
else if (!ids.Contains(prerequisiteId))
|
||||
Error($"Upgrade {data.id}의 선행 조건 {prerequisiteId}가 없습니다.", data, ref errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSettings(IEnumerable<DefaultSettingsData> settings,
|
||||
ref int errors, ref int warnings)
|
||||
{
|
||||
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (DefaultSettingsData data in settings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data.key))
|
||||
{
|
||||
Warn($"DefaultSettings {data.id}의 키가 비어 있습니다.", data, ref warnings);
|
||||
continue;
|
||||
}
|
||||
|
||||
string scopedKey = $"{data.scene}:{data.key}";
|
||||
if (!keys.Add(scopedKey))
|
||||
Error($"DefaultSettings 키가 중복됩니다: {scopedKey}", data, ref errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Error(string message, UnityEngine.Object context, ref int count)
|
||||
{
|
||||
count++;
|
||||
Debug.LogError($"[SO Data Validation] {message}", context);
|
||||
}
|
||||
|
||||
private static void Warn(string message, UnityEngine.Object context, ref int count)
|
||||
{
|
||||
count++;
|
||||
Debug.LogWarning($"[SO Data Validation] {message}", context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d76938c8f2648aa8da462d23bb05021
|
||||
@@ -1,204 +0,0 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using Northbound;
|
||||
|
||||
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)
|
||||
{
|
||||
Transform t = go.transform;
|
||||
t.localPosition = Vector3.zero;
|
||||
t.localRotation = Quaternion.identity;
|
||||
t.localScale = Vector3.one;
|
||||
|
||||
if (go.GetComponent<NetworkObject>() == null)
|
||||
go.AddComponent<NetworkObject>();
|
||||
|
||||
if (go.GetComponent<Building>() == null)
|
||||
go.AddComponent<Building>();
|
||||
|
||||
if (go.GetComponent<BoxCollider>() == null)
|
||||
{
|
||||
BoxCollider collider = go.AddComponent<BoxCollider>();
|
||||
collider.size = new Vector3(1f, 2f, 1f);
|
||||
collider.center = new Vector3(0f, 1f, 0f);
|
||||
}
|
||||
|
||||
if (go.GetComponent<NavMeshObstacle>() == null)
|
||||
{
|
||||
NavMeshObstacle obstacle = go.AddComponent<NavMeshObstacle>();
|
||||
obstacle.shape = NavMeshObstacleShape.Box;
|
||||
obstacle.size = new Vector3(1f, 1f, 1f);
|
||||
obstacle.center = new Vector3(0f, 0.5f, 0f);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b0786fac9db245429b33a2597a2d7b4
|
||||
@@ -1,80 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Northbound;
|
||||
using Northbound.Data;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public class TowerPopulator
|
||||
{
|
||||
private const string TOWER_PREFAB_PATH = "Assets/Prefabs/Tower";
|
||||
private const string TOWER_DATA_PATH = "Assets/Data/ScriptableObjects";
|
||||
|
||||
[MenuItem("Northbound/Diagnose Tower System")]
|
||||
public static void DiagnoseTowerSystem()
|
||||
{
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
Debug.Log($"<color=cyan>[TowerPopulator] DIAGNOSING TOWER SYSTEM</color>");
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
|
||||
string[] prefabGuids = AssetDatabase.FindAssets("t:Tower", new[] { TOWER_PREFAB_PATH });
|
||||
Debug.Log($"<color=cyan>Tower Prefabs in Assets/Prefabs/Tower/:</color>");
|
||||
if (prefabGuids.Length == 0)
|
||||
{
|
||||
Debug.Log($"<color=red>✗ No tower prefabs found!</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string guid in prefabGuids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
||||
Building building = prefab?.GetComponent<Building>();
|
||||
string towerStatus = building != null && building.buildingData != null ? "<color=green>✓</color>" : "<color=red>✗</color>";
|
||||
string towerDataName = building?.buildingData?.name ?? "MISSING";
|
||||
Debug.Log($" {towerStatus} {prefab.name} - Building: {building != null}, TowerData: {towerDataName}");
|
||||
}
|
||||
}
|
||||
|
||||
string[] towerDataGuids = AssetDatabase.FindAssets("t:TowerData", new[] { TOWER_DATA_PATH });
|
||||
Debug.Log($"<color=cyan>TowerData assets in Assets/Data/ScriptableObjects/:</color>");
|
||||
if (towerDataGuids.Length == 0)
|
||||
{
|
||||
Debug.Log($"<color=yellow>⚠ No TowerData assets found - Run 'Tools > Data > Import All CSV' first!</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string guid in towerDataGuids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
TowerData data = AssetDatabase.LoadAssetAtPath<TowerData>(assetPath);
|
||||
string prefabStatus = data?.prefab != null ? "<color=green>✓</color>" : "<color=red>✗</color>";
|
||||
Debug.Log($" {prefabStatus} {data.name} - Prefab: {data?.prefab?.name ?? "MISSING"}, BuildingName: {data?.buildingName}");
|
||||
}
|
||||
}
|
||||
|
||||
BuildingManager buildingManager = GameObject.FindFirstObjectByType<BuildingManager>();
|
||||
Debug.Log($"<color=cyan>BuildingManager in Scene:</color>");
|
||||
if (buildingManager == null)
|
||||
{
|
||||
Debug.Log($"<color=red>✗ BuildingManager not found in scene!</color>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"<color=green>✓ BuildingManager found: {buildingManager.gameObject.name}</color>");
|
||||
Debug.Log($"<color=cyan> Available Buildings: {buildingManager.availableBuildings.Count}</color>");
|
||||
foreach (var building in buildingManager.availableBuildings)
|
||||
{
|
||||
string status = building?.prefab != null ? "<color=green>✓</color>" : "<color=red>✗</color>";
|
||||
string isTower = building is TowerData ? "<color=green>[Tower]</color>" : "<color>yellow>[Building]</color>";
|
||||
Debug.Log($" {status} {isTower} {building?.name ?? "MISSING"} - {building?.buildingName}");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
Debug.Log($"<color=cyan>[TowerPopulator] DIAGNOSIS COMPLETE</color>");
|
||||
Debug.Log($"<color=cyan>========================================</color>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 069a105319ca9cf4a8323e1d5357960f
|
||||
@@ -1,109 +0,0 @@
|
||||
using Northbound.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public class TowerPrefabSetup : IPrefabSetup
|
||||
{
|
||||
public string GetTemplateName()
|
||||
{
|
||||
return "TowerTemplate";
|
||||
}
|
||||
|
||||
public void SetupPrefab(GameObject prefab, ScriptableObject data)
|
||||
{
|
||||
if (!(data is TowerData towerData))
|
||||
{
|
||||
Debug.LogWarning($"[TowerPrefabSetup] Expected TowerData, got {data.GetType().Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set prefab reference
|
||||
towerData.prefab = prefab;
|
||||
|
||||
Transform modelTransform = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(towerData.modelPath))
|
||||
{
|
||||
RemoveOldModel(prefab);
|
||||
|
||||
if (towerData.modelPath.ToLower().EndsWith(".fbx"))
|
||||
{
|
||||
GameObject fbxModel = AssetDatabase.LoadAssetAtPath<GameObject>(towerData.modelPath);
|
||||
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;
|
||||
|
||||
modelTransform = fbxInstance.transform;
|
||||
|
||||
Debug.Log($"[TowerPrefabSetup] Applied FBX model: {towerData.modelPath} with scale {towerData.sizeX}x{towerData.sizeZ}x{towerData.sizeY}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[TowerPrefabSetup] Could not load FBX model: {towerData.modelPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var meshFilter = prefab.GetComponent<MeshFilter>();
|
||||
if (meshFilter == null)
|
||||
{
|
||||
meshFilter = prefab.AddComponent<MeshFilter>();
|
||||
}
|
||||
|
||||
var renderer = prefab.GetComponent<MeshRenderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
renderer = prefab.AddComponent<MeshRenderer>();
|
||||
}
|
||||
|
||||
Mesh mesh = AssetDatabase.LoadAssetAtPath<Mesh>(towerData.modelPath);
|
||||
if (mesh != null)
|
||||
{
|
||||
meshFilter.sharedMesh = mesh;
|
||||
|
||||
modelTransform = renderer.transform;
|
||||
|
||||
Debug.Log($"[TowerPrefabSetup] Applied mesh: {towerData.modelPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[TowerPrefabSetup] Could not load mesh: {towerData.modelPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var collider = prefab.GetComponent<BoxCollider>();
|
||||
if (collider == null)
|
||||
{
|
||||
collider = prefab.AddComponent<BoxCollider>();
|
||||
}
|
||||
collider.size = new Vector3(towerData.sizeX, towerData.sizeY, towerData.sizeZ);
|
||||
collider.center = new Vector3(0f, towerData.sizeY / 2f, 0f);
|
||||
|
||||
var navObstacle = prefab.GetComponent<NavMeshObstacle>();
|
||||
if (navObstacle == null)
|
||||
{
|
||||
navObstacle = prefab.AddComponent<NavMeshObstacle>();
|
||||
navObstacle.shape = NavMeshObstacleShape.Box;
|
||||
}
|
||||
navObstacle.size = new Vector3(towerData.sizeX, towerData.sizeY, towerData.sizeZ);
|
||||
navObstacle.center = new Vector3(0f, towerData.sizeY / 2f, 0f);
|
||||
}
|
||||
|
||||
private void RemoveOldModel(GameObject prefab)
|
||||
{
|
||||
Transform oldModel = prefab.transform.Find("Model");
|
||||
if (oldModel != null)
|
||||
{
|
||||
GameObject.DestroyImmediate(oldModel.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea17487cde4842d45baf5eb92943142a
|
||||
@@ -1,23 +0,0 @@
|
||||
using Northbound.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Upgrade 타입은 프리팹이 필요 없으므로 빈 구현
|
||||
/// </summary>
|
||||
public class UpgradePrefabSetup : IPrefabSetup
|
||||
{
|
||||
public string GetTemplateName()
|
||||
{
|
||||
return ""; // 템플릿 필요 없음
|
||||
}
|
||||
|
||||
public void SetupPrefab(GameObject prefab, ScriptableObject data)
|
||||
{
|
||||
// Upgrade는 프리팹이 필요 없음
|
||||
Debug.LogWarning($"[UpgradePrefabSetup] Upgrade 타입은 프리팹이 필요하지 않습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f33f085cffff92c489c74fc11a89fa49
|
||||
@@ -1027,7 +1027,7 @@ namespace Northbound
|
||||
|
||||
private void FindCore()
|
||||
{
|
||||
_core = FindFirstObjectByType<Core>();
|
||||
_core = FindAnyObjectByType<Core>();
|
||||
if (_core != null)
|
||||
{
|
||||
_coreTransform = _core.transform;
|
||||
@@ -1170,4 +1170,4 @@ namespace Northbound
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace Northbound
|
||||
// 방법 3: 직접 검색 (IsLocalPlayer인 플레이어 찾기)
|
||||
if (localPlayer == null)
|
||||
{
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>();
|
||||
foreach (var player in allPlayers)
|
||||
{
|
||||
if (player.IsLocalPlayer)
|
||||
@@ -412,4 +412,4 @@ namespace Northbound
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ namespace Northbound
|
||||
// 방법 3: 직접 검색 (IsLocalPlayer인 플레이어 찾기)
|
||||
if (localPlayer == null)
|
||||
{
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>();
|
||||
foreach (var player in allPlayers)
|
||||
{
|
||||
if (player.IsLocalPlayer)
|
||||
@@ -311,4 +311,4 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace Northbound
|
||||
// 방법 3: 직접 검색 (IsLocalPlayer인 플레이어 찾기)
|
||||
if (localPlayer == null)
|
||||
{
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>();
|
||||
foreach (var player in allPlayers)
|
||||
{
|
||||
if (player.IsLocalPlayer)
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace Northbound
|
||||
|
||||
private void FindImportantPositions()
|
||||
{
|
||||
Core core = FindFirstObjectByType<Core>();
|
||||
Core core = FindAnyObjectByType<Core>();
|
||||
if (core != null)
|
||||
{
|
||||
_corePosition = new Vector2(core.transform.position.x, core.transform.position.z);
|
||||
|
||||
@@ -21,10 +21,6 @@ namespace Northbound
|
||||
[Tooltip("Death trigger parameter name in Animator")]
|
||||
public string dieTriggerParam = "Die";
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("Auto-load animator controller from MonsterData")]
|
||||
public bool autoLoadFromMonsterData = true;
|
||||
|
||||
[Tooltip("Debug logging")]
|
||||
public bool debugLogging = false;
|
||||
|
||||
@@ -60,10 +56,6 @@ namespace Northbound
|
||||
_enemyUnit.OnDeath += HandleDeath;
|
||||
}
|
||||
|
||||
if (autoLoadFromMonsterData)
|
||||
{
|
||||
LoadAnimatorController();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
@@ -95,29 +87,6 @@ namespace Northbound
|
||||
Debug.Log($"[MonsterAnimationController] Triggered death animation (killer: {killerId})", this);
|
||||
}
|
||||
|
||||
private void LoadAnimatorController()
|
||||
{
|
||||
var monsterDataComponent = GetComponent<MonsterDataComponent>();
|
||||
if (monsterDataComponent != null && monsterDataComponent.monsterData != null)
|
||||
{
|
||||
string animatorPath = monsterDataComponent.monsterData.animationControllerPath;
|
||||
if (!string.IsNullOrEmpty(animatorPath))
|
||||
{
|
||||
RuntimeAnimatorController controller = Resources.Load<RuntimeAnimatorController>(animatorPath);
|
||||
if (controller != null)
|
||||
{
|
||||
_animator.runtimeAnimatorController = controller;
|
||||
if (debugLogging)
|
||||
Debug.Log($"[MonsterAnimationController] Loaded animator from {animatorPath}", this);
|
||||
}
|
||||
else if (debugLogging)
|
||||
{
|
||||
Debug.LogWarning($"[MonsterAnimationController] Could not load animator from {animatorPath}", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsSpawned) return;
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Northbound
|
||||
|
||||
private void FindSpawnPoints()
|
||||
{
|
||||
PlayerSpawnPoint[] points = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
|
||||
PlayerSpawnPoint[] points = FindObjectsByType<PlayerSpawnPoint>();
|
||||
|
||||
// spawnIndex로 정렬
|
||||
var sortedPoints = points.OrderBy(p => p.spawnIndex == -1 ? int.MaxValue : p.spawnIndex);
|
||||
@@ -200,4 +200,4 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class NetworkDebug : MonoBehaviour
|
||||
}
|
||||
|
||||
// 2. 스폰 포인트 검색
|
||||
PlayerSpawnPoint[] spawnPoints = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
|
||||
PlayerSpawnPoint[] spawnPoints = FindObjectsByType<PlayerSpawnPoint>();
|
||||
Debug.Log($"PlayerSpawnPoints: Found {spawnPoints.Length} objects");
|
||||
foreach (var sp in spawnPoints)
|
||||
{
|
||||
@@ -24,14 +24,14 @@ public class NetworkDebug : MonoBehaviour
|
||||
}
|
||||
|
||||
// 3. NetworkConnectionHandler 확인
|
||||
var connectionHandler = FindFirstObjectByType<NetworkConnectionHandler>();
|
||||
var connectionHandler = FindAnyObjectByType<NetworkConnectionHandler>();
|
||||
if (connectionHandler != null)
|
||||
{
|
||||
Debug.Log($"NetworkConnectionHandler: spawnPoints.Count={connectionHandler.spawnPoints.Count}, findAuto={connectionHandler.findSpawnPointsAutomatically}");
|
||||
}
|
||||
|
||||
// 4. NetworkSpawnManager 확인
|
||||
var spawnManager = FindFirstObjectByType<Northbound.NetworkSpawnManager>();
|
||||
var spawnManager = FindAnyObjectByType<Northbound.NetworkSpawnManager>();
|
||||
if (spawnManager != null)
|
||||
{
|
||||
Debug.Log($"NetworkSpawnManager: spawnPoints.Count={spawnManager.spawnPoints.Count}, findAuto={spawnManager.findSpawnPointsAutomatically}");
|
||||
@@ -48,7 +48,7 @@ public class NetworkDebug : MonoBehaviour
|
||||
{
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
var players = GameObject.FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var players = GameObject.FindObjectsByType<NetworkPlayerController>();
|
||||
if (players != null && players.Length > 0)
|
||||
{
|
||||
Debug.Log($"Found {players.Length} player(s) - checking equipment...");
|
||||
@@ -77,7 +77,7 @@ public class NetworkDebug : MonoBehaviour
|
||||
}
|
||||
|
||||
// 5초 후에도 플레이어가 없는지 확인
|
||||
var finalCheck = GameObject.FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var finalCheck = GameObject.FindObjectsByType<NetworkPlayerController>();
|
||||
if (finalCheck == null || finalCheck.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"Player did not spawn within 5 seconds!");
|
||||
|
||||
@@ -61,7 +61,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
||||
// 이 플레이어가 로컬 플레이어인지 확인
|
||||
|
||||
|
||||
public bool IsLocalPlayer => _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId;
|
||||
public new bool IsLocalPlayer => _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId;
|
||||
|
||||
public ulong OwnerPlayerId => _ownerPlayerId.Value;
|
||||
|
||||
@@ -107,7 +107,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
||||
// 로컬 플레이어만 씬에서 리스폰 UI 찾기
|
||||
if (IsLocalPlayer)
|
||||
{
|
||||
_respawnCountdownUI = FindFirstObjectByType<RespawnCountdownUI>();
|
||||
_respawnCountdownUI = FindAnyObjectByType<RespawnCountdownUI>();
|
||||
}
|
||||
|
||||
// 체력바 생성
|
||||
@@ -128,7 +128,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
||||
// 로컬 플레이어가 되면 리스폰 UI 참조 가져오기
|
||||
if (IsLocalPlayer && _respawnCountdownUI == null)
|
||||
{
|
||||
_respawnCountdownUI = FindFirstObjectByType<RespawnCountdownUI>();
|
||||
_respawnCountdownUI = FindAnyObjectByType<RespawnCountdownUI>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
||||
if (!IsLocalPlayer) return;
|
||||
if (_inputActions != null) return; // 이미 초기화됨
|
||||
|
||||
var vcam = GameObject.FindFirstObjectByType<CinemachineCamera>();
|
||||
var vcam = GameObject.FindAnyObjectByType<CinemachineCamera>();
|
||||
|
||||
if (vcam != null)
|
||||
{
|
||||
@@ -524,7 +524,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
||||
_currentHealth.Value = GetMaxHealth();
|
||||
|
||||
// 스폰 포인트로 이동
|
||||
var spawnPoints = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
|
||||
var spawnPoints = FindObjectsByType<PlayerSpawnPoint>();
|
||||
if (spawnPoints.Length > 0)
|
||||
{
|
||||
var spawnPoint = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)];
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace Northbound
|
||||
|
||||
private void FindSpawnPoints()
|
||||
{
|
||||
PlayerSpawnPoint[] points = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
|
||||
PlayerSpawnPoint[] points = FindObjectsByType<PlayerSpawnPoint>();
|
||||
|
||||
// spawnIndex로 정렬
|
||||
var sortedPoints = points.OrderBy(p => p.spawnIndex == -1 ? int.MaxValue : p.spawnIndex);
|
||||
@@ -131,4 +131,4 @@ namespace Northbound
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Northbound
|
||||
private bool _isInputInitialized = false;
|
||||
|
||||
// 로컬 플레이어인지 확인
|
||||
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
||||
private new bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
||||
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
|
||||
|
||||
private void Awake()
|
||||
@@ -137,4 +137,4 @@ namespace Northbound
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Northbound
|
||||
public IInteractable CurrentUnavailableInteractable => _unavailableInteractable;
|
||||
|
||||
// 로컬 플레이어인지 확인
|
||||
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
||||
private new bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
||||
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
|
||||
|
||||
private void Awake()
|
||||
@@ -412,4 +412,4 @@ namespace Northbound
|
||||
// 입력 정리는 NetworkPlayerController에서 담당
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Northbound
|
||||
private NetworkPlayerController _networkPlayerController;
|
||||
private PlayerStats _playerStats;
|
||||
|
||||
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
||||
private new bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
||||
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
|
||||
|
||||
private void Awake()
|
||||
|
||||
@@ -18,7 +18,7 @@ public class SimpleCameraZFollow : MonoBehaviour
|
||||
|
||||
private void FindPlayerTarget()
|
||||
{
|
||||
var players = FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var players = FindObjectsByType<NetworkPlayerController>();
|
||||
foreach (var player in players)
|
||||
{
|
||||
if (player.IsOwner)
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace Northbound
|
||||
}
|
||||
|
||||
// 모든 NetworkPlayerController 찾기
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
|
||||
var allPlayers = FindObjectsByType<NetworkPlayerController>();
|
||||
|
||||
// 기존 매핑 클리어
|
||||
_playerControllers.Clear();
|
||||
|
||||
@@ -5,24 +5,21 @@ using Northbound.Data;
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// 업그레이드 데이터를 자동으로 로드하고 관리하는 싱글톤
|
||||
/// Resources 폴더에서 모든 UpgradeData를 자동으로 로드
|
||||
/// 씬에서 직접 참조한 UpgradeData ScriptableObject를 조회하는 싱글톤
|
||||
/// </summary>
|
||||
public class UpgradeDatabase : MonoBehaviour
|
||||
{
|
||||
private static UpgradeDatabase _instance;
|
||||
public static UpgradeDatabase Instance => _instance;
|
||||
|
||||
// Resources에서 자동 로드된 데이터
|
||||
private List<UpgradeData> _upgradeDataList = new List<UpgradeData>();
|
||||
[Header("ScriptableObject Data")]
|
||||
[Tooltip("업그레이드 데이터의 단일 원본 목록입니다. Resources 자동 검색 대신 SO를 직접 등록합니다.")]
|
||||
[SerializeField] private List<UpgradeData> _upgradeDataList = new List<UpgradeData>();
|
||||
|
||||
// 조회용 딕셔너리
|
||||
private Dictionary<int, UpgradeData> _upgradeById = new Dictionary<int, UpgradeData>();
|
||||
private Dictionary<string, List<UpgradeData>> _upgradesByCategory = new Dictionary<string, List<UpgradeData>>();
|
||||
|
||||
// Resources 경로
|
||||
private const string UPGRADE_RESOURCES_PATH = "Data/ScriptableObjects/Upgrade";
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
@@ -32,23 +29,9 @@ namespace Northbound
|
||||
}
|
||||
|
||||
_instance = this;
|
||||
LoadUpgradeData();
|
||||
BuildLookupTables();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resources에서 모든 UpgradeData 자동 로드
|
||||
/// </summary>
|
||||
private void LoadUpgradeData()
|
||||
{
|
||||
_upgradeDataList.Clear();
|
||||
|
||||
var upgrades = Resources.LoadAll<UpgradeData>(UPGRADE_RESOURCES_PATH);
|
||||
_upgradeDataList.AddRange(upgrades);
|
||||
|
||||
Debug.Log($"[UpgradeDatabase] {_upgradeDataList.Count}개의 업그레이드 데이터를 로드했습니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 조회용 테이블 구축
|
||||
/// </summary>
|
||||
@@ -66,6 +49,11 @@ namespace Northbound
|
||||
{
|
||||
_upgradeById[upgrade.id] = upgrade;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[UpgradeDatabase] 중복 업그레이드 ID: {upgrade.id}", upgrade);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 카테고리로 조회
|
||||
string category = upgrade.upgradeCategory;
|
||||
@@ -82,7 +70,6 @@ namespace Northbound
|
||||
/// </summary>
|
||||
public void ReloadData()
|
||||
{
|
||||
LoadUpgradeData();
|
||||
BuildLookupTables();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,10 +108,10 @@ namespace Northbound
|
||||
|
||||
private void FindCore()
|
||||
{
|
||||
Core[] cores = FindObjectsByType<Core>(FindObjectsSortMode.InstanceID);
|
||||
if (cores.Length > 0)
|
||||
Core core = FindAnyObjectByType<Core>();
|
||||
if (core != null)
|
||||
{
|
||||
_core = cores[0];
|
||||
_core = core;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user