Unity 6.5 업그레이드와 신규 프로토타입 기반 구축

This commit is contained in:
2026-08-31 20:31:37 +09:00
parent 7fc457a384
commit 2f6e80fd57
233 changed files with 15070 additions and 6481 deletions
-410
View File
@@ -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
-118
View File
@@ -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
-10
View File
@@ -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
-118
View File
@@ -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
-204
View File
@@ -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
-80
View File
@@ -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
-109
View File
@@ -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
+2 -2
View File
@@ -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
}
}
}
+2 -2
View File
@@ -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
}
}
}
}
+2 -2
View File
@@ -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
}
}
}
}
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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
}
}
}
}
}
+5 -5
View File
@@ -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!");
+5 -5
View File
@@ -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)];
+2 -2
View File
@@ -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();
}
}
}
}
+2 -2
View File
@@ -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();
}
}
}
}
+2 -2
View File
@@ -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에서 담당
}
}
}
}
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -127,7 +127,7 @@ namespace Northbound
}
// 모든 NetworkPlayerController 찾기
var allPlayers = FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
var allPlayers = FindObjectsByType<NetworkPlayerController>();
// 기존 매핑 클리어
_playerControllers.Clear();
+9 -22
View File
@@ -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();
}
+3 -3
View File
@@ -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;
}
}