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
@@ -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);
}
}
}