diff --git a/Assembly-CSharp-Editor.csproj b/Assembly-CSharp-Editor.csproj
index 6d7e972..7b1079b 100644
--- a/Assembly-CSharp-Editor.csproj
+++ b/Assembly-CSharp-Editor.csproj
@@ -1178,30 +1178,18 @@
C:\Program Files\Unity\Hub\Editor\6000.3.3f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Xml.XPath.XDocument.dll
False
-
- Library\ScriptAssemblies\UnityEditor.TestRunner.dll
- False
-
Library\ScriptAssemblies\UnityEngine.TestRunner.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricEvents.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.dll
+
+ Library\ScriptAssemblies\UnityEditor.TestRunner.dll
False
Library\ScriptAssemblies\Unity.2D.Common.Path.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Netcode.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.2D.Sprite.Editor.dll
False
@@ -1226,14 +1214,6 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricTestData.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSolutionInterface.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStats.dll
- False
-
Library\ScriptAssemblies\Unity.Rendering.LightTransport.Editor.dll
False
@@ -1246,22 +1226,10 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Core.ShaderLibrary.dll
False
-
- Library\ScriptAssemblies\Unity.2D.IK.Runtime.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Runtime.dll
- False
-
Library\ScriptAssemblies\Unity.VisualScripting.SettingsProvider.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.dll
- False
-
Library\ScriptAssemblies\Unity.Networking.Editor.dll
False
@@ -1274,20 +1242,8 @@
Library\ScriptAssemblies\Unity.Splines.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Editor.Overrides.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkProfiler.Runtime.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Timeline.dll
+
+ Library\ScriptAssemblies\PPv2URPConverters.dll
False
@@ -1298,12 +1254,12 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Configuration.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Tooling.Editor.dll
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.Editor.dll
False
-
- Library\ScriptAssemblies\PPv2URPConverters.dll
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.Updater.dll
False
@@ -1322,6 +1278,170 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Runtime.dll
False
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Configuration.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.UnifiedRayTracing.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Aseprite.Common.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Rider.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Aseprite.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.Editor.ConversionSystem.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.VisualScripting.Flow.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.TextMeshPro.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Tilemap.Extras.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.VisualStudio.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Tilemap.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Profiling.Core.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.GPUDriven.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.VisualScripting.State.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Shaders.dll
+ False
+
+
+ Library\ScriptAssemblies\UnityEngine.UI.dll
+ False
+
+
+ Library\ScriptAssemblies\com.unity.multiplayer.tools.window.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Config.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.Common.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Sprite.AIIntegration.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.SpriteShape.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.Shared.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.InputSystem.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Splines.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricEvents.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Netcode.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSolutionInterface.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStats.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.IK.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Animation.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Editor.Overrides.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkProfiler.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Timeline.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Tooling.Editor.dll
+ False
+
Library\ScriptAssemblies\Unity.VisualScripting.Shared.Editor.dll
False
@@ -1334,10 +1454,6 @@
Library\ScriptAssemblies\Unity.PlasticSCM.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Animation.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Burst.Editor.dll
False
@@ -1354,18 +1470,10 @@
Library\ScriptAssemblies\Unity.Mathematics.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Configuration.dll
- False
-
Library\ScriptAssemblies\Unity.Cinemachine.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.UnifiedRayTracing.Runtime.dll
- False
-
Library\ScriptAssemblies\Unity.Cinemachine.dll
False
@@ -1374,26 +1482,10 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Editor.Visualization.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Aseprite.Common.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Rider.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Aseprite.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.2D.SpriteShape.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.UIElements.dll
False
@@ -1406,22 +1498,6 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Core.Runtime.dll
False
-
- Library\ScriptAssemblies\Unity.VisualScripting.Flow.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
- False
-
-
- Library\ScriptAssemblies\Unity.TextMeshPro.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.2D.Tilemap.Extras.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Netcode.PackageChecker.Editor.dll
False
@@ -1446,14 +1522,6 @@
Library\ScriptAssemblies\Unity.2D.SpriteShape.Runtime.dll
False
-
- Library\ScriptAssemblies\Unity.VisualStudio.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.2D.Tilemap.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.2D.Common.Runtime.dll
False
@@ -1462,30 +1530,14 @@
Library\ScriptAssemblies\Unity.Collections.dll
False
-
- Library\ScriptAssemblies\Unity.Profiling.Core.dll
- False
-
Library\ScriptAssemblies\Unity.Networking.Transport.dll
False
-
- Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll
- False
-
Library\ScriptAssemblies\Unity.Timeline.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.VisualScripting.State.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.GPUDriven.Runtime.dll
- False
-
Library\ScriptAssemblies\Unity.Mathematics.dll
False
@@ -1494,14 +1546,6 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkProfiler.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Shaders.dll
- False
-
-
- Library\ScriptAssemblies\UnityEngine.UI.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Center.Common.dll
False
@@ -1522,22 +1566,10 @@
Library\ScriptAssemblies\Unity.Multiplayer.Center.Editor.dll
False
-
- Library\ScriptAssemblies\com.unity.multiplayer.tools.window.dll
- False
-
Library\ScriptAssemblies\Unity.VisualScripting.State.dll
False
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Config.Runtime.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.Common.dll
- False
-
Library\ScriptAssemblies\Unity.RenderPipeline.Universal.ShaderLibrary.dll
False
@@ -1554,38 +1586,22 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Implementation.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Sprite.AIIntegration.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.Shared.dll
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Editor.dll
False
Library\ScriptAssemblies\Unity.Multiplayer.Tools.Initialization.dll
False
-
- Library\ScriptAssemblies\Unity.InputSystem.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Component.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Settings.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Splines.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Editor.UI.dll
False
@@ -1597,9 +1613,9 @@
-
+
diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj
index c99ac11..73efd78 100644
--- a/Assembly-CSharp.csproj
+++ b/Assembly-CSharp.csproj
@@ -51,7 +51,10 @@
+
+
+
@@ -60,9 +63,12 @@
+
+
+
@@ -87,6 +93,7 @@
+
@@ -1186,22 +1193,10 @@
C:\Program Files\Unity\Hub\Editor\6000.3.3f1\Editor\Data\NetStandard\compat\2.1.0\shims\netfx\System.Xml.Serialization.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricEvents.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.2D.Common.Path.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Netcode.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.2D.Sprite.Editor.dll
False
@@ -1226,14 +1221,6 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricTestData.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSolutionInterface.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStats.dll
- False
-
Library\ScriptAssemblies\Unity.Rendering.LightTransport.Editor.dll
False
@@ -1246,22 +1233,10 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Core.ShaderLibrary.dll
False
-
- Library\ScriptAssemblies\Unity.2D.IK.Runtime.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Runtime.dll
- False
-
Library\ScriptAssemblies\Unity.VisualScripting.SettingsProvider.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.dll
- False
-
Library\ScriptAssemblies\Unity.Networking.Editor.dll
False
@@ -1274,20 +1249,8 @@
Library\ScriptAssemblies\Unity.Splines.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Editor.Overrides.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkProfiler.Runtime.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Timeline.dll
+
+ Library\ScriptAssemblies\PPv2URPConverters.dll
False
@@ -1298,12 +1261,12 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Configuration.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Tooling.Editor.dll
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.Editor.dll
False
-
- Library\ScriptAssemblies\PPv2URPConverters.dll
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.Updater.dll
False
@@ -1322,6 +1285,170 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Runtime.dll
False
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Configuration.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.UnifiedRayTracing.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Aseprite.Common.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Rider.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Aseprite.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.Editor.ConversionSystem.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.VisualScripting.Flow.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.TextMeshPro.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Tilemap.Extras.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.VisualStudio.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Tilemap.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Profiling.Core.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.GPUDriven.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.VisualScripting.State.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Shaders.dll
+ False
+
+
+ Library\ScriptAssemblies\UnityEngine.UI.dll
+ False
+
+
+ Library\ScriptAssemblies\com.unity.multiplayer.tools.window.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Config.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.Common.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Sprite.AIIntegration.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.SpriteShape.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.Shared.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.InputSystem.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Splines.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.MetricEvents.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Netcode.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSolutionInterface.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStats.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.AI.Navigation.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.IK.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Animation.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.2D.Editor.Overrides.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Editor.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkProfiler.Runtime.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.Timeline.dll
+ False
+
+
+ Library\ScriptAssemblies\Unity.2D.Tooling.Editor.dll
+ False
+
Library\ScriptAssemblies\Unity.VisualScripting.Shared.Editor.dll
False
@@ -1334,10 +1461,6 @@
Library\ScriptAssemblies\Unity.PlasticSCM.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Animation.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Burst.Editor.dll
False
@@ -1354,18 +1477,10 @@
Library\ScriptAssemblies\Unity.Mathematics.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Configuration.dll
- False
-
Library\ScriptAssemblies\Unity.Cinemachine.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.UnifiedRayTracing.Runtime.dll
- False
-
Library\ScriptAssemblies\Unity.Cinemachine.dll
False
@@ -1374,26 +1489,10 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Editor.Visualization.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Aseprite.Common.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Rider.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Aseprite.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.2D.SpriteShape.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.DependencyInjection.UIElements.dll
False
@@ -1406,22 +1505,6 @@
Library\ScriptAssemblies\Unity.RenderPipelines.Core.Runtime.dll
False
-
- Library\ScriptAssemblies\Unity.VisualScripting.Flow.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll
- False
-
-
- Library\ScriptAssemblies\Unity.TextMeshPro.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.2D.Tilemap.Extras.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Netcode.PackageChecker.Editor.dll
False
@@ -1446,14 +1529,6 @@
Library\ScriptAssemblies\Unity.2D.SpriteShape.Runtime.dll
False
-
- Library\ScriptAssemblies\Unity.VisualStudio.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.2D.Tilemap.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.2D.Common.Runtime.dll
False
@@ -1462,30 +1537,14 @@
Library\ScriptAssemblies\Unity.Collections.dll
False
-
- Library\ScriptAssemblies\Unity.Profiling.Core.dll
- False
-
Library\ScriptAssemblies\Unity.Networking.Transport.dll
False
-
- Library\ScriptAssemblies\Unity.InputSystem.ForUI.dll
- False
-
Library\ScriptAssemblies\Unity.Timeline.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.VisualScripting.State.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.GPUDriven.Runtime.dll
- False
-
Library\ScriptAssemblies\Unity.Mathematics.dll
False
@@ -1494,14 +1553,6 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkProfiler.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Shaders.dll
- False
-
-
- Library\ScriptAssemblies\UnityEngine.UI.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Center.Common.dll
False
@@ -1522,22 +1573,10 @@
Library\ScriptAssemblies\Unity.Multiplayer.Center.Editor.dll
False
-
- Library\ScriptAssemblies\com.unity.multiplayer.tools.window.dll
- False
-
Library\ScriptAssemblies\Unity.VisualScripting.State.dll
False
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Universal.Config.Runtime.dll
- False
-
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.Common.dll
- False
-
Library\ScriptAssemblies\Unity.RenderPipeline.Universal.ShaderLibrary.dll
False
@@ -1554,38 +1593,22 @@
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Implementation.dll
False
-
- Library\ScriptAssemblies\Unity.2D.Sprite.AIIntegration.Editor.dll
- False
-
-
- Library\ScriptAssemblies\Unity.RenderPipelines.Core.Editor.Shared.dll
+
+ Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Editor.dll
False
Library\ScriptAssemblies\Unity.Multiplayer.Tools.Initialization.dll
False
-
- Library\ScriptAssemblies\Unity.InputSystem.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetStatsMonitor.Component.dll
False
-
- Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetworkSimulator.Editor.dll
- False
-
Library\ScriptAssemblies\Unity.Settings.Editor.dll
False
-
- Library\ScriptAssemblies\Unity.Splines.dll
- False
-
Library\ScriptAssemblies\Unity.Multiplayer.Tools.NetVis.Editor.UI.dll
False
@@ -1596,9 +1619,9 @@
-
+
diff --git a/Assets/Data/BuildingData_Core.asset b/Assets/Data/BuildingData_Core.asset
new file mode 100644
index 0000000..231247a
--- /dev/null
+++ b/Assets/Data/BuildingData_Core.asset
@@ -0,0 +1,27 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 937e64980d44d6b46acb35b8046adf34, type: 3}
+ m_Name: BuildingData_Core
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.BuildingData
+ buildingName: BaseTower
+ prefab: {fileID: 3733880183385667081, guid: 1979909431408184b9bc587877c5b4b4, type: 3}
+ width: 5
+ length: 5
+ height: 5
+ placementOffset: {x: 0, y: 0, z: 0}
+ allowRotation: 1
+ maxHealth: 1000
+ isIndestructible: 0
+ autoRegenerate: 0
+ regenPerSecond: 1
+ providesVision: 1
+ visionRange: 40
diff --git a/Assets/Data/BuildingData_Core.asset.meta b/Assets/Data/BuildingData_Core.asset.meta
new file mode 100644
index 0000000..098d3ca
--- /dev/null
+++ b/Assets/Data/BuildingData_Core.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0e495d169ee3bce449f4b1aea83d6818
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Prefabs/Core.prefab b/Assets/Prefabs/Core.prefab
index 8e00086..d68e9b8 100644
--- a/Assets/Prefabs/Core.prefab
+++ b/Assets/Prefabs/Core.prefab
@@ -11,7 +11,7 @@ GameObject:
- component: {fileID: 8064559726283331702}
- component: {fileID: 5173262576415873253}
- component: {fileID: 1287070985890992582}
- - component: {fileID: 945062474581833766}
+ - component: {fileID: 2964705630284685173}
m_Layer: 0
m_Name: Core
m_TagString: Untagged
@@ -47,7 +47,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 1288120633
+ GlobalObjectIdHash: 615747208
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
@@ -85,7 +85,7 @@ MonoBehaviour:
detachOnEnd: 1
depositEffectPrefab: {fileID: 0}
effectSpawnPoint: {fileID: 0}
---- !u!114 &945062474581833766
+--- !u!114 &2964705630284685173
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -94,11 +94,22 @@ MonoBehaviour:
m_GameObject: {fileID: 8124290768227340041}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: b59ae4328ce49c846b20d7a6d7ce7e47, type: 3}
+ m_Script: {fileID: 11500000, guid: 0ceedb9b012d848478813136b65738ae, type: 3}
m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::Northbound.BuildingVisionProvider
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.Building
ShowTopMostFoldoutHeaderGroup: 1
- visionRange: 15
+ buildingData: {fileID: 11400000, guid: 0e495d169ee3bce449f4b1aea83d6818, type: 2}
+ gridPosition: {x: 0, y: 0, z: 0}
+ rotation: 0
+ initialOwnerId: 0
+ useInitialOwner: 0
+ showHealthBar: 1
+ healthBarPrefab: {fileID: 0}
+ destroyEffectPrefab: {fileID: 0}
+ damageEffectPrefab: {fileID: 0}
+ effectSpawnPoint: {fileID: 0}
+ showGridBounds: 1
+ gridBoundsColor: {r: 0, g: 1, b: 1, a: 1}
--- !u!1001 &1876730568674182127
PrefabInstance:
m_ObjectHideFlags: 0
diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab
index 5ce18da..c831156 100644
--- a/Assets/Prefabs/Player.prefab
+++ b/Assets/Prefabs/Player.prefab
@@ -20,7 +20,7 @@ GameObject:
- component: {fileID: 6066313428661204362}
- component: {fileID: 2443072964133329520}
- component: {fileID: 2148255267416253297}
- m_Layer: 0
+ m_Layer: 9
m_Name: Player
m_TagString: Untagged
m_Icon: {fileID: 0}
@@ -83,6 +83,11 @@ MonoBehaviour:
ShowTopMostFoldoutHeaderGroup: 1
moveSpeed: 5
rotationSpeed: 10
+ initialTeam: 1
+ maxHealth: 100
+ showHealthBar: 1
+ damageEffectPrefab: {fileID: 0}
+ deathEffectPrefab: {fileID: 0}
--- !u!95 &1698609800605343773
Animator:
serializedVersion: 7
@@ -265,6 +270,14 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 5887522270574905679}
m_Modifications:
+ - target: {fileID: -9217289772674400175, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -8749280752073934791, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: m_LocalPosition.x
value: 0
@@ -309,6 +322,18 @@ PrefabInstance:
propertyPath: 'm_Materials.Array.data[0]'
value:
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
+ - target: {fileID: -8629495297202134608, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -8343894014087287100, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -7440837640338354081, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: -7164137249434462698, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: 'm_Materials.Array.data[0]'
value:
@@ -317,26 +342,138 @@ PrefabInstance:
propertyPath: 'm_Materials.Array.data[0]'
value:
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
+ - target: {fileID: -6645333679261498596, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -6415790494268509736, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -5821640607724269708, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -5489203338784653783, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -5235451391474362517, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -3771971891951071861, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -3230676936149971385, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -3087093466631822622, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -2565563256467093774, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: -2268532608001192311, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: -1697328484770790153, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: 'm_Materials.Array.data[0]'
value:
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
+ - target: {fileID: -787705004876472881, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: -540077757996713287, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: 'm_Materials.Array.data[0]'
value:
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
+ - target: {fileID: -129758803659467788, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 58325350010635884, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 316027028415609013, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: 919132149155446097, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: m_Name
value: Dummy
objectReference: {fileID: 0}
+ - target: {fileID: 919132149155446097, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 2071513765411332684, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 2870523794981691266, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 3424958915229829536, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 4260319478689324092, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 4700718891997078985, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 4902866164562761394, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: 5737771032674114347, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: 'm_Materials.Array.data[0]'
value:
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
+ - target: {fileID: 6026115103549348504, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 6041330210177057777, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 7116785954168059035, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 7354008096416770832, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 8498165350272959449, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
- target: {fileID: 8594629792745241546, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
propertyPath: 'm_Materials.Array.data[0]'
value:
objectReference: {fileID: 2100000, guid: d64c307f1b4197c44970c29f9845c245, type: 2}
+ - target: {fileID: 8674666463418728774, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
+ - target: {fileID: 9179509530285530577, guid: 4652a9058e767d142b3e889c2983fa9a, type: 3}
+ propertyPath: m_Layer
+ value: 9
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
diff --git a/Assets/Prefabs/Wall.prefab b/Assets/Prefabs/Wall.prefab
index 3b92bb4..77b7db9 100644
--- a/Assets/Prefabs/Wall.prefab
+++ b/Assets/Prefabs/Wall.prefab
@@ -75,9 +75,6 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 244a8d70d41b1a948beb2221c7c0efa9, type: 3}
insertIndex: -1
addedObject: {fileID: 1591641544412467547}
- - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 244a8d70d41b1a948beb2221c7c0efa9, type: 3}
- insertIndex: -1
- addedObject: {fileID: 1638952835164862066}
m_SourcePrefab: {fileID: 100100000, guid: 244a8d70d41b1a948beb2221c7c0efa9, type: 3}
--- !u!1 &2938167817760513538 stripped
GameObject:
@@ -96,7 +93,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 3026494903
+ GlobalObjectIdHash: 2718147317
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
@@ -109,17 +106,3 @@ MonoBehaviour:
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
---- !u!114 &1638952835164862066
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2938167817760513538}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: b59ae4328ce49c846b20d7a6d7ce7e47, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::Northbound.BuildingVisionProvider
- ShowTopMostFoldoutHeaderGroup: 1
- visionRange: 15
diff --git a/Assets/Scenes/GameMain.meta b/Assets/Scenes/GameMain.meta
new file mode 100644
index 0000000..54b6370
--- /dev/null
+++ b/Assets/Scenes/GameMain.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e8051163255a7b946abfc30e245d51c3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scenes/GameMain.unity b/Assets/Scenes/GameMain.unity
index 602e725..66496ff 100644
--- a/Assets/Scenes/GameMain.unity
+++ b/Assets/Scenes/GameMain.unity
@@ -543,6 +543,141 @@ Transform:
m_CorrespondingSourceObject: {fileID: 922888705413710451, guid: 5662d0b0d0eb5f54290edd8dd0980b57, type: 3}
m_PrefabInstance: {fileID: 2588157855179843872}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &513701714
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 513701719}
+ - component: {fileID: 513701718}
+ - component: {fileID: 513701717}
+ - component: {fileID: 513701716}
+ - component: {fileID: 513701715}
+ m_Layer: 0
+ m_Name: EnemyTest
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &513701715
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 513701714}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
+ GlobalObjectIdHash: 1957460564
+ InScenePlacedSourceGlobalObjectIdHash: 0
+ DeferredDespawnTick: 0
+ Ownership: 1
+ AlwaysReplicateAsRoot: 0
+ SynchronizeTransform: 1
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 0
+ SpawnWithObservers: 1
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
+ SyncOwnerTransformWhenParented: 1
+ AllowOwnerToParent: 0
+--- !u!114 &513701716
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 513701714}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 345fc6e7d4f06314f8b548129700eccb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.EnemyUnit
+ ShowTopMostFoldoutHeaderGroup: 1
+ enemyTeam: 2
+ maxHealth: 100
+ visionRange: 10
+ damageEffectPrefab: {fileID: 0}
+ destroyEffectPrefab: {fileID: 0}
+--- !u!23 &513701717
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 513701714}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: e831e374758eb4a019b3031699e35407, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &513701718
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 513701714}
+ m_Mesh: {fileID: 5840309848095958252, guid: a2ea40155b7314a559bca224f68394d6, type: 3}
+--- !u!4 &513701719
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 513701714}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 6, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &519420028
GameObject:
m_ObjectHideFlags: 0
@@ -2338,7 +2473,10 @@ PrefabInstance:
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
- m_AddedComponents: []
+ m_AddedComponents:
+ - targetCorrespondingSourceObject: {fileID: 8124290768227340041, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 8940572951313384068}
m_SourcePrefab: {fileID: 100100000, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
--- !u!1001 &4875211098963642791
PrefabInstance:
@@ -2466,17 +2604,48 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 04e95700704d92248b63ce5674bd9638, type: 3}
+--- !u!1 &8940572951313384064 stripped
+GameObject:
+ m_CorrespondingSourceObject: {fileID: 8124290768227340041, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
+ m_PrefabInstance: {fileID: 4786254629656932894}
+ m_PrefabAsset: {fileID: 0}
--- !u!114 &8940572951313384066 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 1287070985890992582, guid: e56926eda34629f4fbf3e4c53f0f8bd4, type: 3}
m_PrefabInstance: {fileID: 4786254629656932894}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 0}
+ m_GameObject: {fileID: 8940572951313384064}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c94274e2af2c8d4f827fe52b26c4410, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Northbound.Core
+--- !u!114 &8940572951313384068
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8940572951313384064}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0ceedb9b012d848478813136b65738ae, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::Northbound.Building
+ ShowTopMostFoldoutHeaderGroup: 1
+ buildingData: {fileID: 11400000, guid: 23c12a82ea534b34299700b86fffd524, type: 2}
+ gridPosition: {x: 0, y: 0, z: 0}
+ rotation: 0
+ initialTeam: 1
+ initialOwnerId: 0
+ useInitialOwner: 0
+ showHealthBar: 0
+ healthBarPrefab: {fileID: 0}
+ destroyEffectPrefab: {fileID: 0}
+ damageEffectPrefab: {fileID: 0}
+ effectSpawnPoint: {fileID: 0}
+ showGridBounds: 1
+ gridBoundsColor: {r: 0, g: 1, b: 1, a: 1}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -2496,3 +2665,4 @@ SceneRoots:
- {fileID: 1166878644}
- {fileID: 946527919}
- {fileID: 1701756768}
+ - {fileID: 513701719}
diff --git a/Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset b/Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset
new file mode 100644
index 0000000..35461e0
Binary files /dev/null and b/Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset differ
diff --git a/Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset.meta b/Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset.meta
new file mode 100644
index 0000000..a6706df
--- /dev/null
+++ b/Assets/Scenes/GameMain/NavMesh-Primitive_Floor.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a847cf63b54abee4cbd5b6c92c8ad5e6
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 23800000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/AttackAction.cs b/Assets/Scripts/AttackAction.cs
index c6d112c..10fdce2 100644
--- a/Assets/Scripts/AttackAction.cs
+++ b/Assets/Scripts/AttackAction.cs
@@ -4,7 +4,7 @@ using UnityEngine;
namespace Northbound
{
///
- /// 액션 - 공격 (대상 없이도 실행 가능)
+ /// 액션 - 공격 (팀 시스템 적용)
///
public class AttackAction : NetworkBehaviour, IAction
{
@@ -15,7 +15,7 @@ namespace Northbound
public LayerMask attackableLayer = ~0;
[Header("Animation")]
- public string attackAnimationTrigger = "Attack"; // 공격 애니메이션 트리거
+ public string attackAnimationTrigger = "Attack";
[Header("Visual")]
public GameObject attackEffectPrefab;
@@ -23,10 +23,12 @@ namespace Northbound
private float _lastAttackTime;
private Animator _animator;
+ private ITeamMember _teamMember;
private void Awake()
{
_animator = GetComponent();
+ _teamMember = GetComponent();
}
public bool CanExecute(ulong playerId)
@@ -44,7 +46,7 @@ namespace Northbound
// 애니메이션 재생
PlayAttackAnimation();
- // 범위 내 적이 있으면 데미지
+ // 범위 내 적 검색
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
Collider[] hits = Physics.OverlapSphere(attackOrigin, attackRange, attackableLayer);
@@ -54,10 +56,22 @@ namespace Northbound
if (hit.transform.root == transform.root)
continue;
- // 적에게 데미지
- var enemy = hit.GetComponent();
- if (enemy != null)
+ // 대상 확인
+ var targetDamageable = hit.GetComponent();
+ var targetTeamMember = hit.GetComponent();
+
+ if (targetDamageable != null)
{
+ // 팀 확인 - 적대 관계인 경우에만 공격
+ if (_teamMember != null && targetTeamMember != null)
+ {
+ if (!TeamManager.CanAttack(_teamMember, targetTeamMember))
+ {
+ Debug.Log($"[AttackAction] {TeamManager.GetTeamName(_teamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(targetTeamMember.GetTeam())} 팀을 공격할 수 없습니다.");
+ continue;
+ }
+ }
+
var netObj = hit.GetComponent();
if (netObj != null)
{
@@ -66,7 +80,7 @@ namespace Northbound
}
}
- Debug.Log($"플레이어 {playerId} 공격! (적중: {hits.Length}개)");
+ Debug.Log($"[AttackAction] 플레이어 {playerId} ({TeamManager.GetTeamName(_teamMember?.GetTeam() ?? TeamType.Neutral)}) 공격!");
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
diff --git a/Assets/Scripts/AutoTargetSystem.cs b/Assets/Scripts/AutoTargetSystem.cs
new file mode 100644
index 0000000..dccb706
--- /dev/null
+++ b/Assets/Scripts/AutoTargetSystem.cs
@@ -0,0 +1,177 @@
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ ///
+ /// 자동으로 적을 탐지하고 공격하는 시스템
+ ///
+ public class AutoTargetSystem : NetworkBehaviour
+ {
+ [Header("Targeting")]
+ [Tooltip("적을 감지하는 범위")]
+ public float detectionRange = 15f;
+
+ [Tooltip("공격 가능한 범위")]
+ public float attackRange = 10f;
+
+ [Tooltip("공격 간격 (초)")]
+ public float attackInterval = 1f;
+
+ [Tooltip("탐지할 레이어")]
+ public LayerMask targetLayer = ~0;
+
+ [Header("Combat")]
+ [Tooltip("공격 데미지")]
+ public int attackDamage = 10;
+
+ [Header("Debug")]
+ [Tooltip("디버그 정보 표시")]
+ public bool showDebugInfo = true;
+
+ private ITeamMember _teamMember;
+ private float _lastAttackTime;
+
+ private void Awake()
+ {
+ _teamMember = GetComponent();
+
+ if (_teamMember == null)
+ {
+ Debug.LogError($"[AutoTargetSystem] {gameObject.name}에 ITeamMember 컴포넌트가 없습니다!");
+ }
+ }
+
+ private void Update()
+ {
+ if (!IsServer) return;
+ if (_teamMember == null) return;
+
+ if (Time.time - _lastAttackTime >= attackInterval)
+ {
+ FindAndAttackEnemy();
+ }
+ }
+
+ private void FindAndAttackEnemy()
+ {
+ // 범위 내 모든 콜라이더 탐지
+ Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, targetLayer);
+
+ if (showDebugInfo && colliders.Length > 0)
+ {
+ Debug.Log($"[AutoTarget] {gameObject.name}이(가) {colliders.Length}개의 오브젝트를 감지했습니다.");
+ }
+
+ GameObject closestEnemy = null;
+ float closestDistance = float.MaxValue;
+
+ foreach (Collider col in colliders)
+ {
+ // 자기 자신 제외
+ if (col.transform.root == transform.root)
+ continue;
+
+ // 팀 확인
+ ITeamMember targetTeam = col.GetComponent();
+
+ if (targetTeam == null)
+ {
+ // 부모나 자식에서 찾기
+ targetTeam = col.GetComponentInParent();
+ if (targetTeam == null)
+ {
+ targetTeam = col.GetComponentInChildren();
+ }
+ }
+
+ if (targetTeam == null)
+ {
+ if (showDebugInfo)
+ {
+ Debug.Log($"[AutoTarget] {col.gameObject.name}에 ITeamMember가 없습니다.");
+ }
+ continue;
+ }
+
+ // 적대 관계 확인
+ bool canAttack = TeamManager.CanAttack(_teamMember, targetTeam);
+
+ if (showDebugInfo)
+ {
+ Debug.Log($"[AutoTarget] {gameObject.name} ({TeamManager.GetTeamName(_teamMember.GetTeam())}) → {col.gameObject.name} ({TeamManager.GetTeamName(targetTeam.GetTeam())}): 공격가능={canAttack}");
+ }
+
+ if (!canAttack)
+ continue;
+
+ // 가장 가까운 적 찾기
+ float distance = Vector3.Distance(transform.position, col.transform.position);
+ if (distance < closestDistance && distance <= attackRange)
+ {
+ closestDistance = distance;
+ closestEnemy = col.gameObject;
+ }
+ }
+
+ // 공격
+ if (closestEnemy != null)
+ {
+ IDamageable damageable = closestEnemy.GetComponent();
+
+ if (damageable == null)
+ {
+ damageable = closestEnemy.GetComponentInParent();
+ if (damageable == null)
+ {
+ damageable = closestEnemy.GetComponentInChildren();
+ }
+ }
+
+ if (damageable != null)
+ {
+ damageable.TakeDamage(attackDamage, NetworkObjectId);
+ _lastAttackTime = Time.time;
+
+ var targetTeam = closestEnemy.GetComponent() ??
+ closestEnemy.GetComponentInParent() ??
+ closestEnemy.GetComponentInChildren();
+
+ Debug.Log($"[AutoTarget] {gameObject.name} ({TeamManager.GetTeamName(_teamMember.GetTeam())})이(가) {closestEnemy.name} ({TeamManager.GetTeamName(targetTeam?.GetTeam() ?? TeamType.Neutral)})을(를) 공격! (거리: {closestDistance:F2}m, 데미지: {attackDamage})");
+ }
+ else
+ {
+ Debug.LogWarning($"[AutoTarget] {closestEnemy.name}에 IDamageable이 없습니다.");
+ }
+ }
+ else if (showDebugInfo && colliders.Length > 0)
+ {
+ Debug.Log($"[AutoTarget] {gameObject.name}이(가) 공격 가능한 적을 찾지 못했습니다.");
+ }
+ }
+
+ private void OnDrawGizmos()
+ {
+ // 탐지 범위 (노란색)
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(transform.position, detectionRange);
+
+ // 공격 범위 (빨간색)
+ Gizmos.color = Color.red;
+ Gizmos.DrawWireSphere(transform.position, attackRange);
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ OnDrawGizmos();
+
+ #if UNITY_EDITOR
+ if (_teamMember != null && Application.isPlaying)
+ {
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
+ $"Auto Target\nTeam: {TeamManager.GetTeamName(_teamMember.GetTeam())}\nDetection: {detectionRange}m\nAttack: {attackRange}m");
+ }
+ #endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/AutoTargetSystem.cs.meta b/Assets/Scripts/AutoTargetSystem.cs.meta
new file mode 100644
index 0000000..563887a
--- /dev/null
+++ b/Assets/Scripts/AutoTargetSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 8dd1c341faa09554aa2bc35164888453
\ No newline at end of file
diff --git a/Assets/Scripts/Building.cs b/Assets/Scripts/Building.cs
index a39db3f..0d7f814 100644
--- a/Assets/Scripts/Building.cs
+++ b/Assets/Scripts/Building.cs
@@ -4,7 +4,7 @@ using UnityEngine;
namespace Northbound
{
- public class Building : NetworkBehaviour, IDamageable, IVisionProvider
+ public class Building : NetworkBehaviour, IDamageable, IVisionProvider, ITeamMember
{
[Header("References")]
public BuildingData buildingData;
@@ -13,6 +13,10 @@ namespace Northbound
public Vector3Int gridPosition;
public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°)
+ [Header("Team")]
+ [Tooltip("건물의 팀 (플레이어/적대세력/몬스터/중립)")]
+ public TeamType initialTeam = TeamType.Player;
+
[Header("Ownership (for pre-placed buildings)")]
[Tooltip("씬에 미리 배치된 건물의 경우 여기서 소유자 설정 (0 = 중립, 1+ = 플레이어 ID)")]
public ulong initialOwnerId = 0;
@@ -46,9 +50,17 @@ namespace Northbound
NetworkVariableWritePermission.Server
);
+ // 건물 팀
+ private NetworkVariable _team = new NetworkVariable(
+ TeamType.Neutral,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
// 이벤트
public event Action OnHealthChanged; // (current, max)
public event Action OnDestroyed;
+ public event Action OnTeamChanged;
private BuildingHealthBar _healthBar;
private float _lastRegenTime;
@@ -66,11 +78,17 @@ namespace Northbound
_currentHealth.Value = buildingData != null ? buildingData.maxHealth : 100;
}
+ // 팀 초기화
+ if (_team.Value == TeamType.Neutral)
+ {
+ _team.Value = initialTeam;
+ }
+
// 소유자 초기화 (사전 배치 건물 체크)
if (useInitialOwner && _ownerId.Value == 0)
{
_ownerId.Value = initialOwnerId;
- Debug.Log($"[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}");
+ Debug.Log($"[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}, 팀: {_team.Value}");
}
else if (!useInitialOwner && _ownerId.Value == 0)
{
@@ -87,8 +105,9 @@ namespace Northbound
}
}
- // 체력 변경 이벤트 구독
+ // 이벤트 구독
_currentHealth.OnValueChanged += OnHealthValueChanged;
+ _team.OnValueChanged += OnTeamValueChanged;
// 체력바 생성
if (showHealthBar && healthBarPrefab != null)
@@ -98,11 +117,13 @@ namespace Northbound
// 초기 체력 UI 업데이트
UpdateHealthUI();
+ UpdateTeamVisuals();
}
public override void OnNetworkDespawn()
{
_currentHealth.OnValueChanged -= OnHealthValueChanged;
+ _team.OnValueChanged -= OnTeamValueChanged;
// FogOfWar 시스템에서 제거
if (IsServer && buildingData != null && buildingData.providesVision)
@@ -131,7 +152,7 @@ namespace Northbound
///
/// 건물 초기화 (BuildingManager가 동적 생성 시 호출)
///
- public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId)
+ public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId, TeamType team = TeamType.Player)
{
buildingData = data;
gridPosition = gridPos;
@@ -142,6 +163,7 @@ namespace Northbound
{
_currentHealth.Value = data.maxHealth;
_ownerId.Value = ownerId;
+ _team.Value = team;
// 시야 제공자 등록
if (data.providesVision)
@@ -156,14 +178,17 @@ namespace Northbound
///
/// 건물 소유권 변경 (점령 등)
///
- public void SetOwner(ulong newOwnerId)
+ public void SetOwner(ulong newOwnerId, TeamType newTeam)
{
if (!IsServer) return;
ulong previousOwner = _ownerId.Value;
+ TeamType previousTeam = _team.Value;
+
_ownerId.Value = newOwnerId;
+ _team.Value = newTeam;
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} 소유권 변경: {previousOwner} → {newOwnerId}");
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} 소유권 변경: {previousOwner} → {newOwnerId}, 팀: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(newTeam)}");
// 시야 제공자 재등록 (소유자가 바뀌었으므로)
if (buildingData != null && buildingData.providesVision)
@@ -173,6 +198,35 @@ namespace Northbound
}
}
+ #region ITeamMember Implementation
+
+ public TeamType GetTeam() => _team.Value;
+
+ public void SetTeam(TeamType team)
+ {
+ if (!IsServer) return;
+ _team.Value = team;
+ }
+
+ private void OnTeamValueChanged(TeamType previousValue, TeamType newValue)
+ {
+ OnTeamChanged?.Invoke(newValue);
+ UpdateTeamVisuals();
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} 팀 변경: {TeamManager.GetTeamName(previousValue)} → {TeamManager.GetTeamName(newValue)}");
+ }
+
+ private void UpdateTeamVisuals()
+ {
+ // 팀 색상으로 건물 외곽선이나 이펙트 변경 가능
+ // 예: Renderer의 emission 색상 변경
+ Color teamColor = TeamManager.GetTeamColor(_team.Value);
+
+ // 여기에 실제 비주얼 업데이트 로직 추가
+ // 예: outline shader, emission, particle system 색상 등
+ }
+
+ #endregion
+
#region IVisionProvider Implementation
public ulong GetOwnerId() => _ownerId.Value;
@@ -214,11 +268,24 @@ namespace Northbound
if (_currentHealth.Value <= 0)
return;
+ // 공격자의 팀 확인 (팀 공격 방지)
+ var attackerObj = NetworkManager.Singleton.SpawnManager.SpawnedObjects[attackerId];
+ var attackerTeamMember = attackerObj?.GetComponent();
+
+ if (attackerTeamMember != null)
+ {
+ if (!TeamManager.CanAttack(attackerTeamMember, this))
+ {
+ Debug.Log($"[Building] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.");
+ return;
+ }
+ }
+
// 데미지 적용
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
_currentHealth.Value -= actualDamage;
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"}이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}");
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}");
// 데미지 이펙트
ShowDamageEffectClientRpc();
@@ -248,7 +315,7 @@ namespace Northbound
if (!IsServer)
return;
- Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"}이(가) 파괴되었습니다! (공격자: {attackerId})");
+ Debug.Log($"[Building] {buildingData?.buildingName ?? "건물"} ({TeamManager.GetTeamName(_team.Value)})이(가) 파괴되었습니다! (공격자: {attackerId})");
// 파괴 이벤트 발생
OnDestroyed?.Invoke();
@@ -427,7 +494,10 @@ namespace Northbound
if (!showGridBounds || buildingData == null) return;
Bounds bounds = GetGridBounds();
- Gizmos.color = gridBoundsColor;
+
+ // 팀 색상으로 표시
+ Color teamColor = Application.isPlaying ? TeamManager.GetTeamColor(_team.Value) : TeamManager.GetTeamColor(initialTeam);
+ Gizmos.color = new Color(teamColor.r, teamColor.g, teamColor.b, 0.3f);
Gizmos.DrawWireCube(bounds.center, bounds.size);
}
@@ -454,17 +524,19 @@ namespace Northbound
Gizmos.DrawWireSphere(transform.position, buildingData.visionRange);
}
- // Draw owner ID label
+ // Draw team info label
#if UNITY_EDITOR
if (Application.isPlaying)
{
+ string teamName = TeamManager.GetTeamName(_team.Value);
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
- $"Owner: {_ownerId.Value}");
+ $"Owner: {_ownerId.Value}\nTeam: {teamName}");
}
else if (useInitialOwner)
{
+ string teamName = TeamManager.GetTeamName(initialTeam);
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
- $"Initial Owner: {initialOwnerId}");
+ $"Initial Owner: {initialOwnerId}\nTeam: {teamName}");
}
#endif
}
diff --git a/Assets/Scripts/Core.cs b/Assets/Scripts/Core.cs
index 25ef456..c1160ca 100644
--- a/Assets/Scripts/Core.cs
+++ b/Assets/Scripts/Core.cs
@@ -6,12 +6,17 @@ namespace Northbound
///
/// 플레이어가 자원을 건내받아 게임의 전역 자원으로 관리하는 중앙 허브
///
- public class Core : NetworkBehaviour, IInteractable
+ public class Core : NetworkBehaviour, IInteractable, IDamageable, ITeamMember
{
[Header("Core Settings")]
public int maxStorageCapacity = 1000; // 코어의 최대 저장 용량
public bool unlimitedStorage = false; // 무제한 저장소
+ [Header("Health")]
+ public int maxHealth = 1000;
+ public GameObject damageEffectPrefab;
+ public GameObject destroyEffectPrefab;
+
[Header("Deposit Settings")]
public bool depositAll = true; // true: 전부 건네기, false: 일부만 건네기
public int depositAmountPerInteraction = 10; // depositAll이 false일 때 한 번에 건네는 양
@@ -32,17 +37,163 @@ namespace Northbound
NetworkVariableWritePermission.Server
);
+ private NetworkVariable _currentHealth = new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
public int TotalResources => _totalResources.Value;
public int MaxStorageCapacity => maxStorageCapacity;
+ public int CurrentHealth => _currentHealth.Value;
+ public int MaxHealth => maxHealth;
public override void OnNetworkSpawn()
{
if (IsServer)
{
_totalResources.Value = 0;
+ _currentHealth.Value = maxHealth;
+ }
+
+ _currentHealth.OnValueChanged += OnHealthChanged;
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ _currentHealth.OnValueChanged -= OnHealthChanged;
+ }
+
+ private void OnHealthChanged(int previousValue, int newValue)
+ {
+ Debug.Log($"[Core] 코어 체력 변경: {previousValue} → {newValue} ({newValue}/{maxHealth})");
+ }
+
+ #region ITeamMember Implementation
+
+ public TeamType GetTeam()
+ {
+ return TeamType.Player; // 코어는 플레이어 팀
+ }
+
+ public void SetTeam(TeamType team)
+ {
+ // 코어의 팀은 변경할 수 없음 (항상 플레이어 팀)
+ Debug.LogWarning("[Core] 코어의 팀은 변경할 수 없습니다.");
+ }
+
+ #endregion
+
+ #region IDamageable Implementation
+
+ public void TakeDamage(int damage, ulong attackerId)
+ {
+ if (!IsServer) return;
+ if (_currentHealth.Value <= 0) return;
+
+ int actualDamage = Mathf.Min(damage, _currentHealth.Value);
+ _currentHealth.Value -= actualDamage;
+
+ Debug.Log($"[Core] 코어가 {actualDamage} 데미지를 받았습니다! 남은 체력: {_currentHealth.Value}/{maxHealth}");
+
+ // 데미지 이펙트
+ ShowDamageEffectClientRpc();
+
+ // 체력이 0이 되면 게임 오버
+ if (_currentHealth.Value <= 0)
+ {
+ OnCoreDestroyed();
}
}
+ private void OnCoreDestroyed()
+ {
+ if (!IsServer) return;
+
+ Debug.Log($"[Core] 코어가 파괴되었습니다! 게임 오버!");
+
+ // 파괴 이펙트
+ ShowDestroyEffectClientRpc();
+
+ // 게임 오버 로직 (추후 구현)
+ // GameManager.Instance?.OnGameOver();
+ }
+
+ [Rpc(SendTo.ClientsAndHost)]
+ private void ShowDamageEffectClientRpc()
+ {
+ if (damageEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up * 2f, Quaternion.identity);
+ Destroy(effect, 2f);
+ }
+ }
+
+ [Rpc(SendTo.ClientsAndHost)]
+ private void ShowDestroyEffectClientRpc()
+ {
+ if (destroyEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity);
+ Destroy(effect, 5f);
+ }
+ }
+
+ #endregion
+
+ #region Resource Management
+
+ ///
+ /// 자원을 소비할 수 있는지 확인
+ ///
+ public bool CanConsumeResource(int amount)
+ {
+ return _totalResources.Value >= amount;
+ }
+
+ ///
+ /// 자원 소비 (서버에서만)
+ ///
+ [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
+ public void ConsumeResourceServerRpc(int amount)
+ {
+ if (!CanConsumeResource(amount))
+ {
+ Debug.LogWarning($"[Core] 자원이 부족합니다. 필요: {amount}, 보유: {_totalResources.Value}");
+ return;
+ }
+
+ int previousAmount = _totalResources.Value;
+ _totalResources.Value -= amount;
+
+ Debug.Log($"[Core] {amount} 자원 소비. 남은 자원: {_totalResources.Value}/{maxStorageCapacity}");
+ }
+
+ ///
+ /// 자원 추가 (서버에서만)
+ ///
+ public void AddResource(int amount)
+ {
+ if (!IsServer) return;
+
+ if (!unlimitedStorage)
+ {
+ int availableSpace = maxStorageCapacity - _totalResources.Value;
+ amount = Mathf.Min(amount, availableSpace);
+ }
+
+ if (amount > 0)
+ {
+ _totalResources.Value += amount;
+ Debug.Log($"[Core] {amount} 자원 추가. 총 자원: {_totalResources.Value}" +
+ (unlimitedStorage ? "" : $"/{maxStorageCapacity}") + "");
+ }
+ }
+
+ #endregion
+
+ #region IInteractable Implementation
+
public bool CanInteract(ulong playerId)
{
// 저장소가 가득 찼는지 확인 (무제한이 아닐 때)
@@ -75,6 +226,33 @@ namespace Northbound
DepositResourceServerRpc(playerId);
}
+ public string GetInteractionPrompt()
+ {
+ if (unlimitedStorage)
+ {
+ return "자원 보관 (무제한)";
+ }
+ else
+ {
+ return $"자원 보관 ({_totalResources.Value}/{maxStorageCapacity})";
+ }
+ }
+
+ public string GetInteractionAnimation()
+ {
+ return interactionAnimationTrigger;
+ }
+
+ public InteractionEquipmentData GetEquipmentData()
+ {
+ return equipmentData;
+ }
+
+ public Transform GetTransform()
+ {
+ return transform;
+ }
+
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void DepositResourceServerRpc(ulong playerId)
{
@@ -132,8 +310,8 @@ namespace Northbound
// 코어에 자원 추가
_totalResources.Value += depositAmount;
- Debug.Log($"플레이어 {playerId}가 {depositAmount} 자원을 코어에 건넸습니다. 코어 총 자원: {_totalResources.Value}" +
- (unlimitedStorage ? "" : $"/{maxStorageCapacity}"));
+ Debug.Log($"[Core] 플레이어 {playerId}가 {depositAmount} 자원을 건넸습니다. 코어 총 자원: {_totalResources.Value}" +
+ (unlimitedStorage ? "" : $"/{maxStorageCapacity}") + "");
ShowDepositEffectClientRpc();
}
@@ -148,76 +326,6 @@ namespace Northbound
}
}
- ///
- /// 게임 시스템이 코어의 자원을 사용 (건물 건설 등)
- ///
- [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
- public void ConsumeResourceServerRpc(int amount)
- {
- if (amount <= 0) return;
-
- int actualAmount = Mathf.Min(amount, _totalResources.Value);
- _totalResources.Value -= actualAmount;
-
- Debug.Log($"코어에서 {actualAmount} 자원을 사용했습니다. 남은 자원: {_totalResources.Value}");
- }
-
- ///
- /// 자원을 사용할 수 있는지 확인
- ///
- public bool CanConsumeResource(int amount)
- {
- return _totalResources.Value >= amount;
- }
-
- ///
- /// 코어에 자원 추가 (디버그/관리자 기능)
- ///
- [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
- public void AddResourceServerRpc(int amount)
- {
- if (amount <= 0) return;
-
- if (!unlimitedStorage)
- {
- int availableSpace = maxStorageCapacity - _totalResources.Value;
- amount = Mathf.Min(amount, availableSpace);
- }
-
- _totalResources.Value += amount;
- Debug.Log($"코어에 {amount} 자원이 추가되었습니다. 현재: {_totalResources.Value}");
- }
-
- public string GetInteractionPrompt()
- {
- if (unlimitedStorage)
- {
- return depositAll ?
- $"[E] 자원 모두 건네기" :
- $"[E] 자원 건네기 ({depositAmountPerInteraction}개씩)";
- }
-
- if (_totalResources.Value >= maxStorageCapacity)
- return "코어 저장소 가득 찼음";
-
- return depositAll ?
- $"[E] 자원 모두 건네기 ({_totalResources.Value}/{maxStorageCapacity})" :
- $"[E] 자원 건네기 ({_totalResources.Value}/{maxStorageCapacity})";
- }
-
- public string GetInteractionAnimation()
- {
- return interactionAnimationTrigger;
- }
-
- public InteractionEquipmentData GetEquipmentData()
- {
- return equipmentData;
- }
-
- public Transform GetTransform()
- {
- return transform;
- }
+ #endregion
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyAIController.cs b/Assets/Scripts/EnemyAIController.cs
new file mode 100644
index 0000000..fa23c37
--- /dev/null
+++ b/Assets/Scripts/EnemyAIController.cs
@@ -0,0 +1,714 @@
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AI;
+
+namespace Northbound
+{
+ ///
+ /// 몬스터와 적대 세력의 AI 컨트롤러
+ ///
+ [RequireComponent(typeof(NavMeshAgent))]
+ [RequireComponent(typeof(EnemyUnit))]
+ public class EnemyAIController : NetworkBehaviour
+ {
+ [Header("AI Type")]
+ [Tooltip("Monster: 코어로 이동, Hostile: 제자리에서 대기")]
+ public TeamType aiType = TeamType.Monster;
+
+ [Header("Detection")]
+ [Tooltip("플레이어 감지 범위")]
+ public float detectionRange = 15f;
+
+ [Tooltip("시야 각도 (0-360, 360=전방향)")]
+ [Range(0, 360)]
+ public float detectionAngle = 120f;
+
+ [Tooltip("탐지할 레이어")]
+ public LayerMask playerLayer = ~0;
+
+ [Tooltip("시야 체크 장애물 레이어")]
+ public LayerMask obstacleLayer = ~0;
+
+ [Header("Chase Settings")]
+ [Tooltip("추적 최대 거리 (이 거리 이상 추적하면 중단)")]
+ public float maxChaseDistance = 30f;
+
+ [Tooltip("추적 포기 거리 (플레이어와 이 거리 이상 멀어지면 추적 중단)")]
+ public float chaseGiveUpDistance = 25f;
+
+ [Header("Combat")]
+ [Tooltip("공격 범위")]
+ public float attackRange = 2f;
+
+ [Tooltip("공격 간격 (초)")]
+ public float attackInterval = 1.5f;
+
+ [Tooltip("공격 데미지")]
+ public int attackDamage = 10;
+
+ [Header("Movement")]
+ [Tooltip("이동 속도")]
+ public float moveSpeed = 3.5f;
+
+ [Tooltip("추적 중 속도 배율")]
+ public float chaseSpeedMultiplier = 1.5f;
+
+ [Header("Debug")]
+ [Tooltip("디버그 정보 표시")]
+ public bool showDebugInfo = true;
+
+ private NavMeshAgent _agent;
+ private EnemyUnit _enemyUnit;
+ private Transform _coreTransform;
+ private Collider _coreCollider;
+ private Vector3 _originPosition;
+ private Vector3 _chaseStartPosition;
+ private float _lastAttackTime;
+ private bool _hasSetCoreDestination;
+ private float _lastDetectionLogTime;
+
+ private NetworkVariable _currentState = new NetworkVariable(
+ EnemyAIState.Idle,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable _targetPlayerId = new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private GameObject _cachedTargetPlayer;
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ _agent = GetComponent();
+ _enemyUnit = GetComponent();
+ _originPosition = transform.position;
+
+ if (IsServer)
+ {
+ // NavMeshAgent 초기 설정
+ _agent.speed = moveSpeed;
+ _agent.acceleration = 8f;
+ _agent.angularSpeed = 120f;
+ _agent.stoppingDistance = attackRange * 0.7f;
+ _agent.autoBraking = true;
+ _agent.updateRotation = true;
+ _agent.updateUpAxis = false;
+
+ // NavMesh 위에 있는지 확인
+ if (!_agent.isOnNavMesh)
+ {
+ Debug.LogWarning($"[EnemyAI] {gameObject.name}이(가) NavMesh 위에 있지 않습니다!");
+ }
+ else
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name} NavMeshAgent 초기화 완료");
+ }
+
+ // AI 타입에 따라 초기 상태 설정
+ if (aiType == TeamType.Monster)
+ {
+ FindCore();
+ TransitionToState(EnemyAIState.MoveToCore);
+ }
+ else if (aiType == TeamType.Hostile)
+ {
+ TransitionToState(EnemyAIState.Idle);
+ }
+ }
+ }
+
+ private void Update()
+ {
+ if (!IsServer) return;
+ if (!_agent.isOnNavMesh) return;
+
+ switch (_currentState.Value)
+ {
+ case EnemyAIState.Idle:
+ UpdateIdle();
+ break;
+ case EnemyAIState.MoveToCore:
+ UpdateMoveToCore();
+ break;
+ case EnemyAIState.ChasePlayer:
+ UpdateChasePlayer();
+ break;
+ case EnemyAIState.Attack:
+ UpdateAttack();
+ break;
+ case EnemyAIState.ReturnToOrigin:
+ UpdateReturnToOrigin();
+ break;
+ }
+ }
+
+ #region State Updates
+
+ private void UpdateIdle()
+ {
+ GameObject player = DetectPlayer();
+ if (player != null)
+ {
+ SetTargetPlayer(player);
+ TransitionToState(EnemyAIState.ChasePlayer);
+ }
+ }
+
+ private void UpdateMoveToCore()
+ {
+ // 플레이어 감지
+ GameObject player = DetectPlayer();
+ if (player != null)
+ {
+ SetTargetPlayer(player);
+ TransitionToState(EnemyAIState.ChasePlayer);
+ return;
+ }
+
+ // 코어가 없으면 찾기
+ if (_coreTransform == null)
+ {
+ FindCore();
+ _hasSetCoreDestination = false;
+ return;
+ }
+
+ // 코어 표면까지의 실제 거리 계산
+ float distanceToCore = GetDistanceToCoreSurface();
+
+ // 공격 범위 안에 있으면 공격 상태로 전환
+ if (distanceToCore <= attackRange)
+ {
+ TransitionToState(EnemyAIState.Attack);
+ return;
+ }
+
+ // 경로가 설정되지 않았거나 무효화된 경우에만 설정
+ if (!_hasSetCoreDestination || !_agent.hasPath || _agent.pathStatus == NavMeshPathStatus.PathInvalid)
+ {
+ if (_agent.SetDestination(_coreTransform.position))
+ {
+ _hasSetCoreDestination = true;
+
+ if (showDebugInfo)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name} 코어로 경로 설정 (표면 거리: {distanceToCore:F2}m, 공격범위: {attackRange:F2}m)");
+ }
+ }
+ else
+ {
+ Debug.LogWarning($"[EnemyAI] {gameObject.name}이(가) 코어로 가는 경로를 찾을 수 없습니다!");
+ _hasSetCoreDestination = false;
+ }
+ }
+ }
+
+ private void UpdateChasePlayer()
+ {
+ GameObject targetPlayer = GetTargetPlayer();
+
+ if (targetPlayer == null)
+ {
+ OnLostTarget();
+ return;
+ }
+
+ float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.transform.position);
+
+ // 추적 기준점 설정
+ Vector3 chaseReferencePoint = (aiType == TeamType.Monster) ? _chaseStartPosition : _originPosition;
+ float distanceFromReference = Vector3.Distance(transform.position, chaseReferencePoint);
+
+ // 추적 중단 조건 확인
+ if (distanceToPlayer > chaseGiveUpDistance || distanceFromReference > maxChaseDistance)
+ {
+ if (showDebugInfo)
+ {
+ string referenceType = (aiType == TeamType.Monster) ? "추적 시작" : "원점";
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) 추적을 중단합니다. (플레이어 거리: {distanceToPlayer:F2}m, {referenceType} 거리: {distanceFromReference:F2}m)");
+ }
+ OnLostTarget();
+ return;
+ }
+
+ // 공격 범위 확인
+ if (distanceToPlayer <= attackRange)
+ {
+ TransitionToState(EnemyAIState.Attack);
+ return;
+ }
+
+ // 플레이어 추적 - 매 프레임 업데이트
+ if (_agent.isOnNavMesh && !_agent.isStopped)
+ {
+ _agent.SetDestination(targetPlayer.transform.position);
+ }
+ }
+
+ private void UpdateAttack()
+ {
+ // 코어 공격 중인지 확인
+ bool attackingCore = _coreTransform != null &&
+ GetDistanceToCoreSurface() <= attackRange * 1.2f;
+
+ if (attackingCore)
+ {
+ float distanceToCore = GetDistanceToCoreSurface();
+
+ if (distanceToCore > attackRange * 1.2f)
+ {
+ TransitionToState(EnemyAIState.MoveToCore);
+ return;
+ }
+
+ // 코어를 바라보기
+ Vector3 directionToCore = (_coreTransform.position - transform.position).normalized;
+ directionToCore.y = 0;
+ if (directionToCore != Vector3.zero)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(directionToCore);
+ transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
+ }
+
+ // 코어 공격
+ IDamageable coreHealth = _coreTransform.GetComponent();
+ if (coreHealth != null && Time.time - _lastAttackTime >= attackInterval)
+ {
+ coreHealth.TakeDamage(attackDamage, NetworkObjectId);
+ _lastAttackTime = Time.time;
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) 코어를 공격! (데미지: {attackDamage}, 표면 거리: {distanceToCore:F2}m)");
+ }
+ }
+ else
+ {
+ // 플레이어 공격
+ GameObject targetPlayer = GetTargetPlayer();
+
+ if (targetPlayer == null)
+ {
+ OnLostTarget();
+ return;
+ }
+
+ float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.transform.position);
+
+ if (distanceToPlayer > attackRange * 1.2f)
+ {
+ TransitionToState(EnemyAIState.ChasePlayer);
+ return;
+ }
+
+ // 플레이어를 바라보기
+ Vector3 directionToPlayer = (targetPlayer.transform.position - transform.position).normalized;
+ directionToPlayer.y = 0;
+ if (directionToPlayer != Vector3.zero)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(directionToPlayer);
+ transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
+ }
+
+ // 공격
+ if (Time.time - _lastAttackTime >= attackInterval)
+ {
+ AttackPlayer(targetPlayer);
+ }
+ }
+ }
+
+ private void UpdateReturnToOrigin()
+ {
+ GameObject player = DetectPlayer();
+ if (player != null)
+ {
+ SetTargetPlayer(player);
+ TransitionToState(EnemyAIState.ChasePlayer);
+ return;
+ }
+
+ if (!_agent.pathPending && _agent.remainingDistance <= _agent.stoppingDistance)
+ {
+ if (!_agent.hasPath || _agent.velocity.sqrMagnitude == 0f)
+ {
+ if (showDebugInfo)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) 원래 위치로 복귀했습니다.");
+ }
+ TransitionToState(EnemyAIState.Idle);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Detection
+
+ private GameObject DetectPlayer()
+ {
+ Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, playerLayer);
+
+ GameObject closestPlayer = null;
+ float closestDistance = float.MaxValue;
+
+ foreach (Collider col in colliders)
+ {
+ // 자기 자신 제외
+ if (col.transform.root == transform.root)
+ continue;
+
+ // 플레이어 팀 확인 (부모에서 찾기)
+ ITeamMember teamMember = col.GetComponentInParent();
+ if (teamMember == null || teamMember.GetTeam() != TeamType.Player)
+ continue;
+
+ // 플레이어 위치 (루트 오브젝트 사용)
+ Transform playerRoot = col.transform.root;
+ Vector3 playerPosition = playerRoot.position;
+
+ // 거리 체크
+ float distance = Vector3.Distance(transform.position, playerPosition);
+ if (distance > detectionRange)
+ continue;
+
+ // 시야각 확인 (360도면 모든 방향 감지)
+ if (detectionAngle < 360f)
+ {
+ Vector3 directionToTarget = (playerPosition - transform.position).normalized;
+ float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
+
+ if (angleToTarget > detectionAngle / 2f)
+ continue;
+ }
+
+ // 시야 체크 (레이캐스트) - 플레이어 중심으로
+ Vector3 rayStart = transform.position + Vector3.up * 1f; // 적의 눈 높이
+ Vector3 rayTarget = playerPosition + Vector3.up * 1f; // 플레이어 중심
+ Vector3 rayDirection = (rayTarget - rayStart).normalized;
+ float rayDistance = Vector3.Distance(rayStart, rayTarget);
+
+ bool lineOfSight = true;
+
+ // 장애물 체크 (옵션)
+ if (Physics.Raycast(rayStart, rayDirection, out RaycastHit hit, rayDistance, obstacleLayer))
+ {
+ // 맞은 오브젝트가 플레이어의 루트나 자식인지 확인
+ if (hit.transform.root != playerRoot)
+ {
+ lineOfSight = false;
+ }
+ }
+
+ if (lineOfSight)
+ {
+ // 가장 가까운 플레이어 찾기
+ if (distance < closestDistance)
+ {
+ closestDistance = distance;
+ closestPlayer = playerRoot.gameObject;
+ }
+ }
+ }
+
+ // 감지 성공 시 로그 (1초에 한 번만)
+ if (closestPlayer != null && showDebugInfo && Time.time - _lastDetectionLogTime >= 1f)
+ {
+ string angleInfo = detectionAngle >= 360f ? "전방향" : $"{Vector3.Angle(transform.forward, (closestPlayer.transform.position - transform.position).normalized):F1}°";
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) {closestPlayer.name}을(를) 감지! (거리: {closestDistance:F2}m, 각도: {angleInfo})");
+ _lastDetectionLogTime = Time.time;
+ }
+
+ return closestPlayer;
+ }
+
+ #endregion
+
+ #region Combat
+
+ private void AttackPlayer(GameObject player)
+ {
+ IDamageable damageable = player.GetComponentInParent();
+ if (damageable != null)
+ {
+ damageable.TakeDamage(attackDamage, NetworkObjectId);
+ _lastAttackTime = Time.time;
+
+ if (showDebugInfo)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) {player.name}을(를) 공격! (데미지: {attackDamage})");
+ }
+ }
+ }
+
+ #endregion
+
+ #region Distance Calculation
+
+ private float GetDistanceToCoreSurface()
+ {
+ if (_coreTransform == null)
+ return float.MaxValue;
+
+ if (_coreCollider != null)
+ {
+ Vector3 closestPoint = _coreCollider.ClosestPoint(transform.position);
+ float distanceToSurface = Vector3.Distance(transform.position, closestPoint);
+ return distanceToSurface;
+ }
+ else
+ {
+ return Vector3.Distance(transform.position, _coreTransform.position);
+ }
+ }
+
+ #endregion
+
+ #region State Management
+
+ private void TransitionToState(EnemyAIState newState)
+ {
+ if (_currentState.Value == newState) return;
+
+ if (showDebugInfo)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name} 상태 변경: {_currentState.Value} → {newState}");
+ }
+
+ OnExitState(_currentState.Value);
+ _currentState.Value = newState;
+ OnEnterState(newState);
+ }
+
+ private void OnEnterState(EnemyAIState state)
+ {
+ switch (state)
+ {
+ case EnemyAIState.Idle:
+ _agent.isStopped = true;
+ _agent.speed = moveSpeed;
+ _agent.ResetPath();
+ break;
+
+ case EnemyAIState.MoveToCore:
+ _agent.isStopped = false;
+ _agent.speed = moveSpeed;
+ _hasSetCoreDestination = false;
+
+ if (showDebugInfo)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) 코어로 이동 시작");
+ }
+ break;
+
+ case EnemyAIState.ChasePlayer:
+ _agent.isStopped = false;
+ _agent.speed = moveSpeed * chaseSpeedMultiplier;
+ _chaseStartPosition = transform.position;
+
+ if (showDebugInfo)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) 추적 시작! (시작 위치: {_chaseStartPosition})");
+ }
+ break;
+
+ case EnemyAIState.Attack:
+ _agent.isStopped = true;
+ _agent.ResetPath();
+ break;
+
+ case EnemyAIState.ReturnToOrigin:
+ _agent.isStopped = false;
+ _agent.speed = moveSpeed;
+ _agent.stoppingDistance = 1f;
+ _agent.SetDestination(_originPosition);
+ ClearTargetPlayer();
+ break;
+ }
+ }
+
+ private void OnExitState(EnemyAIState state)
+ {
+ if (state == EnemyAIState.ReturnToOrigin)
+ {
+ _agent.stoppingDistance = attackRange * 0.7f;
+ }
+ }
+
+ private void OnLostTarget()
+ {
+ if (aiType == TeamType.Hostile)
+ {
+ TransitionToState(EnemyAIState.ReturnToOrigin);
+ }
+ else if (aiType == TeamType.Monster)
+ {
+ ClearTargetPlayer();
+ _hasSetCoreDestination = false;
+ TransitionToState(EnemyAIState.MoveToCore);
+ }
+ }
+
+ #endregion
+
+ #region Target Management
+
+ private void SetTargetPlayer(GameObject player)
+ {
+ var networkObject = player.GetComponentInParent();
+ if (networkObject != null)
+ {
+ _targetPlayerId.Value = networkObject.NetworkObjectId;
+ _cachedTargetPlayer = player;
+ }
+ }
+
+ private void ClearTargetPlayer()
+ {
+ _targetPlayerId.Value = 0;
+ _cachedTargetPlayer = null;
+ }
+
+ private GameObject GetTargetPlayer()
+ {
+ if (_targetPlayerId.Value == 0) return null;
+
+ if (_cachedTargetPlayer != null && _cachedTargetPlayer.activeSelf)
+ {
+ return _cachedTargetPlayer;
+ }
+
+ if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(_targetPlayerId.Value, out NetworkObject networkObject))
+ {
+ _cachedTargetPlayer = networkObject.gameObject;
+ return _cachedTargetPlayer;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region Utilities
+
+ private void FindCore()
+ {
+ Core core = FindFirstObjectByType();
+ if (core != null)
+ {
+ _coreTransform = core.transform;
+
+ _coreCollider = core.GetComponent();
+ if (_coreCollider == null)
+ {
+ _coreCollider = core.GetComponentInChildren();
+ }
+
+ if (_coreCollider != null)
+ {
+ Debug.Log($"[EnemyAI] {gameObject.name}이(가) 코어를 찾았습니다! (위치: {_coreTransform.position}, Collider: {_coreCollider.GetType().Name})");
+ }
+ else
+ {
+ Debug.LogWarning($"[EnemyAI] {gameObject.name}이(가) 코어를 찾았지만 Collider가 없습니다.");
+ }
+ }
+ else
+ {
+ Debug.LogWarning($"[EnemyAI] {gameObject.name}이(가) 코어를 찾을 수 없습니다!");
+ }
+ }
+
+ #endregion
+
+ #region Gizmos
+
+ private void OnDrawGizmos()
+ {
+ if (!showDebugInfo) return;
+
+ // 감지 범위
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(transform.position, detectionRange);
+
+ // 공격 범위
+ Gizmos.color = Color.red;
+ Gizmos.DrawWireSphere(transform.position, attackRange);
+
+ // 시야각 (360도가 아닐 때만 표시)
+ if (detectionAngle < 360f)
+ {
+ Vector3 forward = transform.forward * detectionRange;
+ Vector3 leftBoundary = Quaternion.Euler(0, -detectionAngle / 2f, 0) * forward;
+ Vector3 rightBoundary = Quaternion.Euler(0, detectionAngle / 2f, 0) * forward;
+
+ Gizmos.color = Color.blue;
+ Gizmos.DrawLine(transform.position, transform.position + leftBoundary);
+ Gizmos.DrawLine(transform.position, transform.position + rightBoundary);
+ }
+
+ // 원점 표시 (적대 세력만)
+ if (aiType == TeamType.Hostile && Application.isPlaying)
+ {
+ Gizmos.color = Color.green;
+ Gizmos.DrawWireSphere(_originPosition, 1f);
+ Gizmos.DrawLine(transform.position, _originPosition);
+ }
+
+ // 추적 시작 위치 표시
+ if (Application.isPlaying && (_currentState.Value == EnemyAIState.ChasePlayer || _currentState.Value == EnemyAIState.Attack))
+ {
+ Gizmos.color = Color.cyan;
+ Gizmos.DrawWireSphere(_chaseStartPosition, 1.5f);
+ Gizmos.DrawLine(transform.position, _chaseStartPosition);
+ }
+
+ // 코어 방향 및 표면까지의 거리 표시
+ if (Application.isPlaying && _currentState.Value == EnemyAIState.MoveToCore && _coreTransform != null)
+ {
+ Gizmos.color = Color.magenta;
+ Gizmos.DrawLine(transform.position, _coreTransform.position);
+ Gizmos.DrawWireSphere(_coreTransform.position, 2f);
+
+ if (_coreCollider != null)
+ {
+ Vector3 closestPoint = _coreCollider.ClosestPoint(transform.position);
+ Gizmos.color = Color.green;
+ Gizmos.DrawLine(transform.position, closestPoint);
+ Gizmos.DrawWireSphere(closestPoint, 0.5f);
+ }
+ }
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ OnDrawGizmos();
+
+ #if UNITY_EDITOR
+ if (Application.isPlaying && _agent != null)
+ {
+ string pathInfo = _agent.hasPath ? $"Path: {_agent.path.status}" : "No Path";
+ string navMeshInfo = _agent.isOnNavMesh ? "On NavMesh" : "OFF NAVMESH!";
+ string velocityInfo = $"Velocity: {_agent.velocity.magnitude:F2}";
+
+ string distanceInfo = "";
+ if (_coreTransform != null && _currentState.Value == EnemyAIState.MoveToCore)
+ {
+ float surfaceDistance = GetDistanceToCoreSurface();
+ distanceInfo = $"\nCore Surface Dist: {surfaceDistance:F2}m";
+ }
+
+ string angleInfo = detectionAngle >= 360f ? "\nDetection: 360° (전방향)" : $"\nDetection: {detectionAngle}°";
+
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
+ $"Enemy AI\nState: {_currentState.Value}\nType: {aiType}\n{navMeshInfo}\n{pathInfo}\n{velocityInfo}{angleInfo}\nRange: {detectionRange}m\nAttack: {attackRange}m{distanceInfo}");
+ }
+ #endif
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyAIController.cs.meta b/Assets/Scripts/EnemyAIController.cs.meta
new file mode 100644
index 0000000..202bdc6
--- /dev/null
+++ b/Assets/Scripts/EnemyAIController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 453e726e48d16214f84c6d5737edd7df
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyAIState.cs b/Assets/Scripts/EnemyAIState.cs
new file mode 100644
index 0000000..9c346f2
--- /dev/null
+++ b/Assets/Scripts/EnemyAIState.cs
@@ -0,0 +1,14 @@
+namespace Northbound
+{
+ ///
+ /// 적 AI의 상태
+ ///
+ public enum EnemyAIState
+ {
+ Idle, // 대기 (적대 세력 기본 상태)
+ MoveToCore, // 코어로 이동 (몬스터 기본 상태)
+ ChasePlayer, // 플레이어 추적
+ Attack, // 공격
+ ReturnToOrigin // 원래 위치로 복귀 (적대 세력)
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyAIState.cs.meta b/Assets/Scripts/EnemyAIState.cs.meta
new file mode 100644
index 0000000..9d8ee64
--- /dev/null
+++ b/Assets/Scripts/EnemyAIState.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f34add9794647d043ae26b47fac8e429
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyUnit.cs b/Assets/Scripts/EnemyUnit.cs
new file mode 100644
index 0000000..634bc64
--- /dev/null
+++ b/Assets/Scripts/EnemyUnit.cs
@@ -0,0 +1,191 @@
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ ///
+ /// 적대 유닛 (적대세력 또는 몬스터)
+ ///
+ public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember, IVisionProvider
+ {
+ [Header("Team Settings")]
+ [Tooltip("이 유닛의 팀 (Hostile = 적대세력, Monster = 몬스터)")]
+ public TeamType enemyTeam = TeamType.Hostile;
+
+ [Header("Combat")]
+ public int maxHealth = 100;
+ public float visionRange = 10f;
+
+ [Header("Visual")]
+ public GameObject damageEffectPrefab;
+ public GameObject destroyEffectPrefab;
+
+ private NetworkVariable _currentHealth = new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable _team = new NetworkVariable(
+ TeamType.Neutral,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (IsServer)
+ {
+ _currentHealth.Value = maxHealth;
+ _team.Value = enemyTeam;
+
+ // FogOfWar 시스템에 등록
+ FogOfWarSystem.Instance?.RegisterVisionProvider(this);
+
+ Debug.Log($"[EnemyUnit] {gameObject.name} 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)})");
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ base.OnNetworkDespawn();
+
+ if (IsServer)
+ {
+ FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
+ }
+ }
+
+ #region IDamageable Implementation
+
+ public void TakeDamage(int damage, ulong attackerId)
+ {
+ if (!IsServer) return;
+ if (_currentHealth.Value <= 0) return;
+
+ // 공격자의 팀 확인
+ if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
+ {
+ var attackerTeamMember = attackerObj.GetComponent();
+ if (attackerTeamMember != null)
+ {
+ if (!TeamManager.CanAttack(attackerTeamMember, this))
+ {
+ Debug.Log($"[EnemyUnit] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.");
+ return;
+ }
+ }
+ }
+
+ int actualDamage = Mathf.Min(damage, _currentHealth.Value);
+ _currentHealth.Value -= actualDamage;
+
+ Debug.Log($"[EnemyUnit] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}");
+
+ // 데미지 이펙트
+ ShowDamageEffectClientRpc();
+
+ // 체력이 0이 되면 파괴
+ if (_currentHealth.Value <= 0)
+ {
+ DestroyUnit(attackerId);
+ }
+ }
+
+ private void DestroyUnit(ulong attackerId)
+ {
+ if (!IsServer) return;
+
+ Debug.Log($"[EnemyUnit] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 파괴되었습니다! (공격자: {attackerId})");
+
+ // 파괴 이펙트
+ ShowDestroyEffectClientRpc();
+
+ // FogOfWar 시스템에서 제거
+ FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
+
+ // 네트워크 오브젝트 파괴
+ Invoke(nameof(DespawnUnit), 0.5f);
+ }
+
+ private void DespawnUnit()
+ {
+ if (IsServer && NetworkObject != null)
+ {
+ NetworkObject.Despawn(true);
+ }
+ }
+
+ [ClientRpc]
+ private void ShowDamageEffectClientRpc()
+ {
+ if (damageEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(damageEffectPrefab, transform.position, Quaternion.identity);
+ Destroy(effect, 2f);
+ }
+ }
+
+ [ClientRpc]
+ private void ShowDestroyEffectClientRpc()
+ {
+ if (destroyEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity);
+ Destroy(effect, 3f);
+ }
+ }
+
+ #endregion
+
+ #region ITeamMember Implementation
+
+ public TeamType GetTeam() => _team.Value;
+
+ public void SetTeam(TeamType team)
+ {
+ if (!IsServer) return;
+ _team.Value = team;
+ }
+
+ #endregion
+
+ #region IVisionProvider Implementation
+
+ public ulong GetOwnerId() => OwnerClientId;
+
+ public float GetVisionRange() => visionRange;
+
+ public Transform GetTransform() => transform;
+
+ public bool IsActive() => IsSpawned && _currentHealth.Value > 0;
+
+ #endregion
+
+ private void OnDrawGizmosSelected()
+ {
+ // 팀 색상으로 시야 범위 표시
+ Color teamColor = Application.isPlaying
+ ? TeamManager.GetTeamColor(_team.Value)
+ : TeamManager.GetTeamColor(enemyTeam);
+
+ Gizmos.color = new Color(teamColor.r, teamColor.g, teamColor.b, 0.3f);
+ Gizmos.DrawWireSphere(transform.position, visionRange);
+
+ #if UNITY_EDITOR
+ if (Application.isPlaying)
+ {
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
+ $"Team: {TeamManager.GetTeamName(_team.Value)}\nHP: {_currentHealth.Value}/{maxHealth}");
+ }
+ else
+ {
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
+ $"Team: {TeamManager.GetTeamName(enemyTeam)}");
+ }
+ #endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/EnemyUnit.cs.meta b/Assets/Scripts/EnemyUnit.cs.meta
new file mode 100644
index 0000000..6561170
--- /dev/null
+++ b/Assets/Scripts/EnemyUnit.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 345fc6e7d4f06314f8b548129700eccb
\ No newline at end of file
diff --git a/Assets/Scripts/ITeamMember.cs b/Assets/Scripts/ITeamMember.cs
new file mode 100644
index 0000000..0e9c127
--- /dev/null
+++ b/Assets/Scripts/ITeamMember.cs
@@ -0,0 +1,11 @@
+namespace Northbound
+{
+ ///
+ /// 팀에 속한 엔티티
+ ///
+ public interface ITeamMember
+ {
+ TeamType GetTeam();
+ void SetTeam(TeamType team);
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/ITeamMember.cs.meta b/Assets/Scripts/ITeamMember.cs.meta
new file mode 100644
index 0000000..366ac40
--- /dev/null
+++ b/Assets/Scripts/ITeamMember.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5e343729a9b720e438ce09faa7886ab0
\ No newline at end of file
diff --git a/Assets/Scripts/NetworkPlayerController.cs b/Assets/Scripts/NetworkPlayerController.cs
index 4f52afa..347b60d 100644
--- a/Assets/Scripts/NetworkPlayerController.cs
+++ b/Assets/Scripts/NetworkPlayerController.cs
@@ -2,13 +2,37 @@ using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Cinemachine;
+using Northbound;
-public class NetworkPlayerController : NetworkBehaviour
+public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable
{
[Header("Movement Settings")]
public float moveSpeed = 5f;
public float rotationSpeed = 10f;
+ [Header("Team Settings")]
+ [SerializeField] private TeamType initialTeam = TeamType.Player;
+
+ [Header("Health Settings")]
+ [SerializeField] private int maxHealth = 100;
+ [SerializeField] private bool showHealthBar = true;
+
+ [Header("Visual Effects")]
+ [SerializeField] private GameObject damageEffectPrefab;
+ [SerializeField] private GameObject deathEffectPrefab;
+
+ private NetworkVariable _team = new NetworkVariable(
+ TeamType.Player,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private NetworkVariable _currentHealth = new NetworkVariable(
+ 100,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
private Vector2 _moveInput;
private CharacterController _controller;
private PlayerInputActions _inputActions;
@@ -22,6 +46,27 @@ public class NetworkPlayerController : NetworkBehaviour
public override void OnNetworkSpawn()
{
+ base.OnNetworkSpawn();
+
+ // 서버에서 초기화
+ if (IsServer)
+ {
+ if (_team.Value == TeamType.Neutral)
+ {
+ _team.Value = initialTeam;
+ }
+
+ if (_currentHealth.Value == 0)
+ {
+ _currentHealth.Value = maxHealth;
+ }
+
+ Debug.Log($"[Player] {gameObject.name} 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})");
+ }
+
+ // 체력 변경 이벤트 구독
+ _currentHealth.OnValueChanged += OnHealthChanged;
+
if (!IsOwner) return;
var vcam = GameObject.FindFirstObjectByType();
@@ -39,16 +84,23 @@ public class NetworkPlayerController : NetworkBehaviour
public override void OnNetworkDespawn()
{
+ _currentHealth.OnValueChanged -= OnHealthChanged;
+
if (IsOwner && _inputActions != null)
{
_inputActions.Disable();
}
+
+ base.OnNetworkDespawn();
}
void Update()
{
if (!IsOwner) return;
+ // 죽었으면 이동 불가
+ if (_currentHealth.Value <= 0) return;
+
_moveInput = _inputActions.Player.Move.ReadValue();
Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;
@@ -68,4 +120,201 @@ public class NetworkPlayerController : NetworkBehaviour
_animator.SetFloat("MoveSpeed", move.magnitude);
}
}
+
+ #region ITeamMember Implementation
+
+ public TeamType GetTeam() => _team.Value;
+
+ public void SetTeam(TeamType team)
+ {
+ if (!IsServer) return;
+
+ TeamType previousTeam = _team.Value;
+ _team.Value = team;
+ Debug.Log($"[Player] 팀 변경: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(team)}");
+ }
+
+ #endregion
+
+ #region IDamageable Implementation
+
+ public void TakeDamage(int damage, ulong attackerId)
+ {
+ if (!IsServer) return;
+
+ // 이미 죽었으면 무시
+ if (_currentHealth.Value <= 0) return;
+
+ // 공격자의 팀 확인
+ if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
+ {
+ var attackerTeamMember = attackerObj.GetComponent();
+ if (attackerTeamMember != null)
+ {
+ if (!TeamManager.CanAttack(attackerTeamMember, this))
+ {
+ Debug.Log($"[Player] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.");
+ return;
+ }
+ }
+ }
+
+ // 데미지 적용
+ int actualDamage = Mathf.Min(damage, _currentHealth.Value);
+ _currentHealth.Value -= actualDamage;
+
+ Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}");
+
+ // 데미지 이펙트
+ ShowDamageEffectClientRpc();
+
+ // 체력이 0이 되면 사망
+ if (_currentHealth.Value <= 0)
+ {
+ Die(attackerId);
+ }
+ }
+
+ private void Die(ulong killerId)
+ {
+ if (!IsServer) return;
+
+ Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})");
+
+ // 사망 이펙트
+ ShowDeathEffectClientRpc();
+
+ // 애니메이션 (있는 경우)
+ if (_animator != null)
+ {
+ _animator.SetTrigger("Die");
+ }
+
+ // 일정 시간 후 리스폰 또는 디스폰
+ Invoke(nameof(HandleDeath), 3f);
+ }
+
+ private void HandleDeath()
+ {
+ if (!IsServer) return;
+
+ // 여기서 리스폰 로직을 추가하거나 게임 오버 처리
+ // 예: 리스폰 위치로 이동 및 체력 회복
+ Respawn();
+ }
+
+ private void Respawn()
+ {
+ if (!IsServer) return;
+
+ // 체력 회복
+ _currentHealth.Value = maxHealth;
+
+ // 스폰 포인트로 이동 (PlayerSpawnPoint 활용)
+ var spawnPoints = FindObjectsByType(FindObjectsSortMode.None);
+ if (spawnPoints.Length > 0)
+ {
+ var spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
+ transform.position = spawnPoint.transform.position;
+ transform.rotation = spawnPoint.transform.rotation;
+ }
+
+ Debug.Log($"[Player] {gameObject.name} 리스폰!");
+ }
+
+ [ClientRpc]
+ private void ShowDamageEffectClientRpc()
+ {
+ if (damageEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up, Quaternion.identity);
+ Destroy(effect, 2f);
+ }
+ }
+
+ [ClientRpc]
+ private void ShowDeathEffectClientRpc()
+ {
+ if (deathEffectPrefab != null)
+ {
+ GameObject effect = Instantiate(deathEffectPrefab, transform.position, Quaternion.identity);
+ Destroy(effect, 3f);
+ }
+ }
+
+ #endregion
+
+ #region Health Management
+
+ ///
+ /// 현재 체력
+ ///
+ public int GetCurrentHealth() => _currentHealth.Value;
+
+ ///
+ /// 최대 체력
+ ///
+ public int GetMaxHealth() => maxHealth;
+
+ ///
+ /// 체력 비율 (0.0 ~ 1.0)
+ ///
+ public float GetHealthPercentage()
+ {
+ return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
+ }
+
+ ///
+ /// 죽었는지 여부
+ ///
+ public bool IsDead() => _currentHealth.Value <= 0;
+
+ ///
+ /// 체력 회복
+ ///
+ public void Heal(int amount)
+ {
+ if (!IsServer) return;
+
+ int healAmount = Mathf.Min(amount, maxHealth - _currentHealth.Value);
+ _currentHealth.Value += healAmount;
+
+ Debug.Log($"[Player] {gameObject.name}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{maxHealth}");
+ }
+
+ private void OnHealthChanged(int previousValue, int newValue)
+ {
+ // 체력바 UI 업데이트 또는 체력 변경 시각 효과
+ Debug.Log($"[Player] 체력 변경: {previousValue} → {newValue}");
+
+ // 클라이언트에서도 체력 변경 인지 가능
+ if (IsOwner)
+ {
+ // UI 업데이트 등
+ }
+ }
+
+ #endregion
+
+ #region Gizmos
+
+ private void OnDrawGizmosSelected()
+ {
+ #if UNITY_EDITOR
+ if (Application.isPlaying)
+ {
+ string teamName = TeamManager.GetTeamName(_team.Value);
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
+ $"Player: {gameObject.name}\nTeam: {teamName}\nHP: {_currentHealth.Value}/{maxHealth}");
+ }
+ else
+ {
+ string teamName = TeamManager.GetTeamName(initialTeam);
+ UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
+ $"Player: {gameObject.name}\nTeam: {teamName}\nHP: {maxHealth}/{maxHealth}");
+ }
+ #endif
+ }
+
+ #endregion
}
diff --git a/Assets/Scripts/TeamManager.cs b/Assets/Scripts/TeamManager.cs
new file mode 100644
index 0000000..1101af0
--- /dev/null
+++ b/Assets/Scripts/TeamManager.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Northbound
+{
+ ///
+ /// 팀 간의 관계 및 적대 관계를 관리
+ ///
+ public static class TeamManager
+ {
+ // 팀 간 적대 관계 테이블
+ private static readonly Dictionary<(TeamType, TeamType), bool> _hostilityTable = new Dictionary<(TeamType, TeamType), bool>
+ {
+ // 플레이어 vs 적대 세력
+ { (TeamType.Player, TeamType.Hostile), true },
+ { (TeamType.Hostile, TeamType.Player), true },
+
+ // 플레이어 vs 몬스터
+ { (TeamType.Player, TeamType.Monster), true },
+ { (TeamType.Monster, TeamType.Player), true },
+
+ // 적대 세력 vs 몬스터 (서로 공격하지 않음)
+ { (TeamType.Hostile, TeamType.Monster), false },
+ { (TeamType.Monster, TeamType.Hostile), false },
+
+ // 같은 팀끼리는 공격하지 않음
+ { (TeamType.Player, TeamType.Player), false },
+ { (TeamType.Hostile, TeamType.Hostile), false },
+ { (TeamType.Monster, TeamType.Monster), false },
+
+ // 중립은 공격받지 않음
+ { (TeamType.Neutral, TeamType.Player), false },
+ { (TeamType.Neutral, TeamType.Hostile), false },
+ { (TeamType.Neutral, TeamType.Monster), false },
+ { (TeamType.Player, TeamType.Neutral), false },
+ { (TeamType.Hostile, TeamType.Neutral), false },
+ { (TeamType.Monster, TeamType.Neutral), false },
+ { (TeamType.Neutral, TeamType.Neutral), false }
+ };
+
+ ///
+ /// 두 팀이 적대 관계인지 확인
+ ///
+ public static bool AreHostile(TeamType team1, TeamType team2)
+ {
+ if (_hostilityTable.TryGetValue((team1, team2), out bool isHostile))
+ {
+ return isHostile;
+ }
+
+ // 기본적으로 다른 팀이면 적대
+ return team1 != team2 && team1 != TeamType.Neutral && team2 != TeamType.Neutral;
+ }
+
+ ///
+ /// 공격 가능한 대상인지 확인
+ ///
+ public static bool CanAttack(ITeamMember attacker, ITeamMember target)
+ {
+ if (attacker == null || target == null)
+ return false;
+
+ return AreHostile(attacker.GetTeam(), target.GetTeam());
+ }
+
+ ///
+ /// 팀의 색상 가져오기 (UI 표시용)
+ ///
+ public static Color GetTeamColor(TeamType team)
+ {
+ return team switch
+ {
+ TeamType.Player => Color.blue,
+ TeamType.Hostile => Color.red,
+ TeamType.Monster => new Color(0.8f, 0f, 0.8f), // 보라색
+ TeamType.Neutral => Color.gray,
+ _ => Color.white
+ };
+ }
+
+ ///
+ /// 팀 이름 가져오기 (한글)
+ ///
+ public static string GetTeamName(TeamType team)
+ {
+ return team switch
+ {
+ TeamType.Player => "플레이어",
+ TeamType.Hostile => "적대 세력",
+ TeamType.Monster => "몬스터",
+ TeamType.Neutral => "중립",
+ _ => "알 수 없음"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/TeamManager.cs.meta b/Assets/Scripts/TeamManager.cs.meta
new file mode 100644
index 0000000..0f66789
--- /dev/null
+++ b/Assets/Scripts/TeamManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 95252f6c80f5e2a40b0d4f95c23b2039
\ No newline at end of file
diff --git a/Assets/Scripts/TeamType.cs b/Assets/Scripts/TeamType.cs
new file mode 100644
index 0000000..2972275
--- /dev/null
+++ b/Assets/Scripts/TeamType.cs
@@ -0,0 +1,13 @@
+namespace Northbound
+{
+ ///
+ /// 게임 내 팀 타입
+ ///
+ public enum TeamType
+ {
+ Neutral = 0, // 중립 (공격받지 않음)
+ Player = 1, // 플레이어 팀
+ Hostile = 2, // 적대 세력 (플레이어 공격)
+ Monster = 3 // 몬스터 (플레이어 공격)
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/TeamType.cs.meta b/Assets/Scripts/TeamType.cs.meta
new file mode 100644
index 0000000..16312e3
--- /dev/null
+++ b/Assets/Scripts/TeamType.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fa67976f76e9a5f4fac1e55e7bfedf52
\ No newline at end of file
diff --git a/ExternAttributes.Editor.csproj b/ExternAttributes.Editor.csproj
index 74feaaf..6f4b026 100644
--- a/ExternAttributes.Editor.csproj
+++ b/ExternAttributes.Editor.csproj
@@ -1198,14 +1198,14 @@
Library\ScriptAssemblies\UnityEngine.UI.dll
False
-
- Library\ScriptAssemblies\UnityEditor.TestRunner.dll
- False
-
Library\ScriptAssemblies\UnityEngine.TestRunner.dll
False
+
+ Library\ScriptAssemblies\UnityEditor.TestRunner.dll
+ False
+
diff --git a/FlatKit.Utils.Editor.csproj b/FlatKit.Utils.Editor.csproj
index f4dd6f4..d2b30be 100644
--- a/FlatKit.Utils.Editor.csproj
+++ b/FlatKit.Utils.Editor.csproj
@@ -1181,14 +1181,14 @@
Library\ScriptAssemblies\UnityEngine.UI.dll
False
-
- Library\ScriptAssemblies\UnityEditor.TestRunner.dll
- False
-
Library\ScriptAssemblies\UnityEngine.TestRunner.dll
False
+
+ Library\ScriptAssemblies\UnityEditor.TestRunner.dll
+ False
+
diff --git a/Packages/manifest.json b/Packages/manifest.json
index c6add5b..efe669f 100644
--- a/Packages/manifest.json
+++ b/Packages/manifest.json
@@ -8,6 +8,7 @@
"com.unity.2d.tilemap": "1.0.0",
"com.unity.2d.tilemap.extras": "6.0.1",
"com.unity.2d.tooling": "1.0.0",
+ "com.unity.ai.navigation": "2.0.9",
"com.unity.cinemachine": "3.1.5",
"com.unity.collab-proxy": "2.10.2",
"com.unity.ide.rider": "3.0.38",
diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json
index 318bc10..4b0f52d 100644
--- a/Packages/packages-lock.json
+++ b/Packages/packages-lock.json
@@ -99,6 +99,15 @@
},
"url": "https://packages.unity.com"
},
+ "com.unity.ai.navigation": {
+ "version": "2.0.9",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.modules.ai": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
"com.unity.burst": {
"version": "1.8.27",
"depth": 1,